116 lines
3.6 KiB
TypeScript
116 lines
3.6 KiB
TypeScript
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<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"),
|
|
),
|
|
);
|
|
});
|