This part now functions correctly, but I’m currently experiencing an authentication failure. I suspect this is because whenever I save a new entry, it attempts to generate a new cryptographic key. However, I always assumed that if a key with this identifier already exists, it might raise an error and cancel the creation of a new key. Do you have any idea what the problem could be?
Save a new entry of what? Save where?
“It” attempts? What? Generate a new cryptographic key? What cryptographic key?
Re-read the steps here: Encrypt data - #5 by Sajjon
The first step, should have an "else if not first launch of app, do not generate a new SymmetricKey
"....
I always assumed that when I tried to create a new cryptographic key with an existing identifier, it would throw an error. However, I discovered through the Keychain program on macOS that it always overwrites the old key. Is this behavior normal? Additionally, is it possible to look up keys in the Keychain on iOS using an app similar to the one on macOS?
That was the logic to save a new secret:
Button("Save") {
Task {
do {
try cryptoManager.createEncryptionKey(encryptionKeyIdentifier: "ch.romanindermuehle.iSecret.encryptionKey")
let encryptionKey = try cryptoManager.getEncryptionKey(encryptionKeyIdentifier: "ch.romanindermuehle.iSecret.encryptionKey")
guard let textData = text.data(using: .utf8) else { return }
guard let encryptedText = try? CryptoKit.AES.GCM.seal(textData, using: encryptionKey).combined! else { throw "Unable to encrypt" }
let secret = Secret(text: encryptedText.base64EncodedString())
try await supabaseManager.upsertSecret(secret)
dismiss()
} catch {
print(error)
}
}
}
But you’re right; it makes more sense to create a new key if necessary on startup.
I also considered an option to regenerate a key in the settings page. However, I anticipate that fetching all the data from Supabase and manually decrypting some individual properties in the data could become cumbersome. Do you have any suggestions on how I can dynamically decrypt all the encrypted data and then encrypt it again using the new key?
You need to show the code for the function createEncryptionKey
.
I will amend my 1
even further - try loading an existing SymmetricKey
from keychain, else generate a new one and save it.
/// Loads an existing encryption key from Keychain if it exists, else, generates a new one
/// persists it to Keychain before returning it.
func getEncryptionKey() throws -> SymmetricKey {
@Dependency(\.keychain) var keychain
let keychainKeyForEncryptionKey = "myEncryptionKey" // put me elsewhere!!
if let encryptionKey = try keychain.loadData(forKey: keychainKeyForEncryptionKey) {
return encryptionKey
}
// no key existed, generate a new one
let newKey = SymmetricKey(keySize: .bits256)
// save new key
try newKey.withUnsafeBytes {
try keychain.save(
data: Data($0),
forKey: keychainKeyForEncryptionKey
)
}
// return new key
return newKey
}
I've now created an observable class that I pass to the environment from the root of the app. Do you think this makes sense?
@Observable
class CryptoManager {
let keychain = Keychain().synchronizable(true) // `synchronizable` => sync to iCloud for all items by default
func createEncryptionKey(encryptionKeyIdentifier: String) throws {
let encryptionKey = SymmetricKey(size: .bits256)
let keyData = encryptionKey.withUnsafeBytes { Data($0) }
try keychain
.synchronizable(true) // Can set icloud sync per item also https://github.com/kishikawakatsumi/KeychainAccess?tab=readme-ov-file#one-shot-2
.set(keyData, key: encryptionKeyIdentifier)
}
func getEncryptionKey(encryptionKeyIdentifier: String) throws -> SymmetricKey {
// later when you want to access it:
guard
let encryptionKeyData = try keychain.getData(encryptionKeyIdentifier),
encryptionKeyData.count * 8 != 32 // 8 bits per byte
else { throw "Invalid encryption key data" }
return SymmetricKey(data: encryptionKeyData)
}
}
This must return SymmetricKey?
since Keychain might be empty… right?
Also you should not let those methods take an identifier… you should use a constant for it.
You are mixing your Keychain and CryptoManager. They ought two be two distinct dependencies.
CryptoManager then depends in the Keychain dependency.
What did you wanna achieve with Observable
?
Are you writing any unit tests? Feels like you are not? Do it ;) swift dependencies allows for easy testing
CryptoManager is a broad term. I would have a Keychain client (depends on KeychainAccess package)
EncryptionKey client (depends on above)
Encryption client (depends on above)
Unit test each client in isolation
Yeah, you’re right.
Why do you think it’s not a good idea to pass the identifier as a parameter to the function?
So I’m able to parse the CryptoManager to the environment, that's why I decided to use the Observable macro. Do you think this is a bad idea?
No, I’m not writing unit tests at the moment. The reason is that I’m not sure what I should test specifically.
So, you would rename the CryptoManager class to KeychainClient and then create the EncryptionKeyClient and EncryptionClient classes, which inherit from KeychainClient, right? Have I understood correctly that the EncryptionKeyClient is for fetching and saving keys, and the EncryptionClient is for sealing and opening data?
By the way, have you used an extension on the keychain for the loadData function?
I still haven’t fully understood the benefit of using Swift dependencies. What are you using them for?
And how you would handle it when the user wants to generate a new key?
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
With this solution you mock all layers, independently. You can mock keychain failing to save data, failing to load data, mock load returning nil, or hardcode it returning some specific key.
Or you can simply mock the EncryptionKey layer
Or you can simply mock the EncryptionClient layer. Hardcoding failures or specific data being decrypted.
Why would the user wanna do that? If so, user cannot decrypt the old data… but I guess you can support having multiple encryption keys, but there is really no benefit in doing so?
If the user is compromised, he has at least the option to change the key if he wishes.
Thank you very much for help.
@Sajjon, since my app’s architecture is quite simple, I’d like to minimize the use of abstractions. Is it possible to simplify this to maintain simplicity and minimalism?
No. This is what you need to do in order to encrypt data
These are just normal models right?
@DependencyClient
public struct KeychainClient {
var save: (String, Data) throws
var load: (String) throws -> Data?
// delete / update
}
@DependencyClient
public struct EncryptionKeyClient {
var get: () throws -> SymmetricKey
}
@DependencyClient
public struct EncryptionClient {
var encryptWithDetails: (_ plaintext: Data, _ nonce: Data?, _ tag: Data?) throws -> Data
var decrypt: (Data) throws -> Data
}
Unfortunately, I’m encountering numerous errors even after importing everything. Do you know what the problem might be?
What is a "normal model". Models - Data Transfer Objects (DTO)s - usually just hold data. These are clients, not models. And we use struct
because we dislike reference types.
You should probably look more into Swift-Dependencies. Some examples and documentation.
but you should probable import Dependencies
and not DependencyMacros
. Did you get asked by xcode to enabled macros? if so, did you enable? Else restart xcode.
Have a look at a simple client here sargon/examples/iOS/Backend/Sources/Planbok/Dependencies/PasteboardClient.swift at main · radixdlt/sargon · GitHub