Encrypt data

Currently, I’m building an app where I use Supabase to store my data. However, I have some sensitive data that I’d like to protect. So, my idea is to generate a RSA key, save it in the keychain, sync it with iCloud, and then encrypt the data.

Does anyone know an easy and simple solution to protect the data?

You probably want AES.GCM?

I then store the Symmetric encryption key in Keychain?

But we would really benefit from knowing more about the parties - if plural.

Is it the user - trough the app - who is encrypting data and then later decrypting the data - with cloud as a data store ?

Or is it some other party then end user who is going to decrypt the data? If so you need some kind of IES, maybe ECIES over Curve25519 - that is a key agreement + encryption

1 Like

Thank you for your response.

The entire security aspect is quite new to me, and I’m also a bit apprehensive about making mistakes.

Currently, I’m developing a mental health app. Before, I used SwiftData. However, when I attempted to integrate CloudKit, the performance significantly degraded. In general, I believe SwiftData is still too much in Beta to be used. Therefore, I decided to switch to Supabase. My primary objective is to ensure the best possible privacy and security. I intend to encrypt only the plain text inputs, rather than all the data models.

My idea was to give the user the ability to encrypt and decrypt their data. To achieve this, I intended to utilize the keychain and synchronize the key with iCloud if the user opts for an alternative device. I had already reviewed the Apple documentation to understand the process, but unfortunately, most of the provided examples didn’t work for me.

Could you recommend some resources for a beginner in this field? I would like to avoid over-engineering my solution.

Ok so it sounds like end user Alice downloads you app. You , Roman, as an app developer wants to store Alices data in a centralized party, Supabase, cloud storage. You are concerned with the privacy of Alices data, since it is senstive, thus, do don't want to store her sensitive data as plaintext in Supabase. Thus you want to develop your app so that Alices data is encrypted in your app, before your app sends it to Supabase. If Alice wants to view her past data, she can do so, by letting your app query Supabase, and once fetched in your app, it will decrypt Alices sensitive data and display it to her.

Is that somewhat accurate?

1 Like

If so, it is very easy:

  1. Upon app first launch you generate a Symmetric encryption key
  2. Store the Symmetric encryption key in Keychain - it is just 32 bytes, so you can save it as a data entry - with iCloud sync enabled - will only work if user has enabled keychain icloud sync in iOS
  3. Gather health data from end user Alice
  4. Encrypt Alices sensitive data using AES.GCM I linked to above
  5. Use encrypted data (send to Supabase)
  6. Later download data from Supabase, decrypt it by:
  7. … loading the Symmetric encryption key from Keychain
  8. And call AES.GCM.open using the encrypted data and the key
  9. Optionally you can allow user to manually export her encryption key
1 Like

Yeah, that's pretty accurate.

Thank you so much for your valuable tips.

I have a question. How can I be certain that it only generates one key pair?

Also, would you recommend changing the key after a few months?

32 byte key with 256bit AES, no you dont need to worry about that. There are more keys then there are atoms in the entire universe.

Generate once and use forever.

But how you generate is ofc important. Must be a Cryptographically Secure Pseudo Random Number Generator (CSPRNG). You need only use this init, and specify 256bits

1 Like

Nice, so that makes it easier.

I found these two pages from Apple Doc about generating and storing keys, do you think they are good?

I’ve also saw that they recommend using the Secure Enclave to make it even more secure. But I’m not sure if it’s then still compatible with iCloud sync. Do you think this is need?

I would advice you to ignore all those articles. They are more confusing than not.

I use KeychainAccess package:

let keychain = Keychain(...).synchronizable(true) // `synchronizable` => sync to iCloud for all items by default

let encryptionKey = SymmetricKey(size: .bits256)
let encryptionKeyIdentifierInKeychain = "my.healt.app.encryption.key"
let keyData = encryptionKey.withUnsafeBytes { Data($0) }
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: encryptionKeyIdentifierInKeychain)



