API Key 传输加解密:从混合加密到密钥流动
MaaS 场景下前后端传 API Key 的加解密方案:威胁模型、混合加密与架构取舍
在 MaaS(Model as a Service,模型即服务)产品的开发里,有一个绕不开的接入方式:用户把自己在 OpenAI、Anthropic 等模型供应商处申请的 API Key 填进产品,让平台替他调用模型。这种做法叫 BYOK(Bring Your Own Key,自带密钥)。Key 第一次从前端传到后端,之后由后端持有、替用户调用第三方模型。这一步传输入口,就是加解密需求出现的地方。
传 Key 有安全隐患,这一点没有争议:密钥在链路上明文流转、落库明文保存、被日志打印出来,任何一个环节泄露都是事故。于是「给前后端传 Key 做加解密」被提上日程。这个需求真正的起点,是先拆清楚 Key 的旅程:从输入框一路走到第三方模型服务,中间要经过哪些层,每一层谁可能看到它,加解密到底能挡住哪一层。
这篇文章按威胁模型把主流方案分层梳理:传输层的 TLS 与应用层混合加密、存储层的 KMS 信封加密、架构层的后端代理与短时令牌、运维层的脱敏与轮换。每个方案先说它防什么,再讲怎么做,最后交代边界。结论先行:加解密各管一段,真正决定安全边界的,是密钥流动的方向。
先拆威胁模型:Key 会经过谁#
把一次 BYOK 接入里 Key 的旅程完整走一遍,每一站都是潜在泄露点:
| 环节 | 谁能看到 | 风险 |
|---|---|---|
| 用户在输入框粘贴 Key | 浏览器内存 | XSS、浏览器插件、恶意脚本可以读走 |
| 浏览器发出请求 | 网络链路 | 网络嗅探与中间人,由 TLS 挡住 |
| TLS 终止点(CDN / WAF / 反向代理) | 云厂商网关、运维人员 | TLS 之后进入明文域,网关日志可能记录请求体 |
| 后端进程 | 后端代码、日志、错误上报 | 一行 console.log 就把 Key 打出去 |
| 数据库 | 数据库泄露、备份泄露 | 明文落库等于把全部用户的 Key 放在同一个篮子里 |
| 后端再次读出 Key 调用第三方 | 后端内存、第三方侧日志 | 调用链路上 Key 再次出现在内存 |
这六站里,加解密能覆盖的只有其中几站。中间的网络、网关、日志、数据库,都可以用加密来挡住;但两端的端点防不住——浏览器这一端,Key 是用户自己输入的,浏览器内存里本来就有一份明文;后端这一端,后端进程必须有解密能力才能用这个 Key,所以拿到了进程权限也就拿到了 Key。
浏览器端这一点值得单独说透。任何运行在用户设备上的代码,用户(以及潜在的攻击者)都能检查。这是 OAuth 术语里 public client(公开客户端)和 confidential client(机密客户端)的分界线:浏览器属于前者,你无法控制它的运行环境,里面藏的密钥都可以被提取;服务器属于后者,用户可以访问的只有接口,碰不到进程内存和文件系统。前端打包时把 Key 塞进 VITE_ 之类的构建变量,只是把明文写进了 JS bundle,DevTools 里一翻就出来;混淆也挡不住,这是所有 DRM 都失败过的同一个原因。
加解密能防住中间的每一层,防不住端点本身。Key 一旦进了浏览器,就没有真正的秘密可言。
主流的几种加解密方案#
抛开层次,先认识加解密工具箱里的四种方案:对称加密、非对称加密、混合加密,以及国内强合规场景绕不开的国密算法。
对称加密:一个密钥管加解密#
对称加密用同一个密钥加密和解密,代表是 AES(Advanced Encryption Standard,高级加密标准),常用 AES-256-GCM 模式——GCM 在加密的同时输出一段认证标签,能发现密文被篡改。它的优点是快、没有明文长度限制;代价是密钥分发:加密和解密两端必须持有同一个密钥,而在前后端场景里,这个共享密钥一旦存在浏览器里,就逃不掉「浏览器里的一切都可被提取」。所以对称加密很少单独承担浏览器到后端的传输加密,它更常出现在两端都能安全持有密钥的地方:存储加密、服务间内部调用、本地密钥库。
非对称加密:一对钥匙,公钥随便给人#
非对称加密用一对密钥,公钥加密、私钥解密,公钥可以公开分发,私钥由持有方保密。它解决了对称加密的密钥分发难题——前端拿着公钥加密、后端拿私钥解密,两端不需要预先共享任何秘密。代价是两点:一是非对称运算慢;二是 RSA-OAEP 对明文长度有硬上限,2048 位密钥配 SHA-256 时一次最多加密 190 字节。另一个主流非对称算法是 ECDH(Elliptic Curve Diffie-Hellman,椭圆曲线密钥交换),它本身不做加密,而是让双方交换各自的临时公钥、各自独立算出同一个共享密钥,再配合对称加密使用,特点是前向保密——会话用的密钥是一次性的,用完作废。
混合加密:非对称护送钥匙,对称加密数据#
混合加密(数字信封)把前两样拼起来:用非对称加密护送一个随机生成的对称密钥,用这个对称密钥加密真正的数据。它同时绕开了非对称的速度与长度限制,又不需要预先共享密钥,是「浏览器 → 后端」传输加密的标准形态。两种主流组合是 RSA-OAEP + AES-GCM 和 ECDH + AES-GCM。标准化的封装格式有 JWE(JSON Web Encryption,JSON 网络加密),把算法标识、密文、密钥材料打包成统一结构,两端按规范即可互解。
国密:强合规行业的指定算法#
国密是国家密码管理局发布的中国商用密码标准:SM2 是非对称算法(对标 RSA / ECDH),SM4 是分组对称密码(对标 AES),SM3 是哈希算法(对标 SHA-256)。它在政企、金融,以及需要等保测评、商用密码应用安全性评估(密评)的行业里是强制要求。关键限制是浏览器原生不支持:Web Crypto API 没有 SM2 / SM4 的实现,前端做国密要引入 sm-crypto 这类第三方库,或者把加解密收敛到后端由网关代转。国内云厂商的 MaaS 一旦面向强合规行业客户,方案里就要为国密留出口。
四种方案的定位#
| 方案 | 密钥分发 | 速度 | 明文长度限制 | 浏览器原生支持 | 典型用途 |
|---|---|---|---|---|---|
| 对称加密(AES-256-GCM) | 需预先共享密钥 | 快 | 无 | 有 | 存储加密、服务间调用 |
| 非对称(RSA-OAEP) | 公钥公开即可 | 慢 | 有(2048 位约 190 字节) | 有 | 前端→后端加密、密钥交换 |
| 混合加密(RSA/AES、ECDH/AES) | 公钥公开即可 | 快 | 无 | 有 | 前后端传输加密的标准形态 |
| 国密(SM2/SM4/SM3) | 同对称/非对称 | 中 | 同对标算法 | 无,需第三方库 | 强合规行业指定 |
回到「前后端传 API Key」这个具体问题:传输的主流落点是混合加密,Key 很短时也可直接用 RSA;存储的主流落点是信封加密。下面按传输层、存储层分别展开。
传输层:HTTPS 是基线,不是全部#
HTTPS(TLS)解决的是链路问题:请求在浏览器和服务器之间的网络传输,不会被窃听、不会被篡改、不会被中间人冒充。这是任何传输敏感数据的产品的强制基线,不是可选配置。
但 TLS 有一个明确边界:它通常在反向代理、CDN 或 WAF 处终止,也就是证书校验和解密发生在网关这一层,而不是你的业务进程。TLS 终止之后,请求体在云厂商网关和后端之间的明文域里流转,网关日志、WAF 日志都可能完整记录下请求内容。对普通请求这无所谓,对 API Key 就是灾难。
所以「前后端传 Key 的加密」,通常不只是指 TLS。它指的是在应用层再加一层加密:让 Key 在浏览器到后端进程之间的任何一层都不可读,只有最终持有私钥的后端进程能解开。这层就叫应用层加解密。
应用层:混合加密,让中间层读不到#
为什么标准形态是混合加密#
上一节那张表里能看到非对称加密的两个约束:明文长度有上限、运算慢。所以即使单条 API Key 通常塞得下 190 字节,主流做法仍倾向混合加密:RSA 只负责护送一个随机生成的对称密钥,真正加密数据的活交给对称加密——速度快、无长度限制、GCM 还自带防篡改。
全局流程#
一次请求的完整路径,前后端各负责一半:
前端(浏览器) 后端
│ ① 取 RSA 公钥(/public-key 接口) │
├───────────────────────────────────────▶ │
│ ② 生成随机 AES-256-GCM 会话密钥 │
│ ③ AES 加密 API Key → 密文 + IV │
│ ④ RSA 公钥加密 AES 密钥 → wrappedKey │
│ ⑤ 发 { wrappedKey, iv, ciphertext } │
├───────────────────────────────────────▶ │
│ ⑥ RSA 私钥解出 AES 密钥
│ ⑦ AES 解密出 API Keytext这里每一步解决一个问题:
- 取公钥:前端需要后端的公钥才能加密。公钥由后端私有,可以安全公开,专门设一个接口下发。
- 生成会话密钥:每次请求都生成一个全新的 AES 密钥,用过即弃。这一次的密钥泄露只影响这一次请求,这就是前向保密。
- AES 加密数据:API Key 是真正的 payload,交给对称加密处理,速度快、无长度限制。GCM 模式自带认证标签,防篡改。
- RSA 包 AES 密钥:RSA 只加密那 32 字节的 AES 密钥,绕开长度限制。只有持有私钥的后端能解开这层。
- 前端组装信封:密文、IV、被包裹的 AES 密钥一起发出。三者分开看都是乱码,单独泄露任何一块都解不出 Key。
- 后端解包:先用 RSA 私钥解出 AES 密钥,再用 AES 解密。私钥永远不离开后端进程。
为什么浏览器能实现这层#
浏览器原生提供了 Web Crypto API(crypto.subtle),它支持 RSA-OAEP、AES-GCM 等算法,且只在 HTTPS(以及 localhost)这样的安全上下文里可用。这是「在浏览器里做非对称加解密」从偏门库走向标准做法的原因——不依赖任何第三方加密库,纯浏览器能力。
前端加密的示意代码(只依赖 crypto.subtle):
async function encryptApiKey(apiKey, publicKeySpkiB64) {
// ① 导入后端的 RSA 公钥(SPKI 格式)
const publicKey = await crypto.subtle.importKey(
'spki', base64ToBuffer(publicKeySpkiB64),
{ name: 'RSA-OAEP', hash: 'SHA-256' },
false, ['encrypt'],
);
// ② 每次请求生成随机的 AES-256-GCM 会话密钥
const aesKey = await crypto.subtle.generateKey(
{ name: 'AES-GCM', length: 256 }, true, ['encrypt'],
);
const iv = crypto.getRandomValues(new Uint8Array(12)); // 96 位随机 nonce
// ③ AES 加密真正的 API Key
const ciphertext = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv }, aesKey,
new TextEncoder().encode(apiKey),
);
// ④ 导出 AES 密钥原始字节,用 RSA 公钥包裹
const rawAesKey = await crypto.subtle.exportKey('raw', aesKey);
const wrappedKey = await crypto.subtle.encrypt(
{ name: 'RSA-OAEP' }, publicKey, rawAesKey,
);
// ⑤ 返回信封,后端只需要这三样
return {
keyId: 'k1',
iv: bufferToBase64(iv),
wrappedKey: bufferToBase64(wrappedKey),
ciphertext: bufferToBase64(ciphertext),
};
}js后端解密(Node.js,私钥从配置中心或 KMS 读取,不出进程):
function decryptEnvelope({ wrappedKey, iv, ciphertext }) {
// ⑥ 用 RSA 私钥解出 AES 密钥
const aesKey = crypto.privateDecrypt(
{
key: process.env.RSA_PRIVATE_KEY,
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
oaepHash: 'sha256',
},
Buffer.from(wrappedKey, 'base64'),
);
// ⑦ AES-GCM:认证标签是密文最后 16 字节,拆出来单独验证
const buf = Buffer.from(ciphertext, 'base64');
const authTag = buf.subarray(buf.length - 16);
const data = buf.subarray(0, buf.length - 16);
const decipher = crypto.createDecipheriv(
'aes-256-gcm', aesKey, Buffer.from(iv, 'base64'),
);
decipher.setAuthTag(authTag);
return Buffer.concat([decipher.update(data), decipher.final()]).toString('utf-8');
}js这层的边界#
混合加密防的是「浏览器到后端进程之间」的所有中间层:网络监控、CDN/WAF/反向代理、TLS 终止后的明文域、网关日志。密文落到这些层手里,没有后端私钥都读不出 Key。
它防不了三件事:
- XSS:Key 在浏览器内存里就是明文,页面脚本可以读走。加密只发生在网络层,救不了被攻陷的页面。
- 用户本人:Key 是用户自己输入的,他本来就知道。加解密不是防用户的。
- 后端内部的泄露:后端进程拿到明文 Key 之后,一行错误日志仍然会把它打出去。应用层加密挡不住自己人。
另外有一个隐含前提:公钥本身是从网络上下发的,它依赖 HTTPS 的证书链来保证「拿到的是真后端」。如果公钥被替换成攻击者的,前端加密就等于替攻击者加密。对公钥做固定(把 SPKI 的哈希写进前端,校验下发内容)是纵深防御的做法,多数实现会加。
存储层:KMS 信封加密#
传输加密解决的是「路上被偷」,存储加密解决的是「家里被偷」。API Key 到了后端之后,明文落库等于把所有用户的密钥集中放在一个地方,数据库一旦被拖,损失是全体用户级别的。存储加密和传输加密经常被一起提,但它们是两件独立的事。
落库加密的标准做法还是信封加密,但这次的两层密钥都在后端这一侧:
| 术语 | 作用 |
|---|---|
| 数据加密密钥(DEK) | 真正加密 API Key 的对称密钥,临时生成 |
| 密钥加密密钥(KEK) | 加密 DEK 的密钥,由 KMS 持有,永不出 KMS |
KMS(Key Management Service,密钥管理服务)是云厂商提供的托管密钥服务,职责是保管密钥并只提供加密解密接口。国内云厂商基本都有:阿里云 KMS、腾讯云 KMS、华为云 KMS,自托管方案有 HashiCorp Vault。
完整流程:
- 应用调 KMS 的
GenerateDataKey,一次拿回两个东西:明文 DEK 和密文 DEK。 - 用明文 DEK 加密 API Key,之后明文 DEK 立即从内存丢弃。
- 把密文 DEK 和密文一起存进数据库。
- 解密时,应用把密文 DEK 发给 KMS,KMS 用 KEK 解出明文 DEK 返回,应用再用它解密数据。
为什么要绕这一圈,而不是让 KMS 直接加密每一条 API Key?两个原因。一是成本与延迟:每加密一条数据都调一次 KMS 接口,就是一次网络往返加一次计费,用户量上来之后账单和延迟都不可接受;信封加密把 KMS 调用压到「每次存储、每次读取各一次」。二是轮换:KMS 的 master key(KEK)轮换时,只需要换掉 KEK,历史数据不需要重新加密,因为密文 DEK 一直留在数据旁边,用新 KEK 解旧 DEK 照常工作。
存储加密防的是「数据库、备份泄露之后,Key 无法直接读」。它不防「攻击者拿到了应用进程权限」——应用进程本来就有解密能力,进程被拿下,钥匙和锁都在人家手里。
架构层:Key 不要流向前端#
前面几层都是加密手段,这一层是设计决策,也是这整件事里最重要的判断。
把存储的 API Key 发回给前端(后端 → 前端方向)是反模式。加密在这个方向上的问题是结构性的:一旦 Key 回到浏览器,任何加解密都救不了。浏览器要用这个 Key,就必须在浏览器里或 JS 里持有它的明文或解密密钥,而浏览器里的一切都可以被提取。
所以 BYOK 场景的第一条设计原则是:Key 只允许单向流动——用户 → 后端。用户填一次,后端加密存储,之后所有模型调用都由后端代理完成,前端只持有它自己的会话身份(JWT 或会话 cookie),永远不接触第三方 API Key。
在这个原则下,有几种落地的架构,区别在于信任模型和 Key 的存放位置:
| 架构 | Key 在哪 | 泄露半径 | 适用场景 |
|---|---|---|---|
| 后端代理(平台持有) | 服务端加密存储,调用时后端注入 | 平台被拖库时风险集中 | 多数 BYOK 产品,如 Raycast |
| 短时受限令牌(credential broker) | 长期密钥留服务端,按任务铸造几分钟 TTL 的令牌 | 令牌只覆盖单次任务、自动过期 | 平台自身凭据、Agent 调用第三方服务 |
| 浏览器扩展钱包 | 隔离的扩展进程或本地加密 vault,网页拿不到 | 不进平台,泄露仅限本机 | 用户自己管理 Key,不经过平台 |
后端代理是最常见的选择。用户填一次 Key,平台加密存储,每次调用由后端替用户带 Key 请求供应商。前端的请求里只有用户自己的会话身份,第三方 API Key 在浏览器里不存在,也就无所谓「传」。
短时受限令牌是另一种思路,参考 OAuth 2.0 Token Exchange(RFC 8693)和各家 ephemeral token 的做法:长期密钥留在服务端,一个代理按任务铸造短命、限定范围的令牌,客户端用令牌做事,令牌几分钟后过期,吊销即刻生效。它比长期 Key 的泄露半径小得多。但对 BYOK 场景有一个现实限制:OpenAI、Anthropic 这类第三方供应商不会提供一个「平台可以替用户铸造的短时 Key」,短时令牌只能用于平台自身的凭据,替代不了用户那枚长期 API Key。所以对第三方模型 Key,后端代理仍是主力,短时令牌管的是平台自己这一侧的凭据。
浏览器扩展钱包是信任模型最极端的一种:Key 存在浏览器扩展的隔离进程或本地加密 vault 里(常见实现是 AES-256-GCM 加密、用 PBKDF2 从主密码派生密钥),网页应用只拿到短时会话令牌,Key 从不离开用户设备。代价是每个使用方都要做一次扩展安装和授权,适合个人工具,不适合多租户的云产品。
运维层:脱敏、验证、轮换#
加密解决不了所有泄露,运维纪律补上剩下的缺口。
日志。 绝不打印完整 Key。调试需要时只记末 4 位(sk-...abcd),请求头在网关和后端的日志里都要脱敏,错误上报平台要过滤请求体。应用层加密再强,一行 console.log(req.body) 就前功尽弃。
入库前验证。 用户提交 Key 时,先做一次最小成本的 live test(真实调用一次验证 Key 有效),避免把无效或写错的 Key 存进库。验证请求本身也要走加密传输。
浏览器防线。 前面说过 XSS 能读走内存里的 Key。CSP(Content Security Policy,内容安全策略)限制页面能加载和执行什么,是浏览器端压缩 XSS 影响面的手段之一,和加密配合使用。
轮换与吊销。 这比任何加密都前置。一个 Key 一旦疑似泄露,能立刻吊销才有意义,否则加密只保护了「坏人偷到手之前的时刻」。加解密不替代吊销机制,两者都要有。
选型与结论#
把四层方案按「防什么、Key 在哪、边界」拉成一张表:
| 方案 | 防的威胁 | 主要边界 |
|---|---|---|
| HTTPS / TLS | 网络窃听、链路中间人 | 在网关终止,不防中间层与端点 |
| 应用层混合加密(RSA-OAEP + AES-GCM) | 浏览器到后端进程之间的所有中间层 | 不防 XSS、不防端点 |
| KMS 信封加密 | 数据库与备份泄露后直接读 Key | 不防应用进程被拿下 |
| 架构层代理 / 短时令牌 | 从根上缩小 Key 暴露面 | 依赖架构决策,换掉成本高 |
加解密按威胁模型分层成几件事:传输、存储、架构、运维,各管一段。对 MaaS/BYOK 场景,决策顺序通常是:先保证 Key 单向流动(架构层),再加密落库(存储层),按需要加应用层传输加密(传输层),最后把日志、验证、轮换的纪律立起来(运维层)。
传输加密是纵深防御的必要零件。真正影响一次泄露严重程度的,是 Key 曾经出现在多少个不该出现的地方;把 Key 的数量和流动方向管住,比把它加密得更复杂更能降低风险。