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

250 lines
5.7 KiB
TypeScript
Raw Normal View History

2023-05-26 14:25:03 +02:00
import {
bytesToString,
createEddsaKeyPair,
decodeCrock,
encodeCrock,
encryptWithDerivedKey,
getRandomBytesF,
stringToBytes,
} from "@gnu-taler/taler-util";
2023-05-19 18:26:47 +02:00
/**
* 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 {
2023-05-19 18:26:47 +02:00
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;
}
2023-05-19 18:26:47 +02:00
/**
* Restore previous session and unlock account
*
* @param salt string from which crypto params will be derived
* @param key secured private key
2023-05-19 18:26:47 +02:00
* @param password password for the private key
* @returns
*/
export async function unlockAccount(
salt: string,
key: string,
2023-05-19 18:26:47 +02:00
password: string,
): Promise<Account> {
const rawKey = str2ab(window.atob(key));
2023-05-19 18:26:47 +02:00
const privateKey = await recoverWithPassword(rawKey, salt, password);
2023-05-19 18:26:47 +02:00
const publicKey = await getPublicFromPrivate(privateKey);
const pubRaw = await crypto.subtle.exportKey("spki", publicKey).catch((e) => {
throw new Error(String(e));
});
const accountId = btoa(ab2str(pubRaw));
2023-05-19 18:26:47 +02:00
return { accountId, secret: privateKey };
2023-05-19 18:26:47 +02:00
}
/**
* Create new account (secured private key) under session
* secured with the given password
*
* @param sessionId
* @param password
* @returns
*/
export async function createNewAccount(password: string) {
2023-05-26 14:25:03 +02:00
const { eddsaPriv } = createEddsaKeyPair();
const salt = createSalt();
2023-05-19 18:26:47 +02:00
2023-05-26 14:25:03 +02:00
const key = stringToBytes(password);
const protectedPrivKey = await encryptWithDerivedKey(
getRandomBytesF(24),
key,
eddsaPriv,
salt,
2023-05-19 18:26:47 +02:00
);
2023-05-26 14:25:03 +02:00
const protectedPriv = bytesToString(protectedPrivKey);
2023-05-19 18:26:47 +02:00
return { accountId: protectedPriv, salt };
2023-05-19 18:26:47 +02:00
}
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);
2023-05-19 18:26:47 +02:00
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);
2023-05-19 18:26:47 +02:00
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): {
2023-05-19 18:26:47 +02:00
salt: Uint8Array;
initVector: Uint8Array;
} {
const [saltId, vectorId] = sessionId.split("-");
return {
salt: decodeCrock(saltId),
initVector: decodeCrock(vectorId),
};
}