← fs.ftruncateSync() process.hasUncaughtExceptionCaptureCallback() →

Node.js keyObject.export()密钥导出:从格式矩阵到加密私钥的工程化用法

著
原创 2026-10-18 Node.js 已有人查阅

它不是一个“通用导出按钮”

KeyObject是Node.js中密钥的进程内表示,具有类型安全和不透明性。keyObject.export()的职责是把这个内部对象转换为外部可用的字节序列或结构化数据。官方文档明确说明,返回类型取决于所选格式:pem返回字符串,der返回Buffer,jwk返回普通对象,原始格式返回Buffer。

关键约束在于:不是所有密钥类型都支持所有格式。对称密钥只能导出为buffer(默认)或jwk;公钥支持pem、der、jwk、raw-public;私钥支持pem、der、jwk、raw-private、raw-seed。EC密钥额外支持原始格式,而RSA不支持。如果给RSA公钥传入format:'raw-public',会直接抛出错误。

选项矩阵:按密钥类型拆解

对称密钥

const crypto = require('crypto');
const secret = crypto.createSecretKey(Buffer.from('a'.repeat(32)));

const raw = secret.export();                    // Buffer,默认 format: 'buffer'
const jwk = secret.export({ format: 'jwk' });   // { kty: 'oct', k: '...' }

对称密钥的jwk导出结果中kty为oct,k是base64url编码的密钥材料。注意:对对称密钥传入format:'pem'不会被接受,因为HMAC密钥没有PEM表示。

公钥

RSA公钥在pem/der下有两个type可选:'pkcs1'和'spki'。'spki'是跨算法通用的SubjectPublicKeyInfo结构,'pkcs1'是RSA专有的PKCS#1结构。EC公钥使用'spki'。对于raw-public,EC密钥可以指定'uncompressed'(默认)或'compressed'点压缩格式。

const { publicKey } = crypto.generateKeyPairSync('rsa', { modulusLength: 2048 });

const spkiPem = publicKey.export({ type: 'spki', format: 'pem' });
const pkcs1Pem = publicKey.export({ type: 'pkcs1', format: 'pem' });

spkiPem以-----BEGINPUBLICKEY-----开头,pkcs1Pem以-----BEGINRSAPUBLICKEY-----开头。两者在TLS证书和多数服务端配置中,spki是更通用的选择。

私钥

私钥的type选项更丰富:'pkcs1'(仅RSA)、'pkcs8'(通用)、'sec1'(仅EC)。'pkcs8'是跨算法标准,也是唯一支持在der格式下进行加密的子类型。'pkcs1'和'sec1'的加密只能在pem格式下生效。

const { privateKey } = crypto.generateKeyPairSync('rsa', { modulusLength: 2048 });

// 无加密导出
const plainPem = privateKey.export({ type: 'pkcs8', format: 'pem' });

// 加密导出(仅 pkcs8 支持 der 加密)
const encryptedPem = privateKey.export({
  type: 'pkcs8',
  format: 'pem',
  cipher: 'aes-256-cbc',
  passphrase: 'project-key-2026'
});

加密导出的PEM会包含Proc-Type:4,ENCRYPTED和DEK-Info头(对于传统PEM加密),或使用PKCS#8自身的加密结构。官方建议:需要加密私钥时优先使用'pkcs8',因为它在pem和der两种格式下都提供加密能力,而'pkcs1'/'sec1'的加密仅限于PEM。

单元测试:覆盖合法与非法组合

以下测试在Node.js20.x上验证了格式支持边界和加密导出行为。

const crypto = require('crypto');
const assert = require('assert');

// 1. RSA 公钥 spki 与 pkcs1 的输出前缀
const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', { modulusLength: 2048 });
const spki = publicKey.export({ type: 'spki', format: 'pem' });
const pkcs1Pub = publicKey.export({ type: 'pkcs1', format: 'pem' });
assert.ok(spki.startsWith('-----BEGIN PUBLIC KEY-----'));
assert.ok(pkcs1Pub.startsWith('-----BEGIN RSA PUBLIC KEY-----'));

