Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 13 additions & 6 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,23 @@ let package = Package(
name: "OAuthKit",
targets: ["OAuthKit"])
],
dependencies: [
// Android / Linux Dependencies
.package(url: "https://github.com/apple/swift-crypto", from: .init(4, 5, 0))
],
targets: [
.target(
name: "OAuthKit",
dependencies: [
.product(name: "Crypto",
package: "swift-crypto",
condition: .when(platforms: [.android, .linux])
)
],
linkerSettings: [
.linkedFramework("CryptoKit"),
.linkedFramework("LocalAuthentication", .when(
platforms: [.iOS]
)),
.linkedFramework("Network"),
.linkedFramework("Security")
.linkedFramework("LocalAuthentication",
.when(platforms: [.iOS])
),
]
),
.testTarget(
Expand Down
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ OAuthKit can be installed using [Swift Package Manager](https://www.swift.org/do

```swift
dependencies: [
.package(url: "https://github.com/codefiesta/OAuthKit", from: "2.1.1")
.package(url: "https://github.com/codefiesta/OAuthKit", from: "2.2.0")
]
```

Expand Down Expand Up @@ -315,3 +315,8 @@ OAuthKit should work with any standard OAuth2 provider. Below is a list of teste

You can find the complete Swift DocC documentation for the [OAuthKit Framework here](https://codefiesta.github.io/OAuthKit/documentation/oauthkit/).


## Linux / Android Support

As of version [2.2.0](https://github.com/codefiesta/OAuthKit/releases/tag/2.2.0) OAuthKit will now compile for both [Android and Linux](https://www.swift.org/documentation/articles/swift-sdk-for-android-getting-started.html). However, secure storage still needs to be implemented for both [Android](https://github.com/codefiesta/OAuthKit/issues/153) and [Linux](https://github.com/codefiesta/OAuthKit/issues/152).

13 changes: 13 additions & 0 deletions Sources/OAuthKit/Extensions/Data+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@
// Created by Kevin McKee
//

#if canImport(CryptoKit)
import CryptoKit
#else
import Crypto
#endif
import Foundation

extension Data {
Expand Down Expand Up @@ -43,7 +47,16 @@ extension Data {
/// - Returns: an array of cryptographically secure random bytes
static func secureRandom(count: Int = 32) -> Data {
var bytes = [UInt8](repeating: 0, count: count)
#if canImport(CryptoKit)
// Apple
_ = SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes)
#else
// Android / Linux
var generator = SystemRandomNumberGenerator()
for i in 0..<count {
bytes[i] = UInt8.random(in: 0...255, using: &generator)
}
#endif
return Data(bytes: &bytes, count: bytes.count)
}
}
4 changes: 4 additions & 0 deletions Sources/OAuthKit/Extensions/Digest+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@
// Created by Kevin McKee
//

#if canImport(CryptoKit)
import CryptoKit
#else
import Crypto
#endif
import Foundation

extension Digest {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
//
// Created by Kevin McKee
//

#if canImport(SwiftUI)
import SwiftUI

public extension EnvironmentValues {
Expand All @@ -21,3 +21,4 @@ struct OAuthKey: @preconcurrency EnvironmentKey {
/// The default OAuth instance that is loaded into the environment.
@MainActor static let defaultValue: OAuth = .init(.main)
}
#endif
4 changes: 4 additions & 0 deletions Sources/OAuthKit/Extensions/String+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@
// Created by Kevin McKee
//

#if canImport(CryptoKit)
import CryptoKit
#else
import Crypto
#endif
import Foundation

extension String {
Expand Down
4 changes: 4 additions & 0 deletions Sources/OAuthKit/Extensions/URL+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@
// Created by Kevin McKee
//

#if canImport(CryptoKit)
import CryptoKit
#else
import Crypto
#endif
import Foundation

extension URL {
Expand Down
3 changes: 3 additions & 0 deletions Sources/OAuthKit/Extensions/URLRequest+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
//

import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif

private let authHeader = "Authorization"

Expand Down
3 changes: 3 additions & 0 deletions Sources/OAuthKit/Extensions/URLResponse+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
//

import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif

public extension URLResponse {

Expand Down
4 changes: 4 additions & 0 deletions Sources/OAuthKit/Extensions/UUID+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@
// Created by Kevin McKee
//

#if canImport(CryptoKit)
import CryptoKit
#else
import Crypto
#endif
import Foundation

extension UUID {
Expand Down
183 changes: 183 additions & 0 deletions Sources/OAuthKit/Keychain/Keychain+Storage.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
//
// Keychain+Extensions.swift
// OAuthKit
//
// Created by Kevin McKee on 8/19/26.
//

import Foundation
#if canImport(Security)
import Security
#endif

/// The default account to use.
private let defaultAccount = "oauthkit"
/// The default token identifier suffix.
private let tokenIdentifier = "oauth-token"

extension Keychain {

// Provides the storage protocols used for storing sensitive data.
// The storage operations should all be wrapped in a locking
// mechanism to prevent any potential data races.
protocol Storage {

/// The owning account identifier.
/// - Parameter account: a key indicating the account owner. Ideally, use the application identifier for this value.
init(account: String)

/// The account owner of this keychain storage.
var account: String { get }

/// Returns a list of keys owned by this account.
var keys: [String] { get }

/// Sets the data for the specified key
/// - Parameters:
/// - data: the data to store for the specified key
/// - key: the key to use for the data
/// - Returns: true if able to set the data, otherwise false
func set(_ data: Data, for key: String) throws -> Bool

/// Fetches storeed data from the data store with the specified key.
/// - Parameter key: the keychain key
/// - Returns: the data for the specified key or nil if not found
func get(key: String) throws -> Data?

/// Deletes the value for the specified key.
/// - Parameter key: the key to delete
/// - Returns: true if able to delete from the storage, otherwise false
func delete(key: String) -> Bool

/// Clears all values and keys for the current account.
/// - Returns: true if values were cleared, otherwise false.
func clear() -> Bool

/// Builds the combined account key by prefixing the specified key with the account.
/// - Parameter key: the key to prefix.
/// - Returns: the unique account key to use
func accountKey(_ key: String) -> String
}

#if canImport(Security)
/// The default token storage used by Apple ecosystems.
struct DefaultStorage: Storage {

var account: String = defaultAccount

init(account: String) {
self.account = account
}

var keys: [String] {
var results = [String]()
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecReturnAttributes as String: true,
kSecMatchLimit as String: kSecMatchLimitAll
]

var result: AnyObject?
let status = withUnsafeMutablePointer(to: &result) { pointer in
SecItemCopyMatching(query as CFDictionary, UnsafeMutablePointer(pointer))
}

guard status == noErr else { return results }

if let items = result as? [[String: Any]] {
for item in items {
if let key = item[kSecAttrAccount as String] as? String {
results.append(key)
}
}
}
return results.filter{ $0.starts(with: account)}.sorted{ $0 < $1}
}

/// Sets the value for the specified key.
/// - Parameters:
/// - value: the value to store
/// - key: the key to use
/// - Returns: true if able to set the value, otherwise false
@discardableResult
func set(_ data: Data, for key: String) throws -> Bool {
assert(key.isNotEmpty, "❌ The keychain key cannot be empty.")

let account = accountKey(key)
delete(key: account)

let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: account,
kSecValueData as String: data
]

let status = SecItemAdd(query as CFDictionary, nil)
return status == errSecSuccess
}

/// Fetches storeed data from the data store with the specified key.
/// - Parameter key: the keychain key
/// - Returns: the data for the specified key or nil if not found
func get(key: String) throws -> Data? {

let account = accountKey(key)

let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecMatchLimit as String: kSecMatchLimitOne,
kSecAttrAccount as String: account,
kSecReturnData as String: true
]

var result: AnyObject?
let status = withUnsafeMutablePointer(to: &result) { pointer in
SecItemCopyMatching(query as CFDictionary, UnsafeMutablePointer(pointer))
}

guard status == noErr, let data = result as? Data else {
return nil
}

return data
}

/// Clears all values and keys for the current account.
/// - Returns: true if values were cleared, otherwise false.
@discardableResult
func clear() -> Bool {

var results: [Bool] = []
for key in keys {
results.append(delete(key: key))
}

guard results.isNotEmpty else { return true }
return results.allSatisfy{ $0 == true }
}

/// Deletes the value for the specified key.
/// - Parameter key: the key to delete
/// - Returns: true if able to delete from the storage, otherwise false
@discardableResult
func delete(key: String) -> Bool {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key
]
let status = SecItemDelete(query as CFDictionary)
return status == noErr
}
}
#endif
}

extension Keychain.Storage {

/// Builds the combined account key by prefixing the specified key with the account.
/// - Parameter key: the key to prefix.
/// - Returns: the unique account key to use
func accountKey(_ key: String) -> String {
account + "." + key + "." + tokenIdentifier
}
}
Loading
Loading