aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2023-05-26 10:54:42 -0300
committerSebastian <sebasjm@gmail.com>2023-05-26 10:54:42 -0300
commit62cec679833db48465d67194e39b29db985aa430 (patch)
tree55545326674a9f1a37a2b37c36a5434bbcc9afdc
parent77fb6c0d88e9889199c0e859dbade53f638ddfd4 (diff)
using taler crypto
-rw-r--r--packages/exchange-backoffice-ui/src/Dashboard.tsx55
-rw-r--r--packages/exchange-backoffice-ui/src/account.ts227
-rw-r--r--packages/exchange-backoffice-ui/src/pages/Officer.tsx53
-rw-r--r--packages/taler-util/src/taler-crypto.ts2
4 files changed, 75 insertions, 262 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);
diff --git a/packages/taler-util/src/taler-crypto.ts b/packages/taler-util/src/taler-crypto.ts
index 6fc6d14f6..4a657b621 100644
--- a/packages/taler-util/src/taler-crypto.ts
+++ b/packages/taler-util/src/taler-crypto.ts
@@ -1406,7 +1406,7 @@ export async function encryptWithDerivedKey(
const nonceSize = 24;
-async function decryptWithDerivedKey(
+export async function decryptWithDerivedKey(
ciphertext: OpaqueData,
keySeed: OpaqueData,
salt: string,