aboutsummaryrefslogtreecommitdiff
path: root/packages/exchange-backoffice-ui/src/account.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/exchange-backoffice-ui/src/account.ts')
-rw-r--r--packages/exchange-backoffice-ui/src/account.ts243
1 files changed, 243 insertions, 0 deletions
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),
+ };
+}