gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[taler-wallet-core] branch master updated: accounts and notifications


From: gnunet
Subject: [taler-wallet-core] branch master updated: accounts and notifications
Date: Fri, 19 May 2023 18:26:55 +0200

This is an automated email from the git hooks/post-receive script.

sebasjm pushed a commit to branch master
in repository wallet-core.

The following commit(s) were added to refs/heads/master by this push:
     new 0544b8358 accounts and notifications
0544b8358 is described below

commit 0544b8358af68df87dbc472221d8c0842c2b2db0
Author: Sebastian <sebasjm@gmail.com>
AuthorDate: Fri May 19 13:26:47 2023 -0300

    accounts and notifications
---
 packages/exchange-backoffice-ui/src/Dashboard.tsx  | 217 +++++++++++---
 packages/exchange-backoffice-ui/src/NiceForm.tsx   |   1 +
 packages/exchange-backoffice-ui/src/account.ts     | 243 ++++++++++++++++
 .../src/handlers/FormProvider.tsx                  |  12 +-
 .../src/handlers/InputLine.tsx                     |   8 +-
 .../src/handlers/InputText.tsx                     |   2 +-
 .../exchange-backoffice-ui/src/handlers/forms.ts   |  13 +
 .../exchange-backoffice-ui/src/pages/Officer.tsx   | 313 +++++++++++++++------
 8 files changed, 679 insertions(+), 130 deletions(-)

diff --git a/packages/exchange-backoffice-ui/src/Dashboard.tsx 
b/packages/exchange-backoffice-ui/src/Dashboard.tsx
index 9a0ba41d5..9be86c533 100644
--- a/packages/exchange-backoffice-ui/src/Dashboard.tsx
+++ b/packages/exchange-backoffice-ui/src/Dashboard.tsx
@@ -3,19 +3,26 @@ import {
   ChevronDownIcon,
   MagnifyingGlassIcon,
   UserIcon,
+  XCircleIcon,
 } from "@heroicons/react/20/solid";
 import {
   Bars3Icon,
   BellIcon,
+  CheckCircleIcon,
   Cog6ToothIcon,
   XMarkIcon,
 } from "@heroicons/react/24/outline";
 import { ComponentChildren, Fragment, VNode, h } from "preact";
 import { ForwardedRef, forwardRef } from "preact/compat";
