Files
ts-scripts/libraries/deno-teencrypt-mod.ts

117 lines
3.6 KiB
TypeScript

import {
decodeBase64Url,
encodeBase64Url,
hexStringToUint8Array,
resolveFilename,
} 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 = resolveFilename(
"~/.cache/commons-local-encrypt-tiny-encrypt-master-key",
);
interface TinyEncryptSimpleDecryptObject {
code: number;
result: string;
}
let cachedCryptoMasterKey: CryptoKey | null = null;
export async function lazyLoadCryptoMasterKey(): Promise<CryptoKey> {
if (cachedCryptoMasterKey == null) {
cachedCryptoMasterKey = await loadCryptoMasterKey();
}
return cachedCryptoMasterKey;
}
async function loadCryptoMasterKey(): Promise<CryptoKey> {
const key = await loadMasterKey();
return await crypto.subtle.importKey(
"raw",
key,
"AES-GCM",
false,
["encrypt", "decrypt"],
);
}
async function loadMasterKey(): Promise<Uint8Array> {
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<string> {
const decryptedValue = await teDecrypt(ciphertext);
return new TextDecoder().decode(decryptedValue);
}
export async function teDecrypt(ciphertext: string): Promise<ArrayBufferLike> {
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<string> {
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"),
),
);
});