Attic is a cozy space with lofty ambitions.
attic.social
1import { Buffer } from "node:buffer";
2
3const { crypto, crypto: { subtle } } = globalThis;
4
5export const sha256Hash = (value: string): Promise<ArrayBuffer> =>
6 subtle.digest("SHA-256", new TextEncoder().encode(value));
7
8export const randomIV = (length: number): Uint8Array<ArrayBuffer> =>
9 crypto.getRandomValues(new Uint8Array(length));
10
11export const importKey = async (password: string): Promise<CryptoKey> => {
12 const key = await subtle.importKey(
13 "raw",
14 await sha256Hash(password),
15 { name: "AES-GCM" },
16 false,
17 ["encrypt", "decrypt"],
18 );
19 return key;
20};
21
22export const encryptText = async (
23 value: string,
24 key: CryptoKey | string,
25): Promise<string> => {
26 const theKey = key instanceof CryptoKey ? key : await importKey(key);
27 const iv = randomIV(12);
28 const decryptedValue = new TextEncoder().encode(value);
29 const encryptedValue = await subtle.encrypt(
30 {
31 name: "AES-GCM",
32 iv,
33 },
34 theKey,
35 decryptedValue,
36 );
37 const ivBase64 = Buffer.from(iv).toString("base64");
38 const encryptedBase64 = Buffer.from(encryptedValue).toString("base64");
39 return `${ivBase64}:${encryptedBase64}`;
40};
41
42export const decryptText = async (
43 value: string,
44 key: CryptoKey | string,
45): Promise<string> => {
46 const base64 = value.split(":");
47 const iv = Buffer.from(base64[0], "base64");
48 const encryptedValue = Buffer.from(base64[1], "base64");
49 const theKey = key instanceof CryptoKey ? key : await importKey(key);
50 const decryptedValue = await subtle.decrypt(
51 {
52 name: "AES-GCM",
53 iv,
54 },
55 theKey,
56 encryptedValue,
57 );
58 return new TextDecoder().decode(decryptedValue);
59};