using taler crypto
This commit is contained in:
parent
77fb6c0d88
commit
62cec67983
@ -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">
|
||||
|
@ -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);
|
||||
const protectedPriv = encodeCrock(protectedPrivKey);
|
||||
|
||||
return { accountId: protectedPriv, salt };
|
||||
return protectedPriv as LockedAccount;
|
||||
}
|
||||
|
||||
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;
|
||||
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),
|
||||
};
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -1406,7 +1406,7 @@ export async function encryptWithDerivedKey(
|
||||
|
||||
const nonceSize = 24;
|
||||
|
||||
async function decryptWithDerivedKey(
|
||||
export async function decryptWithDerivedKey(
|
||||
ciphertext: OpaqueData,
|
||||
keySeed: OpaqueData,
|
||||
salt: string,
|
||||
|
Loading…
Reference in New Issue
Block a user