0

I have a third party api which returns n and e value of a rsa public key. And I have to create the rsa public key from n,e and then use it to encrypt some data.

But I could not found any api which can create pkcs8-public-pem form rsa public key in the crypto module of node.

See https://nodejs.org/api/crypto.html#cryptocreatepublickeykey.

Now I have to use some third party lib like node-rsa to create the public key.

const NodeRSA = require('node-rsa');
const crypto = require('crypto');

// create an empty key
const key = new NodeRSA();
// create special public key from n and e
key.importKey({
    n: Buffer.from('0086fa9ba066685845fc03833a9699c8baefb53cfbf19052a7f10f1eaa30488cec1ceb752bdff2df9fad6c64b3498956e7dbab4035b4823c99a44cc57088a23783', 'hex'),
    e: 0x10001,
}, 'components-public');
publicKey = key.exportKey('pkcs8-public-pem');

// TODO: use crypto.createPublicKey() instead of NodeRSA
// crypto.createPublicKey()

const data = '123';
encryptedText = crypto.publicEncrypt(publicKey, Buffer.from(data))
console.info(`Encrypted text: ${encryptedText.toString('base64')}`);
Donghua Liu
  • 1,776
  • 2
  • 21
  • 30
  • Note `node-rsa` is wrong to call this 'pkcs8' format; PKCS8 is for private keys _only_, and this format is actually, as builtin-crypto correctly calls it, SubjectPublicKeyInfo aka SPKI from X.509/PKIX – dave_thompson_085 Feb 22 '23 at 05:47
  • @dave_thompson_085 Hi, I do not agree with `PKCS8 is for private keys only`, see https://stackoverflow.com/questions/2957742/how-to-convert-pkcs8-formatted-pem-private-key-to-the-traditional-format/65661751#65661751. PKCS1 is just for RSA public/private key only, and PKCS8 support RSA/ECC and so on. – Donghua Liu Feb 22 '23 at 07:13
  • You can disagree in as many different places as you like but you're wrong. (And so is `ssh-keygen` in OpenSSH, from people who usually are better than this.) PKCS1 is indeed only RSA both public and private; PKCS8 is all algorithms but only private; X.509/PKIX SPKI is all algorithms but only public. With RSA Labs gone, see RFC 8017 et pred for PKSC1, 5208 and maybe 5958 for PKCS8, 5280 and 3279 et rel for (PKIX) SPKI, and 7468 sections 10,11,13 to confirm PEM(ish) formats for the latter two. (5958 is based on 5208, and still only private, but no longer officially called PKCS8.) – dave_thompson_085 Feb 22 '23 at 19:54
  • @dave_thompson_085 Thanks for the detailed explanation, now I got it. The format of public key in node builtin crypto module is named `spki` which is more accurate then node-rsa. – Donghua Liu Feb 24 '23 at 00:38

3 Answers3

1

It's much easier to build (fake?) JWK which is supported in 15.12.0 up:

const crypto = require("crypto");
const n = 'a3706a9390eab4091deb4f1d19490488753ac3ac5018976f7ad75c4df1a560310216b4307f943d16e53a4a3ee53d89a886fec4a7fd562de84ccded2af7df68e3edcca5d96aeb1c8eae8d1b30372e07a2332eaad7da6f91450e94660aebe5df97e07b5f0ebe84132442e1849d44bc9e4a51e662b89deedd8c8c9f1c57ccbb8a6d';
const e = '010001'; // note must be even hexits (i.e. full bytes)
const key = crypto.createPublicKey({ format: "jwk", key: { "kty": "RSA", "n": Buffer.from(n,'hex').toString('base64url'), "e": Buffer.from(e,'hex').toString('base64url') }});
console.log(key.export({format:'pem',type:'spki'}));
->
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCjcGqTkOq0CR3rTx0ZSQSIdTrD
rFAYl29611xN8aVgMQIWtDB/lD0W5TpKPuU9iaiG/sSn/VYt6EzN7Sr332jj7cyl
2WrrHI6ujRswNy4HojMuqtfab5FFDpRmCuvl35fge18OvoQTJELhhJ1EvJ5KUeZi
uJ3u3YyMnxxXzLuKbQIDAQAB
-----END PUBLIC KEY-----
dave_thompson_085
  • 34,712
  • 6
  • 50
  • 70
0

I create a js function which manual create asn.1 pkcs1 der format of RSA public key to achieve this.

/**
 * create a public key object from n and e
 * @param {*} n RSA modulus
 * @param {*} e RSA public exponent
 * @returns Crypto.keyObject, see https://nodejs.org/api/crypto.html
 */
