---
title: JWE敏感负载加密与密钥轮换(AES-GCM/RSA-OAEP)最佳实践
keywords:
- JWE
- AES-GCM
- RSA-OAEP
- CEK
- 密钥轮换
description: 通过AES-GCM加密敏感负载与RSA-OAEP封装CEK,并实施kid标识与轮换策略,保障传输与存储的机密性与可追溯性。
categories:
- 文章资讯
- 编程技术
---
背景与价值
JWE以会话密钥加密负载并用公钥封装CEK,配合kid与轮换策略提升机密性与可控性。
统一规范
- 算法组合:
enc=A256GCM与alg=RSA-OAEP-256。 - kid标识:公钥以
kid标识,便于轮换与追踪。 - 轮换策略:新旧公钥在窗口内共存,旧kid逐步下线。
核心实现
加密与封装
type Jwe = { header: { alg: 'RSA-OAEP-256'; enc: 'A256GCM'; kid: string }; iv: string; ciphertext: string; tag: string; cek: string }
function b64(b: ArrayBuffer): string { const u = new Uint8Array(b); let s=''; for (let i=0;i<u.length;i++) s+=String.fromCharCode(u[i]); return btoa(s).replace(/\+/g,'-').replace(/\//g,'_').replace(/=+$/,'') }
function enc(s: string): Uint8Array { return new TextEncoder().encode(s) }
async function genCek(): Promise<CryptoKey> { return crypto.subtle.generateKey({ name: 'AES-GCM', length: 256 }, true, ['encrypt','decrypt']) }
async function importRsaSpki(spki: ArrayBuffer): Promise<CryptoKey> { return crypto.subtle.importKey('spki', spki, { name: 'RSA-OAEP', hash: 'SHA-256' }, false, ['encrypt']) }
async function importRsaPkcs8(pkcs8: ArrayBuffer): Promise<CryptoKey> { return crypto.subtle.importKey('pkcs8', pkcs8, { name: 'RSA-OAEP', hash: 'SHA-256' }, false, ['decrypt']) }
async function aeadEncrypt(cek: CryptoKey, plain: string): Promise<{ iv: string; ct: string; tag: string }> {
const iv = crypto.getRandomValues(new Uint8Array(12))
const ct = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, cek, enc(plain))
return { iv: b64(iv.buffer), ct: b64(ct), tag: '' }
}
async function wrapCek(pub: CryptoKey, cek: CryptoKey): Promise<string> { const raw = await crypto.subtle.exportKey('raw', cek); const out = await crypto.subtle.encrypt({ name: 'RSA-OAEP' }, pub, raw); return b64(out) }
async function unwrapCek(priv: CryptoKey, cekB64: string): Promise<CryptoKey> { const raw = Uint8Array.from(atob(cekB64.replace(/-/g,'+').replace(/_/g,'/')), c => c.charCodeAt(0)).buffer; return crypto.subtle.importKey('raw', raw, { name: 'AES-GCM', length: 256 }, false, ['encrypt','decrypt']) }
async function jweEncrypt(plain: string, spki: ArrayBuffer, kid: string): Promise<Jwe> {
const cek = await genCek()
const pub = await importRsaSpki(spki)
const a = await aeadEncrypt(cek, plain)
const cekw = await wrapCek(pub, cek)
return { header: { alg: 'RSA-OAEP-256', enc: 'A256GCM', kid }, iv: a.iv, ciphertext: a.ct, tag: a.tag, cek: cekw }
}
解封与解密
async function aeadDecrypt(cek: CryptoKey, ivB64: string, ctB64: string): Promise<string> {
const iv = Uint8Array.from(atob(ivB64.replace(/-/g,'+').replace(/_/g,'/')), c => c.charCodeAt(0))
const ct = Uint8Array.from(atob(ctB64.replace(/-/g,'+').replace(/_/g,'/')), c => c.charCodeAt(0)).buffer
const out = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, cek, ct)
return new TextDecoder().decode(out)
}
async function jweDecrypt(jwe: Jwe, pkcs8: ArrayBuffer): Promise<string> {
const priv = await importRsaPkcs8(pkcs8)
const cek = await unwrapCek(priv, jwe.cek)
return aeadDecrypt(cek, jwe.iv, jwe.ciphertext)
}
落地建议
- 采用
RSA-OAEP-256 + A256GCM,以kid标识公钥并在窗口内轮换。 - 存储仅保存JWE对象,避免明文日志与持久化。
验证清单
- 加密与解密是否成功;
kid是否可追溯到当前有效公钥。

发表评论 取消回复