-import { useRef, useState } from "preact/hooks";
+import { useEffect, useRef, useState } from "preact/hooks";
 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";
 
 /**
  * references between forms
@@ -259,6 +266,7 @@ export function Dashboard({
             setSidebarOpen(true);
           }}
         />
+        <Notifications />
         <main class="py-10 px-4 sm:px-6 lg:px-8">
           <div class="mx-auto max-w-3xl">
             <Router
@@ -355,6 +363,9 @@ function NavigationBar({
 }
 
 function TopBar({ onOpenSidebar }: { onOpenSidebar: () => void }) {
+  const password = useMemoryStorage("password");
+  const officer = useLocalStorage("officer");
+
   return (
     <div class="sticky top-0 z-40 flex h-16 shrink-0 items-center gap-x-4 
border-b border-gray-200 bg-white px-4 shadow-sm sm:gap-x-6 sm:px-6 lg:px-8">
       <button
@@ -402,60 +413,66 @@ function TopBar({ onOpenSidebar }: { onOpenSidebar: () => 
void }) {
             aria-hidden="true"
           />
 
-          {/* Profile dropdown */}
-          <Menu
-            as="div"
-            /* @ts-ignore */
-            class="relative"
-          >
-            <Menu.Button class="-m-1.5 flex items-center p-1.5">
-              <span class="sr-only">Open user menu</span>
-              <img
-                class="h-8 w-8 rounded-full bg-gray-50"
-                
src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80";
-                alt=""
-              />
-              <span class="hidden lg:flex lg:items-center">
-                <span
-                  class="ml-4 text-sm font-semibold leading-6 text-gray-900"
-                  aria-hidden="true"
-                >
-                  Tom Cook
-                </span>
-                <ChevronDownIcon
-                  class="ml-2 h-5 w-5 text-gray-400"
-                  aria-hidden="true"
-                />
-              </span>
-            </Menu.Button>
-            <Transition
-              as={Fragment}
-              enter="transition ease-out duration-100"
-              enterFrom="transform opacity-0 scale-95"
-              enterTo="transform opacity-100 scale-100"
-              leave="transition ease-in duration-75"
-              leaveFrom="transform opacity-100 scale-100"
-              leaveTo="transform opacity-0 scale-95"
+          {officer.value === undefined ? (
+            <div />
+          ) : (
+            <Menu
+              as="div"
+              /* @ts-ignore */
+              class="relative"
             >
-              <Menu.Items class="absolute right-0 z-10 mt-2.5 w-32 
origin-top-right rounded-md bg-white py-2 shadow-lg ring-1 ring-gray-900/5 
focus:outline-none">
-                {userNavigation.map((item) => (
-                  <Menu.Item key={item.name}>
+              <Menu.Button class="-m-1.5 flex items-center p-1.5">
+                <span class="sr-only">Open user menu</span>
+                <img
+                  class="h-8 w-8 rounded-full bg-gray-50"
+                  
src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80";
+                  alt=""
+                />
+                <span class="hidden lg:flex lg:items-center">
+                  <span
+                    class="ml-4 text-sm font-semibold leading-6 text-gray-900"
+                    aria-hidden="true"
+                  >
+                    {/* Tom Cook */}
+                    {officer.value?.substring(0, 6)}
+                  </span>
+                  <ChevronDownIcon
+                    class="ml-2 h-5 w-5 text-gray-400"
+                    aria-hidden="true"
+                  />
+                </span>
+              </Menu.Button>
+              <Transition
+                as={Fragment}
+                enter="transition ease-out duration-100"
+                enterFrom="transform opacity-0 scale-95"
+                enterTo="transform opacity-100 scale-100"
+                leave="transition ease-in duration-75"
+                leaveFrom="transform opacity-100 scale-100"
+                leaveTo="transform opacity-0 scale-95"
+              >
+                <Menu.Items class="absolute right-0 z-10 mt-2.5 w-48 
origin-top-right rounded-md bg-white py-2 shadow-lg ring-1 ring-gray-900/5 
focus:outline-none">
+                  <Menu.Item>
                     {({ active }: { active: boolean }) => (
                       <a
-                        href={item.href}
+                        // href={item.href}
+                        onClick={() => {
+                          officer.reset();
+                          password.reset();
+                        }}
                         class={classNames(
                           active ? "bg-gray-50" : "",
                           "block px-3 py-1 text-sm leading-6 text-gray-900",
                         )}
                       >
-                        {item.name}
+                        Forget account
                       </a>
                     )}
                   </Menu.Item>
-                ))}
-              </Menu.Items>
-            </Transition>
-          </Menu>
+                </Menu.Items>
+              </Transition>
+            </Menu>
+          )}
         </div>
       </div>
     </div>
@@ -473,3 +490,115 @@ function Footer() {
     </footer>
   );
 }