/// later when you want to access it:
guard 
    let encryptionKeyData = try keychain[encryptionKeyIdentifierInKeychain],
    encryptionKeyData.count * 8 != SymmetricKeySize.bits256 // 8 bits per byte
else { // throw if nil or wrong size... }
let encryptionKey = SymmetricKey(data: encryptionKeyData)

That is is.

All those APIs about specific KEYS in Keychain will just confuse things. That is more about doing Elliptic Curve Cryptography - i.e. asymmetric keys that you wanna store in KeyChain, maybe in Secure Enclave and use for EC signing. Which is not related to encryption.

If you take a look at the SecureEnclave namespace: SecureEnclave | Apple Developer Documentation you will see that it only relates to asymmetric / public-key / elliptic curve cryptograhy.

If you want to make the app "even safer" you can do two things:

  • Not sync the SymmetricKey to Keychain iCloud
  • Chose to store the SymmetricKey only on the user device using kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly - which you can configure using KeychainAccess package. The implications of this is that if user removes her passcode => encryption key gets deleted => user can cannot decrypt her previous data. Also if user changes phone => user cannot decrypt her previous data.
1 Like

Thank you very much for your example.

I noticed that the last commit was made a year ago. So, is it still safe to use?

I think it would be better to leave that feature with the Secure Enclave. It probably wouldn’t be possible to sync it with iCloud.

I’m facing a tough decision between security and availability. I believe I need to make a trade-off. In this case, I prioritize that users can access my app across devices and that their key won’t be lost suddenly. I’m curious about Apple’s approach to this issue with the Apple Health app.

Or, how do you perceive the situation?

When I initialize the key, do I need to check if it’s already stored in iCloud? Additionally, do I need to enable the iCloud capability for the app?

We use Keychain Access in the Radix Wallet, it is a very popular package, it has not been updated in a while because the Keychain APIs does not really change. So yes I consider it safe to use. You can also take a look at Valet, but I don't like that they want us to use their identifier, does not really mean anything, but feels more... "boxed in".

I think it would be better to leave that feature with the Secure Enclave

What do you mean? which feature? You cannot use Secure Enclave. It does not support symmetric encryption.

A common misconception is that not using Secure Enclave is not safe... that is not true, Apples "root Keychain key" itself is stored in Secure Enclave, so whatever you store in ""just"" Keychain, is safe!

It probably wouldn’t be possible to sync it with iCloud.

Correct, that is not possible - yet again, not relevant for you and your needs - but an Elliptic Curve Cryptography private key, e.g. on curve P256 store in Secure Enclave cannot leave the Secure Enclave, that is the whole point! The Secure Enclave is a HSM: hardware security module, much like e.g. Ledger Hardware wallets, and Elliptic Curve Cryptography signing happens inside of the HSM. But again, this is irrelevant for you.

I’m facing a tough decision between security and availability. I believe I need to make a trade-off.

Go with availability is my recommendation.
kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly is typically for crypto wallets, where we protect a user entire life savings... I think the frustration of a user not being able to access her previous (health) data would have a bigger negative impact.

do I need to enable the iCloud capability for the app?

(reversed the order of your two last questions...)
No, this is NOT iCloud container. This is Keychain with Apples own extra feature of gratis also being able to sync it to iCloud. If and only if end user has enabled that config in iOS settings!!!.

When I initialize the key, do I need to check if it’s already stored in iCloud

No. I think you would benefit from thinking about Keychain (possibly with iCloud sync, once again, not iCloud container) as UserDefault, but safe, and if end user has enable config of Keychain iCloud sync and if you set the key-value item as synchronizable(true) it will be synced to iCloud.

Just generate a SymmetricKey, save it to Keychain. Done. Use it.

1 Like

Thank you for your in-depth explanation.

Sorry if I misspoke. I'll just leave Secure Enclave out of my app. When I read the text from Apple docs that it's a potential risk not to use Secure Enclave, I thought I had to use it. But in this case it was a misunderstanding. Thanks for the clarification.

Keeping a private key in a keychain is a great way to secure it. The key data is encrypted on disk and accessible only to your app or the apps you authorize. However, to use the key, you must briefly copy a plain-text version of it into system memory. While this presents a reasonably small attack surface, there’s still the chance that if your app is compromised, the key could also become compromised. As an added layer of protection, you can protect a private key using the Secure Enclave.

I have now tried to implement it in my demo app. Do you think it's necessary to put it in a func or separate struct? Unfortunately it didn't work. Do you have any recommendations for me?

Ah just assert that count is 32. It is ok to hard code that

Would you outsource these API accesses into a func or separate struct?

What do you think what the error is here?

Ah you are using the subscript reader on the keychain which returns String, use try keychain.getData(...) instead.

Yes ofc you should write somekind of dependency for this. You probably wanna use GitHub - pointfreeco/swift-dependencies: A dependency management library inspired by SwiftUI's "environment."

At the moment, I’m attempting to implement the entire encryption and deception logic into my demo application. Regrettably, I haven’t been able to make it work. I consistently encounter an error while encrypting, but I’m at a loss as to why. Currently, it appears that I need to convert the „Data“ obtained after encryption back to a String before uploading it to Supabase. Alternatively, do you have any insights into whether it’s possible to upload the data directly from the encryption process to Supabase?

Here’s my code for encryption and decryption:

   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? String(data: CryptoKit.AES.GCM.seal(textData, using: encryptionKey).ciphertext, encoding: .utf8) else { throw "Unable to encrypt" // I always get an error here }
                            
                            let secret = Secret(text: encryptedText)
                            
                            try await supabaseManager.upsertSecret(secret)
                            
                            dismiss()
                        } catch {
                            print(error)
                        }
                    }
