Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to load encrypted private key in Node.JS

I would like to use the node Crypto:Sign module with an encrypted private key. Is there functionality within Crypto module that allows me to decrypt my private key?

For example, in Python there is an OpenSSL.crypto.load_privatekey function that takes a passphrase to decrypt the private key. I am looking to achieve the same functionality but using Node.JS libraries.

const crypto = require('crypto');
const sign = crypto.createSign('sha256');

sign.update('some data to sign');

let private_key = '-----BEGIN ENCRYPTED PRIVATE KEY-----\n' +
                    'ABCDEFGHIJKLMNOP\n' +  
                    '-----END ENCRYPTED PRIVATE KEY-----\n';

// Somehow decrypt private_key using passphrase.    
what_to_do(????);

console.log(sign.sign(private_key).toString('hex'));
like image 884
hofan41 Avatar asked May 12 '16 23:05

hofan41


1 Answers

I realize this question is old AF by now, but I came across the question while trying to solve it myself and was eventually able to discover the answer, so I will share for posterity:

I'm working on an app, trying to secure it even in the absence of an HTTPS connection. I am handling login with asymmetric encryption. Users provide their usernames, which causes a lookup on the backend that retrieves the public key for the user computed at registration. The public key is then used to encrypt the password on the client using JSEncrypt, which is then sent back to the backend to be decrypted and validated with the private key that is also computed and stored in its encrypted form at registration.

The trick is, since the encrypted private key is stored as plain-text, that it has to be reloaded into a KeyObject before it can be utilized by the crypto methods. My keys are created as follows:

CRYPTO.generateKeyPair('rsa', {
        'modulusLength': 4096,
        'publicKeyEncoding': {
            'type': 'spki',
            'format': 'pem',
        },
        'privateKeyEncoding': {
            'type': 'pkcs8',
            'format': 'pem',
            'cipher': 'aes-256-cbc',
            'passphrase': 'passphrase'
        }
    }, (err, publicKey, privateKey) => { /* store keys, return public key */ }

According to the documentation, if encoding options are provided, the function behaves as if KeyObject.export() has been called against the result, so you get the strings instead of a KeyObject in the callback. This is needed anyway to store the keys in a database, as I am doing and as I presume OP is doing since he's trying to load the key up from a string.

So now we have to change it back. This is done with CRYPTO.createPrivateKey(). My function call looks like this:

var privateKey = CRYPTO.createPrivateKey({
    'key': encodedPrivateKeyString,
    'format': 'pem',
    'type': 'pkcs8',
    'cipher': 'aes-256-cbc',
    'passphrase': 'passphrase'
});

It mirrors the initial creation of the private key and results in a KeyObject that can be used for CRYPTO.privateDecrypt() or CRYPTO.Sign.sign().

I am actually using sign immediately after generating the keys for token verification, and was able to get it to work with this:

var signToken = CRYPTO.createSign('SHA256');
signToken.write('random text to sign');
signToken.end();
var token = signToken.sign({ 'key': privateKey, 'passphrase': 'passphrase' }, 'hex');

But at this point I am inside the CRYPTO.generateKeyPair() callback so the original variables are still available, whatever their precise type is. I'm assuming that at this point privateKey is not really a string even though I am able to store it to the database like it is, but a KeyObject, or that it somehow knows or defaults the parameter options I am not providing. HOWEVER, when I come back into the backend to authenticate the user's password at login, I have to create the private key again from plain text, so when I call CRYPTO.privateDecrypt(), I had to recreate the private key KeyObject first as above and then call CRYPTO.privateDecrypt() like this:

var hash = CRYPTO.privateDecrypt({
    'key': privateKey,
    'passphrase': 'passphrase',
    'cipher': 'aes-256-cbc',
    'padding': CRYPTO.constants.RSA_PKCS1_PADDING
}, buffer).toString();

This string can then be compared to the hash stored at registration to authenticate the user. If all you're doing is the signing, you may be able to get away with not providing the additional parameters as I did, but if you're recreating the private key from a string, you might have to provide more. Alternatively, you may be able to just provide your passphrase as I did with the encoded private key as a string, and most of this explanation is unnecessary since it is dealing with decryption rather than signing.

Either way, I've documented my struggle. Hope this helps someone.

like image 167
citizenslave Avatar answered Oct 21 '22 02:10

citizenslave