diff options
Diffstat (limited to 'packages/exchange-backoffice-ui/src/account.ts')
-rw-r--r-- | packages/exchange-backoffice-ui/src/account.ts | 243 |
1 files changed, 243 insertions, 0 deletions
diff --git a/packages/exchange-backoffice-ui/src/account.ts b/packages/exchange-backoffice-ui/src/account.ts new file mode 100644 index 000000000..1e770794a --- /dev/null +++ b/packages/exchange-backoffice-ui/src/account.ts @@ -0,0 +1,243 @@ +import { decodeCrock, encodeCrock } 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 createNewSessionId(): string { + const salt = crypto.getRandomValues(new Uint8Array(8)); + const iv = crypto.getRandomValues(new Uint8Array(12)); + return encodeCrock(salt.buffer) + "-" + encodeCrock(iv.buffer); +} + +/** + * Restore previous session and unlock account + * + * @param sessionId string from which crypto params will be derived + * @param accountId secured private key + * @param password password for the private key + * @returns + */ +export async function unlockAccount( + sessionId: string, + accountId: string, + password: string, +) { + const key = str2ab(window.atob(accountId)); + + const privateKey = await recoverWithPassword(key, sessionId, password); + + const publicKey = await getPublicFromPrivate(privateKey); + + const pubRaw = await crypto.subtle.exportKey("spki", publicKey).catch((e) => { + throw new Error(String(e)); + }); + + const pub = btoa(ab2str(pubRaw)); + + return { accountId, pub }; +} + +/** + * Create new account (secured private key) under session + * secured with the given password + * + * @param sessionId + * @param password + * @returns + */ +export async function createNewAccount(sessionId: string, password: string) { + const { privateKey, publicKey } = await createPair(); + + const protectedPrivKey = await protectWithPassword( + privateKey, + sessionId, + password, + ); + + // const privRaw = await crypto.subtle + // .exportKey("pkcs8", privateKey) + // .catch((e) => { + // throw new Error(String(e)); + // }); + + const pubRaw = await crypto.subtle.exportKey("spki", publicKey).catch((e) => { + throw new Error(String(e)); + }); + + const pub = btoa(ab2str(pubRaw)); + const protectedPriv = btoa(ab2str(protectedPrivKey)); + + return { accountId: protectedPriv, pub }; +} + +const rsaAlgorithm: RsaHashedKeyGenParams = { + name: "RSA-OAEP", + modulusLength: 2048, + publicExponent: new Uint8Array([0x01, 0x00, 0x01]), + hash: "SHA-256", +}; + +async function createPair(): Promise<CryptoKeyPair> { + 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<ArrayBuffer> { + 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<CryptoKey> { + 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<CryptoKey> { + 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), + }; +} |