本项目旨在自建一套基于 Go 的 KMS(Key Management Service,密钥管理服务),为内部业务提供统一的密钥管理与加解密能力,主要目标包括:
- 统一管理密钥:集中管理主密钥(Master Key)、业务密钥(Business Key/CMK)、数据密钥(DEK)。
- 提供标准加密服务接口:对业务系统提供
Encrypt/Decrypt/GenerateDataKey/DecryptDataKey等接口。 - 提高安全性:避免业务系统直接持有主密钥,通过密钥分层和受控解锁流程降低密钥泄露风险。
- 支持主密钥分片模式:通过多合伙人分片输入的方式,控制 KMS 解锁权限;可扩展到实体 U 盘 + 分片方案。
KMS 主要面向以下加密场景(可按需要逐步接入):
- 数据库字段加密:如手机号、身份证号、银行卡号等敏感字段。
- 文件加密:如存储在对象存储/共享存储中的敏感文件。
- API 鉴权/签名(可选,后期扩展):对外接口的请求签名与验签。
- 令牌/票据加密(可选):如 JWT、会话票据等。
密钥类型:
- 对称密钥:如 AES-256-GCM、SM4 等。
- 非对称密钥(后期扩展):如 RSA、ECC、SM2 等,用于签名/验签。
- 分层密钥结构:
- 主密钥(Master Key / Root Key)。
- 业务密钥(Business Key / CMK):对应不同业务用途。
- 数据密钥(DEK):短生命周期,用于实际数据加解密。
管理面主要给安全管理员/运维人员使用,功能包括:
- 主密钥/业务密钥管理:创建、启用、禁用、轮换、销毁(后续迭代逐步完善)。
- 调用方(Client/App)管理:为不同业务服务分配
client_id/client_secret或证书。 - 访问策略与权限:配置某个 Client 能否对某个 Key 执行
Encrypt/Decrypt/GenerateDataKey等操作。 - 审计与日志:查看密钥管理操作和加解密调用日志。
- 解锁流程:在 Locked 状态下,通过合伙人分片输入解锁 KMS。
管理面将通过 HTTP(S) 管理接口 + 简易管理网页提供能力,仅在内网/管理网络访问。
数据面主要面向业务系统,对外提供 KMS 核心功能接口:
Encrypt:加密明文数据,返回密文。Decrypt:解密密文数据,返回明文。GenerateDataKey:生成数据密钥(DEK),返回明文 DEK + 被业务密钥加密过的 DEK 密文。DecryptDataKey:根据 DEK 密文解出明文 DEK,用于本地解密数据。- 后续可扩展
Sign/Verify等签名/验签接口。
业务服务与 KMS 之间通过内部网络通信,KMS 要对每次调用进行身份认证、权限校验与审计记录。
采用"单服务 + 内部分层清晰"的方式实现,便于后续拆分:
- 接口层(API / Handler):
- HTTP Handler,负责解析请求参数、调用应用层服务、返回统一响应。
- 应用层(App / Service / Usecase):
- 编排认证、授权、密钥加载、加解密调用、审计记录等多个步骤。
- 领域层(Domain):
- 核心业务对象和规则,例如 Key/KeyVersion、Client、Policy、AuditLog 等。
- 密钥生命周期(创建/轮换/禁用/销毁)与策略校验逻辑。
- 抽象的加解密接口
CryptoEngine,不依赖具体实现库。
- 基础设施层(Infra):
- 存储(DB/Redis)、日志、配置、加解密实现(软实现或对接 HSM)、认证实现、审计日志落地、监控等。
示例目录结构(仅示意):
cmd/
kms/
main.go
internal/
api/
http/
router.go
middleware.go
handler_kms.go # Encrypt/Decrypt 等
handler_admin.go # 密钥管理、Client 管理、解锁/分片提交等
app/
kms/
encrypt_service.go
datakey_service.go
admin/
key_mgmt_service.go
client_mgmt_service.go
policy_service.go
unlock_service.go # 解锁与分片收集相关逻辑
domain/
entity/
key.go
client.go
policy.go
audit_log.go
repo/
key_repo.go
client_repo.go
policy_repo.go
audit_repo.go
service/
key_lifecycle.go
policy_checker.go
crypto_engine.go
shard_collector.go # 分片收集器抽象
infra/
db/
mysql.go
key_mysql_repo.go
cache/
redis.go
crypto/
sw_engine.go
hsm_engine.go # 预留对接硬件 HSM
auth/
client_auth.go
audit/
audit_logger.go
config/
config.go
logger/
logger.go
metrics/
prometheus.go
pkg/
errors/
utils/
- 用途:使用指定业务密钥(或默认密钥)对明文数据进行加密,返回密文。
- 建议 HTTP 形式:
POST /api/kms/encrypt
- 请求示例:
{
"key_id": "cmk-123456",
"plaintext": "MTIzNDU2",
"algorithm": "AES_256_GCM",
"aad": "YmluZGVkLWRhdGE=",
"client_id": "user-service"
}- 响应示例:
{
"ciphertext": "BASE64_CIPHERTEXT",
"key_version": "v3",
"algorithm": "AES_256_GCM",
"aad": "YmluZGVkLWRhdGE="
}明文与密文建议统一使用 Base64 编码进行传输,避免字符集问题。
- 用途:使用指定业务密钥(或根据密文元数据)解密密文数据,返回明文。
- 建议 HTTP 形式:
POST /api/kms/decrypt
- 请求示例:
{
"key_id": "cmk-123456",
"ciphertext": "BASE64_CIPHERTEXT",
"algorithm": "AES_256_GCM",
"aad": "YmluZGVkLWRhdGE=",
"client_id": "user-service"
}- 响应示例:
{
"plaintext": "MTIzNDU2",
"key_version": "v3",
"algorithm": "AES_256_GCM"
}- 用途:根据 CMK 生成数据密钥(DEK),用于业务方本地对大量数据加密。
- 建议 HTTP 形式:
POST /api/kms/generate-data-key
- 请求示例:
{
"key_id": "cmk-123456",
"key_spec": "AES_256",
"client_id": "user-service"
}- 响应示例:
{
"plaintext_data_key": "BASE64_DEK",
"ciphertext_data_key": "BASE64_ENCRYPTED_DEK",
"key_version": "v3"
}业务服务:
- 用
plaintext_data_key在内存中对实际业务数据加密。 - 将
ciphertext_data_key与密文数据一起存储到数据库或文件元数据中,之后解密数据时再通过 KMS 解出 DEK。
- 用途:根据存储的 DEK 密文解出明文 DEK。
- 建议 HTTP 形式:
POST /api/kms/decrypt-data-key
- 请求示例:
{
"ciphertext_data_key": "BASE64_ENCRYPTED_DEK",
"client_id": "user-service"
}- 响应示例:
{
"plaintext_data_key": "BASE64_DEK",
"key_id": "cmk-123456",
"key_version": "v3"
}生产场景建议优先采用数据密钥模式,而不是每次都直接对业务数据调用 Encrypt/Decrypt 接口。
- KMS 已部署并处于 Unlocked 状态(管理员在
/admin/shard页面完成解锁)。 - 已存在至少一个业务密钥(默认会在初始化时创建
key_id = "default"的业务密钥)。
适用于数据量不大、性能要求一般的场景,例如配置、小量敏感字段。
- 加密示例:
PLAINTEXT="13800000000" # 明文手机号
B64_PLAIN=$(printf "%s" "$PLAINTEXT" | base64)
curl -s http://localhost:9455/api/kms/encrypt \
-H 'Content-Type: application/json' \
-d '{
"key_id": "default",
"plaintext": "'"$B64_PLAIN"'",
"algorithm": "AES_256_GCM"
}'返回示例(精简):
{
"code": 0,
"data": {
"ciphertext": "BASE64_CIPHERTEXT",
"key_version": "v1"
}
}业务侧把 BASE64_CIPHERTEXT 存到数据库字段中即可。
- 解密示例:
B64_CIPHERTEXT="BASE64_CIPHERTEXT_FROM_DB"
curl -s http://localhost:9455/api/kms/decrypt \
-H 'Content-Type: application/json' \
-d '{
"key_id": "default",
"ciphertext": "'"$B64_CIPHERTEXT"'",
"algorithm": "AES_256_GCM"
}'返回示例:
{
"code": 0,
"data": {
"plaintext": "TVQyM0ZEMg=="
}
}业务侧再做一次 Base64 解码得到原始明文。
适用于大量数据加解密(如文件、批量记录),减少对 KMS 的压力:
- 调用
GenerateDataKey获取 一次性 DEK:
curl -s http://localhost:9455/api/kms/generate-data-key \
-H 'Content-Type: application/json' \
-d '{
"key_id": "default",
"key_spec": "AES_256"
}'返回示例:
{
"code": 0,
"data": {
"plaintext_data_key": "BASE64_DEK",
"ciphertext_data_key": "BASE64_ENCRYPTED_DEK",
"key_version": "v1"
}
}- 业务服务:
- 本地解出
plaintext_data_key(Base64 解码得到 32 字节 AES key)。 - 使用该 DEK 在本地对大文件或大量记录进行加密(不需要再调 KMS)。
- 将
ciphertext_data_key与业务密文一起存入数据库/对象存储元数据中。
- 需要解密时:
- 从数据库/元数据中取出
ciphertext_data_key。 - 调用
DecryptDataKey解出明文 DEK:
curl -s http://localhost:9455/api/kms/decrypt-data-key \
-H 'Content-Type: application/json' \
-d '{
"ciphertext_data_key": "BASE64_ENCRYPTED_DEK"
}'得到明文 DEK(Base64),本地解码后用来解密业务数据。
- 在业务服务中封装一个 KMS Client(SDK 或简单 HTTP 封装):
- 提供
EncryptField,DecryptField,GenerateDataKey,DecryptDataKey等方法。 - 统一处理 Base64 编码/解码、错误码转换、重试等逻辑。
- 提供
- 所有访问 KMS 的逻辑集中在一处,方便未来:
- 更换 KMS 地址 / 协议。
- 增加认证(client_id/client_secret、mTLS 等)。
- 统一打审计日志和监控。
- 不在代码、配置文件、环境变量中存储主密钥明文。
- 服务器本地不持久化主密钥明文,仅可存储被主密钥加密过的业务密钥密文。
- KMS 服务进程在正常运行时不应长期持有主密钥,仅在启动解锁阶段短暂使用。
由于当前不引入实体 U 盘和真实 HSM,采取:
- 通过内网部署网页操作面板模拟 U 盘插入和主密钥注入过程。
- 支持将主密钥分片到多名合伙人,服务启动时集齐各合伙人的密钥分片,达到门限后在内存中合成主密钥,完成解锁。
该方案相比直接写死主密钥在一个文件中更安全,且在逻辑上与"物理 U 盘 + 分片"模式相似,未来可直接升级为 U 盘读取。
定义两种状态:
- Locked(上锁):
- 业务接口
Encrypt/Decrypt/GenerateDataKey/DecryptDataKey/...全部拒绝服务,返回状态码如KMS_LOCKED。 - 内存中不存在主密钥,也未加载业务密钥明文。
- 业务接口
- Unlocked(解锁):
- 已通过分片/主密钥注入流程解密并加载业务密钥明文。
- 业务接口正常可用。
规则:
- 服务启动后默认为 Locked 状态。
- 仅在完成合伙人分片输入并成功合成主密钥之后,才进入 Unlocked 状态。
- 可选支持手动 Lock 操作,将服务重新打回 Locked 状态并清除内存中业务密钥。
-
磁盘上仅存放被主密钥加密过的业务密钥密文文件,例如:
EncMK(BK_USER_DB)EncMK(BK_FILES)
-
每个业务密钥文件可包括:
- KeyId
- 算法与用途
- 版本号
- 被主密钥加密过的密文(以及必要的 IV、Tag 等)
-
不在磁盘上存储主密钥本体。
- KMS 启动,处于 Locked 状态。
- 合伙人访问管理页面
/admin/shard,依次输入各自的密钥分片。 - 后端收集到预设门限数量的分片(M-of-N)后:
- 在内存中合成主密钥
MK。 - 使用
MK解密磁盘上的业务密钥密文EncMK(BKx),得到明文BKx。 - 将
BKx管理在一个内存中的安全结构中(仅在进程生命周期内有效)。 - 立刻从内存中清除
MK和所有分片内容。 - 更新状态为 Unlocked。
- 在内存中合成主密钥
- 之后的加解密请求将直接使用内存中的业务密钥
BKx,不再需要主密钥参与。 - 重启 KMS 时,状态重置为 Locked,需要重新收集合伙人分片并解锁。
当前阶段采用简单 XOR 拼接方案,后期可升级为 Shamir Secret Sharing。
-
分片生成(离线工具完成):
- 假设主密钥
MK为 32 字节。 - 若门限 M = 3,则生成 3 个分片:
shard_1= 随机 32 字节shard_2= 随机 32 字节shard_3= MK XOR shard_1 XOR shard_2
- 这 3 个分片分别交给 3 位合伙人保管。
- 假设主密钥
-
分片合成(KMS 运行时):
- 收集到全部 M 个分片后:
MK = shard_1 XOR shard_2 XOR shard_3
- 合成完毕后立刻清除所有分片内容。
- 收集到全部 M 个分片后:
-
配置项:
KMS_SHARD_REQUIRED=3:需要收集的分片数量(门限 M)。
-
后期升级:
- 若需要"任意 M-of-N"(例如 5 人中任意 3 人即可还原),可改用 Shamir Secret Sharing 算法(Go 库:
hashicorp/vault/shamir)。 - 当前简单 XOR 方案要求必须全部 M 人到齐。
- 若需要"任意 M-of-N"(例如 5 人中任意 3 人即可还原),可改用 Shamir Secret Sharing 算法(Go 库:
业务密钥以 JSON 文件形式存储在 /data/kms/keys/ 目录下,文件名为 {key_id}.json。
示例文件 /data/kms/keys/cmk-123456.json:
{
"key_id": "cmk-123456",
"name": "用户数据加密密钥",
"algorithm": "AES_256_GCM",
"usage": "DATA_ENCRYPTION",
"versions": [
{
"version": "v1",
"status": "DISABLED",
"iv": "BASE64_IV_12_BYTES",
"ciphertext": "BASE64_ENCRYPTED_KEY_MATERIAL",
"tag": "BASE64_TAG_16_BYTES",
"created_at": "2025-01-01T00:00:00Z"
},
{
"version": "v2",
"status": "ACTIVE",
"iv": "BASE64_IV_12_BYTES",
"ciphertext": "BASE64_ENCRYPTED_KEY_MATERIAL",
"tag": "BASE64_TAG_16_BYTES",
"created_at": "2025-06-01T00:00:00Z"
}
],
"current_version": "v2",
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-06-01T00:00:00Z"
}字段说明:
key_id:业务密钥唯一标识。algorithm:该密钥使用的算法。versions:密钥版本列表,每个版本包含:iv:初始化向量(AES-GCM 需要 12 字节)。ciphertext:被主密钥加密后的密钥材料。tag:认证标签(AES-GCM 需要 16 字节)。status:ACTIVE/DISABLED/DESTROYED。
current_version:当前用于加密的默认版本。
提供一个网页操作面板,仅用于在 KMS 处于 Locked 状态时,让多名合伙人依次输入密钥分片,用于合成主密钥并解锁 KMS。
要求:
- 网页仅在内网/管理网络访问。
- 提供当前 KMS 状态与已提交分片数量的可视化提示。
- 分片内容只在内存中短暂存在,不写入日志或磁盘。
- 每次提交分片都要记录审计信息(不包含分片明文)。
假设门限为 M-of-N:
- 合伙人访问
https://kms-admin.internal/shard。 - 页面展示:
- 当前 KMS 状态:Locked / Unlocked。
- 已提交分片数量:
k / M。
- 每位合伙人在页面上输入:
- 合伙人标识(可选):如 "partner_id"。
- 分片内容(字符串或 Base64 字符串)。
- 提交后,前端调用
POST /api/admin/shard/submit接口。 - 后端:
- 校验当前仍为 Locked 状态。
- 将分片暂存在内存中的
ShardCollector。 - 做去重处理(同一合伙人重复提交时覆盖或拒绝)。
- 记录审计日志(时间、来源 IP、合伙人标识,不记录分片内容)。
- 返回当前已提交分片数
k与门限M。
- 当
k == M时:- 后端调用领域层的分片合成逻辑,获得主密钥
MK。 - 调用解锁流程,用
MK解密业务密钥并加载到内存,清除MK与所有分片。 - 更新 KMS 状态为 Unlocked。
- 返回前端
unlocked: true,前端页面提示"已解锁"。
- 后端调用领域层的分片合成逻辑,获得主密钥
- URL:
POST /api/admin/shard/submit - 请求示例:
{
"partner_id": "partner-1",
"shard": "BASE64_OR_TEXT"
}- 响应示例:
{
"accepted": true,
"current_count": 2,
"required": 3,
"unlocked": false
}所有管理接口都必须有强认证(如 mTLS + 管理员 Token + IP 白名单)。
- URL:
GET /api/admin/shard/status - 响应示例:
{
"status": "LOCKED",
"current_count": 2,
"required": 3
}前端在页面加载时和分片提交后调用该接口,刷新显示当前状态。
-
ShardCollector(分片收集器):
- 负责在 Locked 状态下收集各合伙人分片。
- 管理分片列表、去重逻辑和计数。
- 当分片数量达到门限时,触发主密钥合成逻辑。
-
KeyUnlocker(解锁器):
- 接收合成后的主密钥
MK。 - 解密磁盘上的业务密钥密文
EncMK(BKx)并加载到内存。 - 清理
MK与所有分片。 - 更新 KMS 状态为 Unlocked。
- 接收合成后的主密钥
-
认证与授权:
- 数据面接口:通过
client_id + 签名/token、mTLS 等方式认证业务服务身份。 - 管理面接口:仅限管理网络访问,使用更强的认证(mTLS + 管理员 Token + IP 白名单)。
- 数据面接口:通过
-
审计日志:
- 每一次密钥相关操作、解锁操作、分片提交、加解密调用都需要审计记录。
- 审计记录包含:时间、操作人/Client、KeyId、操作类型、结果等,不包含敏感明文。
- 禁止记录的内容:plaintext、shard 内容、DEK 明文、主密钥材料。
-
内存安全:
- 主密钥
MK和分片只在内存中短暂存在,使用后应覆盖清理。 - 业务密钥
BKx在进程内常驻,需结合系统加固策略(限制调试/内存 dump/进程注入等)。
- 主密钥
-
高可用与扩展性:
- KMS 服务以多实例部署,前面通过负载均衡;状态(Locked/Unlocked)和已提交分片计数可以放在集中存储(例如 Redis 或 DB)中统一管理。
- 密钥元数据、审计日志等持久化在可靠的数据库中。
- 对接硬件 HSM 或云厂商 KMS,将主密钥托管到更高安全等级的组件中。
- 完善密钥生命周期管理:密钥版本、轮换策略、吊销与销毁流程。
- 增强搜索与报表:统计各密钥的使用情况、各业务的加解密调用量等。
- 引入更复杂的策略引擎:按业务、环境(测试/生产)、操作类型进行精细化权限控制。
- 分片算法升级:从简单 XOR 升级为 Shamir Secret Sharing,支持"任意 M-of-N"门限。
- 提供多语言客户端 SDK(Go/PHP/Java),封装认证、重试、DEK 缓存等逻辑,降低业务接入成本。
-
配置通过环境变量或配置文件路径注入:
- 示例环境变量:
KMS_HTTP_PORT=9455KMS_DB_DSN=mysql://user:pass@tcp(db:3306)/kmsKMS_REDIS_ADDR=redis:6379KMS_SHARD_REQUIRED=3KMS_ENV=prod
- 示例环境变量:
-
单命令启动:
- 容器入口命令简单,例如:
./kms或./kms-server。 - 解锁(分片输入)过程在运行时通过管理接口完成,而不是在容器启动命令中交互式完成。
- 容器入口命令简单,例如:
-
提供健康检查接口:
- 提供
GET /health接口,用于容器编排系统(docker-compose、Kubernetes 等)判定服务是否存活。 - 健康检查仅用于进程/依赖存活判断,不代表 KMS 已处于 Unlocked 状态;是否解锁可通过
/api/admin/shard/status等管理接口查看。
- 提供
-
日志输出到 stdout/stderr:
- 主业务日志输出到标准输出,便于
docker logs以及后续集中日志系统(如 ELK、Loki 等)收集。
- 主业务日志输出到标准输出,便于
-
优雅退出:
- 捕获
SIGTERM、SIGINT信号,完成:- 停止接收新请求。
- 等待正在执行的请求完成。
- 清理内存中的业务密钥(可选)。
- 捕获
-
持久化数据不打包进镜像:
- 数据库(MySQL/PG)、Redis 等作为单独容器或外部服务。
- 若磁盘上存储业务密钥密文(
EncMK(BKx))文件:- 例如目录
/data/kms/keys。 - 通过 volume 挂载到宿主机或数据卷,使其不随镜像更新而丢失。
- 例如目录
在此基础上,可用多阶段构建 Dockerfile:构建阶段使用官方 Go 镜像编译,运行阶段使用轻量镜像(如 alpine),将二进制复制进去后以 CMD ["/app/kms"] 形式启动。
假设已有一个整体大项目,使用 docker-compose.yml 管理多个服务,KMS 可以作为其中一个普通 service 挂载进去。示例:
services:
kms:
image: your-registry/kms:latest
container_name: kms
restart: always
environment:
- KMS_HTTP_PORT=9455
- KMS_DB_DSN=mysql://user:pass@tcp(db:3306)/kms
- KMS_REDIS_ADDR=redis:6379
- KMS_SHARD_REQUIRED=3
- KMS_ENV=prod
ports:
- "9455:9455" # 如需对宿主机暴露端口;若只在内部网络访问可省略
volumes:
- kms-keys:/data/kms/keys # 存放 EncMK(BKx) 等业务密钥密文
depends_on:
- db
- redis
networks:
- internal_net
user-service:
image: your-registry/user-service:latest
environment:
- KMS_BASE_URL=http://kms:9455
- KMS_CLIENT_ID=user-service
- KMS_CLIENT_SECRET=xxxxx
depends_on:
- kms
networks:
- internal_net
volumes:
kms-keys:
networks:
internal_net:
driver: bridge要点说明:
-
同一网络:
- KMS 与其它需要调用它的服务(如
user-service)置于同一个 docker 网络(例如internal_net)中,使其能通过http://kms:9455访问。
- KMS 与其它需要调用它的服务(如
-
环境变量传递:
- 业务服务通过环境变量获取 KMS 的地址与认证信息,例如:
KMS_BASE_URL、KMS_CLIENT_ID、KMS_CLIENT_SECRET等。
- 业务服务通过环境变量获取 KMS 的地址与认证信息,例如:
-
卷挂载:
- 使用
kms-keys数据卷挂载/data/kms/keys,保证容器重启或镜像升级后,业务密钥密文文件不会丢失。
- 使用
-
端口暴露策略:
- 若 KMS 仅供内部服务使用,可不在
ports中暴露端口,仅使用内部网络访问,提高安全性。 - 管理面(含解锁/分片输入网页)建议仅通过内网访问,必要时可使用单独的管理网络或进一步的访问控制。
- 若 KMS 仅供内部服务使用,可不在
容器化后,KMS 的 Locked/Unlocked 状态机和解锁流程本质不变:
- docker-compose 启动容器 → KMS 进程随容器启动,初始状态为 Locked。
- 运维人员/合伙人通过管理网络访问简版网页
/admin/shard或管理 API:- 依次提交主密钥分片。
- 分片收集到门限数量后,服务在内存中合成主密钥
MK,解密业务密钥密文并加载到内存。 - 清理主密钥与分片,状态切换为 Unlocked。
- 内部业务服务通过
KMS_BASE_URL调用Encrypt/Decrypt/GenerateDataKey/DecryptDataKey等接口进行加解密操作。
从整体大项目的角度看,KMS 在 docker-compose 中就是一个普通后端服务:
- 由编排系统负责拉起和重启。
- 由环境变量与网络配置决定与其它服务的集成方式。
- 由管理端接口负责完成密钥注入与解锁流程。
-
协议与数据格式:
- 所有接口使用 HTTPS/HTTP,路径前缀区分数据面与管理面,例如:
- 数据面:
/api/kms/... - 管理面:
/api/admin/...
- 数据面:
- 请求与响应均使用 JSON 格式。
- 所有接口使用 HTTPS/HTTP,路径前缀区分数据面与管理面,例如:
-
统一响应结构(示例):
{
"code": 0,
"message": "OK",
"data": { }
}- 错误码约定(示例,可按需扩展):
0:成功1001:认证失败(无效 client_id / 凭证)1002:无权限访问指定密钥或操作1003:KMS 当前处于 Locked 状态2001:请求参数错误(缺失/格式不合法)2002:指定的 Key 不存在或不可用2003:算法不支持3001:内部错误(DB/加密模块异常)
- URL:
POST /api/kms/encrypt - 请求字段:
key_id(string,必填):业务密钥 ID。plaintext(string,必填):Base64 编码的明文数据。algorithm(string,可选):加密算法,默认使用 key 绑定算法,例如AES_256_GCM。aad(string,可选):Base64 编码的附加认证数据(AAD)。
- 响应字段(data):
ciphertext(string):Base64 编码的密文数据。key_version(string):实际使用的密钥版本号。algorithm(string):实际使用的算法。
- URL:
POST /api/kms/decrypt - 请求字段:
key_id(string,必填):业务密钥 ID(或可从密文元数据推断)。ciphertext(string,必填):Base64 编码的密文数据。algorithm(string,可选):加密算法,默认使用 key 绑定算法。aad(string,可选):Base64 编码的 AAD,需与加密时一致。
- 响应字段(data):
plaintext(string):Base64 编码的明文数据。key_version(string):实际使用的密钥版本号。algorithm(string):实际使用的算法。
- URL:
POST /api/kms/generate-data-key - 请求字段:
key_id(string,必填):用于加密 DEK 的 CMK/业务密钥 ID。key_spec(string,必填):DEK 类型,例如AES_256。
- 响应字段(data):
plaintext_data_key(string):Base64 编码的 DEK 明文(仅在安全场景下返回)。ciphertext_data_key(string):Base64 编码的被 CMK 加密的 DEK 密文。key_version(string):用于加密该 DEK 的 CMK 版本号。
- URL:
POST /api/kms/decrypt-data-key - 请求字段:
ciphertext_data_key(string,必填):Base64 编码的 DEK 密文。
- 响应字段(data):
plaintext_data_key(string):Base64 编码的 DEK 明文。key_id(string):对应的 CMK ID。key_version(string):对应的 CMK 版本号。
- URL:
GET /health - 响应示例:
{
"status": "UP",
"db": "OK",
"redis": "OK"
}- URL:
GET /api/admin/status - 响应字段(data):
status(string):LOCKED或UNLOCKED。
- URL:
POST /api/admin/shard/submit - 请求字段:
partner_id(string,可选):合伙人标识。shard(string,必填):分片内容(可为 Base64 文本)。
- 响应字段(data):
accepted(bool):是否接受该分片。current_count(int):当前已收集的分片数量。required(int):门限 M。unlocked(bool):是否已完成解锁。
- URL:
GET /api/admin/shard/status - 响应字段(data):
status(string):LOCKED/UNLOCKED。current_count(int):当前已收集的分片数量。required(int):门限 M。
其他如密钥管理(创建/禁用/轮换)、Client 管理等接口可在后续迭代中补充。
以下表结构为初稿,可在实际建表时根据具体数据库(MySQL/PG)语法和索引需求调整。
id(bigint, PK)key_id(varchar(64), 唯一索引):业务密钥标识,例如cmk-123456。name(varchar(128)):密钥名称/描述。usage(varchar(32)):用途,例如DATA_ENCRYPTION、DATA_KEY_ENCRYPTION。algorithm(varchar(32)):算法,例如AES_256_GCM。current_version(varchar(16)):当前活跃版本号。enabled(tinyint):是否启用,1=是,2=否。created_at(datetime)updated_at(datetime)
id(bigint, PK)key_id(varchar(64), 索引):业务密钥标识,对应kms_keys.key_id。version(varchar(16)):版本号,例如v1、v2。iv(varchar(64)):Base64 编码的初始化向量。ciphertext_key(text):Base64 编码的被主密钥加密后的密钥材料。tag(varchar(64)):Base64 编码的认证标签。algorithm(varchar(32)):算法。status(varchar(16)):ACTIVE/DISABLED/DESTROYED。created_at(datetime)updated_at(datetime)- 联合唯一索引:(key_id, version)
id(bigint, PK)client_id(varchar(64), 唯一索引):调用方标识,例如user-service。client_secret(varchar(256)):调用方密钥(建议加密存储)。name(varchar(128)):调用方名称。status(varchar(16)):ACTIVE/DISABLED。created_at(datetime)updated_at(datetime)
id(bigint, PK)client_id(varchar(64), 索引):对应kms_clients.client_id。key_id(varchar(64), 索引):对应kms_keys.key_id。operations(varchar(256)):允许的操作列表,例如ENCRYPT,DECRYPT,GENERATE_DATA_KEY。created_at(datetime)updated_at(datetime)- 联合唯一索引:(client_id, key_id)
id(bigint, PK)timestamp(datetime, 索引):操作时间。client_id(varchar(64)):调用方。key_id(varchar(64)):涉及的密钥(若有)。key_version(varchar(16)):涉及的密钥版本(若有)。operation(varchar(32)):操作类型,例如ENCRYPT、DECRYPT、GENERATE_DATA_KEY、UNLOCK、SHARD_SUBMIT等。result(varchar(16)):SUCCESS/FAILURE。error_code(int, 可空):若失败,对应错误码。remote_ip(varchar(64)):调用方 IP。extra(text/json):扩展信息(不包括敏感明文)。
若需要管理分片持久化记录,可增加
kms_shard_events表,仅记录分片提交事件的元信息(不包含分片内容)。
解锁后,业务密钥明文常驻内存。Go 中推荐使用以下结构:
// KeyStore 管理内存中的业务密钥
type KeyStore struct {
mu sync.RWMutex
status string // "LOCKED" / "UNLOCKED"
keys map[string]*KeyEntry // key_id -> KeyEntry
}
// KeyEntry 单个业务密钥的内存结构
type KeyEntry struct {
KeyID string
Name string
Algorithm string
CurrentVersion string
Versions map[string]*KeyVersion // version -> KeyVersion
}
// KeyVersion 密钥版本
type KeyVersion struct {
Version string
Status string // "ACTIVE" / "DISABLED"
PlainKey []byte // 明文密钥材料(仅在内存中)
}- 使用
sync.RWMutex保护并发访问。 PlainKey只在解锁后存在于内存中,进程退出即消失。- 若对内存安全有更高要求,可引入
awnumar/memguard库对敏感字节做保护。
当 KMS 以多实例方式部署时,需要解决两个问题:
- 分片收集状态:多个实例共享已提交分片的进度。
- 解锁状态:一个实例解锁后,其他实例如何感知并解锁。
推荐方案(简单稳健):
-
分片收集使用 Redis 集中存储:
kms:unlock:shards(Hash):存放各合伙人已提交的分片(注意:仅在 Locked 期间短暂存在,解锁后立即删除)。kms:unlock:count(String):当前已收集的分片数量。- 任一实例在收到分片后,写入 Redis;任一实例检测到数量达标后,触发解锁。
-
解锁后的业务密钥不跨实例共享:
- 每个实例独立执行:从磁盘/DB 读取业务密钥密文 → 用合成的主密钥解密 → 加载到本实例内存。
- 这样即使某实例挂掉重启,只需重新走解锁流程即可。
-
解锁状态同步:
- 使用 Redis 记录全局状态:
kms:status = "LOCKED" / "UNLOCKED"。 - 任一实例完成解锁后,更新
kms:status = "UNLOCKED"并发布通知(可用 Redis Pub/Sub 或轮询)。 - 其他实例收到通知后,自行从磁盘/DB 加载业务密钥(此时主密钥已被清除,所以其他实例需要在首次解锁时也参与分片合成,或者采用"一次解锁 + 临时密钥中转"的方式)。
- 使用 Redis 记录全局状态:
- 定期轮换:根据安全策略,每 N 个月/年轮换一次。
- 事件触发:怀疑密钥泄露、人员变动等。
-
创建新版本:
- 管理员通过管理接口
POST /api/admin/key/{key_id}/rotate触发。 - 后端生成新的密钥材料,用主密钥加密后存入密钥版本表/文件,状态为
ACTIVE。 - 更新
current_version指向新版本。
- 管理员通过管理接口
-
新数据使用新版本:
- 所有新的加密请求自动使用
current_version(新版本)。 - 响应中返回
key_version,业务可随密文一起存储。
- 所有新的加密请求自动使用
-
旧数据解密不受影响:
- 解密时根据密文携带的
key_version选择对应版本的密钥。 - 旧版本保持
ACTIVE或DISABLED(只允许解密,不允许加密)。
- 解密时根据密文携带的
-
可选:后台迁移旧数据:
- 若需要将旧数据迁移到新版本加密,可开发一个后台任务:
- 查询使用旧版本加密的数据。
- 解密后用新版本重新加密,更新存储。
- 此步骤可选,视安全需求而定。
- 若需要将旧数据迁移到新版本加密,可开发一个后台任务:
-
旧版本销毁:
- 当确认旧版本不再被任何数据引用时,可将其状态改为
DESTROYED。 - 销毁后该版本密钥材料从内存和存储中彻底删除,无法恢复。
- 当确认旧版本不再被任何数据引用时,可将其状态改为
-
加解密相关:
kms_encrypt_total{key_id, status}:Encrypt 请求总数(按 key_id、成功/失败分组)。kms_decrypt_total{key_id, status}:Decrypt 请求总数。kms_generate_datakey_total{key_id, status}:GenerateDataKey 请求总数。kms_decrypt_datakey_total{key_id, status}:DecryptDataKey 请求总数。kms_request_duration_seconds{operation}:各操作的延迟分布(Histogram)。
-
状态相关:
kms_status{status}:当前 KMS 状态(Locked=0, Unlocked=1)。kms_shard_collected:当前已收集的分片数量。kms_shard_required:需要的分片数量(门限 M)。
-
密钥相关:
kms_keys_total{status}:密钥数量(按启用/禁用分组)。kms_key_versions_total{key_id, status}:各密钥的版本数量。
-
Go 运行时指标(通过
promhttp默认暴露):go_goroutines:goroutine 数量。go_memstats_*:内存使用。
-
HTTP 服务指标:
http_requests_total{method, path, status_code}:HTTP 请求总数。http_request_duration_seconds{method, path}:HTTP 请求延迟。
- KMS 长时间处于 Locked 状态:服务启动超过 N 分钟仍未解锁。
- 加解密错误率突增:短时间内失败率超过阈值。
- 异常 QPS:某个 client_id 或 key_id 的调用量异常(可能被滥用或攻击)。
- 读取配置:
- 从环境变量或配置文件中读取:端口、DB/Redis 连接、日志级别、分片门限等。
- 初始化基础设施:
- 初始化 logger。
- 建立 DB 连接池(MySQL/PG)。
- 初始化 Redis 连接(用于多实例状态同步)。
- 初始化 metrics(Prometheus)。
- 初始化领域/应用服务:
- 创建 KeyStore(内存密钥存储,初始为空)。
- 创建 ShardCollector(分片收集器)。
- 创建各仓储实现(repo),注入 DB/Redis 依赖。
- 创建领域服务(密钥生命周期、策略校验、解锁器等)。
- 创建应用层服务(EncryptService、DataKeyService、UnlockService 等)。
- 注册路由与中间件:
- 注册数据面接口(
/api/kms/...)与管理面接口(/api/admin/...)。 - 中间件:日志、鉴权、限流、panic 恢复、Locked 状态拦截等。
- 注册数据面接口(
- 启动 HTTP 服务:
- 使用
KMS_HTTP_PORT=9455,监听:9455。 - 初始状态为
Locked。
- 使用
- 信号处理与优雅退出:
- 监听
SIGTERM、SIGINT信号。 - 收到信号后:
- 停止接收新请求,等待当前请求处理完成。
- 清理内存中缓存的业务密钥(覆盖
[]byte后置空)。 - 关闭 DB/Redis 连接等资源。
- 监听
| 功能模块 | 状态 | 说明 |
|---|---|---|
| 基础骨架 | ✅ | main.go, 配置加载, 日志, 路由 |
| 健康检查 | ✅ | GET /health |
| 状态查询 | ✅ | GET /api/admin/status |
| 分片收集 | ✅ | XOR 分片方案, ShardCollector |
| 分片解锁 | ✅ | POST /api/admin/shard/submit |
| 合伙人初始化 | ✅ | 多步骤初始化向导 |
| 密码保护 | ✅ | bcrypt 哈希, 三要素验证 |
| SQLite 存储 | ✅ | 合伙人信息, 初始化状态 |
| 业务密钥管理 | ✅ | 创建/列表/禁用/删除 CMK |
| 加密接口 | ✅ | POST /api/kms/encrypt |
| 解密接口 | ✅ | POST /api/kms/decrypt |
| GenerateDataKey | ✅ | POST /api/kms/generate-data-key |
| DecryptDataKey | ✅ | POST /api/kms/decrypt-data-key |
| 管理页面 | ✅ | 解锁页/初始化/密钥管理/合伙人管理 |
| Docker 支持 | ✅ | Dockerfile |
mykms/
├── cmd/kms/main.go # 入口
├── internal/
│ ├── api/http/
│ │ ├── router.go # 路由配置
│ │ ├── handler.go # 解锁处理器
│ │ ├── handler_init.go # 初始化处理器
│ │ ├── handler_key.go # 密钥管理处理器
│ │ └── handler_kms.go # 加解密处理器
│ ├── app/
│ │ ├── admin/
│ │ │ ├── init_service.go # 初始化服务
│ │ │ ├── key_service.go # 密钥管理服务
│ │ │ └── unlock_service.go # 解锁服务
│ │ └── kms/
│ │ └── crypto_service.go # 加解密服务
│ ├── domain/
│ │ ├── entity/partner.go # 合伙人实体
│ │ ├── repo/partner_repo.go # 仓储接口
│ │ └── service/
│ │ ├── keystore.go # 密钥存储
│ │ └── shard_collector.go # 分片收集器
│ └── infra/
│ ├── config/config.go # 配置
│ ├── db/
│ │ ├── sqlite.go # SQLite 初始化
│ │ └── partner_sqlite_repo.go # 仓储实现
│ └── logger/logger.go # 日志
├── pkg/response/response.go # 统一响应
├── tools/gen_shards.go # 测试工具
├── data/kms/ # 运行时数据
├── Dockerfile
├── go.mod
└── README.md
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /health |
健康检查 |
| GET | /api/admin/status |
KMS 状态 |
| POST | /api/admin/shard/submit |
提交分片 (含密码验证) |
| GET | /api/admin/init/status |
初始化状态 |
| POST | /api/admin/init/start |
开始初始化 |
| POST | /api/admin/init/register |
注册合伙人 |
| POST | /api/admin/init/complete |
完成初始化 |
| POST | /api/admin/init/cancel |
取消初始化 |
| GET | /api/admin/partners |
合伙人列表 |
| GET | /api/admin/keys |
密钥列表 |
| POST | /api/admin/keys |
创建密钥 |
| POST | /api/admin/keys/disable |
禁用密钥 |
| POST | /api/admin/keys/delete |
删除密钥 |
| 方法 | 路径 | 说明 |
|---|---|---|
| POST | /api/kms/encrypt |
加密数据 |
| POST | /api/kms/decrypt |
解密数据 |
| POST | /api/kms/generate-data-key |
生成数据密钥 |
| POST | /api/kms/decrypt-data-key |
解密数据密钥 |
| 路径 | 说明 |
|---|---|
/admin/shard |
解锁页面 |
/admin/init |
初始化向导 |
/admin/keys |
密钥管理 |
/admin/partners |
合伙人管理 |
| 环境变量 | 默认值 | 说明 |
|---|---|---|
KMS_HTTP_PORT |
9455 | HTTP 端口 |
KMS_SHARD_REQUIRED |
3 | 解锁门限 |
KMS_KEYS_PATH |
./data/kms/keys | 密钥文件路径 |
KMS_DB_PATH |
./data/kms/kms.db | SQLite 路径 |
KMS_ENV |
dev | 环境 |
- Client 认证与授权 (client_id/client_secret)
- 审计日志持久化
- 密钥轮换
- Prometheus 监控指标
- 多实例状态同步
- 实体 U 盘支持