244 lines
5.8 KiB
TypeScript
244 lines
5.8 KiB
TypeScript
|
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),
|
||
|
};
|
||
|
}
|