如何使用NodeJS加密模块将ECDH密钥转换为PEM格式

wfveoks0  于 2023-01-01  发布在  Node.js
关注(0)|答案(1)|浏览(338)

我只希望有一对密钥,可用于ECDH函数和node:crypto模块中的其他函数。
我知道用node:crypto模块生成密钥有两种方法。
一种方法是使用crypto.generateKeyPairSync,它生成的密钥格式几乎为node:crypto模块中的所有加密函数所接受:

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()

这将以pem格式输出密钥。
但是如果我想使用ECDH,我需要以下面的方式生成密钥:

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

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

它只生成原始密钥。
如何从原始密钥生成PEM或从PEM生成原始密钥,以便我可以只使用一组密钥,而不是为ECDH生成新的密钥组?

yx2lnoni

yx2lnoni1#

加密模块不直接支持转换ASN.1/DER <->raw。支持此转换的第三方库是eckey-utils
从原始密钥到PEM密钥的转换是可能的,例如如下所述。

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;

因此,私钥以SEC 1格式导出。如果需要PKCS#8格式,则可以使用加密模块进行转换:

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

反之则为:

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

如果私钥为PKCS#8格式,则必须事先转换为SEC 1格式:

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

注意,在parsePem()中使用之前,这里需要一个trim()来删除尾随换行符(0x0a),而parsePem()不允许这样做。
用于将原始密钥转换成PEM密钥的另一种方法是替换嵌入在ASN.1/DER字节序列中的原始密钥,例如在下面用于将原始私有密钥转换成PKCS#8密钥(其还包含公共密钥)以及将原始公共密钥转换成用于曲线secp 224 r1的X.509/SPKI密钥:

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'});

对于相反方向,可以在ASN.1/DER编码中原始密钥的相应位置提取原始密钥。
这种方法的优点是没有依赖性,缺点是privAprivBpubA是包含元数据的ASN.1/DER编码,元数据例如是曲线或长度信息(当检查ASN.1解析器中的PEM键时可以看到,例如https://lapo.it/asn1js/),因此它们对于每条曲线是不同的。

相关问题