// 2. EC 公钥 raw-public 压缩格式
const ecPair = crypto.generateKeyPairSync('ec', { namedCurve: 'prime256v1' });
const rawCompressed = ecPair.publicKey.export({
  format: 'raw-public',
  type: 'compressed'
});
assert.ok(Buffer.isBuffer(rawCompressed));
assert.strictEqual(rawCompressed.length, 33); // 压缩点:1 + 32 字节

// 3. RSA 公钥尝试 raw-public 应抛出
assert.throws(() => {
  publicKey.export({ format: 'raw-public' });
}, /Invalid export format|raw/);

// 4. 加密私钥导出后可用相同 passphrase 重新导入
const encPem = privateKey.export({
  type: 'pkcs8', format: 'pem',
  cipher: 'aes-256-cbc', passphrase: 'test-pass'
});
const reimported = crypto.createPrivateKey({
  key: encPem, format: 'pem', passphrase: 'test-pass'
});
assert.strictEqual(reimported.type, 'private');

// 5. 对称密钥 jwk 导出结构
const secret = crypto.createSecretKey(crypto.randomBytes(32));
const jwk = secret.export({ format: 'jwk' });
assert.strictEqual(jwk.kty, 'oct');
assert.ok(typeof jwk.k === 'string');

测试3的行为在不同Node.js版本中报错信息的措辞可能有差异,但抛出异常这一行为是稳定的。测试4验证了导出的加密私钥可以被createPrivateKey重新读取,这是密钥持久化流程的基本闭环。

常见错误与异常语义

错误场景 抛出行为 处理思路
RSA公钥传raw-public 抛出Error,信息涉及无效格式 调用前根据keyObject.asymmetricKeyType判断
对称密钥传format:'pem' 抛出错误 对称密钥仅支持buffer/jwk
私钥指定cipher但缺passphrase 抛出错误 封装时校验cipher与passphrase成对出现
用pkcs1类型加密EC私钥 抛出错误 EC私钥的加密导出应使用pkcs8
错误passphrase重新导入 createPrivateKey抛出解密错误 将导入错误与导出错误分开捕获

一个容易被忽略的点:export()本身不会因为私钥未加密而报错,它忠实地返回明文私钥字符串。安全性责任在调用方——是否给私钥加cipher,是使用决策而非API约束。

JWK的特殊行为:选项被忽略

当format指定为'jwk'时,其他所有选项都会被忽略。这意味着:

// 以下 cipher 和 passphrase 不会生效
const jwk = privateKey.export({
  format: 'jwk',
  cipher: 'aes-256-cbc',
  passphrase: 'ignored'
});

JWK输出是一个明文JSON对象,私钥材料以base64url形式存在于字段中。如果需要对JWK做保护,必须在应用层对整个JSON对象进行加密或访问控制,不能依赖export()的cipher选项。

rsa-pss密钥不支持JWK导出,因为JWK规范未定义PSS参数。在需要跨系统交换密钥元数据的场景中,如果接收方依赖JWK格式,应避免选用rsa-pss类型。

与crypto.createPrivateKey()/createPublicKey()的职责对称

export()和createPrivateKey()/createPublicKey()构成一对逆操作。generateKeyPairSync()的publicKeyEncoding/privateKeyEncoding选项,在内部实际上就是调用keyObject.export()的等价行为。

// 以下两种方式产生相同结果
const pairA = crypto.generateKeyPairSync('rsa', {
  modulusLength: 2048,
  publicKeyEncoding: { type: 'spki', format: 'pem' }
});

const pairB = crypto.generateKeyPairSync('rsa', { modulusLength: 2048 });
const pemB = pairB.publicKey.export({ type: 'spki', format: 'pem' });

区别在于:方式B保留了KeyObject句柄,可以在导出前检查密钥属性(如asymmetricKeyType、asymmetricKeyDetails),或在导出后继续使用同一对象进行签名/验证操作。方式A直接返回序列化结果,丢失了KeyObject层面的可控性。在需要根据密钥类型动态选择导出格式的代码中,方式B更灵活。

常见误区

误区一:认为export()的format是唯一决定项。type和format必须匹配,pkcs1不能用于EC私钥,sec1不能用于RSA私钥。不匹配的组合会在调用时抛出错误,而非返回空值。

