Because it gives zero value and is just error prone?
Why do you think this is better?
let key = try encryptionKeyClient.get(identifier: "if you make a typo here no key is found")
vs
let key = try encryptionKeyClient.get()
?
The first alternative has no advantage at all over the second. And is just bad code.
At LEAST what you wanna do is:
extension String {
static let encryptionKeyIdentifier = "my.key"
}
and then:
let key = try encryptionKeyClient.get(identifier: .encryptionKeyIdentifier)
But it still gives you no advantage what so ever over
let key = try encryptionKeyClient.get()
The notion of an identifier - or rather "persistence key" is something a KeychainClient should concern itself with. Not the EncryptionKeyClient. So you are conflating things a bit.
No, not at all. My best beginners advice for you is: Never use inheritance, ever. In fact, never use class ever. You get side effects very easily which makes code hard to follow, understand and reason with.And do use swift dependencies for "managers", it makes it easy to mock and test.
So instead of inheritance - is a - use composition - has a.
The KeychainClient is a testable wrapper around keychain, essentially CRUD (Create, Read, Update, Delete) operations on Data for some persistence key.
The EncryptionKeyClient has a KeychainClient (depends on), which you use like this with swift dependencies:
import DependenciesMacros
@DependencyClient
public struct KeychainClient {
var save: (String, Data) throws
var load: (String) throws -> Data?
// delete / update
}
@DependencyClient
public struct EncryptionKeyClient {
var get: () throws -> SymmetricKey
}
extension EncryptionKeyClient {
public let liveValue = Self(
get: {
@Dependency(KeychainClient.self) var keychainClient
let persistenceKey = "my.encryption.key"
if let data = try keychainClient.load(persistenceKey) {
guard data.count == 32 else { throw Error.invalidLength }
return SymmetricKey(data: data)
} else {
let newKey = SymmetricKey(size: .bits256)
let keyData = newKey.withUnsafeBytes { Data($0 }
try keychainClient.save(persistenceKey, keyData)
return newKey
}
}
)
}
@DependencyClient
public struct EncryptionClient {
var encryptWithDetails: (_ plaintext: Data, _ nonce: Data?, _ tag: Data?) throws -> Data
var decrypt: (Data) throws -> Data
}
extension EncryptionClient {
public func encrypt(data: Data) throws -> Data {
try self.encryptWithDetails(data, nil, nil)
}
public let liveValue: Self = {
@Dependency(EncryptionKeyClient) var encryptionKeyClient
return Self(
encryptWithDetails: { (plaintext, nonce, tag) in
if let nonce {
// https://github.com/apple/swift-crypto/blob/a84771015fc5e2823946e83fb3db80c597c432ec/Sources/Crypto/AEADs/AES/GCM/AES-GCM.swift#L30-L31
precondition(nonce.count, 12)
}
if let tag {
// https://github.com/apple/swift-crypto/blob/a84771015fc5e2823946e83fb3db80c597c432ec/Sources/Crypto/AEADs/AES/GCM/AES-GCM.swift#L30-L31
precondition(tag, 16)
}
let key = try encryptionKeyClient.get()
let sealedBox = try AES.GCM.seal(plaintext, using: key, nonce : nonce, authenticating: tag)
sealedBox.combined! // safe to force unwrap since we validated length of nonce and tag
},
decrypt: {
let sealedBox = try AES.GCM.SealedBox(combined: $0)
let key = try encryptionKeyClient.get()
try AES.GCM.open(sealedBox, using: key)
},
)
}()
}
This is fully testable! look into documentation of swift dependencies
And in your ViewModel/View(or Reducer if you are using TCA you will simply put
@Dependency(EncryptionClient.self) var encryptionClient
try encryptionClient.decrypt(myEncryptedData)
Put probably you wanna write some HealthDataClient dependency which has a dependency on EncryptionClient and a Supabase client