rwadurian/tools/mnemonic-test/node_modules/hash-wasm/lib/argon2.ts

397 lines
9.7 KiB
TypeScript

import wasmJson from "../wasm/argon2.wasm.json";
import { type IHasher, WASMInterface } from "./WASMInterface";
import { createBLAKE2b } from "./blake2b";
import {
type IDataType,
decodeBase64,
encodeBase64,
getDecodeBase64Length,
getDigestHex,
getUInt8Buffer,
writeHexToUInt8,
} from "./util";
export interface IArgon2Options {
/**
* Password (or message) to be hashed
*/
password: IDataType;
/**
* Salt (usually containing random bytes)
*/
salt: IDataType;
/**
* Secret for keyed hashing
*/
secret?: IDataType;
/**
* Number of iterations to perform
*/
iterations: number;
/**
* Degree of parallelism
*/
parallelism: number;
/**
* Amount of memory to be used in kibibytes (1024 bytes)
*/
memorySize: number;
/**
* Output size in bytes
*/
hashLength: number;
/**
* Desired output type. Defaults to 'hex'
*/
outputType?: "hex" | "binary" | "encoded";
}
interface IArgon2OptionsExtended extends IArgon2Options {
hashType: "i" | "d" | "id";
}
function encodeResult(
salt: Uint8Array,
options: IArgon2OptionsExtended,
res: Uint8Array,
): string {
const parameters = [
`m=${options.memorySize}`,
`t=${options.iterations}`,
`p=${options.parallelism}`,
].join(",");
return `$argon2${options.hashType}$v=19$${parameters}$${encodeBase64(
salt,
false,
)}$${encodeBase64(res, false)}`;
}
const uint32View = new DataView(new ArrayBuffer(4));
function int32LE(x: number): Uint8Array {
uint32View.setInt32(0, x, true);
return new Uint8Array(uint32View.buffer);
}
async function hashFunc(
blake512: IHasher,
buf: Uint8Array,
len: number,
): Promise<Uint8Array> {
if (len <= 64) {
const blake = await createBLAKE2b(len * 8);
blake.update(int32LE(len));
blake.update(buf);
return blake.digest("binary");
}
const r = Math.ceil(len / 32) - 2;
const ret = new Uint8Array(len);
blake512.init();
blake512.update(int32LE(len));
blake512.update(buf);
let vp = blake512.digest("binary");
ret.set(vp.subarray(0, 32), 0);
for (let i = 1; i < r; i++) {
blake512.init();
blake512.update(vp);
vp = blake512.digest("binary");
ret.set(vp.subarray(0, 32), i * 32);
}
const partialBytesNeeded = len - 32 * r;
let blakeSmall: IHasher;
if (partialBytesNeeded === 64) {
blakeSmall = blake512;
blakeSmall.init();
} else {
blakeSmall = await createBLAKE2b(partialBytesNeeded * 8);
}
blakeSmall.update(vp);
vp = blakeSmall.digest("binary");
ret.set(vp.subarray(0, partialBytesNeeded), r * 32);
return ret;
}
function getHashType(type: IArgon2OptionsExtended["hashType"]): number {
switch (type) {
case "d":
return 0;
case "i":
return 1;
default:
return 2;
}
}
async function argon2Internal(
options: IArgon2OptionsExtended,
): Promise<string | Uint8Array> {
const { parallelism, iterations, hashLength } = options;
const password = getUInt8Buffer(options.password);
const salt = getUInt8Buffer(options.salt);
const version = 0x13;
const hashType = getHashType(options.hashType);
const { memorySize } = options; // in KB
const secret = getUInt8Buffer(options.secret ?? "");
const [argon2Interface, blake512] = await Promise.all([
WASMInterface(wasmJson, 1024),
createBLAKE2b(512),
]);
// last block is for storing the init vector
argon2Interface.setMemorySize(memorySize * 1024 + 1024);
const initVector = new Uint8Array(24);
const initVectorView = new DataView(initVector.buffer);
initVectorView.setInt32(0, parallelism, true);
initVectorView.setInt32(4, hashLength, true);
initVectorView.setInt32(8, memorySize, true);
initVectorView.setInt32(12, iterations, true);
initVectorView.setInt32(16, version, true);
initVectorView.setInt32(20, hashType, true);
argon2Interface.writeMemory(initVector, memorySize * 1024);
blake512.init();
blake512.update(initVector);
blake512.update(int32LE(password.length));
blake512.update(password);
blake512.update(int32LE(salt.length));
blake512.update(salt);
blake512.update(int32LE(secret.length));
blake512.update(secret);
blake512.update(int32LE(0)); // associatedData length + associatedData
const segments = Math.floor(memorySize / (parallelism * 4)); // length of each lane
const lanes = segments * 4;
const param = new Uint8Array(72);
const H0 = blake512.digest("binary");
param.set(H0);
for (let lane = 0; lane < parallelism; lane++) {
param.set(int32LE(0), 64);
param.set(int32LE(lane), 68);
let position = lane * lanes;
let chunk = await hashFunc(blake512, param, 1024);
argon2Interface.writeMemory(chunk, position * 1024);
position += 1;
param.set(int32LE(1), 64);
chunk = await hashFunc(blake512, param, 1024);
argon2Interface.writeMemory(chunk, position * 1024);
}
const C = new Uint8Array(1024);
writeHexToUInt8(C, argon2Interface.calculate(new Uint8Array([]), memorySize));
const res = await hashFunc(blake512, C, hashLength);
if (options.outputType === "hex") {
const digestChars = new Uint8Array(hashLength * 2);
return getDigestHex(digestChars, res, hashLength);
}
if (options.outputType === "encoded") {
return encodeResult(salt, options, res);
}
// return binary format
return res;
}
const validateOptions = (options: IArgon2Options) => {
if (!options || typeof options !== "object") {
throw new Error("Invalid options parameter. It requires an object.");
}
if (!options.password) {
throw new Error("Password must be specified");
}
options.password = getUInt8Buffer(options.password);
if (options.password.length < 1) {
throw new Error("Password must be specified");
}
if (!options.salt) {
throw new Error("Salt must be specified");
}
options.salt = getUInt8Buffer(options.salt);
if (options.salt.length < 8) {
throw new Error("Salt should be at least 8 bytes long");
}
options.secret = getUInt8Buffer(options.secret ?? "");
if (!Number.isInteger(options.iterations) || options.iterations < 1) {
throw new Error("Iterations should be a positive number");
}
if (!Number.isInteger(options.parallelism) || options.parallelism < 1) {
throw new Error("Parallelism should be a positive number");
}
if (!Number.isInteger(options.hashLength) || options.hashLength < 4) {
throw new Error("Hash length should be at least 4 bytes.");
}
if (!Number.isInteger(options.memorySize)) {
throw new Error("Memory size should be specified.");
}
if (options.memorySize < 8 * options.parallelism) {
throw new Error("Memory size should be at least 8 * parallelism.");
}
if (options.outputType === undefined) {
options.outputType = "hex";
}
if (!["hex", "binary", "encoded"].includes(options.outputType)) {
throw new Error(
`Insupported output type ${options.outputType}. Valid values: ['hex', 'binary', 'encoded']`,
);
}
};
interface IArgon2OptionsBinary {
outputType: "binary";
}
type Argon2ReturnType<T> = T extends IArgon2OptionsBinary ? Uint8Array : string;
/**
* Calculates hash using the argon2i password-hashing function
* @returns Computed hash
*/
export async function argon2i<T extends IArgon2Options>(
options: T,
): Promise<Argon2ReturnType<T>> {
validateOptions(options);
return argon2Internal({
...options,
hashType: "i",
}) as Promise<Argon2ReturnType<T>>;
}
/**
* Calculates hash using the argon2id password-hashing function
* @returns Computed hash
*/
export async function argon2id<T extends IArgon2Options>(
options: T,
): Promise<Argon2ReturnType<T>> {
validateOptions(options);
return argon2Internal({
...options,
hashType: "id",
}) as Promise<Argon2ReturnType<T>>;
}
/**
* Calculates hash using the argon2d password-hashing function
* @returns Computed hash
*/
export async function argon2d<T extends IArgon2Options>(
options: T,
): Promise<Argon2ReturnType<T>> {
validateOptions(options);
return argon2Internal({
...options,
hashType: "d",
}) as Promise<Argon2ReturnType<T>>;
}
export interface Argon2VerifyOptions {
/**
* Password to be verified
*/
password: IDataType;
/**
* Secret used on hash creation
*/
secret?: IDataType;
/**
* A previously generated argon2 hash in the 'encoded' output format
*/
hash: string;
}
const getHashParameters = (
password: IDataType,
encoded: string,
secret?: IDataType,
): IArgon2OptionsExtended => {
const regex =
/^\$argon2(id|i|d)\$v=([0-9]+)\$((?:[mtp]=[0-9]+,){2}[mtp]=[0-9]+)\$([A-Za-z0-9+/]+)\$([A-Za-z0-9+/]+)$/;
const match = encoded.match(regex);
if (!match) {
throw new Error("Invalid hash");
}
const [, hashType, version, parameters, salt, hash] = match;
if (version !== "19") {
throw new Error(`Unsupported version: ${version}`);
}
const parsedParameters: Partial<IArgon2Options> = {};
const paramMap = { m: "memorySize", p: "parallelism", t: "iterations" };
for (const x of parameters.split(",")) {
const [n, v] = x.split("=");
parsedParameters[paramMap[n]] = Number(v);
}
return {
...parsedParameters,
password,
secret,
hashType: hashType as IArgon2OptionsExtended["hashType"],
salt: decodeBase64(salt),
hashLength: getDecodeBase64Length(hash),
outputType: "encoded",
} as IArgon2OptionsExtended;
};
const validateVerifyOptions = (options: Argon2VerifyOptions) => {
if (!options || typeof options !== "object") {
throw new Error("Invalid options parameter. It requires an object.");
}
if (options.hash === undefined || typeof options.hash !== "string") {
throw new Error("Hash should be specified");
}
};
/**
* Verifies password using the argon2 password-hashing function
* @returns True if the encoded hash matches the password
*/
export async function argon2Verify(
options: Argon2VerifyOptions,
): Promise<boolean> {
validateVerifyOptions(options);
const params = getHashParameters(
options.password,
options.hash,
options.secret,
);
validateOptions(params);
const hashStart = options.hash.lastIndexOf("$") + 1;
const result = (await argon2Internal(params)) as string;
return result.substring(hashStart) === options.hash.substring(hashStart);
}