diff --git a/libraries/deno-commons-mod.ts b/libraries/deno-commons-mod.ts index 125053f..d54a660 100644 --- a/libraries/deno-commons-mod.ts +++ b/libraries/deno-commons-mod.ts @@ -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 { + 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 { + 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 { + 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 { + 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 { + return encodeBase64(await encryptDefault(plaintext)); +} + +export async function decryptDefault( + ciphertext: string | BufferSource, +): Promise { + 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 { + 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;