import { decodeBase64Url, encodeBase64Url, getHomeDirOrDie, hexStringToUint8Array, } from "https://global.hatter.ink/script/get/@9/deno-commons-mod.ts"; import {getRandomValues} from "node:crypto"; import {assertEquals} from "jsr:@std/assert"; const COMMONS_LOCAL_ENCRYPT_TINY_ENCRYPT_MASTER_KEY_FILE = getHomeDirOrDie() + "/.cache/commons-local-encrypt-tiny-encrypt-master-key"; interface TinyEncryptSimpleDecryptObject { code: number; result: string; } let cachedCryptoMasterKey: CryptoKey | null = null; export async function lazyLoadCryptoMasterKey(): Promise { if (cachedCryptoMasterKey == null) { cachedCryptoMasterKey = await loadCryptoMasterKey(); } return cachedCryptoMasterKey; } async function loadCryptoMasterKey(): Promise { const key = await loadMasterKey(); return await crypto.subtle.importKey( "raw", key, "AES-GCM", false, ["encrypt", "decrypt"], ); } async function loadMasterKey(): Promise { const masterKeyContent = Deno.readTextFileSync( COMMONS_LOCAL_ENCRYPT_TINY_ENCRYPT_MASTER_KEY_FILE, ); const command = new Deno.Command("tiny-encrypt", { args: ["simple-decrypt", "--value", masterKeyContent], }); const { code, stdout, stderr } = command.outputSync(); if (code !== 0) { console.error(`Execute command tiny-encrypt simple-decrypt failed: code: ${code} stdout: ${new TextDecoder().decode(stdout)} stderr: ${new TextDecoder().decode(stderr)}`); throw new Error(`Decrypt master key failed, code: ${code}`); } const tinyEncryptSimpleDecryptObject = JSON.parse( new TextDecoder().decode(stdout), ) as TinyEncryptSimpleDecryptObject; if (tinyEncryptSimpleDecryptObject.code !== 0) { throw new Error( `Decrypt master key failed, response code: ${tinyEncryptSimpleDecryptObject.code}`, ); } return hexStringToUint8Array(tinyEncryptSimpleDecryptObject.result); } export async function teDecryptToString(ciphertext: string): Promise { const decryptedValue = await teDecrypt(ciphertext); return new TextDecoder().decode(decryptedValue); } export async function teDecrypt(ciphertext: string): Promise { if (!ciphertext.startsWith("te:")) { throw new Error(`Invalid ciphertext: ${ciphertext}`); } const ciphertextParts = ciphertext.split(":"); if (ciphertextParts.length !== 3) { throw new Error(`Invalid ciphertext: ${ciphertext}`); } const nonce = decodeBase64Url(ciphertextParts[1]); const ciphertextAndTag = decodeBase64Url(ciphertextParts[2]); const cryptoKey = await lazyLoadCryptoMasterKey(); return await crypto.subtle.decrypt( { name: "AES-GCM", iv: nonce }, cryptoKey, ciphertextAndTag, ); } export async function teEncrypt(plaintext: string): Promise { const nonce = randomNonce(); const plaintextBuffer = new TextEncoder().encode(plaintext); const cryptoKey = await lazyLoadCryptoMasterKey(); const encryptedData = await crypto.subtle.encrypt( { name: "AES-GCM", iv: nonce }, cryptoKey, plaintextBuffer, ); return `te:${encodeBase64Url(nonce)}:${encodeBase64Url(encryptedData)}`; } function randomNonce(): ArrayBufferLike { const buffer = new Uint8Array(12); getRandomValues(buffer); return buffer; } Deno.test("teEncryptDecrypt", async () => { assertEquals( "hello world", await teDecryptToString( await teEncrypt("hello world"), ), ); });