diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c4c8826..bb4ea8a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,3 +14,5 @@ permissions: jobs: call-workflow: uses: skiptools/actions/.github/workflows/skip-framework.yml@v1 + with: + runs-on: "['macos-15-intel', 'ubuntu-24.04']" diff --git a/Sources/SkipKeychain/SkipKeychain.swift b/Sources/SkipKeychain/SkipKeychain.swift index b78752e..5b501d8 100644 --- a/Sources/SkipKeychain/SkipKeychain.swift +++ b/Sources/SkipKeychain/SkipKeychain.swift @@ -3,7 +3,11 @@ #if !SKIP_BRIDGE import Foundation #if !SKIP +// Platforms without the Security framework (e.g. the Linux host pass of +// `skip export`) compile a throwing stub so the package can build there. +#if canImport(Security) import Security +#endif #else import android.content.Context import android.content.SharedPreferences @@ -23,6 +27,7 @@ public struct Keychain { /// Retrieve a value. public func string(forKey key: String) throws -> String? { #if !SKIP + #if canImport(Security) guard let data = try data(forKey: key) else { return nil } @@ -31,6 +36,9 @@ public struct Keychain { } return string #else + throw KeychainError(message: "Keychain is not supported on this platform") + #endif + #else do { let prefs = try initializePreferences() return prefs.getString(key, nil) @@ -67,11 +75,15 @@ public struct Keychain { /// Store a key value pair. public func set(_ string: String, forKey key: String, access: KeychainAccess = .unlocked) throws { #if !SKIP + #if canImport(Security) guard let data = string.data(using: .utf8) else { throw KeychainError(invalidValue: true) } try set(data, forKey: key, access: access) #else + throw KeychainError(message: "Keychain is not supported on this platform") + #endif + #else do { let prefs = try initializePreferences() let editor = prefs.edit() @@ -99,6 +111,7 @@ public struct Keychain { } #if !SKIP + #if canImport(Security) private func data(forKey key: String) throws -> Data? { lock.lock() defer { lock.unlock() } @@ -135,6 +148,7 @@ public struct Keychain { throw KeychainError(code: code) } } + #endif #else private var preferences: SharedPreferences? @@ -167,10 +181,14 @@ public struct Keychain { /// Delete the value stored for the given key. public func removeValue(forKey key: String) throws { #if !SKIP + #if canImport(Security) lock.lock() defer { lock.unlock() } try removeHoldingLock(forKey: key) #else + throw KeychainError(message: "Keychain is not supported on this platform") + #endif + #else do { let prefs = try initializePreferences() let editor = prefs.edit() @@ -183,6 +201,7 @@ public struct Keychain { } #if !SKIP + #if canImport(Security) private func removeHoldingLock(forKey key: String) throws { let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, @@ -194,10 +213,12 @@ public struct Keychain { } } #endif + #endif /// Return the set of all stored keys. public func keys() throws -> [String] { #if !SKIP + #if canImport(Security) lock.lock() defer { lock.unlock() } @@ -219,6 +240,9 @@ public struct Keychain { } return dicts.compactMap { $0[kSecAttrAccount as String] as? String } #else + throw KeychainError(message: "Keychain is not supported on this platform") + #endif + #else do { return Array(initializePreferences().getAll().keys) } catch { @@ -230,6 +254,7 @@ public struct Keychain { /// Remove all stored key value pairs. public func removeAll() throws { #if !SKIP + #if canImport(Security) lock.lock() defer { lock.unlock() } @@ -239,6 +264,9 @@ public struct Keychain { throw KeychainError(code: code) } #else + throw KeychainError(message: "Keychain is not supported on this platform") + #endif + #else do { let editor = initializePreferences().edit() editor.clear() @@ -264,6 +292,7 @@ public enum KeychainAccess { case passcodeSetThisDeviceOnly #if !SKIP + #if canImport(Security) var value: String { switch self { case .unlocked: @@ -279,6 +308,7 @@ public enum KeychainAccess { } } #endif + #endif } /// Thrown on keychain error. @@ -294,10 +324,12 @@ public struct KeychainError: Error, CustomStringConvertible { } #if !SKIP + #if canImport(Security) init(code: OSStatus) { self.message = SecCopyErrorMessageString(code, nil) as? String ?? "Unknown error" } #endif + #endif public var description: String { return message diff --git a/Tests/SkipKeychainTests/SkipKeychainTests.swift b/Tests/SkipKeychainTests/SkipKeychainTests.swift index bcfe3d3..a3bc038 100644 --- a/Tests/SkipKeychainTests/SkipKeychainTests.swift +++ b/Tests/SkipKeychainTests/SkipKeychainTests.swift @@ -1,9 +1,11 @@ import XCTest -import OSLog import Foundation @testable import SkipKeychain +#if SKIP || canImport(OSLog) +import OSLog let logger: Logger = Logger(subsystem: "test", category: "SkipKeychainTests") +#endif final class SkipKeychainTests: XCTestCase { @@ -11,6 +13,7 @@ final class SkipKeychainTests: XCTestCase { let key = "SkipKeychainTestsStringKey" try skipRoboelectric() try skipiOSSimulator() + try skipNoKeychainPlatform() let keychain = Keychain.shared try keychain.removeValue(forKey: key) try XCTAssertNil(keychain.string(forKey: key)) @@ -22,6 +25,7 @@ final class SkipKeychainTests: XCTestCase { let key = "SkipKeychainTestsUpdateKey" try skipRoboelectric() try skipiOSSimulator() + try skipNoKeychainPlatform() let keychain = Keychain.shared try keychain.removeValue(forKey: key) try keychain.set("value", forKey: key) @@ -34,6 +38,7 @@ final class SkipKeychainTests: XCTestCase { let key = "SkipKeychainTestsValueForKey" try skipRoboelectric() try skipiOSSimulator() + try skipNoKeychainPlatform() let keychain = Keychain.shared try keychain.removeValue(forKey: key) try keychain.removeValue(forKey: "nonexistantkey") @@ -47,6 +52,7 @@ final class SkipKeychainTests: XCTestCase { let key = "SkipKeychainTestsKey" try skipRoboelectric() try skipiOSSimulator() + try skipNoKeychainPlatform() let keychain = Keychain.shared try keychain.removeValue(forKey: key) try XCTAssertFalse(keychain.keys().contains(key)) @@ -58,6 +64,7 @@ final class SkipKeychainTests: XCTestCase { let key = "SkipKeychainTestsBoolKey" try skipRoboelectric() try skipiOSSimulator() + try skipNoKeychainPlatform() let keychain = Keychain.shared try keychain.removeValue(forKey: key) try XCTAssertNil(keychain.bool(forKey: key)) @@ -71,6 +78,7 @@ final class SkipKeychainTests: XCTestCase { let key = "SkipKeychainTestsIntKey" try skipRoboelectric() try skipiOSSimulator() + try skipNoKeychainPlatform() let keychain = Keychain.shared try keychain.removeValue(forKey: key) try XCTAssertNil(keychain.int(forKey: key)) @@ -82,6 +90,7 @@ final class SkipKeychainTests: XCTestCase { let key = "SkipKeychainTestsDoubleKey" try skipRoboelectric() try skipiOSSimulator() + try skipNoKeychainPlatform() let keychain = Keychain.shared try keychain.removeValue(forKey: key) try XCTAssertNil(keychain.double(forKey: key)) @@ -95,6 +104,12 @@ final class SkipKeychainTests: XCTestCase { } } + private func skipNoKeychainPlatform() throws { + #if !SKIP && !canImport(Security) + throw XCTSkip("Keychain is not supported on this platform") + #endif + } + private func skipiOSSimulator() throws { // e.g.: [SkipKeychainTests.SkipKeychainTests testUpdate] : failed: caught error: "A required entitlement isn't present." // we would need to somehow add the entitlement to the test case runner