- 后端
- 云原生
- 运维
- 虚拟化
- 人工智能
- AI 应用
【免费下载链接】cloudpods
开源、云原生的多云管理及混合云融合平台
导读
本文以 cloudpods 仓库内置的 vendor/github.com/lestrrat-go/jwx/jwe 包为核心,系统讲解 JWE(JSON Web Encryption,RFC 7516)的实现原理与 Go 语言实战用法。该包是 cloudpods 中 API 网关会话令牌加密 等敏感场景的底层加密组件,阅读本文后你将掌握:JWE 的紧凑序列化与 JSON 序列化格式、密钥加密算法与内容加密算法的完整清单与选用原则、jwe.Encrypt/jwe.Decrypt的调用方式,以及如何结合源码理解 CEK 生成、AAD 绑定、压缩与解压等底层机制。
JWE 包概览:定位、能力与源码结构
github.com/lestrrat-go/jwx/jwe是 github.com/lestrrat-go/jwx 库中负责 JWE 加密的独立子包,按 RFC 7516)声明的三项核心能力是:
- 加密与解密任意数据:
jwe.Encrypt/jwe.Decrypt处理任意[]byte载荷,不限定业务格式; - 内容压缩与解压:通过
zip头支持 DEFLATE 压缩(见 compress.go); - 在 JWE 头对象中添加任意字段:标准头之外可携带私有参数(
privateParams,见 headers_gen.go)。
从源码结构看,该包由以下文件组成,职责清晰:
| 文件 | 职责 |
|---|---|
| jwe.go | 顶层Encrypt/Decrypt/Parse入口与消息解析 |
| encrypt.go | 加密上下文encryptCtx的完整加密流水线 |
| decrypt.go | 解密器Decrypter及各类密钥解密实现 |
| message.go | Message数据结构、JSON 序列化/反序列化 |
| serializer.go | 紧凑格式Compact与 JSON 格式JSON序列化 |
| headers.go / headers_gen.go | Headers接口与标准头字段定义 |
| compress.go | DEFLATE 压缩/解压 |
| options.go | EncryptOption/DecryptOption/SerializerOption选项 |
| io.go | ReadFile从文件解析 JWE 消息 |
算法常量统一定义在 vendor/github.com/lestrrat-go/jwx/jwa(JWA,RFC 7518),jwe 包与 jwa 包解耦:算法标识在 jwa 中声明与校验,加解密逻辑在 jwe 中实现。
JWE 核心概念:四段式结构与两种序列化
紧凑序列化(Compact Serialization)
JWE 紧凑格式是五段、以.分隔的 Base64URL 编码串,这在 serializer.go 的Compact和 jwe.go 的parseCompact中都有严格体现:
BASE64URL(UTF8(Protected Header)) . BASE64URL(Encrypted Key) . BASE64URL(IV) . BASE64URL(Ciphertext) . BASE64URL(Authentication Tag)- Protected Header:受保护的 JWE 头,Base64URL 编码的 JSON 对象;
- Encrypted Key:用密钥加密算法加密 CEK 后的结果(
dir与ECDH-ES模式下为空字符串,因为 CEK 直接派生); - Initialization Vector:内容加密使用的随机 IV;
- Ciphertext:密文;
- Authentication Tag:认证标签,用于完整性校验。
解析侧对段数有硬校验:parseCompact中bytes.Split(buf, []byte{'.'})后必须得到恰好 5 段,否则报错compact JWE format must have five parts(jwe.go)。同时,紧凑格式只支持单接收者(serializer.go 校验len(m.recipients) != 1即报错)。
JSON 序列化(JSON Serialization)
JSON 函数输出完整 JSON 对象,支持多接收者;接收者唯一时采用扁平化结构(header/encrypted_key字段),多接收者时使用recipients数组——这一分支逻辑在 message.go 的序列化代码中直接可见。典型 JSON 形态如下:
{ "protected": "BASE64URL(...)", "header": { "alg": "RSA1_5", "kid": "..." }, "encrypted_key": "BASE64URL(...)", "iv": "BASE64URL(...)", "ciphertext": "BASE64URL(...)", "tag": "BASE64URL(...)" }注意区分:protected头参与 AAD(附加认证数据)计算,而unprotected头与接收者header不参与认证,这是选择字段放置位置时的安全准则。
JWE 消息内部结构
message.go 定义的Message持有六类核心字段:
authenticatedData(aad)、cipherText、initializationVector(iv)、tag:密文四要素;recipients:接收者列表,每个接收者包含自己的头与加密密钥;protectedHeaders/unprotectedHeaders:受保护与不受保护的头。
headers_gen.go 定义了全部标准头字段名常量:
| 常量 | JSON 字段 | 含义 |
|---|---|---|
AlgorithmKey | alg | 密钥加密算法 |
ContentEncryptionKey | enc | 内容加密算法 |
CompressionKey | zip | 压缩算法 |
KeyIDKey | kid | 密钥标识 |
ContentTypeKey | cty | 内容类型 |
CriticalKey | crit | 必须理解的头字段列表 |
EphemeralPublicKeyKey | epk | ECDH 临时公钥 |
AgreementPartyUInfoKey/AgreementPartyVInfoKey | apu/apv | ECDH 双方信息 |
JWKKey/JWKSetURLKey | jwk/jku | 内嵌公钥 / JWK Set 地址 |
TypeKey | typ | 类型声明 |
X509CertChainKey等 | x5c/x5t/x5t#S256/x5u | X.509 证书相关 |
SaltKey/CountKey | p2s/p2c | PBES2 盐值与迭代次数 |
支持的算法矩阵
包文档给出了两套完整算法表,本小节将其与 jwa 源码中的常量定义一一对应,并补充关键参数。
密钥加密算法(Key Encryption Algorithm,alg)
密钥加密算法负责加密 CEK,常量定义在 key_encryption_gen.go:
| 算法 | 支持 | jwa 常量 | 适用密钥 |
|---|---|---|---|
| RSA-PKCS1v1.5 | ✅ | jwa.RSA1_5 | RSA 公钥 |
| RSA-OAEP-SHA1 | ✅ | jwa.RSA_OAEP | RSA 公钥 |
| RSA-OAEP-SHA256 | ✅ | jwa.RSA_OAEP_256 | RSA 公钥 |
| AES key wrap (128) | ✅ | jwa.A128KW | 16 字节对称密钥 |
| AES key wrap (192) | ✅ | jwa.A192KW | 24 字节对称密钥 |
| AES key wrap (256) | ✅ | jwa.A256KW | 32 字节对称密钥 |
| Direct encryption | ✅(仅单接收者) | jwa.DIRECT | 对称密钥,CEK 即密钥本身 |
| ECDH-ES | ✅(仅单接收者) | jwa.ECDH_ES | ECDSA / X25519 密钥 |
| ECDH-ES + AES key wrap (128) | ✅ | jwa.ECDH_ES_A128KW | ECDSA / X25519 密钥 |
| ECDH-ES + AES key wrap (192) | ✅ | jwa.ECDH_ES_A192KW | ECDSA / X25519 密钥 |
| ECDH-ES + AES key wrap (256) | ✅ | jwa.ECDH_ES_A256KW | ECDSA / X25519 密钥 |
| AES-GCM key wrap (128) | ✅ | jwa.A128GCMKW | 16 字节对称密钥 |
| AES-GCM key wrap (192) | ✅ | jwa.A192GCMKW | 24 字节对称密钥 |
| AES-GCM key wrap (256) | ✅ | jwa.A256GCMKW | 32 字节对称密钥 |
| PBES2 + HMAC-SHA256 + AES key wrap (128) | ✅ | jwa.PBES2_HS256_A128KW | 口令(密码) |
| PBES2 + HMAC-SHA384 + AES key wrap (192) | ✅ | jwa.PBES2_HS384_A192KW | 口令(密码) |
| PBES2 + HMAC-SHA512 + AES key wrap (256) | ✅ | jwa.PBES2_HS512_A256KW | 口令(密码) |
注意:
dir(Direct)与ECDH-ES两种模式在 README 中标注“Single-recipient only”。这一点在 encrypt.go 有硬性保障:当这两种算法出现在多接收者场景时,直接返回unable to support multiple recipients for ECDH-ES错误——因为此时 CEK 不是被逐接收者封装,而是直接替换为共享密钥/派生密钥。
内容加密算法(Content Encryption Algorithm,enc)
内容加密算法负责加密实际载荷,常量定义在 content_encryption_gen.go:
| 算法 | 支持 | jwa 常量 | CEK 长度 |
|---|---|---|---|
| AES-CBC + HMAC-SHA256 (128) | ✅ | jwa.A128CBC_HS256 | 32 字节(16 加密 + 16 完整性) |
| AES-CBC + HMAC-SHA384 (192) | ✅ | jwa.A192CBC_HS384 | 48 字节 |
| AES-CBC + HMAC-SHA512 (256) | ✅ | jwa.A256CBC_HS512 | 64 字节 |
| AES-GCM (128) | ✅ | jwa.A128GCM | 16 字节 |
| AES-GCM (192) | ✅ | jwa.A192GCM | 24 字节 |
| AES-GCM (256) | ✅ | jwa.A256GCM | 32 字节 |
CBC+HMAC 组合(A*CBC_HS*)采用“加密密钥 + 完整性密钥”的双密钥设计,CEK 长度是标签长度的两倍——这一换算逻辑见 internal/cipher/cipher.go(keysize = tagsize * 2)。GCM 系则复用标准 AEAD,Tag 长度固定 16 字节(cipher.go)。CBC 模式的实现细节(PKCS#7 填充、HMAC 计算与subtle.ConstantTimeCompare常量时间比较)位于 internal/aescbc/aescbc.go。
压缩算法
jwa/compression_gen.go 只定义了两个值:
jwa.Deflate("DEF"):DEFLATE 压缩,压缩实现在 compress.go,内部使用flate.NewWriter(buf, 1)(压缩级别 1,偏重速度);jwa.NoCompress(""):不压缩,此时compress()原样返回输入(compress.go)。
快速上手:加密与解密的完整代码
包 README 以ExampleEncrypt/ExampleDecrypt两个函数示范了最常用路径(RSA1_5 + A128CBC_HS256)。下面给出可直接编译运行的完整示例:
package main import ( "crypto/rand" "crypto/rsa" "log" "github.com/lestrrat-go/jwx/jwa" "github.com/lestrrat-go/jwx/jwe" ) // 加密:使用 RSA 公钥 + RSA1_5 密钥封装 + AES-CBC-HMAC 内容加密 func ExampleEncrypt() { privkey, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { log.Printf("failed to generate private key: %s", err) return } payload := []byte("Lorem Ipsum") encrypted, err := jwe.Encrypt(payload, jwa.RSA1_5, &privkey.PublicKey, jwa.A128CBC_HS256, jwa.NoCompress) if err != nil { log.Printf("failed to encrypt payload: %s", err) return } _ = encrypted } // 解密:使用 RSA 私钥还原明文 func ExampleDecrypt() { privkey, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { log.Printf("failed to generate private key: %s", err) return } encrypted, err := jwe.Encrypt([]byte("Lorem Ipsum"), jwa.RSA1_5, &privkey.PublicKey, jwa.A128CBC_HS256, jwa.NoCompress) if err != nil { log.Printf("failed to encrypt payload: %s", err) return } decrypted, err := jwe.Decrypt(encrypted, jwa.RSA1_5, privkey) if err != nil { log.Printf("failed to decrypt: %s", err) return } if string(decrypted) != "Lorem Ipsum" { log.Printf("WHAT?!") return } }要点:
Encrypt(payload, keyalg, key, contentalg, compressalg, options...)中key传公钥(*rsa.PublicKey、[]byte或jwk.Key均可,见 jwe.go);Decrypt(buf, alg, key, options...)中key必须传私钥(*rsa.PrivateKey或jwk.Key,jwe.go);- 输出为紧凑格式字符串(
[]byte),可直接存储或传输。
实战案例:cloudpods 中如何用 JWE 保护 API 网关会话
cloudpods 的 API 网关模块在会话令牌安全中实际使用了本包。pkg/apigateway/clientman/authtoken.go 中,SAuthToken(携带认证 token 与 TOTP 状态的会话对象)在启用了 RSA 私钥时,通过 JWE 完成加密编码:
func EncryptString(in []byte) string { enc, _ := jwe.Encrypt(in, jwa.RSA1_5, &privateKey.PublicKey, jwa.A128GCM, jwa.Deflate) return string(enc) } func DecryptString(in string) ([]byte, error) { return jwe.Decrypt([]byte(in), jwa.RSA1_5, privateKey) }这段生产代码体现了三个值得借鉴的组合策略:
- 算法选择:密钥封装用
jwa.RSA1_5(RSA 非对称,公钥可公开分发、私钥留存网关);内容加密用jwa.A128GCM(AEAD 模式,密文与认证一步完成,性能好); - 压缩启用:
jwa.Deflate让包含令牌的会话字节在加密前先压缩,减小落库/落 Cookie 的体积——这正是 compress.go 中zip: DEF头的用途; - 容错降级:
Encode()/Decode()在privateKey == nil时退化为纯 DEFLATE + Base64URL 压缩,保证未配置密钥的部署仍可用(authtoken.go)。
此外,同一文件中还调用jwt.Sign(t, jwa.RS256, privateKey)(authtoken.go)生成签名 JWT,与 JWE 加密构成“签名(JWS)+ 加密(JWE)”的完整凭据体系。若你的业务需要结构化解析 JWE(而非直接解密),可用jwe.Parse/jwe.ParseString/jwe.ParseReader(jwe.go)得到*Message后再读取各字段。
深入源码:加密流水线与解密流程
加密流水线(encryptCtx.Encrypt)
encrypt.go 完整呈现了一次 JWE 加密的六个步骤:
- 生成 CEK:
keygen.NewRandom(keysize).Generate()用crypto/rand生成与内容加密算法匹配长度的随机内容加密密钥(internal/keygen/keygen.go); - 写保护头:
enc(必写)、zip(启用压缩时写入),见 encrypt.go; - 封装 CEK:遍历
keyEncrypters,为每个接收者用对应keyenc.Encrypter加密 CEK(RSA、AES-KW、AES-GCM-KW、PBES2、ECDH-ES 的实现全部位于 internal/keyenc/keyenc.go); - 计算 AAD:
protected.Encode()把保护头 JSON 序列化后 Base64URL 编码,作为认证附加数据绑定到密文(encrypt.go)——因此任何对保护头的篡改都会导致解密时认证失败; - 压缩并加密内容:先
compress(plaintext, compression),再调用contentEncrypter.Encrypt(cek, plaintext, aad)得到iv, ciphertext, tag三元组(encrypt.go); - 组装消息:
Message.Set依次写入 aad / ciphertext / iv / protected / recipients / tag,最后Compact(msg)输出紧凑串(jwe.go)。
注意 encrypt.go:接收者唯一时,接收者头(alg、kid)会被合并进保护头,这正是紧凑格式能只保留一份头的原因。
解密流程(Decrypt → doDecryptCtx)
jwe.Decrypt的内部路径(jwe.go + message.go)为:
parseJSONOrCompact按首字节{判定 JSON 或紧凑格式,自动识别两种序列化;- 克隆并合并保护头/非保护头/接收者头,取
enc算法,还原 AAD(优先使用原始rawProtectedHeaders以保证与签名时完全一致的字节序列,见 message.go); - 构造
Decrypter,并针对算法补充参数:ECDH-ES 系取epk/apu/apv,GCM-KW 取iv/tag,PBES2 取p2s/p2c(message.go); - 遍历接收者,跳过
alg不匹配的接收者,用 CEK 解密内容; - 若保护头声明
zip: DEF,对明文做uncompress还原(message.go)。
Decrypter本身是一个可链式配置的对象(decrypt.go),提供AuthenticatedData、InitializationVector、KeySalt、KeyCount、PublicKey等 setter,适合高级用法下手工组装解密上下文。
高级 API:选项、解析与自定义字段
三个选项类别(options.go)
options.go 定义了加密、解密、序列化三个方向的选项:
WithProtectedHeaders(h Headers)(EncryptOption):预置保护头内容;但注意注释明确说明enc与zip会在加密时被覆盖(options.go),调用方不应手动设置这两个字段;WithMessage(m *Message)(DecryptOption):解密的同时把解析出的Message填充进对象,便于解密后检查头信息(options.go);WithPostParser(p PostParser)(DecryptOption):在消息解析完成、解密开始前插入钩子,可基于头信息动态决定密钥/算法。官方注释特别警告:该选项用于高级场景,务必先彻底理解 JWE 处理流程,警惕依据非标准提示推断密钥算法的安全风险(options.go);WithPrettyFormat(b bool)(SerializerOption):让jwe.JSON输出缩进美化后的 JSON(options.go)。
消息解析与自定义字段
Parse/ParseString/ParseReader/ReadFile:将紧凑或 JSON 格式解析为*Message,供检查CipherText()、Tag()、ProtectedHeaders()、Recipients()等(message.go);RegisterCustomField(name, object):把私有头字段注册为指定类型解码。例如注册x-birthday为time.Time后,hdr.Get("x-birthday")返回的就是time.Time而非interface{}(jwe.go),这实现了“在 JWE 头对象中添加任意字段”的能力。
实践建议与安全提示
基于对 README 与源码的分析,给出以下选型与使用建议:
- 优先 AEAD 内容加密:新系统建议选
jwa.A128GCM/jwa.A256GCM,认证与加密一体、实现简单;CBC+HMAC 系因双密钥切分与 HMAC 计算更易出错,仅在兼容老系统互操作时使用; - 对称与非对称按场景取舍:单方加密(如网关持私钥)用 RSA 系(
RSA_OAEP_256优于已被弃用风险的RSA1_5,但 cloudpods 现网为兼容性选用了RSA1_5);双方共享密钥用 AES-KW / AES-GCM-KW;口令加密用 PBES2 系(迭代次数在源码中固定为 10000,见 keyenc.go); - 保护头参与认证:密钥 ID(
kid)、算法(alg/enc)等应放在 protected 头中,避免被剥离或降级;crit字段用于声明必须理解的扩展字段; Message仅作检查用途:官方在 message.go 明确警告:Message不适合用来重新加密或重建上下文(如受保护头的换行差异会导致重算 AAD 不匹配),Message.Decrypt已标记弃用,请统一走包级jwe.Decrypt;- 密钥长度匹配:
dir模式要求对称密钥长度与内容加密算法 CEK 长度一致(A128GCM为 16 字节);ECDH_ES模式由 Concat KDF 按enc的密钥长度派生 CEK(jwe.go 中keysize = contentcrypt.KeySize()); - 压缩需权衡:压缩可减体积但会引入长度泄露等侧信道风险,敏感度高的场景建议
jwa.NoCompress。
小结
vendor/github.com/lestrrat-go/jwx/jwe是一个完整、可独立使用的 RFC 7516 实现,覆盖从算法矩阵、紧凑/JSON 双序列化,到 CEK 生成、AAD 绑定、压缩解压与自定义头的全部环节;它在 cloudpods 中已被 API 网关用于会话令牌加密(pkg/apigateway/clientman/authtoken.go),属于经过生产验证的加密路径。开发者既可像 README 示例那样在两行调用内完成加解密,也可深入 encrypt.go 与 decrypt.go 理解每一步的底层原理,从而在多云平台、API 网关、微服务等任何需要“持有公钥即可加密、私钥才能解密”的场景中安全落地。
- 后端
- 云原生
- 运维
- 虚拟化
- 人工智能
- AI 应用
【免费下载链接】cloudpods
开源、云原生的多云管理及混合云融合平台
相关推荐
深入理解lestrrat-go/jwx中的JWE加密与解密
深入理解lestrrat go/jwx中的JWE加密与解密 前言 JSON Web Encryption JWE 是JOSE Javascript Object
密码学应用安全深入理解lestrrat-go/jwx中的JWE JSON加密示例
深入理解lestrrat go/jwx中的JWE JSON加密示例 在现代应用开发中,数据安全传输是至关重要的。lestrrat go/jwx库提供了强大的JW
密码学应用安全Authlib 中的 RFC 7516 实现:JSON Web Encryption(JWE)加解密完整指南
Authlib 中的 RFC 7516 实现:JSON Web Encryption(JWE)加解密完整指南 JSON Web Encryption(JWE)是
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考