using taler crypto

This commit is contained in:
Sebastian 2023-05-26 10:54:42 -03:00
parent 77fb6c0d88
commit 62cec67983
No known key found for this signature in database
GPG Key ID: 173909D1A5F66069
4 changed files with 75 additions and 262 deletions

View File

@ -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">

View File

@ -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),
};
}

View File

@ -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);

View File

@ -1406,7 +1406,7 @@ export async function encryptWithDerivedKey(
const nonceSize = 24;
async function decryptWithDerivedKey(
export async function decryptWithDerivedKey(
ciphertext: OpaqueData,
keySeed: OpaqueData,
salt: string,