Update Models, Protocols, Services + tests + docs
Summary: - Sources: Models, Protocols, Services - Tests: EncryptionHelperTests.swift - Docs: README - Added symbols: struct RemoteKeyProvider, func keyMaterial, struct KeyMaterialSource, protocol KeyMaterialProviding, func registerKeyMaterialProvider, struct StaticKeyMaterialProvider Stats: - 6 files changed, 104 insertions(+), 14 deletions(-)
This commit is contained in:
parent
12e1f3b008
commit
8f3fc8da51
19
README.md
19
README.md
@ -20,6 +20,7 @@ StorageRouter (main entry point)
|
|||||||
### Protocols
|
### Protocols
|
||||||
- **StorageKey** - Define storage configuration for each data type
|
- **StorageKey** - Define storage configuration for each data type
|
||||||
- **StorageProviding** - Abstraction for storage operations
|
- **StorageProviding** - Abstraction for storage operations
|
||||||
|
- **KeyMaterialProviding** - Supplies external key material for encryption
|
||||||
|
|
||||||
### Services (Actors)
|
### Services (Actors)
|
||||||
- **StorageRouter** - Main entry point for all storage operations
|
- **StorageRouter** - Main entry point for all storage operations
|
||||||
@ -115,6 +116,24 @@ try await StorageRouter.shared.remove(key)
|
|||||||
- Configurable PBKDF2 iteration count
|
- Configurable PBKDF2 iteration count
|
||||||
- Master key stored securely in keychain
|
- Master key stored securely in keychain
|
||||||
- Default security policy: `SecurityPolicy.recommended` (ChaCha20-Poly1305 + HKDF)
|
- 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
|
## Sync Behavior
|
||||||
|
|
||||||
|
|||||||
9
Sources/LocalData/Models/KeyMaterialSource.swift
Normal file
9
Sources/LocalData/Models/KeyMaterialSource.swift
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
public struct KeyMaterialSource: Hashable, Sendable {
|
||||||
|
public let id: String
|
||||||
|
|
||||||
|
public init(id: String) {
|
||||||
|
self.id = id
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -12,8 +12,12 @@ public enum SecurityPolicy: Sendable {
|
|||||||
public enum EncryptionPolicy: Sendable {
|
public enum EncryptionPolicy: Sendable {
|
||||||
case aes256(keyDerivation: KeyDerivation)
|
case aes256(keyDerivation: KeyDerivation)
|
||||||
case chacha20Poly1305(keyDerivation: KeyDerivation)
|
case chacha20Poly1305(keyDerivation: KeyDerivation)
|
||||||
|
case external(source: KeyMaterialSource, keyDerivation: KeyDerivation)
|
||||||
|
|
||||||
public static let recommended: EncryptionPolicy = .chacha20Poly1305(keyDerivation: .hkdf())
|
public static let recommended: EncryptionPolicy = .chacha20Poly1305(keyDerivation: .hkdf())
|
||||||
|
public static func external(source: KeyMaterialSource) -> EncryptionPolicy {
|
||||||
|
.external(source: source, keyDerivation: .hkdf())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum KeyDerivation: Sendable {
|
public enum KeyDerivation: Sendable {
|
||||||
|
|||||||
5
Sources/LocalData/Protocols/KeyMaterialProviding.swift
Normal file
5
Sources/LocalData/Protocols/KeyMaterialProviding.swift
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
public protocol KeyMaterialProviding: Sendable {
|
||||||
|
func keyMaterial(for keyName: String) async throws -> Data
|
||||||
|
}
|
||||||
@ -14,10 +14,20 @@ public actor EncryptionHelper {
|
|||||||
static let defaultHKDFInfo = "LocalData.Encryption"
|
static let defaultHKDFInfo = "LocalData.Encryption"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var keyMaterialProviders: [KeyMaterialSource: any KeyMaterialProviding] = [:]
|
||||||
|
|
||||||
private init() {}
|
private init() {}
|
||||||
|
|
||||||
// MARK: - Public Interface
|
// 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.
|
/// Encrypts data using AES-GCM.
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - data: The plaintext data to encrypt.
|
/// - data: The plaintext data to encrypt.
|
||||||
@ -57,32 +67,39 @@ public actor EncryptionHelper {
|
|||||||
keyName: String,
|
keyName: String,
|
||||||
policy: SecurityPolicy.EncryptionPolicy
|
policy: SecurityPolicy.EncryptionPolicy
|
||||||
) async throws -> SymmetricKey {
|
) async throws -> SymmetricKey {
|
||||||
let keyDerivation: SecurityPolicy.KeyDerivation
|
|
||||||
switch policy {
|
switch policy {
|
||||||
case .aes256(let derivation),
|
case .aes256(let derivation),
|
||||||
.chacha20Poly1305(let derivation):
|
.chacha20Poly1305(let derivation):
|
||||||
keyDerivation = derivation
|
|
||||||
}
|
|
||||||
|
|
||||||
let masterKey = try await getMasterKey()
|
let masterKey = try await getMasterKey()
|
||||||
return try deriveKeyMaterial(
|
return try deriveKeyMaterial(
|
||||||
keyName: keyName,
|
keyName: keyName,
|
||||||
derivation: keyDerivation,
|
derivation: derivation,
|
||||||
masterKey: masterKey
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Derives key material based on the provided key derivation strategy.
|
/// Derives key material based on the provided key derivation strategy.
|
||||||
private func deriveKeyMaterial(
|
private func deriveKeyMaterial(
|
||||||
keyName: String,
|
keyName: String,
|
||||||
derivation: SecurityPolicy.KeyDerivation,
|
derivation: SecurityPolicy.KeyDerivation,
|
||||||
masterKey: Data
|
baseKeyMaterial: Data
|
||||||
) throws -> SymmetricKey {
|
) throws -> SymmetricKey {
|
||||||
switch derivation {
|
switch derivation {
|
||||||
case .pbkdf2(let iterations, let customSalt):
|
case .pbkdf2(let iterations, let customSalt):
|
||||||
let salt = customSalt ?? defaultSalt(for: keyName)
|
let salt = customSalt ?? defaultSalt(for: keyName)
|
||||||
let derivedKeyData = try pbkdf2SHA256(
|
let derivedKeyData = try pbkdf2SHA256(
|
||||||
password: masterKey,
|
password: baseKeyMaterial,
|
||||||
salt: salt,
|
salt: salt,
|
||||||
iterations: iterations,
|
iterations: iterations,
|
||||||
keyLength: Constants.masterKeyLength
|
keyLength: Constants.masterKeyLength
|
||||||
@ -91,7 +108,7 @@ public actor EncryptionHelper {
|
|||||||
case .hkdf(let customSalt, let customInfo):
|
case .hkdf(let customSalt, let customInfo):
|
||||||
let salt = customSalt ?? defaultSalt(for: keyName)
|
let salt = customSalt ?? defaultSalt(for: keyName)
|
||||||
let info = customInfo ?? Data(Constants.defaultHKDFInfo.utf8)
|
let info = customInfo ?? Data(Constants.defaultHKDFInfo.utf8)
|
||||||
let inputKey = SymmetricKey(data: masterKey)
|
let inputKey = SymmetricKey(data: baseKeyMaterial)
|
||||||
return HKDF<SHA256>.deriveKey(
|
return HKDF<SHA256>.deriveKey(
|
||||||
inputKeyMaterial: inputKey,
|
inputKeyMaterial: inputKey,
|
||||||
salt: salt,
|
salt: salt,
|
||||||
@ -143,6 +160,8 @@ public actor EncryptionHelper {
|
|||||||
return try encryptWithAESGCM(data, using: key)
|
return try encryptWithAESGCM(data, using: key)
|
||||||
case .chacha20Poly1305:
|
case .chacha20Poly1305:
|
||||||
return try encryptWithChaChaPoly(data, using: key)
|
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)
|
return try decryptWithAESGCM(data, using: key)
|
||||||
case .chacha20Poly1305:
|
case .chacha20Poly1305:
|
||||||
return try decryptWithChaChaPoly(data, using: key)
|
return try decryptWithChaChaPoly(data, using: key)
|
||||||
|
case .external:
|
||||||
|
return try decryptWithChaChaPoly(data, using: key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -51,7 +51,39 @@ struct EncryptionHelperTests {
|
|||||||
await clearMasterKey()
|
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 {
|
private func clearMasterKey() async {
|
||||||
try? await KeychainHelper.shared.deleteAll(service: masterKeyService)
|
try? await KeychainHelper.shared.deleteAll(service: masterKeyService)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private struct StaticKeyMaterialProvider: KeyMaterialProviding {
|
||||||
|
let material: Data
|
||||||
|
|
||||||
|
func keyMaterial(for keyName: String) async throws -> Data {
|
||||||
|
material
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user