🔒 Add encryption and decryption utilities with master key management
This commit is contained in:
@@ -9,6 +9,7 @@ import {spawn, SpawnOptionsWithoutStdio} from "node:child_process";
|
||||
import {createWriteStream, mkdir, readFile, readFileSync, rm, stat, writeFile,} from "node:fs";
|
||||
import {pipeline} from "node:stream";
|
||||
import {promisify} from "node:util";
|
||||
import {getRandomValues} from "node:crypto";
|
||||
|
||||
// reference: https://docs.deno.com/examples/hex_base64_encoding/
|
||||
// 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(
|
||||
service: string,
|
||||
user: string,
|
||||
@@ -1012,7 +1111,7 @@ export async function getKeyRingPassword(
|
||||
return null;
|
||||
}
|
||||
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 {
|
||||
@@ -1035,7 +1134,7 @@ export async function setKeyRingPassword(
|
||||
const stderrString = processOutput.getStderrAsStringThenTrim();
|
||||
if (processOutput.code != 0) {
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user