Open scottc-WellSky opened 10 months ago
@scottc-WellSky I agree that the example could be annotated more clearly. I think you're referring to this code in the README:
If so, the text right above says that it's switching behaviour based on which browser you're using:
I believe that this example is using ECDH, which works differently from RSA+KEK (explination follows).
In RSA, you encrypt with the public key, and decrypt with the private key, but if you want to encrypt more than a handful of bytes (a limitation of public key crypto) you need to use some "key wrap" or "key-encryption key" (KEK): use the public key to encrypt a different symmetric key (e.g. AES), and encrypt the payload with the symmetric key. It's a bunch of steps, basically.
flowchart TD
subgraph rsa[RSA Encryption Envelope]
aes[AES Key]
end
subgraph aes_env[AES Key Envelope]
payload
end
gen_aes[Independently\nGenerate AES] -.-> aes
aes -->|decrypts| aes_env
private_key[Private Key] -->|decrypts| rsa
public_key[Public Key] --> |encrypts| rsa
In ECDH (which is also supported in the WebCrypto API), you derive a shared secret (the AES key) directly from your private key and their public key — no extra step required. The recipient does the reverse (their private key and your public key) to get the same secret (it's kind of magic). This mechanism underlies a bunch of modern crypto protocols such as TLS, Signal, and MLS. Since you derive the symetric key directly with a well-known, deterministic mechanism, the APIs can do a lot more of the work for you and it's fewer steps in your code.
flowchart TD
subgraph aes_env[Shared AES Envelope]
Payload
end
aes
subgraph bob[Bob]
alice_pk[Alice\nPublic Key]
bob_sk[Bob\nPrivate Key]
b_merge
alice_pk --> b_merge((ECDH\nmerge))
bob_sk --> b_merge
end
subgraph alice[Alice]
alice_sk[Alice\nPrivate Key]
bob_pk[Bob\nPublic Key]
a_merge
bob_pk --> a_merge((ECDH\nmerge))
alice_sk --> a_merge
end
a_merge -.-> aes[Shared AES]
b_merge -.-> aes
aes -->|decrypts| aes_env
In the ECDH version ☝️, it doesn't really which is the encryptor and which is the decryptor... they automagically get a shared symmetric key that's special to the two of them. I've only included Bob above (presumably the recipient) to show this symmetry.
We should make this clearer (with some more comments), because I also had to trace though the code to understand what was happening in the example.
@matheus23 I should probably tag you in to make sure that I'm not missing anything. I never actually worked on this repo 😛
Excellent explanation @expede!
Given that ECC and RCA have different flows and keystore-idb automatically falls back to RSA when ECC not available, are there any gotchas to watch out for so my code doesn't break? One failure I found was changing the sample so that ks2 used RSA while ks1 continued to use ECC, it failed on the ks2.verify and ks1.encrypt (as expected). Are there things I should do in my code to help ensure I don't cause errors with an ECC/RSA mismatch?
I'm just trying to protect offline data where all encrypt/decrypt are done in a single user's browser so I think I'm safe just coding to use RSA with a single key store for now, but would like to better understand possible issues between RSA and ECC so I'm prepared to use this library with other scenarios.
Can someone help me understand the example? It's not clear to me how you're able to decrypt using a key pair that is different from what was used to encrypt it. I've ran the example and can see that each key is actually a key pair with a public and private key. I would expect the encrypt and decrypt to use the same key pair, where the encrypt uses the public key and the decrypt uses the private key. This would seem more aligned with the description of asymmetric encryption.
const cipher = await ks1.encrypt(msg, exchangeKey2)
const decipher = await ks2.decrypt(cipher, exchangeKey1)
I'm sure I'm missing something, but can't pinpoint what it is yet.