using taler crypto
This commit is contained in:
parent
77fb6c0d88
commit
62cec67983
@ -1,36 +1,13 @@
|
|||||||
import { Dialog, Menu, Transition } from "@headlessui/react";
|
import { useNotifications } from "@gnu-taler/web-util/browser";
|
||||||
import {
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
ChevronDownIcon,
|
import { UserIcon, XCircleIcon } from "@heroicons/react/20/solid";
|
||||||
MagnifyingGlassIcon,
|
import { CheckCircleIcon, XMarkIcon } from "@heroicons/react/24/outline";
|
||||||
UserIcon,
|
import { InformationCircleIcon } from "@heroicons/react/24/solid";
|
||||||
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 { ComponentChildren, Fragment, VNode, h } from "preact";
|
||||||
import { ForwardedRef, forwardRef } from "preact/compat";
|
import { useState } from "preact/hooks";
|
||||||
import { useEffect, useRef, useState } from "preact/hooks";
|
import logo from "./assets/logo-2021.svg";
|
||||||
import { Pages } from "./pages.js";
|
import { Pages } from "./pages.js";
|
||||||
import { Router, useCurrentLocation } from "./route.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[]) {
|
function classNames(...classes: string[]) {
|
||||||
return classes.filter(Boolean).join(" ");
|
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 }) {
|
function TopBar({ onOpenSidebar }: { onOpenSidebar: () => void }) {
|
||||||
const password = useMemoryStorage("password");
|
|
||||||
const officer = useLocalStorage("officer", {
|
|
||||||
codec: codecForOfficer(),
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="relative flex h-16 justify-between">
|
<div class="relative flex h-16 justify-between">
|
||||||
<div class="relative z-10 flex p-2 lg:hidden">
|
<div class="relative z-10 flex p-2 lg:hidden">
|
||||||
|
@ -2,28 +2,17 @@ import {
|
|||||||
bytesToString,
|
bytesToString,
|
||||||
createEddsaKeyPair,
|
createEddsaKeyPair,
|
||||||
decodeCrock,
|
decodeCrock,
|
||||||
|
decryptWithDerivedKey,
|
||||||
|
eddsaGetPublic,
|
||||||
encodeCrock,
|
encodeCrock,
|
||||||
encryptWithDerivedKey,
|
encryptWithDerivedKey,
|
||||||
getRandomBytesF,
|
getRandomBytesF,
|
||||||
stringToBytes,
|
stringToBytes,
|
||||||
} from "@gnu-taler/taler-util";
|
} 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 {
|
export interface Account {
|
||||||
accountId: string;
|
accountId: AccountId;
|
||||||
secret: CryptoKey;
|
signingKey: SigningKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -35,25 +24,36 @@ export interface Account {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export async function unlockAccount(
|
export async function unlockAccount(
|
||||||
salt: string,
|
account: LockedAccount,
|
||||||
key: string,
|
|
||||||
password: string,
|
password: string,
|
||||||
): Promise<Account> {
|
): 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) => {
|
const accountId = encodeCrock(publicKey) as AccountId;
|
||||||
throw new Error(String(e));
|
|
||||||
});
|
|
||||||
|
|
||||||
const accountId = btoa(ab2str(pubRaw));
|
return { accountId, signingKey };
|
||||||
|
|
||||||
return { accountId, secret: privateKey };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
* Create new account (secured private key) under session
|
||||||
* secured with the given password
|
* secured with the given password
|
||||||
@ -62,9 +62,10 @@ export async function unlockAccount(
|
|||||||
* @param password
|
* @param password
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export async function createNewAccount(password: string) {
|
export async function createNewAccount(
|
||||||
|
password: string,
|
||||||
|
): Promise<LockedAccount> {
|
||||||
const { eddsaPriv } = createEddsaKeyPair();
|
const { eddsaPriv } = createEddsaKeyPair();
|
||||||
const salt = createSalt();
|
|
||||||
|
|
||||||
const key = stringToBytes(password);
|
const key = stringToBytes(password);
|
||||||
|
|
||||||
@ -72,178 +73,18 @@ export async function createNewAccount(password: string) {
|
|||||||
getRandomBytesF(24),
|
getRandomBytesF(24),
|
||||||
key,
|
key,
|
||||||
eddsaPriv,
|
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 {
|
export class UnwrapKeyError extends Error {
|
||||||
public step: Steps;
|
|
||||||
public cause: string;
|
public cause: string;
|
||||||
constructor(step: Steps, cause: string) {
|
constructor(cause: string) {
|
||||||
super(`Recovering private key failed on "${step}": ${cause}`);
|
super(`Recovering private key failed on: ${cause}`);
|
||||||
this.step = step;
|
|
||||||
this.cause = 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 {
|
import {
|
||||||
notifyError,
|
notifyError,
|
||||||
notifyInfo,
|
notifyInfo,
|
||||||
@ -10,12 +17,25 @@ import { VNode, h } from "preact";
|
|||||||
import { useEffect, useState } from "preact/hooks";
|
import { useEffect, useState } from "preact/hooks";
|
||||||
import {
|
import {
|
||||||
Account,
|
Account,
|
||||||
|
LockedAccount,
|
||||||
UnwrapKeyError,
|
UnwrapKeyError,
|
||||||
createNewAccount,
|
createNewAccount,
|
||||||
unlockAccount,
|
unlockAccount,
|
||||||
} from "../account.js";
|
} from "../account.js";
|
||||||
import { createNewForm } from "../handlers/forms.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() {
|
export function Officer() {
|
||||||
const password = useMemoryStorage("password");
|
const password = useMemoryStorage("password");
|
||||||
@ -29,7 +49,7 @@ export function Officer() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
unlockAccount(officer.value.salt, officer.value.key, password.value)
|
unlockAccount(officer.value.account, password.value)
|
||||||
.then((keys) => setKeys(keys ?? { accountId: "", pub: "" }))
|
.then((keys) => setKeys(keys ?? { accountId: "", pub: "" }))
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
if (e instanceof UnwrapKeyError) {
|
if (e instanceof UnwrapKeyError) {
|
||||||
@ -38,16 +58,12 @@ export function Officer() {
|
|||||||
});
|
});
|
||||||
}, [officer.value, password.value]);
|
}, [officer.value, password.value]);
|
||||||
|
|
||||||
if (
|
if (officer.value === undefined || !officer.value.account) {
|
||||||
officer.value === undefined ||
|
|
||||||
!officer.value.key ||
|
|
||||||
!officer.value.salt
|
|
||||||
) {
|
|
||||||
return (
|
return (
|
||||||
<CreateAccount
|
<CreateAccount
|
||||||
onNewAccount={(salt, key, pwd) => {
|
onNewAccount={(account, pwd) => {
|
||||||
password.update(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) {
|
if (password.value === undefined) {
|
||||||
return (
|
return (
|
||||||
<UnlockAccount
|
<UnlockAccount
|
||||||
salt={officer.value.salt}
|
lockedAccount={officer.value.account}
|
||||||
sealedKey={officer.value.key}
|
|
||||||
onAccountUnlocked={(pwd) => {
|
onAccountUnlocked={(pwd) => {
|
||||||
password.update(pwd);
|
password.update(pwd);
|
||||||
}}
|
}}
|
||||||
@ -114,7 +129,7 @@ export function Officer() {
|
|||||||
function CreateAccount({
|
function CreateAccount({
|
||||||
onNewAccount,
|
onNewAccount,
|
||||||
}: {
|
}: {
|
||||||
onNewAccount: (salt: string, accountId: string, password: string) => void;
|
onNewAccount: (account: LockedAccount, password: string) => void;
|
||||||
}): VNode {
|
}): VNode {
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
const Form = createNewForm<{
|
const Form = createNewForm<{
|
||||||
@ -158,8 +173,8 @@ function CreateAccount({
|
|||||||
};
|
};
|
||||||
}}
|
}}
|
||||||
onSubmit={async (v) => {
|
onSubmit={async (v) => {
|
||||||
const keys = await createNewAccount(v.password);
|
const account = await createNewAccount(v.password);
|
||||||
onNewAccount(keys.salt, keys.accountId, v.password);
|
onNewAccount(account, v.password);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
@ -198,12 +213,10 @@ function CreateAccount({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function UnlockAccount({
|
function UnlockAccount({
|
||||||
salt,
|
lockedAccount,
|
||||||
sealedKey,
|
|
||||||
onAccountUnlocked,
|
onAccountUnlocked,
|
||||||
}: {
|
}: {
|
||||||
salt: string;
|
lockedAccount: LockedAccount;
|
||||||
sealedKey: string;
|
|
||||||
onAccountUnlocked: (password: string) => void;
|
onAccountUnlocked: (password: string) => void;
|
||||||
}): VNode {
|
}): VNode {
|
||||||
const Form = createNewForm<{
|
const Form = createNewForm<{
|
||||||
@ -228,7 +241,7 @@ function UnlockAccount({
|
|||||||
onSubmit={async (v) => {
|
onSubmit={async (v) => {
|
||||||
try {
|
try {
|
||||||
// test login
|
// test login
|
||||||
await unlockAccount(salt, sealedKey, v.password);
|
await unlockAccount(lockedAccount, v.password);
|
||||||
|
|
||||||
onAccountUnlocked(v.password ?? "");
|
onAccountUnlocked(v.password ?? "");
|
||||||
notifyInfo("Account unlocked" as TranslatedString);
|
notifyInfo("Account unlocked" as TranslatedString);
|
||||||
|
@ -1406,7 +1406,7 @@ export async function encryptWithDerivedKey(
|
|||||||
|
|
||||||
const nonceSize = 24;
|
const nonceSize = 24;
|
||||||
|
|
||||||
async function decryptWithDerivedKey(
|
export async function decryptWithDerivedKey(
|
||||||
ciphertext: OpaqueData,
|
ciphertext: OpaqueData,
|
||||||
keySeed: OpaqueData,
|
keySeed: OpaqueData,
|
||||||
salt: string,
|
salt: string,
|
||||||
|
Loading…
Reference in New Issue
Block a user