diff options
Diffstat (limited to 'packages/exchange-backoffice-ui/src/account.ts')
-rw-r--r-- | packages/exchange-backoffice-ui/src/account.ts | 227 |
1 files changed, 34 insertions, 193 deletions
diff --git a/packages/exchange-backoffice-ui/src/account.ts b/packages/exchange-backoffice-ui/src/account.ts index 6c3766940..05f0f8984 100644 --- a/packages/exchange-backoffice-ui/src/account.ts +++ b/packages/exchange-backoffice-ui/src/account.ts @@ -2,28 +2,17 @@ import { bytesToString, createEddsaKeyPair, decodeCrock, + decryptWithDerivedKey, + eddsaGetPublic, 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; + accountId: AccountId; + signingKey: SigningKey; } /** @@ -35,25 +24,36 @@ export interface Account { * @returns */ export async function unlockAccount( - salt: string, - key: string, + account: LockedAccount, password: string, ): Promise<Account> { - const rawKey = str2ab(window.atob(key)); + const rawKey = decodeCrock(account); + const rawPassword = stringToBytes(password); - const privateKey = await recoverWithPassword(rawKey, salt, password); + const signingKey = (await decryptWithDerivedKey( + rawKey, + rawPassword, + password, + ).catch((e: Error) => { + throw new UnwrapKeyError(e.message); + })) as SigningKey; - const publicKey = await getPublicFromPrivate(privateKey); + const publicKey = eddsaGetPublic(signingKey); - const pubRaw = await crypto.subtle.exportKey("spki", publicKey).catch((e) => { - throw new Error(String(e)); - }); + const accountId = encodeCrock(publicKey) as AccountId; - const accountId = btoa(ab2str(pubRaw)); - - return { accountId, secret: privateKey }; + return { accountId, signingKey }; } +declare const opaque_Account: unique symbol; +export type LockedAccount = string & { [opaque_Account]: true }; + +declare const opaque_AccountId: unique symbol; +export type AccountId = string & { [opaque_AccountId]: true }; + +declare const opaque_SigningKey: unique symbol; +export type SigningKey = Uint8Array & { [opaque_SigningKey]: true }; + /** * Create new account (secured private key) under session * secured with the given password @@ -62,9 +62,10 @@ export async function unlockAccount( * @param password * @returns */ -export async function createNewAccount(password: string) { +export async function createNewAccount( + password: string, +): Promise<LockedAccount> { const { eddsaPriv } = createEddsaKeyPair(); - const salt = createSalt(); const key = stringToBytes(password); @@ -72,178 +73,18 @@ export async function createNewAccount(password: string) { getRandomBytesF(24), key, eddsaPriv, - salt, + password, ); - 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<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 protectedPriv = encodeCrock(protectedPrivKey); - 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; + return protectedPriv as LockedAccount; } -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; + constructor(cause: string) { + super(`Recovering private key failed on: ${cause}`); 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), - }; -} |