cpns / apps /web /src /lib /crypto.ts
pramudya-yb
Rename project from Labas to Pram
2b62b0c
Raw
History Blame Contribute Delete
3.37 kB
const DB_NAME = "pram-crypto";
const STORE_NAME = "keys";
const KEY_NAME = "pram-master-key";
function openDB(): Promise<IDBDatabase> {
return new Promise((resolve, reject) => {
const request = indexedDB.open(DB_NAME, 1);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
request.onupgradeneeded = () => {
const db = request.result;
if (!db.objectStoreNames.contains(STORE_NAME)) {
db.createObjectStore(STORE_NAME);
}
};
});
}
async function getFromStore(db: IDBDatabase, key: string): Promise<Uint8Array | undefined> {
return new Promise((resolve, reject) => {
const tx = db.transaction(STORE_NAME, "readonly");
const store = tx.objectStore(STORE_NAME);
const request = store.get(key);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async function putToStore(db: IDBDatabase, key: string, value: Uint8Array): Promise<void> {
return new Promise((resolve, reject) => {
const tx = db.transaction(STORE_NAME, "readwrite");
const store = tx.objectStore(STORE_NAME);
const request = store.put(value, key);
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}
async function getOrCreateMasterKey(): Promise<CryptoKey> {
const db = await openDB();
const stored = await getFromStore(db, KEY_NAME);
if (stored) {
return crypto.subtle.importKey(
"raw",
stored.buffer as ArrayBuffer,
{ name: "AES-GCM" },
false,
["encrypt", "decrypt"],
);
}
const key = await crypto.subtle.generateKey(
{ name: "AES-GCM", length: 256 },
true,
["encrypt", "decrypt"],
);
const exported = await crypto.subtle.exportKey("raw", key);
await putToStore(db, KEY_NAME, new Uint8Array(exported));
return key;
}
interface EncryptedData {
iv: string;
data: string;
}
export async function encryptText(plaintext: string): Promise<string> {
const key = await getOrCreateMasterKey();
const iv = crypto.getRandomValues(new Uint8Array(12));
const encoded = new TextEncoder().encode(plaintext);
const ciphertext = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv },
key,
encoded,
);
const combined: EncryptedData = {
iv: arrayBufferToBase64(iv),
data: arrayBufferToBase64(ciphertext),
};
return JSON.stringify(combined);
}
export async function decryptText(encrypted: string): Promise<string> {
const key = await getOrCreateMasterKey();
const parsed: EncryptedData = JSON.parse(encrypted);
const iv = base64ToArrayBuffer(parsed.iv);
const ciphertext = base64ToArrayBuffer(parsed.data);
const decrypted = await crypto.subtle.decrypt(
{ name: "AES-GCM", iv },
key,
ciphertext,
);
return new TextDecoder().decode(decrypted);
}
function arrayBufferToBase64(buffer: ArrayBuffer | Uint8Array): string {
const bytes = new Uint8Array(buffer);
let binary = "";
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary);
}
function base64ToArrayBuffer(base64: string): ArrayBuffer {
const binary = atob(base64);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes.buffer;
}