wallet-core/packages/exchange-backoffice-ui/src/account.ts

244 lines
5.8 KiB
TypeScript
Raw Normal View History

2023-05-19 18:26:47 +02:00
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),
};
}