apple / swift-crypto

Open-source implementation of a substantial portion of the API of Apple CryptoKit suitable for use on Linux platforms.
https://apple.github.io/swift-crypto
Apache License 2.0
1.47k stars 166 forks source link

How to re-access `SecureEnclave.P256.Signing.PrivateKey` that was created earlier? Proposal for missing init! #109

Closed Sajjon closed 2 years ago

Sajjon commented 2 years ago

This question should probably go on Apple Developer Forum, however, I think users of this lib might find it useful and I also have an improvment proposal for CryptoKit, which might affect swift-crypto?

If I create a new SecureEnclave.P256.Signing.PrivateKey using this initializer (e.g. using default SecAccessControl afterFirstUnlockThisDeviceOnly) -- let's call this newly created private key Key0 -- it is my understanding that (assuming the iPhone supports it) Key0 lives in the secure memory of the Secure Enclave. And any sign operations is performed in the Secure Enclave, meaning that this is a true hardware wallet, i.e. the private key Key0 never ever leaves the memory of the secure enclave, is that correct?

That raises the question, how do I re-access Key0 upon restart of the app? It looks like I need to use the initializer dataRepresentation: Data (and possibly with the optional LAContext), but then I need to pass in the Private Key as data! Which requires the user to have exported the private key, which means that the private key has left the secure enclave!!! I.e. we are forced to store the private key in keychain, which items are encrypted with an encryption key that lives in the secure enclave if I have understood it correctly?

So I guess the ""only"" (still a big one) win of SecureEnclave.P256.Signing.PrivateKey vs non-secure enclave variant P256.Signing.PrivateKey is that once initialized, it lives inside the memory of the secure enclave, but we still need to go through unsafe realm of memory outside of secure enclave to get there, for the path of re-accessing an earlier created key, e.g. Key0??

If my understanding is correct, then here comes a question and a possible improvement proposal!

Isn't Key0 persisted (between iPhone/device restarts)? Because Secure Enclave holds my credit cards (if any) for Apple Pay etc, right? So it must support persistence! So it feels like we are missing an initializer for SecureEnclave.P256.Signing.PrivateKey! The one where we want to get a handle to it by matching it against its public key! So the flow and code are:

  1. User uses App "CoolCryptoApp" for the first time, the app creates a brand new SecureEnclave.P256.Signing.PrivateKey, let us once again call itKey0, the app CoolCryptApp persists the publicKey of the private key, and it saves this public key in keychain (or possibly UserDefaults, since it is a public key)
  2. When user restarts her phone and re-opens CoolCryptoApp, the app re-access Key0 by use of a new static func (or iniitalizer) SecureEnclave.P256.Signing.PrivateKey.restore(matching: P256.Signing.PublicKey) -> SecureEnclave.P256.Signing.PrivateKey? which either is returning an Optional (or failing init) or throws.

That way we can re-access Key0 without the private key ever leaving the secure enclave!

Am I missing something?

Cheers!

Sajjon commented 2 years ago

We could even make it easier to restore the SecureEnclave.P256.Signing.PrivateKey by use of a new type KeyRestorationID, being based on the HASH of the PublicKey, this way we can let developers store the KeyRestorationID in UserDefaults. We could even make it a CHECKSUMMED HASH! Similar to schemes like bech32m can be used for that, it is basically: Human Readable Prefix (optional) || HASH(publicKey) || CHECKSUM, the advantage of using a checksummed type is we developers can display an error to the user if she ever is involved with KeyRestorationID, or if the app incorrectly stored the wrong KeyRestorationID, e,g, the developer accident stored a substring of it, dropping a char in the beginning of the end, the initiliazer of KeyRestorationID can validate that is is a checksummed (and thus probably correct) id!

FredericJacobs commented 2 years ago

As you rightly guessed, the best place to ask this question is the Developer Forum.

Sajjon commented 2 years ago

@FredericJacobs where questions seldomly gets answered...

@Lukasa might appreciate my proposed API improvement above, so please don't just ignore this 🙏

Lukasa commented 2 years ago

FWIW, you are missing something. An important note here is that the SecureEnclave keys do not have an x963Representation or similar, only a dataRepresentation. Nothing says that this representation represents the unencrypted private key.

Sajjon commented 2 years ago

FWIW, you are missing something. An important note here is that the SecureEnclave keys do not have an x963Representation or similar, only a dataRepresentation. Nothing says that this representation represents the unencrypted private key.

Perfect! Thank you! So dataRepresentation property is an encrypted form of the private key!

Then why is this not documented? Why not name the property and the label of the matching initializer encryptedData? This would change documentation from impossible to understand to clear as day!

Would also be good to add that a developer safely can store this encryptedData in e.g. UserDefaults.

Cheers! 🥳

FredericJacobs commented 2 years ago

@Sajjon : See sample project (https://developer.apple.com/documentation/cryptokit/storing_cryptokit_keys_in_the_keychain)

Keys that you store in the Secure Enclave expose a raw representation as well, but in this case the data isn’t the raw key. Instead, the Secure Enclave exports an encrypted block that only the same Secure Enclave can later use to restore the key. You can adopt the same convertibility protocol to store the Secure Enclave’s encrypted data in the keychain as a generic password, and later allow the Secure Enclave to reconstruct the key on the same device:


extension SecureEnclave.P256.Signing.PrivateKey: GenericPasswordConvertible {
init<D>(rawRepresentation data: D) throws where D: ContiguousBytes {
try self.init(dataRepresentation: data.dataRepresentation)
}
var rawRepresentation: Data {
    return dataRepresentation  // Contiguous bytes repackaged as a Data instance.
}

}

Sajjon commented 2 years ago

@FredericJacobs nice, thanks a lot, this is very helpful indeed! But still, dont you think it would be good to update the documentation? And make it clear in the documentation that it is an encrypted block of data?

I had completely missed that sample code repo... and I think it a very bad idea if APIs rely on sample projects to be easy to understand, documentation really should specify this. Don't you agree? 😄

Sajjon commented 1 year ago

@Lukasa Is there any special reason why we cannot import of already created P256.Signing.PrivateKey into Secure Enclave? Would you ever be open to adding support for that? The use case is the Secure Enclave does not support hierarchical deterministic derivation. So for P256.Signing.PrivateKeys that are derived with SLIP10, these cannot be imported by Secure Enclave.

The idea here is that it would be safer to use Secure Enclave for signing, than signing in our iOS apps memory.

Which, at the end of the day, makes our iPhone users and Apple users safer :)

Lukasa commented 1 year ago

That's a great question that sadly I'm not qualified to answer. I've passed the feedback request on to the relevant internal team, who are taking it into consideration. I also recommend filing a request via Feedback Assistant.