60 lines
1.8 KiB
TypeScript
60 lines
1.8 KiB
TypeScript
import {
|
||
createDecipheriv,
|
||
createCipheriv,
|
||
createHash,
|
||
createHmac,
|
||
randomBytes,
|
||
} from 'node:crypto';
|
||
|
||
export type EncryptedStoreSecret = {
|
||
ciphertext: Buffer;
|
||
nonce: Buffer;
|
||
fingerprint: string;
|
||
};
|
||
|
||
/**
|
||
* 密钥只在此边界短暂以明文存在。数据库保存 AES-GCM 密文和随机 nonce,
|
||
* fingerprint 是不可逆 HMAC,仅用于安全地识别同一份密钥。
|
||
*/
|
||
export function encryptStoreSecret(
|
||
plaintext: string,
|
||
keyMaterial: string,
|
||
): EncryptedStoreSecret {
|
||
const key = createHash('sha256').update(keyMaterial).digest();
|
||
const nonce = randomBytes(12);
|
||
const cipher = createCipheriv('aes-256-gcm', key, nonce);
|
||
const ciphertext = Buffer.concat([
|
||
cipher.update(plaintext, 'utf8'),
|
||
cipher.final(),
|
||
cipher.getAuthTag(),
|
||
]);
|
||
return {
|
||
ciphertext,
|
||
nonce,
|
||
fingerprint: createHmac('sha256', key)
|
||
.update(plaintext)
|
||
.digest('base64url'),
|
||
};
|
||
}
|
||
|
||
/** 仅供受信任的后台 Worker 使用;调用方不得记录返回值。 */
|
||
export function decryptStoreSecret(
|
||
ciphertextWithTag: Buffer,
|
||
nonce: Buffer,
|
||
keyMaterial: string,
|
||
): string {
|
||
const authenticationTagLength = 16;
|
||
if (ciphertextWithTag.length <= authenticationTagLength) {
|
||
throw new Error('Invalid encrypted store secret');
|
||
}
|
||
const key = createHash('sha256').update(keyMaterial).digest();
|
||
const ciphertext = ciphertextWithTag.subarray(0, -authenticationTagLength);
|
||
const tag = ciphertextWithTag.subarray(-authenticationTagLength);
|
||
const decipher = createDecipheriv('aes-256-gcm', key, nonce);
|
||
decipher.setAuthTag(tag);
|
||
return Buffer.concat([
|
||
decipher.update(ciphertext),
|
||
decipher.final(),
|
||
]).toString('utf8');
|
||
}
|