javascripttypescriptrsaencryption-asymmetricdeterministic

How to generate safe RSA keys deterministically using a seed?


How do we use a mnemonic list of words as a seed (like we have been become accustomed using cryptocurrency wallets) to be able to recover a private key in case it gets lost, accidentally erased, or stuck on a broken device?

This could be useful for e2e encryption between clients: the keys are supposed to be generated on the client, only the public key will be shared with the server.

Users could be regenerating the keys offline when needed, as long as they are able to provide the mnemonic again, obviously to be stored safely and offline.

The mnemonic seed should be long enough to provide a safe amount of entropy.

Some Q&A appear to be very outdated: how can we achieve this in Javascript/Typescript, possibly using maintained libraries?


Solution

  • Solution: bip39 and node-forge

    To quote this answer which has guided me to achieve this solution:

    in this scenario, the resulting public key is, by nature, public, and thus can serve for offline dictionary attacks. The attacker just has to try possible passwords until he finds the same public key. That's intrinsic to what you want to achieve.

    We can assume that 128 bits of entropy should be enough for preventing this kind of attacks, in the foreseeable future, however we can fortunately decide how strong our mnemonic will be.

    1. Generate mnemonic

    First of all we can generate a mnemonic using the bip-39 JS implementation.

    import { generateMnemonic } from "bip39";
    
    const mnemonic = generateMnemonic(256) // 256 to be on the _really safe_ side. Default is 128 bit.
    
    console.log(mnemonic) // prints 24 words
    

    2. Create deterministic PRNG function

    Now we can use node-forge to generate our keys. The pki.rsa.generateKeyPair function accepts a pseudo-random number generator function in input. The goal is getting this function to NOT compute a pseudo-random number (this would not be deterministic anymore), but rather return a value computed from the mnemonic.

    import { mnemonicToSeed } from "bip39";
    import { pki, random } from "node-forge";
    
    const seed = (await mnemonicToSeed(mnemonic)).toString('hex')
    
    const prng = random.createInstance();
    prng.seedFileSync = () => seed
    

    3. Generating keypair

    We can now feed the generateKeyPair function with our "rigged" prng:

    const { privateKey, publicKey } = pki.rsa.generateKeyPair({ bits: 4096, prng, workers: 2 })
    

    Et voilà!

    We now have safe and deterministic RSA keys, directly generated on the client and restorable with the same mnemonic as a input. Please consider the risks involved using deterministic keys and make sure your users will NOT store the mnemonic online or anywhere else on their client (generally, it is suggested to write it down on paper and store it somewhere safe).