Skip to content

Instantly share code, notes, and snippets.

@sadiqsalau
Created July 31, 2025 19:54
Show Gist options
  • Save sadiqsalau/434df3e9b1f1095fa85168439dbd8570 to your computer and use it in GitHub Desktop.
Save sadiqsalau/434df3e9b1f1095fa85168439dbd8570 to your computer and use it in GitHub Desktop.
PWA Offline Encryption
import { gcm as aes256gcm, randomBytes } from "@noble/ciphers/webcrypto";
import { base64 } from "@scure/base";
import { scryptAsync } from "@noble/hashes/scrypt";
const VERSION = 1;
const SALT_BYTES = 32;
const NONCE_BYTES = 12;
const KEY_BYTES = 32;
function generateSalt() {
return base64.encode(randomBytes(SALT_BYTES));
}
async function scryptPass(password, saltB64) {
const salt = base64.decode(saltB64);
return await scryptAsync(password, salt, {
N: 2 ** 15,
r: 8,
p: 1,
dkLen: KEY_BYTES,
});
}
export async function encrypt({ text, password, salt, nonce }) {
const saltB64 = salt || generateSalt();
const iv = nonce || randomBytes(NONCE_BYTES);
const key = await scryptPass(password, saltB64);
const cipher = aes256gcm(key, iv);
const encrypted = await cipher.encrypt(new TextEncoder().encode(text));
const bundle = new Uint8Array(1 + iv.length + encrypted.length);
bundle.set([VERSION]);
bundle.set(iv, 1);
bundle.set(encrypted, 1 + iv.length);
return {
encrypted: base64.encode(bundle),
salt: saltB64,
};
}
export async function decrypt({ encrypted, password, salt }) {
const bundle = base64.decode(encrypted);
const version = bundle[0];
if (version !== VERSION) throw new Error("Unsupported cipher version");
const iv = bundle.slice(1, 1 + NONCE_BYTES);
const cipherText = bundle.slice(1 + NONCE_BYTES);
const key = await scryptPass(password, salt);
const cipher = aes256gcm(key, iv);
const decrypted = await cipher.decrypt(cipherText);
if (!decrypted) throw new Error("Invalid password or corrupted data");
return new TextDecoder().decode(decrypted);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment