Update Models, Services + tests
Summary: - Sources: Models, Services - Tests: EncryptionHelperTests.swift - Added symbols: struct EncryptionConfiguration, func updateConfiguration, func updateEncryptionConfiguration - Removed symbols: enum Constants Stats: - 4 files changed, 93 insertions(+), 20 deletions(-)
This commit is contained in:
parent
99b3875469
commit
71ef9da223
23
Sources/LocalData/Models/EncryptionConfiguration.swift
Normal file
23
Sources/LocalData/Models/EncryptionConfiguration.swift
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// Configuration for the EncryptionHelper.
|
||||||
|
public struct EncryptionConfiguration: Sendable {
|
||||||
|
public let masterKeyService: String
|
||||||
|
public let masterKeyAccount: String
|
||||||
|
public let masterKeyLength: Int
|
||||||
|
public let defaultHKDFInfo: String
|
||||||
|
|
||||||
|
public init(
|
||||||
|
masterKeyService: String = "LocalData",
|
||||||
|
masterKeyAccount: String = "MasterKey",
|
||||||
|
masterKeyLength: Int = 32,
|
||||||
|
defaultHKDFInfo: String = "LocalData.Encryption"
|
||||||
|
) {
|
||||||
|
self.masterKeyService = masterKeyService
|
||||||
|
self.masterKeyAccount = masterKeyAccount
|
||||||
|
self.masterKeyLength = masterKeyLength
|
||||||
|
self.defaultHKDFInfo = defaultHKDFInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
public static let `default` = EncryptionConfiguration()
|
||||||
|
}
|
||||||
@ -2,21 +2,27 @@ import Foundation
|
|||||||
import CryptoKit
|
import CryptoKit
|
||||||
|
|
||||||
/// Actor that handles all encryption and decryption operations.
|
/// Actor that handles all encryption and decryption operations.
|
||||||
/// Uses AES-GCM for symmetric encryption with PBKDF2 key derivation.
|
/// Uses AES-GCM or ChaChaPoly for symmetric encryption with derived keys.
|
||||||
actor EncryptionHelper {
|
actor EncryptionHelper {
|
||||||
|
|
||||||
public static let shared = EncryptionHelper()
|
public static let shared = EncryptionHelper()
|
||||||
|
|
||||||
private enum Constants {
|
private var configuration: EncryptionConfiguration
|
||||||
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 var keyMaterialProviders: [KeyMaterialSource: any KeyMaterialProviding] = [:]
|
||||||
|
|
||||||
private init() {}
|
private init(configuration: EncryptionConfiguration = .default) {
|
||||||
|
self.configuration = configuration
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Configuration
|
||||||
|
|
||||||
|
/// Updates the configuration for the actor.
|
||||||
|
/// > [!WARNING]
|
||||||
|
/// > Changing the configuration (specifically service or account) on an existing instance
|
||||||
|
/// > will cause it to look for the master key in a new location.
|
||||||
|
public func updateConfiguration(_ configuration: EncryptionConfiguration) {
|
||||||
|
self.configuration = configuration
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Public Interface
|
// MARK: - Public Interface
|
||||||
|
|
||||||
@ -28,7 +34,7 @@ actor EncryptionHelper {
|
|||||||
keyMaterialProviders[source] = provider
|
keyMaterialProviders[source] = provider
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Encrypts data using AES-GCM.
|
/// Encrypts data using AES-GCM or ChaChaPoly.
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - data: The plaintext data to encrypt.
|
/// - data: The plaintext data to encrypt.
|
||||||
/// - keyName: A unique name used for key derivation salt.
|
/// - keyName: A unique name used for key derivation salt.
|
||||||
@ -44,7 +50,7 @@ actor EncryptionHelper {
|
|||||||
return try encryptWithKey(data, using: key, policy: policy)
|
return try encryptWithKey(data, using: key, policy: policy)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Decrypts data using AES-GCM.
|
/// Decrypts data using AES-GCM or ChaChaPoly.
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - data: The encrypted data (nonce + ciphertext + tag combined).
|
/// - data: The encrypted data (nonce + ciphertext + tag combined).
|
||||||
/// - keyName: The same unique name used during encryption.
|
/// - keyName: The same unique name used during encryption.
|
||||||
@ -102,18 +108,18 @@ actor EncryptionHelper {
|
|||||||
password: baseKeyMaterial,
|
password: baseKeyMaterial,
|
||||||
salt: salt,
|
salt: salt,
|
||||||
iterations: iterations,
|
iterations: iterations,
|
||||||
keyLength: Constants.masterKeyLength
|
keyLength: configuration.masterKeyLength
|
||||||
)
|
)
|
||||||
return SymmetricKey(data: derivedKeyData)
|
return SymmetricKey(data: derivedKeyData)
|
||||||
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(configuration.defaultHKDFInfo.utf8)
|
||||||
let inputKey = SymmetricKey(data: baseKeyMaterial)
|
let inputKey = SymmetricKey(data: baseKeyMaterial)
|
||||||
return HKDF<SHA256>.deriveKey(
|
return HKDF<SHA256>.deriveKey(
|
||||||
inputKeyMaterial: inputKey,
|
inputKeyMaterial: inputKey,
|
||||||
salt: salt,
|
salt: salt,
|
||||||
info: info,
|
info: info,
|
||||||
outputByteCount: Constants.masterKeyLength
|
outputByteCount: configuration.masterKeyLength
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -121,14 +127,14 @@ actor EncryptionHelper {
|
|||||||
/// Gets or creates the master key stored in keychain.
|
/// Gets or creates the master key stored in keychain.
|
||||||
private func getMasterKey() async throws -> Data {
|
private func getMasterKey() async throws -> Data {
|
||||||
if let existing = try await KeychainHelper.shared.get(
|
if let existing = try await KeychainHelper.shared.get(
|
||||||
service: Constants.masterKeyService,
|
service: configuration.masterKeyService,
|
||||||
key: Constants.masterKeyAccount
|
key: configuration.masterKeyAccount
|
||||||
) {
|
) {
|
||||||
return existing
|
return existing
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate new master key
|
// Generate new master key
|
||||||
var bytes = [UInt8](repeating: 0, count: Constants.masterKeyLength)
|
var bytes = [UInt8](repeating: 0, count: configuration.masterKeyLength)
|
||||||
let status = SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes)
|
let status = SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes)
|
||||||
guard status == errSecSuccess else {
|
guard status == errSecSuccess else {
|
||||||
throw StorageError.securityApplicationFailed
|
throw StorageError.securityApplicationFailed
|
||||||
@ -139,8 +145,8 @@ actor EncryptionHelper {
|
|||||||
// Store in keychain
|
// Store in keychain
|
||||||
try await KeychainHelper.shared.set(
|
try await KeychainHelper.shared.set(
|
||||||
masterKey,
|
masterKey,
|
||||||
service: Constants.masterKeyService,
|
service: configuration.masterKeyService,
|
||||||
key: Constants.masterKeyAccount,
|
key: configuration.masterKeyAccount,
|
||||||
accessibility: .afterFirstUnlock,
|
accessibility: .afterFirstUnlock,
|
||||||
accessControl: nil
|
accessControl: nil
|
||||||
)
|
)
|
||||||
|
|||||||
@ -13,6 +13,16 @@ public actor StorageRouter: StorageProviding {
|
|||||||
|
|
||||||
private init() {}
|
private init() {}
|
||||||
|
|
||||||
|
// MARK: - Configuration
|
||||||
|
|
||||||
|
/// Updates the encryption configuration.
|
||||||
|
/// > [!WARNING]
|
||||||
|
/// > Changing these constants in an existing app will cause the app to look for the master key
|
||||||
|
/// > under a new name. Previously encrypted data will be lost.
|
||||||
|
public func updateEncryptionConfiguration(_ configuration: EncryptionConfiguration) async {
|
||||||
|
await EncryptionHelper.shared.updateConfiguration(configuration)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Key Material Providers
|
// MARK: - Key Material Providers
|
||||||
|
|
||||||
/// Registers a key material provider for external encryption policies.
|
/// Registers a key material provider for external encryption policies.
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import Testing
|
|||||||
@testable import LocalData
|
@testable import LocalData
|
||||||
|
|
||||||
struct EncryptionHelperTests {
|
struct EncryptionHelperTests {
|
||||||
private let masterKeyService = "LocalData.MasterKey"
|
private let masterKeyService = "LocalData"
|
||||||
private let keyName = "LocalDataTests.encryption"
|
private let keyName = "LocalDataTests.encryption"
|
||||||
private let payload = Data("payload".utf8)
|
private let payload = Data("payload".utf8)
|
||||||
|
|
||||||
@ -51,6 +51,40 @@ struct EncryptionHelperTests {
|
|||||||
await clearMasterKey()
|
await clearMasterKey()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test func customConfigurationRoundTrip() async throws {
|
||||||
|
let customService = "Test.CustomService"
|
||||||
|
let customAccount = "Test.CustomAccount"
|
||||||
|
let config = EncryptionConfiguration(
|
||||||
|
masterKeyService: customService,
|
||||||
|
masterKeyAccount: customAccount
|
||||||
|
)
|
||||||
|
|
||||||
|
await EncryptionHelper.shared.updateConfiguration(config)
|
||||||
|
|
||||||
|
let policy: SecurityPolicy.EncryptionPolicy = .chacha20Poly1305(
|
||||||
|
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)
|
||||||
|
|
||||||
|
// Cleanup keychain
|
||||||
|
try? await KeychainHelper.shared.deleteAll(service: customService)
|
||||||
|
|
||||||
|
// Reset to default
|
||||||
|
await EncryptionHelper.shared.updateConfiguration(.default)
|
||||||
|
}
|
||||||
|
|
||||||
@Test func externalProviderWithHKDFRoundTrip() async throws {
|
@Test func externalProviderWithHKDFRoundTrip() async throws {
|
||||||
let source = KeyMaterialSource(id: "test.external")
|
let source = KeyMaterialSource(id: "test.external")
|
||||||
let provider = StaticKeyMaterialProvider(material: Data(repeating: 7, count: 32))
|
let provider = StaticKeyMaterialProvider(material: Data(repeating: 7, count: 32))
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user