import { bytesToString, createEddsaKeyPair, decodeCrock, encodeCrock, encryptWithDerivedKey, getRandomBytesF, stringToBytes, } from "@gnu-taler/taler-util"; /** * Create a new session id from which it will * be derive the crypto parameters from * securing the private key * * @returns session id as string */ export function createSalt(): string { const salt = crypto.getRandomValues(new Uint8Array(8)); const iv = crypto.getRandomValues(new Uint8Array(12)); return encodeCrock(salt.buffer) + "-" + encodeCrock(iv.buffer); } export interface Account { accountId: string; secret: CryptoKey; } /** * Restore previous session and unlock account * * @param salt string from which crypto params will be derived * @param key secured private key * @param password password for the private key * @returns */ export async function unlockAccount( salt: string, key: string, password: string, ): Promise { const rawKey = str2ab(window.atob(key)); const privateKey = await recoverWithPassword(rawKey, salt, password); const publicKey = await getPublicFromPrivate(privateKey); const pubRaw = await crypto.subtle.exportKey("spki", publicKey).catch((e) => { throw new Error(String(e)); }); const accountId = btoa(ab2str(pubRaw)); return { accountId, secret: privateKey }; } /** * Create new account (secured private key) under session * secured with the given password * * @param sessionId * @param password * @returns */ export async function createNewAccount(password: string) { const { eddsaPriv } = createEddsaKeyPair(); const salt = createSalt(); const key = stringToBytes(password); const protectedPrivKey = await encryptWithDerivedKey( getRandomBytesF(24), key, eddsaPriv, salt, ); const protectedPriv = bytesToString(protectedPrivKey); return { accountId: protectedPriv, salt }; } const rsaAlgorithm: RsaHashedKeyGenParams = { name: "RSA-OAEP", modulusLength: 2048, publicExponent: new Uint8Array([0x01, 0x00, 0x01]), hash: "SHA-256", }; async function createPair(): Promise { const key = await crypto.subtle .generateKey(rsaAlgorithm, true, ["encrypt", "decrypt"]) .catch((e) => { throw new Error(String(e)); }); return key; } const textEncoder = new TextEncoder(); async function protectWithPassword( privateKey: CryptoKey, sessionId: string, password: string, ): Promise { const { salt, initVector: iv } = getCryptoParameters(sessionId); const passwordAsKey = await crypto.subtle .importKey("raw", textEncoder.encode(password), { name: "PBKDF2" }, false, [ "deriveBits", "deriveKey", ]) .catch((e) => { throw new Error(String(e)); }); const wrappingKey = await crypto.subtle .deriveKey( { name: "PBKDF2", salt, iterations: 100000, hash: "SHA-256", }, passwordAsKey, { name: "AES-GCM", length: 256 }, true, ["wrapKey", "unwrapKey"], ) .catch((e) => { throw new Error(String(e)); }); const protectedPrivKey = await crypto.subtle .wrapKey("pkcs8", privateKey, wrappingKey, { name: "AES-GCM", iv, }) .catch((e) => { throw new Error(String(e)); }); return protectedPrivKey; } async function recoverWithPassword( value: ArrayBuffer, sessionId: string, password: string, ): Promise { const { salt, initVector: iv } = getCryptoParameters(sessionId); const master = await crypto.subtle .importKey("raw", textEncoder.encode(password), { name: "PBKDF2" }, false, [ "deriveBits", "deriveKey", ]) .catch((e) => { throw new UnwrapKeyError("starting", String(e)); }); const unwrappingKey = await crypto.subtle .deriveKey( { name: "PBKDF2", salt, iterations: 100000, hash: "SHA-256", }, master, { name: "AES-GCM", length: 256 }, true, ["wrapKey", "unwrapKey"], ) .catch((e) => { throw new UnwrapKeyError("deriving", String(e)); }); const privKey = await crypto.subtle .unwrapKey( "pkcs8", value, unwrappingKey, { name: "AES-GCM", iv, }, rsaAlgorithm, true, ["decrypt"], ) .catch((e) => { throw new UnwrapKeyError("unwrapping", String(e)); }); return privKey; } type Steps = "starting" | "deriving" | "unwrapping"; export class UnwrapKeyError extends Error { public step: Steps; public cause: string; constructor(step: Steps, cause: string) { super(`Recovering private key failed on "${step}": ${cause}`); this.step = step; this.cause = cause; } } /** * Looks like there is no easy way to do it with the Web Crypto API */ async function getPublicFromPrivate(key: CryptoKey): Promise { const jwk = await crypto.subtle.exportKey("jwk", key).catch((e) => { throw new Error(String(e)); }); delete jwk.d; delete jwk.dp; delete jwk.dq; delete jwk.q; delete jwk.qi; jwk.key_ops = ["encrypt"]; return crypto.subtle .importKey("jwk", jwk, rsaAlgorithm, true, ["encrypt"]) .catch((e) => { throw new Error(String(e)); }); } function ab2str(buf: ArrayBuffer) { return String.fromCharCode.apply(null, Array.from(new Uint8Array(buf))); } function str2ab(str: string) { const buf = new ArrayBuffer(str.length); const bufView = new Uint8Array(buf); for (let i = 0, strLen = str.length; i < strLen; i++) { bufView[i] = str.charCodeAt(i); } return buf; } function getCryptoParameters(sessionId: string): { salt: Uint8Array; initVector: Uint8Array; } { const [saltId, vectorId] = sessionId.split("-"); return { salt: decodeCrock(saltId), initVector: decodeCrock(vectorId), }; }