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
|
||||
- **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
|
||||
|
||||
|
||||
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 {
|
||||
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 {
|
||||
|
||||
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
|
||||
}
|
||||
@ -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<SHA256>.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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user