🔒 Add encryption and decryption utilities with master key management

This commit is contained in:
2026-04-06 11:16:23 +08:00
parent 539b0a927f
commit 7689b6be11

View File

@@ -9,6 +9,7 @@ import {spawn, SpawnOptionsWithoutStdio} from "node:child_process";
import {createWriteStream, mkdir, readFile, readFileSync, rm, stat, writeFile,} from "node:fs"; import {createWriteStream, mkdir, readFile, readFileSync, rm, stat, writeFile,} from "node:fs";
import {pipeline} from "node:stream"; import {pipeline} from "node:stream";
import {promisify} from "node:util"; import {promisify} from "node:util";
import {getRandomValues} from "node:crypto";
// reference: https://docs.deno.com/examples/hex_base64_encoding/ // reference: https://docs.deno.com/examples/hex_base64_encoding/
// import { decodeBase64, encodeBase64 } from "jsr:@std/encoding/base64"; // import { decodeBase64, encodeBase64 } from "jsr:@std/encoding/base64";
@@ -996,6 +997,104 @@ export function encodeBase64Url(
); );
} }
export function random(length: number): ArrayBufferLike {
const buffer = new Uint8Array(length);
getRandomValues(buffer);
return buffer;
}
let __masterEncryptionKey1 = null;
let __masterEncryptionKey2 = null;
async function __getMasterEncryptionKey1(): Promise<string> {
if (!__masterEncryptionKey1) {
const service = "deno-commons-mode";
const user = "master-encryption-key-1";
__masterEncryptionKey1 = await getKeyRingPassword(service, user);
if (!__masterEncryptionKey1) {
__masterEncryptionKey1 = encodeBase64(random(1024));
await setKeyRingPassword(service, user, __masterEncryptionKey1);
}
}
return __masterEncryptionKey1;
}
async function __getMasterEncryptionKey2(): Promise<string> {
if (!__masterEncryptionKey2) {
const filename = "~/.deno-common-mod-master-encryption-key-2";
__masterEncryptionKey2 = await readFileToString(filename);
if (!__masterEncryptionKey2) {
__masterEncryptionKey2 = encodeBase64(random(4096));
await writeStringToFile(filename, __masterEncryptionKey2);
}
}
return __masterEncryptionKey2;
}
let __masterEncryptionKey = null;
async function __getMasterEncryptionKey(): Promise<CryptoKey> {
if (!__masterEncryptionKey) {
const key1 = await __getMasterEncryptionKey1();
const key2 = await __getMasterEncryptionKey2();
__masterEncryptionKey = await sha256OfString(
"master-encryption-key-prefix:" + key1 + ":" + key2,
);
}
return await crypto.subtle.importKey(
"raw",
__masterEncryptionKey,
"AES-GCM",
false,
["encrypt", "decrypt"],
);
}
export async function encryptDefault(
plaintext: string | BufferSource,
): Promise<ArrayBuffer> {
const input = typeof plaintext === "string"
? new TextEncoder().encode(plaintext)
: plaintext;
const key = await __getMasterEncryptionKey();
const nonce = await random(12);
const ciphertext = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv: nonce },
key,
input,
);
const blob = new Blob([nonce, ciphertext]);
return await blob.arrayBuffer();
}
export async function encryptDefaultAsBase64(
plaintext: string | BufferSource,
): Promise<ArrayBuffer> {
return encodeBase64(await encryptDefault(plaintext));
}
export async function decryptDefault(
ciphertext: string | BufferSource,
): Promise<ArrayBuffer> {
const input = typeof ciphertext === "string"
? decodeBase64(ciphertext)
: ciphertext;
const key = await __getMasterEncryptionKey();
const nonce = input.slice(0, 12);
const ciphertextWithTag = input.slice(12);
const plaintext = await crypto.subtle.decrypt(
{ name: "AES-GCM", iv: nonce },
key,
ciphertextWithTag,
);
return plaintext;
}
export async function decryptDefaultAsString(
ciphertext: string | BufferSource,
): Promise<string> {
const plaintext = await decryptDefault(ciphertext);
return new TextDecoder().decode(plaintext);
}
export async function getKeyRingPassword( export async function getKeyRingPassword(
service: string, service: string,
user: string, user: string,
@@ -1012,7 +1111,7 @@ export async function getKeyRingPassword(
return null; return null;
} }
throw new Error( throw new Error(
`keyring.rs -g failed, code: ${code}, stdout: ${stdoutString}, stderr: ${stderrString}`, `keyring.rs -g failed, code: ${processOutput.code}, stdout: ${stdoutString}, stderr: ${stderrString}`,
); );
} }
const result = JSON.parse(stdoutString) as { const result = JSON.parse(stdoutString) as {
@@ -1035,7 +1134,7 @@ export async function setKeyRingPassword(
const stderrString = processOutput.getStderrAsStringThenTrim(); const stderrString = processOutput.getStderrAsStringThenTrim();
if (processOutput.code != 0) { if (processOutput.code != 0) {
throw new Error( throw new Error(
`keyring.rs -s failed, code: ${code}, stdout: ${stdoutString}, stderr: ${stderrString}`, `keyring.rs -s failed, code: ${processOutput.code}, stdout: ${stdoutString}, stderr: ${stderrString}`,
); );
} }
return; return;