diff options
Diffstat (limited to 'packages/exchange-backoffice-ui/src')
| -rw-r--r-- | packages/exchange-backoffice-ui/src/Dashboard.tsx | 55 | ||||
| -rw-r--r-- | packages/exchange-backoffice-ui/src/account.ts | 227 | ||||
| -rw-r--r-- | packages/exchange-backoffice-ui/src/pages/Officer.tsx | 53 | 
3 files changed, 74 insertions, 261 deletions
| diff --git a/packages/exchange-backoffice-ui/src/Dashboard.tsx b/packages/exchange-backoffice-ui/src/Dashboard.tsx index 9f4a43513..6794ca1f8 100644 --- a/packages/exchange-backoffice-ui/src/Dashboard.tsx +++ b/packages/exchange-backoffice-ui/src/Dashboard.tsx @@ -1,36 +1,13 @@ -import { Dialog, Menu, Transition } from "@headlessui/react"; -import { -  ChevronDownIcon, -  MagnifyingGlassIcon, -  UserIcon, -  XCircleIcon, -} from "@heroicons/react/20/solid"; -import { -  Bars3Icon, -  BellIcon, -  CheckCircleIcon, -  Cog6ToothIcon, -  XMarkIcon, -} from "@heroicons/react/24/outline"; +import { useNotifications } from "@gnu-taler/web-util/browser"; +import { Dialog, Transition } from "@headlessui/react"; +import { UserIcon, XCircleIcon } from "@heroicons/react/20/solid"; +import { CheckCircleIcon, XMarkIcon } from "@heroicons/react/24/outline"; +import { InformationCircleIcon } from "@heroicons/react/24/solid";  import { ComponentChildren, Fragment, VNode, h } from "preact"; -import { ForwardedRef, forwardRef } from "preact/compat"; -import { useEffect, useRef, useState } from "preact/hooks"; +import { useState } from "preact/hooks"; +import logo from "./assets/logo-2021.svg";  import { Pages } from "./pages.js";  import { Router, useCurrentLocation } from "./route.js"; -import { InformationCircleIcon } from "@heroicons/react/24/solid"; -import { -  useLocalStorage, -  useMemoryStorage, -  useNotifications, -} from "@gnu-taler/web-util/browser"; -import { -  AbsoluteTime, -  Codec, -  buildCodecForObject, -  codecForAbsoluteTime, -  codecForString, -} from "@gnu-taler/taler-util"; -import logo from "./assets/logo-2021.svg";  function classNames(...classes: string[]) {    return classes.filter(Boolean).join(" "); @@ -329,25 +306,7 @@ function NavigationBar({    );  } -export interface Officer { -  salt: string; -  when: AbsoluteTime; -  key: string; -} - -export const codecForOfficer = (): Codec<Officer> => -  buildCodecForObject<Officer>() -    .property("salt", codecForString()) // FIXME -    .property("when", codecForAbsoluteTime) // FIXME -    .property("key", codecForString()) -    .build("Officer"); -  function TopBar({ onOpenSidebar }: { onOpenSidebar: () => void }) { -  const password = useMemoryStorage("password"); -  const officer = useLocalStorage("officer", { -    codec: codecForOfficer(), -  }); -    return (      <div class="relative flex h-16 justify-between">        <div class="relative z-10 flex p-2 lg:hidden"> 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), -  }; -} diff --git a/packages/exchange-backoffice-ui/src/pages/Officer.tsx b/packages/exchange-backoffice-ui/src/pages/Officer.tsx index 39e368b37..40ec33018 100644 --- a/packages/exchange-backoffice-ui/src/pages/Officer.tsx +++ b/packages/exchange-backoffice-ui/src/pages/Officer.tsx @@ -1,4 +1,11 @@ -import { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util"; +import { +  AbsoluteTime, +  Codec, +  TranslatedString, +  buildCodecForObject, +  codecForAbsoluteTime, +  codecForString, +} from "@gnu-taler/taler-util";  import {    notifyError,    notifyInfo, @@ -10,12 +17,25 @@ import { VNode, h } from "preact";  import { useEffect, useState } from "preact/hooks";  import {    Account, +  LockedAccount,    UnwrapKeyError,    createNewAccount,    unlockAccount,  } from "../account.js";  import { createNewForm } from "../handlers/forms.js"; -import { Officer, codecForOfficer } from "../Dashboard.js"; + +export interface Officer { +  account: LockedAccount; +  when: AbsoluteTime; +} + +const codecForLockedAccount = codecForString() as Codec<LockedAccount>; + +export const codecForOfficer = (): Codec<Officer> => +  buildCodecForObject<Officer>() +    .property("account", codecForLockedAccount) // FIXME +    .property("when", codecForAbsoluteTime) // FIXME +    .build("Officer");  export function Officer() {    const password = useMemoryStorage("password"); @@ -29,7 +49,7 @@ export function Officer() {        return;      } -    unlockAccount(officer.value.salt, officer.value.key, password.value) +    unlockAccount(officer.value.account, password.value)        .then((keys) => setKeys(keys ?? { accountId: "", pub: "" }))        .catch((e) => {          if (e instanceof UnwrapKeyError) { @@ -38,16 +58,12 @@ export function Officer() {        });    }, [officer.value, password.value]); -  if ( -    officer.value === undefined || -    !officer.value.key || -    !officer.value.salt -  ) { +  if (officer.value === undefined || !officer.value.account) {      return (        <CreateAccount -        onNewAccount={(salt, key, pwd) => { +        onNewAccount={(account, pwd) => {            password.update(pwd); -          officer.update({ salt, when: AbsoluteTime.now(), key }); +          officer.update({ account, when: AbsoluteTime.now() });          }}        />      ); @@ -56,8 +72,7 @@ export function Officer() {    if (password.value === undefined) {      return (        <UnlockAccount -        salt={officer.value.salt} -        sealedKey={officer.value.key} +        lockedAccount={officer.value.account}          onAccountUnlocked={(pwd) => {            password.update(pwd);          }} @@ -114,7 +129,7 @@ export function Officer() {  function CreateAccount({    onNewAccount,  }: { -  onNewAccount: (salt: string, accountId: string, password: string) => void; +  onNewAccount: (account: LockedAccount, password: string) => void;  }): VNode {    const { i18n } = useTranslationContext();    const Form = createNewForm<{ @@ -158,8 +173,8 @@ function CreateAccount({                };              }}              onSubmit={async (v) => { -              const keys = await createNewAccount(v.password); -              onNewAccount(keys.salt, keys.accountId, v.password); +              const account = await createNewAccount(v.password); +              onNewAccount(account, v.password);              }}            >              <div class="mb-4"> @@ -198,12 +213,10 @@ function CreateAccount({  }  function UnlockAccount({ -  salt, -  sealedKey, +  lockedAccount,    onAccountUnlocked,  }: { -  salt: string; -  sealedKey: string; +  lockedAccount: LockedAccount;    onAccountUnlocked: (password: string) => void;  }): VNode {    const Form = createNewForm<{ @@ -228,7 +241,7 @@ function UnlockAccount({              onSubmit={async (v) => {                try {                  // test login -                await unlockAccount(salt, sealedKey, v.password); +                await unlockAccount(lockedAccount, v.password);                  onAccountUnlocked(v.password ?? "");                  notifyInfo("Account unlocked" as TranslatedString); | 