+
+function Notifications() {
+  const ns = useNotifications();
+
+  // useEffect(() => {
+  //   if (ns.length) {
+  //     // remove notifications after some timeout
+  //   }
+  // }, []);
+  {
+    /* <!-- Global notification live region, render this permanently at the 
end of the document --> */
+  }
+  console.log("render", ns.length);
+  return (
+    <div
+      aria-live="assertive"
+      class="pointer-events-none fixed inset-0 flex items-end px-4 py-6 
sm:items-start sm:p-6 z-50"
+    >
+      <div class="flex w-full flex-col items-center space-y-4 sm:items-end ">
+        {/* <!--
+  Notification panel, dynamically insert this into the live region when it 
needs to be displayed
+
+  Entering: "transform ease-out duration-300 transition"
+    From: "translate-y-2 opacity-0 sm:translate-y-0 sm:translate-x-2"
+    To: "translate-y-0 opacity-100 sm:translate-x-0"
+  Leaving: "transition ease-in duration-100"
+    From: "opacity-100"
+    To: "opacity-0"
+--> */}
+        {ns.map(({ message, remove }) => {
+          switch (message.type) {
+            case "error": {
+              return (
+                <div class="pointer-events-auto w-full max-w-sm 
overflow-hidden rounded-lg bg-white shadow-lg ring-1 ring-black ring-opacity-5 
">
+                  <div class="p-4 ">
+                    <div class="flex items-start ">
+                      <div class="flex-shrink-0">
+                        <XCircleIcon class="h-6 w-6 text-red-400" />
+                      </div>
+                      <div class="ml-3 w-0 flex-1 pt-0.5">
+                        <p class="text-sm font-medium text-gray-900">
+                          {message.title}
+                        </p>
+                        {message.description && (
+                          <p class="mt-1 text-sm text-gray-500">
+                            {message.description}
+                          </p>
+                        )}
+                      </div>
+                      <div class="ml-4 flex flex-shrink-0">
+                        <button
+                          type="button"
+                          onClick={remove}
+                          class="inline-flex rounded-md bg-white text-gray-400 
hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 
focus:ring-offset-2"
+                        >
+                          <span class="sr-only">Close</span>
+                          <svg
+                            class="h-5 w-5"
+                            viewBox="0 0 20 20"
+                            fill="currentColor"
+                            aria-hidden="true"
+                          >
+                            <path d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 
10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 
101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" />
+                          </svg>
+                        </button>
+                      </div>
+                    </div>
+                  </div>
+                </div>
+              );
+            }
+            case "info": {
+              return (
+                <div class="pointer-events-auto w-full max-w-sm 
overflow-hidden rounded-lg bg-white shadow-lg ring-1 ring-black ring-opacity-5 
">
+                  <div class="p-4 ">
+                    <div class="flex items-start ">
+                      <div class="flex-shrink-0">
+                        <CheckCircleIcon class="h-6 w-6 text-green-400" />
+                      </div>
+                      <div class="ml-3 w-0 flex-1 pt-0.5">
+                        <p class="text-sm font-medium text-gray-900">
+                          {message.title}
+                        </p>
+                      </div>
+                      <div class="ml-4 flex flex-shrink-0">
+                        <button
+                          type="button"
+                          onClick={remove}
+                          class="inline-flex rounded-md bg-white text-gray-400 
hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 
focus:ring-offset-2"
+                        >
+                          <span class="sr-only">Close</span>
+                          <svg
+                            class="h-5 w-5"
+                            viewBox="0 0 20 20"
+                            fill="currentColor"
+                            aria-hidden="true"
+                          >
+                            <path d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 
10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 
101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" />
+                          </svg>
+                        </button>
+                      </div>
+                    </div>
+                  </div>
+                </div>
+              );
+            }
+          }
+        })}
+      </div>
+    </div>
+  );
+}
diff --git a/packages/exchange-backoffice-ui/src/NiceForm.tsx 
b/packages/exchange-backoffice-ui/src/NiceForm.tsx
index b7790bbec..593a373c1 100644
--- a/packages/exchange-backoffice-ui/src/NiceForm.tsx
+++ b/packages/exchange-backoffice-ui/src/NiceForm.tsx
@@ -18,6 +18,7 @@ export function NiceForm<T extends object>({
     <FormProvider
       initialValue={initial}
       onUpdate={onUpdate}
+      onSubmit={() => {}}
       computeFormState={form.behavior}
     >
       <div class="space-y-10 divide-y -mt-5 divide-gray-900/10">
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),
+  };
+}
diff --git a/packages/exchange-backoffice-ui/src/handlers/FormProvider.tsx 
b/packages/exchange-backoffice-ui/src/handlers/FormProvider.tsx
index d8877333c..87c4c43fb 100644
--- a/packages/exchange-backoffice-ui/src/handlers/FormProvider.tsx
+++ b/packages/exchange-backoffice-ui/src/handlers/FormProvider.tsx
@@ -41,10 +41,12 @@ export function FormProvider<T>({
   children,
   initialValue,
   onUpdate,
+  onSubmit,
   computeFormState,
 }: {
   initialValue?: Partial<T>;
   onUpdate?: (v: Partial<T>) => void;
+  onSubmit: (v: T) => void;
   computeFormState?: (v: T) => FormState<T>;
   children: ComponentChildren;
 }): VNode {
@@ -58,7 +60,15 @@ export function FormProvider<T>({
     <FormContext.Provider
       value={{ initialValue, value, onUpdate, computeFormState }}
     >
-      <form>{children}</form>
+      <form
+        onSubmit={(e) => {
+          e.preventDefault();
+          //@ts-ignore
+          onSubmit(value.current);
+        }}
+      >
+        {children}
+      </form>
     </FormContext.Provider>
   );
 }
diff --git a/packages/exchange-backoffice-ui/src/handlers/InputLine.tsx 
b/packages/exchange-backoffice-ui/src/handlers/InputLine.tsx
index 255654949..32b16313d 100644
--- a/packages/exchange-backoffice-ui/src/handlers/InputLine.tsx
+++ b/packages/exchange-backoffice-ui/src/handlers/InputLine.tsx
@@ -23,7 +23,7 @@ interface StringConverter<T> {
 }
 
 export interface UIFormProps<T> {
-  name: string;
+  name: keyof T;
   label: TranslatedString;
   placeholder?: TranslatedString;
   tooltip?: TranslatedString;
@@ -181,7 +181,11 @@ function defaultFromString(v: string) {
   return v;
 }
 
-export function InputLine<T>(props: { type: string } & UIFormProps<T>): VNode {
+type InputType = "text" | "text-area" | "password" | "email";
+
+export function InputLine<T>(
+  props: { type: InputType } & UIFormProps<T>,
+): VNode {
   const { name, placeholder, before, after, converter, type } = props;
   const { value, onChange, state, isDirty } = useField(name);
 
diff --git a/packages/exchange-backoffice-ui/src/handlers/InputText.tsx 
b/packages/exchange-backoffice-ui/src/handlers/InputText.tsx
index 107d87860..014730d92 100644
--- a/packages/exchange-backoffice-ui/src/handlers/InputText.tsx
+++ b/packages/exchange-backoffice-ui/src/handlers/InputText.tsx
@@ -1,6 +1,6 @@
 import { VNode, h } from "preact";
 import { InputLine, UIFormProps } from "./InputLine.js";
 
-export function InputText(props: UIFormProps<string>): VNode {
+export function InputText<T>(props: UIFormProps<T>): VNode {
   return <InputLine type="text" {...props} />;
 }
diff --git a/packages/exchange-backoffice-ui/src/handlers/forms.ts 
b/packages/exchange-backoffice-ui/src/handlers/forms.ts
index 1d6a7daa4..a97b8561d 100644
--- a/packages/exchange-backoffice-ui/src/handlers/forms.ts
+++ b/packages/exchange-backoffice-ui/src/handlers/forms.ts
@@ -11,6 +11,8 @@ import { InputFile } from "./InputFile.js";
 import { Caption } from "./Caption.js";
 import { Group } from "./Group.js";
 import { InputSelectOne } from "./InputSelectOne.js";
+import { FormProvider } from "./FormProvider.js";
+import { InputLine } from "./InputLine.js";
 
 export type DoubleColumnForm = DoubleColumnFormSection[];
 
@@ -94,3 +96,14 @@ export function RenderAllFieldsByUiConfig({
     }),
   );
 }
+
+type FormSet<T> = {
+  Provider: typeof FormProvider<T>;
+  InputLine: typeof InputLine<T>;
+};
+export function createNewForm<T>(): FormSet<T> {
+  return {
+    Provider: FormProvider,
+    InputLine: InputLine,
+  };
+}
diff --git a/packages/exchange-backoffice-ui/src/pages/Officer.tsx 
b/packages/exchange-backoffice-ui/src/pages/Officer.tsx
index c72ca0720..4d8b90228 100644
--- a/packages/exchange-backoffice-ui/src/pages/Officer.tsx
+++ b/packages/exchange-backoffice-ui/src/pages/Officer.tsx
@@ -1,116 +1,265 @@
-import { useLocalStorage } from "@gnu-taler/web-util/browser";
-import { h } from "preact";
+import { TranslatedString } from "@gnu-taler/taler-util";
+import {
+  notifyError,
+  notifyInfo,
+  useLocalStorage,
+  useMemoryStorage,
+} from "@gnu-taler/web-util/browser";
+import { VNode, h } from "preact";
 import { useEffect, useState } from "preact/hooks";
+import {
+  UnwrapKeyError,
+  createNewAccount,
+  createNewSessionId,
+  unlockAccount,
+} from "../account.js";
+import { createNewForm } from "../handlers/forms.js";
 
-const oldKey =
-  
"MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDPQVq8F0Ce6kTXKQ5Ea2fZRoap6poFYs0FOln8o8+ehGI8rDdMBzNU3pLIlOMKs/vKvhDNMG4m4xxb92wDbvefDxkxaEkbRSZnRiJd4MIbh8Lx8zvFbLp03rkXu9KPN8IprKOXxgN7xbxm0KKcu03rtqLiOvC1gMqja2LMIPCi32nyNneduszHZ57d+CqIKZdVnaqAcXOSMAQsVoEq2joBOeIaSAnIJHg+T8HQ+VcLV8Y722jhX/bH84IyEMup9e7mhgVFnHgINc77c6TONH8H+dHlXCQ+hMPGw9wM+wgpJgIDzrhIN+QSjn283EOXD6z6dpiWBdEYfJRLHwEWk8wNAgMBAAECggEAB/anZrMasQsoXP9qBG1Uvq+r4fXZODFtK5vBNGi+RAWAhCX2iU3SMPB3wbby0wj1DlESR91qBhrTjqG+/TzIz
 [...]
 export function Officer() {
-  const storage = useLocalStorage("officer");
-  const [keys, setKeys] = useState({ priv: "", pub: "" });
+  const password = useMemoryStorage("password");
+  const session = useLocalStorage("session");
+  const officer = useLocalStorage("officer");
+  const [keys, setKeys] = useState({ accountId: "", pub: "" });
+
   useEffect(() => {
-    loadPreviousSession(oldKey).then((keys) =>
-      setKeys(keys ?? { priv: "", pub: "" }),
-    );
-    // generateNewId().then((keys) => setKeys(keys));
+    if (
+      officer.value === undefined ||
+      session.value === undefined ||
+      password.value === undefined
+    ) {
+      return;
+    }
+    unlockAccount(session.value, officer.value, password.value)
+      .then((keys) => setKeys(keys ?? { accountId: "", pub: "" }))
+      .catch((e) => {
+        if (e instanceof UnwrapKeyError) {
+          console.log(e);
+        }
+      });
+  }, [officer.value, session.value, password.value]);
+
+  useEffect(() => {
+    if (!session.value) {
+      session.update(createNewSessionId());
+    }
   }, []);
 
-  console.log(keys.pub);
-  console.log(keys.priv);
+  const { value: sessionId } = session;
+  if (!sessionId) {
+    return <div>loading...</div>;
+  }
+
+  if (officer.value === undefined) {
+    return (
+      <CreateAccount
+        sessionId={sessionId}
+        onNewAccount={(id) => {
+          password.reset();
+          officer.update(id);
+        }}
+      />
+    );
+  }
+
+  console.log("pwd", password.value);
+  if (password.value === undefined) {
+    return (
+      <UnlockAccount
+        sessionId={sessionId}
+        accountId={officer.value}
+        onAccountUnlocked={(pwd) => {
+          password.update(pwd);
+        }}
+      />
+    );
+  }
+
   return (
     <div>
       <div>Officer</div>
+      <h1>{sessionId}</h1>
       <h1 class="my-2 text-3xl font-bold tracking-tight text-gray-900 ">
         Public key
       </h1>
       <div>
-        -----BEGIN PUBLIC KEY-----
-        <p class="mt-6 leading-8 text-gray-700 break-all">{keys.pub}</p>
-        -----END PUBLIC KEY-----
+        <p class="mt-6 leading-8 text-gray-700 break-all">
+          -----BEGIN PUBLIC KEY-----
+          <div>{keys.pub}</div>
+          -----END PUBLIC KEY-----
+        </p>
       </div>
       <h1 class="my-2 text-3xl font-bold tracking-tight text-gray-900 ">
         Private key
       </h1>
       <div>
-        -----BEGIN PRIVATE KEY-----
-        <p class="mt-6 leading-8 text-gray-700 break-all">{keys.priv}</p>
-        -----END PRIVATE KEY-----
+        <p class="mt-6 leading-8 text-gray-700 break-all">
+          -----BEGIN PRIVATE KEY-----
+          <div>{keys.accountId}</div>
+          -----END PRIVATE KEY-----
+        </p>
       </div>
     </div>
   );
 }
 
-const rsaAlgorithm: RsaHashedKeyGenParams = {
-  name: "RSA-OAEP",
-  modulusLength: 2048,
-  publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
-  hash: "SHA-256",
-};
-
-async function generateNewId() {
-  const key = await crypto.subtle.generateKey(rsaAlgorithm, true, [
-    "encrypt",
-    "decrypt",
-  ]);
-
-  if (key instanceof CryptoKey) {
-    throw Error("unexpected key without pair");
-  }
-  const { privateKey, publicKey } = key;
-  const privRaw = await crypto.subtle.exportKey("pkcs8", privateKey);
-
-  const pubRaw = await crypto.subtle.exportKey("spki", publicKey);
+function CreateAccount({
+  sessionId,
+  onNewAccount,
+}: {
+  sessionId: string;
+  onNewAccount: (accountId: string) => void;
+}): VNode {
+  const Form = createNewForm<{
+    email: string;
+    password: string;
+  }>();
 
-  const priv = btoa(ab2str(privRaw));
-
-  const pub = btoa(ab2str(pubRaw));
-  return { priv, pub };
-}
+  return (
+    <div class="flex min-h-full flex-col ">
+      <div class="sm:mx-auto sm:w-full sm:max-w-md">
+        <h2 class="mt-6 text-center text-2xl font-bold leading-9 
tracking-tight text-gray-900">
+          Create account
+        </h2>
+      </div>
 
-async function loadPreviousSession(priv: string) {
-  const key = str2ab(window.atob(priv));
-  const privateKey = await window.crypto.subtle
-    .importKey("pkcs8", key, rsaAlgorithm, true, ["decrypt"])
-    .catch(throwErrorWithStack);
+      <div class="mt-10 sm:mx-auto sm:w-full sm:max-w-[480px] ">
+        <div class="bg-gray-100 px-6 py-6 shadow sm:rounded-lg sm:px-12">
+          <Form.Provider
+            onSubmit={async (v) => {
+              const keys = await createNewAccount(sessionId, v.password);
+              onNewAccount(keys.accountId);
+            }}
+          >
+            <div class="mb-4">
+              <Form.InputLine
+                label={"Email" as TranslatedString}
+                name="email"
+                type="email"
+                required
+              />
+            </div>
 
-  if (!privateKey) return undefined;
+            <div class="mb-4">
+              <Form.InputLine
+                label={"Password" as TranslatedString}
+                name="password"
+                type="password"
+                required
+              />
+            </div>
 
-  // export private key to JWK
-  const jwk = await crypto.subtle
-    .exportKey("jwk", privateKey)
-    .catch(throwErrorWithStack);
+            <div class="mt-8">
+              <button
+                type="submit"
+                class="flex w-full justify-center rounded-md bg-indigo-600 
px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm 
hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 
focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
+              >
+                Create
+              </button>
+            </div>
+          </Form.Provider>
+        </div>
+      </div>
+    </div>
+  );
+}
 
-  // remove private data from JWK
-  delete jwk.d;
-  delete jwk.dp;
-  delete jwk.dq;
-  delete jwk.q;
-  delete jwk.qi;
-  jwk.key_ops = ["encrypt"];
+function UnlockAccount({
+  sessionId,
+  accountId,
+  onAccountUnlocked,
+}: {
+  sessionId: string;
+  accountId: string;
+  onAccountUnlocked: (password: string) => void;
+}): VNode {
+  const Form = createNewForm<{
+    sessionId: string;
+    accountId: string;
+    password: string;
+  }>();
 
-  const publicKey = await crypto.subtle
-    .importKey("jwk", jwk, rsaAlgorithm, true, ["encrypt"])
-    .catch(throwErrorWithStack);
+  return (
+    <div class="flex min-h-full flex-col ">
+      <div class="sm:mx-auto sm:w-full sm:max-w-md">
+        <h2 class="mt-6 text-center text-2xl font-bold leading-9 
tracking-tight text-gray-900">
+          Unlock account
+        </h2>
+      </div>
 
-  const pubRaw = await crypto.subtle
-    .exportKey("spki", publicKey)
-    .catch(throwErrorWithStack);
+      <div class="mt-10 sm:mx-auto sm:w-full sm:max-w-[480px] ">
+        <div class="bg-gray-100 px-6 py-6 shadow sm:rounded-lg sm:px-12">
+          <Form.Provider
+            initialValue={{
+              sessionId,
+              accountId:
+                accountId.substring(0, 6) +
+                "..." +
+                accountId.substring(accountId.length - 6),
+            }}
+            computeFormState={(v) => {
+              return {
+                accountId: {
+                  disabled: true,
+                },
+                sessionId: {
+                  disabled: true,
+                },
+              };
+            }}
+            onSubmit={async (v) => {
+              try {
+                // test login
+                await unlockAccount(sessionId, accountId, v.password);
 
-  const pub = btoa(ab2str(pubRaw));
+                onAccountUnlocked(v.password ?? "");
+                notifyInfo("Account unlocked" as TranslatedString);
+              } catch (e) {
+                if (e instanceof UnwrapKeyError) {
+                  notifyError(
+                    "Could not unlock account" as any,
+                    e.message as any,
+                  );
+                } else {
+                  throw e;
+                }
+              }
+            }}
+          >
+            <div class="mb-4">
+              <Form.InputLine
+                label={"Session" as TranslatedString}
+                name="sessionId"
+                type="text"
+              />
+            </div>
+            <div class="mb-4">
+              <Form.InputLine
+                label={"AccountId" as TranslatedString}
+                name="accountId"
+                type="text"
+              />
+            </div>
 
-  return { priv, pub };
-}
+            <div class="mb-4">
+              <Form.InputLine
+                label={"Password" as TranslatedString}
+                name="password"
+                type="password"
+                required
+              />
+            </div>
 
-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 throwErrorWithStack(e: Error): never {
-  throw new Error(e.message);
+            <div class="mt-8">
+              <button
+                type="submit"
+                class="flex w-full justify-center rounded-md bg-indigo-600 
px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm 
hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 
focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
+              >
+                Unlock
+              </button>
+            </div>
+          </Form.Provider>
+        </div>
+      </div>
+    </div>
+  );
 }

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]