do {
                    secrets = try await supabaseManager.getSecrets().map { secret in
                        let encryptionKey = try cryptoManager.getEncryptionKey(encryptionKeyIdentifier: "ch.romanindermuehle.iSecret.encryptionKey")
                        
                        let encryptedText = try CryptoKit.AES.GCM.SealedBox(combined: Data(secret.text.utf8))
                        
                        let decreptedText = try CryptoKit.AES.GCM.open(encryptedText, using: encryptionKey)
                        
                        return Secret(id: secret.id, text: String(decoding: decreptedText, as: UTF8.self), createdAt: secret.createdAt)
                    }
                    
                    
                } catch {
                    print(error)
                }

This won’t work; the result of GCM.seal() is not valid UTF8 data. You want to take the data that comes out and work with that directly. That’s the ideal option.

let encryptedData = CryptoKit.AES.GCM.seal(textData, using: encryptionKey).ciphertext
// send encryptedData to the server

If you NEED to convert it to a string, you’d need to use an encoding that can handle arbitrary data, like hexadecimal or base64.

let base64String = encryptedData.base64EncodedStrinf()
1 Like

Thank you for your suggestion.

I’ve tried it out, but unfortunately, I’m encountering another error. It says that the parameter size is incorrect when I try to open the SealedBox. Do you have any idea why it’s throwing this error?

secrets = try await supabaseManager.getSecrets().map { secret in
                        let encryptionKey = try cryptoManager.getEncryptionKey(encryptionKeyIdentifier: "ch.romanindermuehle.iSecret.encryptionKey")
                        
                        let encryptedText = try CryptoKit.AES.GCM.SealedBox(combined: Data(base64Encoded: secret.text)!)
                        
                        let decreptedText = try CryptoKit.AES.GCM.open(encryptedText, using: encryptionKey)
                        
                        return Secret(id: secret.id, text: String(data: decreptedText, encoding: .utf8) ?? "", createdAt: secret.createdAt)
                    }
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).ciphertext else { throw "Unable to encrypt" }
                            
                            let secret = Secret(text: encryptedText.base64EncodedString())
                            
                            try await supabaseManager.upsertSecret(secret)

You need to read out combined and not the ciphertext from the SealedBox:

1 Like