diff --git a/README.md b/README.md index fb9286e..e98bfb4 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ StorageRouter (main entry point) ### Protocols - **StorageKey** - Define storage configuration for each data type - **StorageProviding** - Abstraction for storage operations +- **KeyMaterialProviding** - Supplies external key material for encryption ### Services (Actors) - **StorageRouter** - Main entry point for all storage operations @@ -115,6 +116,24 @@ try await StorageRouter.shared.remove(key) - Configurable PBKDF2 iteration count - Master key stored securely in keychain - Default security policy: `SecurityPolicy.recommended` (ChaCha20-Poly1305 + HKDF) +- External key material providers can be registered via `EncryptionHelper` + +```swift +struct RemoteKeyProvider: KeyMaterialProviding { + func keyMaterial(for keyName: String) async throws -> Data { + // Example only: fetch from service or keychain + Data(repeating: 1, count: 32) + } +} + +let source = KeyMaterialSource(id: "remote.key") +await EncryptionHelper.shared.registerKeyMaterialProvider(RemoteKeyProvider(), for: source) + +let policy: SecurityPolicy.EncryptionPolicy = .external( + source: source, + keyDerivation: .hkdf() +) +``` ## Sync Behavior diff --git a/Sources/LocalData/Models/KeyMaterialSource.swift b/Sources/LocalData/Models/KeyMaterialSource.swift new file mode 100644 index 0000000..3ba6a2b --- /dev/null +++ b/Sources/LocalData/Models/KeyMaterialSource.swift @@ -0,0 +1,9 @@ +import Foundation + +public struct KeyMaterialSource: Hashable, Sendable { + public let id: String + + public init(id: String) { + self.id = id + } +} diff --git a/Sources/LocalData/Models/SecurityPolicy.swift b/Sources/LocalData/Models/SecurityPolicy.swift index 6c605e1..3801bb2 100644 --- a/Sources/LocalData/Models/SecurityPolicy.swift +++ b/Sources/LocalData/Models/SecurityPolicy.swift @@ -12,8 +12,12 @@ public enum SecurityPolicy: Sendable { public enum EncryptionPolicy: Sendable { case aes256(keyDerivation: KeyDerivation) case chacha20Poly1305(keyDerivation: KeyDerivation) + case external(source: KeyMaterialSource, keyDerivation: KeyDerivation) public static let recommended: EncryptionPolicy = .chacha20Poly1305(keyDerivation: .hkdf()) + public static func external(source: KeyMaterialSource) -> EncryptionPolicy { + .external(source: source, keyDerivation: .hkdf()) + } } public enum KeyDerivation: Sendable { diff --git a/Sources/LocalData/Protocols/KeyMaterialProviding.swift b/Sources/LocalData/Protocols/KeyMaterialProviding.swift new file mode 100644 index 0000000..ba364b7 --- /dev/null +++ b/Sources/LocalData/Protocols/KeyMaterialProviding.swift @@ -0,0 +1,5 @@ +import Foundation + +public protocol KeyMaterialProviding: Sendable { + func keyMaterial(for keyName: String) async throws -> Data +} diff --git a/Sources/LocalData/Services/EncryptionHelper.swift b/Sources/LocalData/Services/EncryptionHelper.swift index 4633a11..b80570e 100644 --- a/Sources/LocalData/Services/EncryptionHelper.swift +++ b/Sources/LocalData/Services/EncryptionHelper.swift @@ -6,18 +6,28 @@ import CryptoKit public actor EncryptionHelper { public static let shared = EncryptionHelper() - + private enum Constants { static let masterKeyService = "LocalData.MasterKey" static let masterKeyAccount = "LocalData.MasterKey" static let masterKeyLength = 32 static let defaultHKDFInfo = "LocalData.Encryption" } + + private var keyMaterialProviders: [KeyMaterialSource: any KeyMaterialProviding] = [:] private init() {} - + // MARK: - Public Interface + /// Registers a key material provider for external encryption policies. + public func registerKeyMaterialProvider( + _ provider: any KeyMaterialProviding, + for source: KeyMaterialSource + ) { + keyMaterialProviders[source] = provider + } + /// Encrypts data using AES-GCM. /// - Parameters: /// - data: The plaintext data to encrypt. @@ -57,32 +67,39 @@ public actor EncryptionHelper { keyName: String, policy: SecurityPolicy.EncryptionPolicy ) async throws -> SymmetricKey { - let keyDerivation: SecurityPolicy.KeyDerivation switch policy { case .aes256(let derivation), .chacha20Poly1305(let derivation): - keyDerivation = derivation + let masterKey = try await getMasterKey() + return try deriveKeyMaterial( + keyName: keyName, + derivation: derivation, + baseKeyMaterial: masterKey + ) + case .external(let source, let derivation): + guard let provider = keyMaterialProviders[source] else { + throw StorageError.securityApplicationFailed + } + let baseMaterial = try await provider.keyMaterial(for: keyName) + return try deriveKeyMaterial( + keyName: keyName, + derivation: derivation, + baseKeyMaterial: baseMaterial + ) } - - let masterKey = try await getMasterKey() - return try deriveKeyMaterial( - keyName: keyName, - derivation: keyDerivation, - masterKey: masterKey - ) } /// Derives key material based on the provided key derivation strategy. private func deriveKeyMaterial( keyName: String, derivation: SecurityPolicy.KeyDerivation, - masterKey: Data + baseKeyMaterial: Data ) throws -> SymmetricKey { switch derivation { case .pbkdf2(let iterations, let customSalt): let salt = customSalt ?? defaultSalt(for: keyName) let derivedKeyData = try pbkdf2SHA256( - password: masterKey, + password: baseKeyMaterial, salt: salt, iterations: iterations, keyLength: Constants.masterKeyLength @@ -91,7 +108,7 @@ public actor EncryptionHelper { case .hkdf(let customSalt, let customInfo): let salt = customSalt ?? defaultSalt(for: keyName) let info = customInfo ?? Data(Constants.defaultHKDFInfo.utf8) - let inputKey = SymmetricKey(data: masterKey) + let inputKey = SymmetricKey(data: baseKeyMaterial) return HKDF.deriveKey( inputKeyMaterial: inputKey, salt: salt, @@ -143,6 +160,8 @@ public actor EncryptionHelper { return try encryptWithAESGCM(data, using: key) case .chacha20Poly1305: return try encryptWithChaChaPoly(data, using: key) + case .external: + return try encryptWithChaChaPoly(data, using: key) } } @@ -168,6 +187,8 @@ public actor EncryptionHelper { return try decryptWithAESGCM(data, using: key) case .chacha20Poly1305: return try decryptWithChaChaPoly(data, using: key) + case .external: + return try decryptWithChaChaPoly(data, using: key) } } diff --git a/Tests/LocalDataTests/EncryptionHelperTests.swift b/Tests/LocalDataTests/EncryptionHelperTests.swift index ec28324..223d254 100644 --- a/Tests/LocalDataTests/EncryptionHelperTests.swift +++ b/Tests/LocalDataTests/EncryptionHelperTests.swift @@ -50,8 +50,40 @@ struct EncryptionHelperTests { #expect(decrypted == payload) await clearMasterKey() } + + @Test func externalProviderWithHKDFRoundTrip() async throws { + let source = KeyMaterialSource(id: "test.external") + let provider = StaticKeyMaterialProvider(material: Data(repeating: 7, count: 32)) + await EncryptionHelper.shared.registerKeyMaterialProvider(provider, for: source) + + let policy: SecurityPolicy.EncryptionPolicy = .external( + source: source, + keyDerivation: .hkdf() + ) + + let encrypted = try await EncryptionHelper.shared.encrypt( + payload, + keyName: keyName, + policy: policy + ) + let decrypted = try await EncryptionHelper.shared.decrypt( + encrypted, + keyName: keyName, + policy: policy + ) + + #expect(decrypted == payload) + } private func clearMasterKey() async { try? await KeychainHelper.shared.deleteAll(service: masterKeyService) } } + +private struct StaticKeyMaterialProvider: KeyMaterialProviding { + let material: Data + + func keyMaterial(for keyName: String) async throws -> Data { + material + } +}