Search code examples
node.jscryptographynode-cryptoecdh

How to convert ECDH keys to PEM format with NodeJS crypto module


I want to have only one pair of keys that I can use for ECDH functions and for other function in node:crypto module.

I know there are two ways how to generate keys with node:crypto module.

One way is to use crypto.generateKeyPairSync. This generates keys in format that is accepted by almost all cryptographic functions in node:crypto module:

const crypto = require('node:crypto')
const {publicKey, privateKey} = crypto.generateKeyPairSync('ec', {
    namedCurve: 'secp224r1'
})

const pubKey = publicKey.export({type: 'spki', format: 'pem'}).toString()
const privKey = privateKey.export({type: 'pkcs8', format: 'pem'}).toString()

This outputs keys in pem format.

But if I want to use ECDH I need to generate keys in the following way:

const crypto = require('node:crypto')
const ecdh = crypto.createECDH('secp224r1')
ecdh.generateKeys()

const rawPublic = ecdh.getPublicKey('base64', 'uncompressed')
const rawPrivate = ecdh.getPrivateKey('base64')

Which generates just the raw keys.

How do I generate PEM from raw key or raw key from PEM so I can use only one set of keys instead of generating new set of keys for ECDH?


Solution

  • The crypto module does not directly support the conversion ASN.1/DER <-> raw. A third party library that supports this is e.g. eckey-utils.

    The conversion from raw to PEM key is possible e.g. as follows.

    const privKey = Buffer.from('765573f9676d39f1256d01f1fb2806d30bbfaab8b04ae745d0a77c03', 'hex');
    const pubKey = Buffer.from('04468a685192db85873baa45dbec2bcc8217f5291e09e1b581c7f27f3f5585dc535a13e1862563aeb99de167a49557f1a2d49fee67af017eba', 'hex'); // uncompressed
    
    const ecKeyUtils = require('eckey-utils');
    const curveName = 'secp224r1';
    const pems = ecKeyUtils.generatePem({curveName, privateKey: privKey, publicKey: pubKey});
    const x509Pem = pems.publicKey;
    const sec1Pem = pems.privateKey;
    

    Thereby the private key is exported in SEC1 format. If the PKCS#8 format is needed, a conversion with the crypto module is possible:

    const crypto = require('crypto')
    const pkcs8PemFromSec1 = crypto.createPrivateKey({key: sec1Pem, format: 'pem', type: 'sec1'}).export({type: 'pkcs8', format: 'pem'}).toString();
    

    The reverse is:

    const privKey = ecKeyUtils.parsePem(sec1Pem).privateKey;
    const pubKeyFromPriv = ecKeyUtils.parsePem(sec1Pem).publicKey;
    const pubKey = ecKeyUtils.parsePem(x509Pem).publicKey;
    

    If the private key is in PKCS#8 format, it must be converted to SEC1 format beforehand:

    const crypto = require('crypto');
    const sec1PemFromPkcs8 = crypto.createPrivateKey({key: pkcs8Pem, format: 'pem', type: 'pkcs8'}).export({type: 'sec1', format: 'pem'});
    

    Note that a trim() is needed here before use in parsePem() to remove the trailing newline (0x0a), which parsePem() does not allow.


    Another approach for the conversion of raw to PEM keys is to replace the raw keys embedded in the ASN.1/DER byte sequences, as e.g. in the following for the conversion of a raw private key into a PKCS#8 key (which also contains the public key) and of a raw public key into an X.509/SPKI key for curve secp224r1:

    const privKey = Buffer.from('765573f9676d39f1256d01f1fb2806d30bbfaab8b04ae745d0a77c03', 'hex');
    const pubKey = Buffer.from('04468a685192db85873baa45dbec2bcc8217f5291e09e1b581c7f27f3f5585dc535a13e1862563aeb99de167a49557f1a2d49fee67af017eba', 'hex'); // uncompressed
    
    const crypto = require('crypto');
    const privA = Buffer.from('3078020100301006072a8648ce3d020106052b810400210461305f020101041c', 'hex');
    const privB = Buffer.from('a13c033a00', 'hex');
    const pkcs8Der = Buffer.concat([privA, privKey, privB, pubKey]);
    const pkcs8 = crypto.createPrivateKey({key: pkcs8Der, format: 'der', type: 'pkcs8'}).export({type: 'pkcs8', format: 'pem'});
    
    const pubA = Buffer.from('304e301006072a8648ce3d020106052b81040021033a00', 'hex');
    const x509Der = Buffer.concat([pubA, pubKey]);
    const x509 = crypto.createPublicKey({key: x509Der, format: 'der', type: 'spki'}).export({type: 'spki', format: 'pem'});
    

    For the reverse direction, the raw keys can be extracted at their respective positions in the ASN.1/DER encoding.

    The advantage of this approach is no dependency, the disadvantage that privA, privB and pubA are ASN.1/DER encodings that contain metadata, such as the curve or length information (as can be seen when examining the PEM keys in an ASN.1 parser, e.g. https://lapo.it/asn1js/), so they are different for each curve.