← 中文资源

简体中文版本

密码学密钥管理

密码学密钥管理覆盖密钥的完整生命周期:生成、存储、分发、使用、轮换和销毁,每一步都有策略与审计。

阅读英文原文

密钥有生命周期

密钥不是生成一次的字符串,而是被创建、启用、使用、暂停、轮换,最终销毁。每个状态都有要求:生成时的熵、静态存储的硬件保护、使用时的最小权限、轮换时的重叠、销毁时的证明。审计关注全部环节。

工作机制

成熟设计使用层次结构。根密钥留在 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'))