误区二:对已加密的PKCS#8密钥再套一层PEM加密。PKCS#8自带加密机制,PEM级别的传统加密头(DEK-Info)不会出现在PKCS#8加密输出中。如果你用type:'pkcs8'加cipher导出,得到的是PKCS#8加密结构,而非传统PEM加密。

误区三:把JWK导出当作“另一种PEM”。JWK是JSON对象,不是字符串。直接JSON.stringify后存储是可行的,但不要期望接收方以PEM解析器读取它。

误区四:忽略raw-seed的存在。Ed25519/Ed448私钥的raw-seed格式返回的是种子材料,而非完整的私钥结构。如果下游系统期望的是PKCS#8DER,传入rawseed会导致导入失败。

工程化封装:带类型校验的导出函数

在项目中,密钥导出往往需要根据运行环境或下游系统要求动态选择格式。以下封装将类型检查前置,把export()的原始错误转化为可操作的诊断信息:

function exportKeySafely(keyObject, options) {
  if (!(keyObject instanceof crypto.KeyObject)) {
    throw new TypeError('参数必须是 crypto.KeyObject 实例');
  }

  const { format, type, cipher, passphrase } = options;

  if (cipher && !passphrase) {
    throw new Error('指定 cipher 时必须提供 passphrase');
  }

  try {
    return keyObject.export(options);
  } catch (err) {
    const hint = {
      'secret': '对称密钥仅支持 buffer 和 jwk 格式',
      'public': 'RSA 公钥不支持 raw-public;EC 公钥使用 spki',
      'private': 'EC 私钥加密请使用 pkcs8;pkcs1 仅限 RSA'
    }[keyObject.type];
    throw new Error(`密钥导出失败(${keyObject.type}/${format || 'buffer'}):${err.message}。${hint || ''}`);
  }
}

这个封装的收益不在于替代export(),而在于把“格式与密钥类型不匹配”这类运行时错误,转化为包含上下文和修正方向的信息。

练习与思考

  1. 生成一个Ed25519密钥对,分别用jwk和pkcs8/pem导出公钥与私钥。比较两种格式下字段结构的差异。

  2. 编写一个函数,接收一个KeyObject,如果它是私钥且asymmetricKeyType为'ec',自动使用'pkcs8'加密导出;如果是RSA,使用'pkcs1'非加密导出。思考:为什么不应该对EC私钥使用'sec1'加密导出?

  3. 尝试用cipher:'aes-128-cbc'和8字节的passphrase导出私钥,观察是否报错。查阅Node.js对passphrase长度和cipher密钥长度的约束。

延伸阅读与参考文献

Node.js官方文档—keyObject.export([options])
https://nodejs.org/api/crypto.html#keyobjectexportoptions
选项矩阵的权威来源。重点阅读对称密钥、公钥、私钥三段的格式限制,以及PKCS#8加密与PEM级别加密的区分说明。

Node.js官方文档—非对称密钥类型
https://nodejs.org/api/crypto.html#asymmetric-key-types
表格列出了每种密钥类型(rsa、ec、ed25519、rsa-pss等)支持的导出格式。当你不确定某个算法能否导出为jwk或raw-public时,这是第一手参考。

RFC5208—Public-KeyCryptographyStandards(PKCS)#8:Private-KeyInformationSyntaxSpecification
https://datatracker.ietf.org/doc/html/rfc5208
PKCS#8私钥格式的规范定义,包括其内置的加密机制。理解为什么pkcs8加密不依赖PEM头,需要从这份文档入手。

Node.jsGitHubCommit—crypto:addkeyObject.export()'jwk'formatoption
https://github.com/nodejs/node/commit/a8d7de1
JWK格式加入export()的历史提交,说明了支持的密钥类型(ec、rsa、ed25519、ed448、x25519、x448、对称密钥)以及rsa-pss不被JWK支持的原因。适合需要了解格式支持边界的开发者。

← fs.ftruncateSync() process.hasUncaughtExceptionCaptureCallback() →
分享笔记 (共有 篇笔记)
验证码:
微信公众号