How to do 1-of-X or Y-of-X public key based encrypt/ decrypt in NodeJs?

Viewed 180

I would like to be able to encrypt data using public keys, and decrypt the encrypted data using private keys.

Encryption essentially needs to accept inputs:

  1. Clear data to be encrypted
  2. A list of several public keys
  3. The minimum number of private keys corresponding to those public keys that are needed to decrypt the encrypted

How can this be done in NodeJs?


Scenarios

By way of concrete scenarios, where there are 5 users (A - E) with crypto key pairs in the system.

A 1-of-X scenario:

  • encrypted = crypto_encrypt(clearText, [A.publicKey, B.publicKey], 1) (1-of-2)
  • decrypted = crypto_decrypt(encrypted, [A.privateKey])
    • success: decrypted === clearText
    • because A.publicKey was used in encryption
  • decrypted = crypto_decrypt(encrypted, [C.privateKey])
    • failure: unable to decrypt
    • because C.publicKey was not used in encryption

A Y-of-X scenario:

  • encrypted = crypto_encrypt(clearText, [A.publicKey, B.publicKey, C.publicKey], 2) (2-of-3)
  • decrypted = crypto_decrypt(encrypted, [A.privateKey, C.privateKey])
    • success: decrypted === clearText
    • because both A.publicKey and C.publicKey was used in encryption
  • decrypted = crypto_decrypt(encrypted, [C.privateKey, E.privateKey])
    • failure: unable to decrypt
    • because while C.publicKey was used in encryption, E.publicKey was not

Ideally...

  • At minimum I need to be able to support the 1-of-X scenario, but if Y-of-X is also possible, that would be better
  • What the actual key pairs are is not so important here, could be RSA, could be any of the elliptic curves. If the method supports a number of different ones, and allows one to pick, that would be better
  • Preferably not tied to the use of any particular toolset or framework
2 Answers

PGP can do this.

Specifically for node, openpgpjs has a section in the README - https://github.com/openpgpjs/openpgpjs#encrypt-and-decrypt-string-data-with-pgp-keys - which could be condensed into:

    const encryptedText = await openpgp.encrypt({ message: clearText, publicKeys });
    const decryptedText = await openpgp.decrypt({ message: encryptedText, privateKeys });

However:

  • for number of keys required to decrypt, it only supports the 1 of many scenario, not the more general some of many scenario you'd ideally want
  • supports both RSA and many elliptic curve based keys, but the key format is designed for use by PGP, as the name of the library implies (so it is specific to the PGP toolchain)

As noted by Luke Joshua Park in the comments, this sounds like a textbook use case for a secret sharing scheme. Specifically, I would recommend that you:

  1. Generate a random AES (or other symmetric cipher) key. Make sure to use a cryptographically secure RNG (such as Crypto.randomBytes()) for this, since an attacker who can guess this key can also break the entire scheme!
  2. Encrypt the data with this key, using an authenticated encryption mode such as AES-SIV (as provided e.g. by miscreant).
  3. Split the AES key into multiple shares using Shamir's secret sharing scheme with the desired reconstruction threshold. (Some JS implementations I found with a quick Google search include secrets.js, jsss and ThresholdJS.)
  4. Encrypt each share using a different user's public key.
  5. Send each user their encrypted share and a copy of the AES-encrypted data.

Disclaimer: I have not reviewed the security or correctness of any of the APIs or libraries linked above. The cryptographic techniques they claim to use appear to be sound and suitable for this task, but I cannot guarantee that they have been implemented safely and correctly. Caveat emptor.

To decrypt the data, each user can first decrypt their share of the AES key using their private key, and a sufficient number of the decrypted shares can then be combined (using the same implementation of Shamir's secret sharing as used to create them) to reconstruct the original AES key, which can then be used to decrypt (and verify the integrity of) the data.

Note that Shamir's secret sharing implicitly assumes that the users who combine their shares to reconstruct the secret will trust each other and not lie about their shares or otherwise misbehave. If that's not necessarily true, there are various ways for a malicious user to trick the others — perhaps most simply by waiting for everyone else to reveal their share to them and then refusing to reveal their own share to the others. In general, preventing such attacks is all but impossible without the help of some kind of a mutually trusted party.

At the very least, though, using an encryption mode like AES-SIV with built-in authentication should ensure that users will detect if the reconstructed AES key is incorrect, since the decryption will then fail. If you want to be extra sure of this, you may wish to also send each of the users a secure cryptographic hash (e.g. SHA-512) of the AES key, so that they can verify its correctness before attempting decryption.

Related