TelegramPWA/src/util/passcode.ts
2023-01-28 02:21:33 +01:00

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);
}