130 lines
3.8 KiB
TypeScript
130 lines
3.8 KiB
TypeScript
import * as cacheApi from './cacheApi';
|
|
import { PASSCODE_CACHE_NAME } from '../config';
|
|
|
|
const IV_LENGTH = 12;
|
|
const SALT = 'harder better faster stronger';
|
|
|
|
let currentPasscodeHash: ArrayBuffer | undefined;
|
|
|
|
export function getPasscodeHash() {
|
|
return currentPasscodeHash;
|
|
}
|
|
|
|
export function setPasscodeHash(passcodeHash: ArrayBuffer) {
|
|
currentPasscodeHash = passcodeHash;
|
|
}
|
|
|
|
export async function setupPasscode(passcode: string) {
|
|
currentPasscodeHash = await sha256(passcode);
|
|
}
|
|
|
|
export async function encryptSession(sessionJson?: string, globalJson?: string) {
|
|
if (!currentPasscodeHash) {
|
|
throw new Error('[api/passcode] Missing current passcode');
|
|
}
|
|
|
|
await Promise.all([
|
|
(async () => {
|
|
if (!sessionJson) return;
|
|
|
|
const sessionEncrypted = await aesEncrypt(sessionJson, currentPasscodeHash);
|
|
await store('sessionEncrypted', sessionEncrypted);
|
|
})(),
|
|
(async () => {
|
|
if (!globalJson) return;
|
|
|
|
const globalEncrypted = await aesEncrypt(globalJson, currentPasscodeHash);
|
|
await store('globalEncrypted', globalEncrypted);
|
|
})(),
|
|
]);
|
|
}
|
|
|
|
export async function decryptSessionByCurrentHash() {
|
|
if (!currentPasscodeHash) {
|
|
throw new Error('[api/passcode] Missing current passcode');
|
|
}
|
|
|
|
const [sessionEncrypted, globalEncrypted] = await Promise.all([
|
|
load('sessionEncrypted'),
|
|
load('globalEncrypted'),
|
|
]);
|
|
|
|
if (!sessionEncrypted || !globalEncrypted) {
|
|
throw new Error('[api/passcode] Missing required stored fields');
|
|
}
|
|
|
|
const [sessionJson, globalJson] = await Promise.all([
|
|
aesDecrypt(sessionEncrypted, currentPasscodeHash),
|
|
aesDecrypt(globalEncrypted, currentPasscodeHash),
|
|
]);
|
|
|
|
return { sessionJson, globalJson };
|
|
}
|
|
|
|
export async function decryptSession(passcode: string) {
|
|
const passcodeHash = await sha256(passcode);
|
|
|
|
const [sessionEncrypted, globalEncrypted] = await Promise.all([
|
|
load('sessionEncrypted'),
|
|
load('globalEncrypted'),
|
|
]);
|
|
|
|
if (!sessionEncrypted || !globalEncrypted) {
|
|
throw new Error('[api/passcode] Missing required stored fields');
|
|
}
|
|
|
|
const [sessionJson, globalJson] = await Promise.all([
|
|
aesDecrypt(sessionEncrypted, passcodeHash),
|
|
aesDecrypt(globalEncrypted, passcodeHash),
|
|
]);
|
|
|
|
currentPasscodeHash = passcodeHash;
|
|
|
|
return { sessionJson, globalJson };
|
|
}
|
|
|
|
export function forgetPasscode() {
|
|
currentPasscodeHash = undefined;
|
|
}
|
|
|
|
export function clearEncryptedSession() {
|
|
forgetPasscode();
|
|
return cacheApi.clear(PASSCODE_CACHE_NAME);
|
|
}
|
|
|
|
function sha256(plaintext: string) {
|
|
return crypto.subtle.digest('SHA-256', new TextEncoder().encode(`${plaintext}${SALT}`));
|
|
}
|
|
|
|
async function store(key: string, value: ArrayBuffer) {
|
|
await cacheApi.save(PASSCODE_CACHE_NAME, key, value);
|
|
}
|
|
|
|
function load(key: string) {
|
|
return cacheApi.fetch(PASSCODE_CACHE_NAME, key, cacheApi.Type.ArrayBuffer);
|
|
}
|
|
|
|
async function aesEncrypt(plaintext: string, pwHash: ArrayBuffer) {
|
|
const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH));
|
|
const alg = { name: 'AES-GCM', iv };
|
|
const key = await crypto.subtle.importKey('raw', pwHash, alg, false, ['encrypt']);
|
|
const ptUint8 = new TextEncoder().encode(plaintext);
|
|
const ctBuffer = await crypto.subtle.encrypt(alg, key, ptUint8);
|
|
const ct = new Uint8Array(ctBuffer);
|
|
const result = new Uint8Array(IV_LENGTH + ct.length);
|
|
result.set(iv, 0);
|
|
result.set(ct, IV_LENGTH);
|
|
return result.buffer;
|
|
}
|
|
|
|
async function aesDecrypt(data: ArrayBuffer, pwHash: ArrayBuffer) {
|
|
const dataArray = new Uint8Array(data);
|
|
const iv = dataArray.slice(0, IV_LENGTH);
|
|
const alg = { name: 'AES-GCM', iv };
|
|
const key = await crypto.subtle.importKey('raw', pwHash, alg, false, ['decrypt']);
|
|
const ct = dataArray.slice(IV_LENGTH);
|
|
const plainBuffer = await crypto.subtle.decrypt(alg, key, ct);
|
|
|
|
return new TextDecoder().decode(plainBuffer);
|
|
}
|