function create_crypto_public_key_object(n, e) {
    // The n hex value should always prefixed 00, why?
    if (!n.startsWith('00')) {
        n = `00${n}`
    }
    const hex_length = (hex_str) => {
        // fix the data if the length is even
        if (hex_str.length % 2 == 1) {
            hex_str = `0${hex_str}`
        }
        hex_str_length = (hex_str.length / 2).toString(16)
        // if the length is even like '101', then the expected returns should be '0101'
        if (hex_str_length.length % 2 == 1) {
            hex_str_length = `0${hex_str_length}`
        }
        // extra length byte is needed if the length is larger than 127
        // see http://luca.ntop.org/Teaching/Appunti/asn1.html
        if (hex_str_length.length / 2 > 1) {
            hex_str_length = `${(0x80 | hex_str_length.length / 2).toString(16)}${hex_str_length}`
        }
        return hex_str_length
    }
    // https://polarssl.org/kb/cryptography/asn1-key-structures-in-der-and-pem/
    // The ASN.1 structure for a public key is:
    // RSAPublicKey ::= SEQUENCE {
    //     modulus           INTEGER,  -- n
    //     publicExponent    INTEGER   -- e
    // }
    asn1_part_n_hex_string = `02${hex_length(n)}${n}`
    asn1_part_e_hex_string = `02${hex_length(e)}${e}`
    asn1_hex_string = `30${hex_length(asn1_part_n_hex_string + asn1_part_e_hex_string)}${asn1_part_n_hex_string}${asn1_part_e_hex_string}`;
    return crypto.createPublicKey({ key: Buffer.from(asn1_hex_string, 'hex'), format: 'der', type: 'pkcs1' });
}

/**
 * 
 * @param {*} n 
 * @param {*} e 
 * @param {*} type, can be 'pkcs1' (RSA only), 'pkcs8' or 'sec1' (EC only)
 * @param {*} format can be 'pem', 'der', or 'jwk',
 * @returns 
 */
function create_public_key(n, e, type = 'spki', format = 'pem') {
    key_object = create_crypto_public_key_object(n, e)
    // see https://nodejs.org/api/crypto.html#keyobjectexportoptions
    publicKey = key_object.export({ type, format })
    return publicKey;
}

// Test for short n, which the length is one byte
n1 = '0086fa9ba066685845fc03833a9699c8baefb53cfbf19052a7f10f1eaa30488cec1ceb752bdff2df9fad6c64b3498956e7dbab4035b4823c99a44cc57088a23783'
// Test for long n, which the length is multi bytes, the first byte is 0x80 bitwise and with the bytes count of the n
n2 = 'B347A3185DE515D6E123A94CDA2DB2884892A7F27D40B536A8E258F4DF8531029A2997F37994940E1CBAE09E96975482CCB99C37E71E6B83E86EEE1AC82F73CA084D3354765EE9B671DAB0E9DE5F2EDB798DF88CFA4C6586F84440A66AEEAD352901BE9CE8F49872E9DA53A9329F2197128F097CD3ECA99C91B93032F3D30F655C1C540BC71BD53DA7BE933433367FFE247BC0D51CF5905395589079B6B98AC5826741BF08762937F4B56C30669778E2EFE58565D029040E96579488468693A81B85FBC29641DD55A39254FBB7E1DF9F4F1540125C233758DB3C0BCFADEFF7A9FC3CAE2366B419776B35BE60CE8BBE1460F84C74AC068951FD26AD5EE6EB6BE1'
e = '010001'

publicKey1 = create_public_key(n1, e);
publicKey2 = create_public_key(n2, e);
console.info(`publicKey1: ${publicKey1}`)
console.info(`publicKey2: ${publicKey2}`)

encryptedText1 = crypto.publicEncrypt(publicKey1, Buffer.from(data))
console.info(`Encrypted text 1: ${encryptedText1.toString('base64')}`);

encryptedText2 = crypto.publicEncrypt(publicKey2, Buffer.from(data))
console.info(`Encrypted text 2: ${encryptedText2.toString('base64')}`);
Donghua Liu
  • 1,776
  • 2
  • 21
  • 30
0

Thanks for the clean solution provided by @dave_thompson_085. I wrapped it into a function.

/**
 * generate RSA public key from n and e directly.
 * @param {*} n modulus of RSA
 * @param {*} e public exponent of RSA
 * @param {*} type type of key, can be 'pkcs1' (RSA only), 'pkcs8' or 'sec1' (EC only)
 * @param {*} format format of key, can be 'pem', 'der', or 'jwk',
 * @returns 
 */
function generate_public_key(n, e, type = 'spki', format = 'pem') {
    // see https://nodejs.org/api/crypto.html#cryptocreatepublickeykey
    const key_object = crypto.createPublicKey({ format: "jwk", key: { "kty": "RSA", "n": Buffer.from(n,'hex').toString('base64url'), "e": Buffer.from(e,'hex').toString('base64url') }});
    return key_object.export({format, type});
}
Donghua Liu
  • 1,776
  • 2
  • 21
  • 30