密钥有生命周期
密钥不是生成一次的字符串,而是被创建、启用、使用、暂停、轮换,最终销毁。每个状态都有要求:生成时的熵、静态存储的硬件保护、使用时的最小权限、轮换时的重叠、销毁时的证明。审计关注全部环节。
工作机制
成熟设计使用层次结构。根密钥留在 HSM,支撑按区域或服务划分的密钥加密密钥,再由它们包裹数据密钥。应用看不到根密钥材料,只带身份和操作调用 KMS,由 KMS 执行策略并写审计记录。
访问策略分离角色:安全工程师管理密钥策略,服务使用密钥,审计员读取日志,没有人同时拥有使用和导出权限。轮换按计划和版本进行,旧版本保留用于解密但不再用于新加密。销毁要延迟并双人控制,避免误删导致数据永久丢失。
何时使用与取舍
任何涉及静态加密、令牌签名、证书签发或字段级加密的系统都需要密钥管理。受监管环境常要求 FIPS 140-3 验证模块和客户管理密钥。小项目可以从托管 KMS 起步,而不是自建层次。
集中式 KMS 提升控制力,却增加延迟和单点故障;多区域副本与短缓存可以缓解。客户管理密钥提高隔离,也把运维负担转移给客户。硬件模块比软件密钥更慢更贵。每一次简化都会牺牲部分可审计性。
示例与常见误解
某团队把 AES 密钥放在环境变量里,供所有租户使用。密钥随崩溃转储泄露后,全部数据暴露,轮换还要重新加密所有内容。基于 KMS 的层次结构本可以把泄露限制在一个包裹密钥版本。
常见误解是密钥管理等于加密。加密只是用途之一,签名、证明和证书签发都依赖同样的纪律。相关概念:密钥轮换、信封加密、密钥管理(Secret Management)。
代码示例
import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from "node:crypto";
const ALGORITHM = "aes-256-gcm";
export interface EncryptedPayload {
salt: string;
iv: string;
tag: string;
data: string;
}
export function deriveAesKey(passphrase: string, salt: Buffer): Buffer {
if (typeof passphrase !== "string" || passphrase.length < 12) {
throw new RangeError("passphrase must be at least 12 characters");
}
return scryptSync(passphrase.normalize("NFKC"), salt, 32);
}
export function encryptString(plaintext: string, passphrase: string): EncryptedPayload {
const salt = randomBytes(16);
const iv = randomBytes(12);
const cipher = createCipheriv(ALGORITHM, deriveAesKey(passphrase, salt), iv);
const data = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
return {
salt: salt.toString("base64"),
iv: iv.toString("base64"),
tag: cipher.getAuthTag().toString("base64"),
data: data.toString("base64"),
};
}
export function decryptString(payload: EncryptedPayload, passphrase: string): string {
const salt = Buffer.from(payload.salt, "base64");
const iv = Buffer.from(payload.iv, "base64");
const tag = Buffer.from(payload.tag, "base64");
const decipher = createDecipheriv(ALGORITHM, deriveAesKey(passphrase, salt), iv);
decipher.setAuthTag(tag);
return Buffer.concat([
decipher.update(Buffer.from(payload.data, "base64")),
decipher.final(),
]).toString("utf8");
}
import hmac
import secrets
ALPHABET = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
def api_key(prefix: str = 'sk') -> str:
body = ''.join(secrets.choice(ALPHABET) for _ in range(40))
return f'{prefix}_{body}'
def url_token(nbytes: int = 32) -> str:
return secrets.token_urlsafe(nbytes)
def otp_code(digits: int = 6) -> str:
# 朴素做法 random.randint 可预测,必须用 secrets
floor = 10 ** (digits - 1)
return str(floor + secrets.randbelow(9 * floor))
def constant_time_match(candidate: str, expected: str) -> bool:
if len(candidate) != len(expected):
return False
return hmac.compare_digest(
candidate.encode(), expected.encode()
)
def scoped_token(scope: str, nbytes: int = 16) -> dict:
return {
'token': url_token(nbytes),
'scope': scope,
'hint': f'scope={scope}',
}
key = api_key()
code = otp_code()
print('key length:', len(key), 'otp:', code)
print('compare ok:', constant_time_match(code, code))
print('compare tampered:', constant_time_match(code, '123456'))