fission-codes / keystore-idb

In-browser key management with IndexedDB and the Web Crypto API
Apache License 2.0
56 stars 8 forks source link

Question: Explain Example #73

Open scottc-WellSky opened 10 months ago

scottc-WellSky commented 10 months ago

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.

expede commented 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:

https://github.com/fission-codes/keystore-idb/blob/0bbb92aeba7a33a5372bd2ef64dce1c3ee1f7213/README.md?plain=1#L44-L79

If so, the text right above says that it's switching behaviour based on which browser you're using:

https://github.com/fission-codes/keystore-idb/blob/0bbb92aeba7a33a5372bd2ef64dce1c3ee1f7213/README.md?plain=1#L40

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.

expede commented 10 months ago

@matheus23 I should probably tag you in to make sure that I'm not missing anything. I never actually worked on this repo 😛

scottc-WellSky commented 10 months ago

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.