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
|
|
|
|
*/
|
2023-05-25 23:08:20 +02:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2023-05-25 23:08:20 +02:00
|
|
|
export interface Account {
|
|
|
|
accountId: string;
|
|
|
|
secret: CryptoKey;
|
|
|
|
}
|
|
|
|
|
2023-05-19 18:26:47 +02:00
|
|
|
/**
|
|
|
|
* Restore previous session and unlock account
|
|
|
|
*
|
2023-05-25 23:08:20 +02:00
|
|
|
* @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(
|
2023-05-25 23:08:20 +02:00
|
|
|
salt: string,
|
|
|
|
key: string,
|
2023-05-19 18:26:47 +02:00
|
|
|
password: string,
|
2023-05-25 23:08:20 +02:00
|
|
|
): Promise<Account> {
|
|
|
|
const rawKey = str2ab(window.atob(key));
|
2023-05-19 18:26:47 +02:00
|
|
|
|
2023-05-25 23:08:20 +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));
|
|
|
|
});
|
|
|
|
|
2023-05-25 23:08:20 +02:00
|
|
|
const accountId = btoa(ab2str(pubRaw));
|
2023-05-19 18:26:47 +02:00
|
|
|
|
2023-05-25 23:08:20 +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
|
|
|
|
*/
|
2023-05-25 23:08:20 +02:00
|
|
|
export async function createNewAccount(password: string) {
|
2023-05-26 14:25:03 +02:00
|
|
|
const { eddsaPriv } = createEddsaKeyPair();
|
2023-05-25 23:08:20 +02:00
|
|
|
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,
|
2023-05-25 23:08:20 +02:00
|
|
|
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
|
|
|
|
2023-05-25 23:08:20 +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> {
|
2023-05-25 23:08:20 +02:00
|
|
|
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> {
|
2023-05-25 23:08:20 +02:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2023-05-25 23:08:20 +02:00
|
|
|
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),
|
|
|
|
};
|
|
|
|
}
|