166 lines
4.8 KiB
JavaScript
166 lines
4.8 KiB
JavaScript
(function() {
|
|
"use strict";
|
|
function bufferStartsWith(buffer, prefix) {
|
|
if (prefix.length > buffer.length) {
|
|
return false;
|
|
}
|
|
for (let i = 0; i < prefix.length; i++) {
|
|
if (buffer[i] !== prefix[i]) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
function bufferToHex(buffer) {
|
|
return Array.from(new Uint8Array(buffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
}
|
|
function concatBuffers(a, b) {
|
|
const out = new Uint8Array(a.length + b.length);
|
|
out.set(a, 0);
|
|
out.set(b, a.length);
|
|
return out;
|
|
}
|
|
function hexToBuffer(hex) {
|
|
if (hex.length % 2 !== 0) {
|
|
throw new Error(`Hex string must have an even length. Got: ${hex}`);
|
|
}
|
|
const buffer = new ArrayBuffer(hex.length / 2);
|
|
const view = new DataView(buffer);
|
|
for (let i = 0; i < hex.length; i += 2) {
|
|
const byteString = hex.substring(i, i + 2);
|
|
const byteValue = parseInt(byteString, 16);
|
|
view.setUint8(i / 2, byteValue);
|
|
}
|
|
return new Uint8Array(buffer);
|
|
}
|
|
async function delay(ms) {
|
|
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
}
|
|
function timeDuration(start) {
|
|
return Math.floor((performance.now() - start) * 10) / 10;
|
|
}
|
|
class PasswordBuffer {
|
|
constructor(nonce, mode = "uint32") {
|
|
this.nonce = nonce;
|
|
this.mode = mode;
|
|
this.buffer = new Uint8Array(this.nonce.length + this.COUNTER_BYTES);
|
|
this.buffer.set(this.nonce, 0);
|
|
this.dataView = new DataView(this.buffer.buffer);
|
|
}
|
|
nonce;
|
|
mode;
|
|
COUNTER_BYTES = 4;
|
|
buffer;
|
|
dataView;
|
|
encoder = new TextEncoder();
|
|
/**
|
|
* Appends the counter to the nonce buffer.
|
|
* In 'string' mode, encodes the counter as a UTF-8 string.
|
|
* In 'uint32' mode, writes the counter as a big-endian 32-bit integer.
|
|
*/
|
|
setCounter(n) {
|
|
if (this.mode === "string") {
|
|
return concatBuffers(this.nonce, this.encoder.encode(n.toString()));
|
|
}
|
|
this.dataView.setUint32(this.nonce.length, n, false);
|
|
return this.buffer;
|
|
}
|
|
}
|
|
async function solveChallenge(options) {
|
|
const {
|
|
challenge,
|
|
controller,
|
|
counterMode = "uint32",
|
|
counterStart = 0,
|
|
counterStep = 1,
|
|
deriveKey: deriveKey2,
|
|
timeout = 9e4
|
|
} = options;
|
|
const { nonce, keyPrefix, salt } = challenge.parameters;
|
|
const nonceBuf = hexToBuffer(nonce);
|
|
const saltBuf = hexToBuffer(salt);
|
|
const keyPrefixBuf = keyPrefix.length % 2 === 0 ? hexToBuffer(keyPrefix) : null;
|
|
const password = new PasswordBuffer(nonceBuf, counterMode);
|
|
const start = performance.now();
|
|
let counter = counterStart;
|
|
let iterations = 0;
|
|
let derivedKeyHex = "";
|
|
let lastYield = start;
|
|
while (true) {
|
|
if (controller?.signal.aborted || timeout && iterations % 10 === 0 && performance.now() - start > timeout) {
|
|
return null;
|
|
}
|
|
const { derivedKey } = await deriveKey2(
|
|
challenge.parameters,
|
|
saltBuf,
|
|
password.setCounter(counter)
|
|
);
|
|
if (iterations % 10 === 0 && performance.now() - lastYield > 200) {
|
|
await delay(0);
|
|
lastYield = performance.now();
|
|
}
|
|
if (keyPrefixBuf ? bufferStartsWith(derivedKey, keyPrefixBuf) : bufferToHex(derivedKey).startsWith(keyPrefix)) {
|
|
derivedKeyHex = bufferToHex(derivedKey);
|
|
break;
|
|
}
|
|
counter = counter + counterStep;
|
|
iterations = iterations + 1;
|
|
}
|
|
return {
|
|
counter,
|
|
derivedKey: derivedKeyHex,
|
|
time: timeDuration(start)
|
|
};
|
|
}
|
|
function handler(options) {
|
|
const { deriveKey: deriveKey2 } = options;
|
|
let controller = void 0;
|
|
self.onmessage = async (message) => {
|
|
const { challenge, counterMode, counterStart, counterStep, timeout, type } = message.data;
|
|
if (type === "abort") {
|
|
controller?.abort();
|
|
} else if (type === "work") {
|
|
controller = new AbortController();
|
|
let solution;
|
|
try {
|
|
solution = await solveChallenge({
|
|
challenge,
|
|
controller,
|
|
counterStart,
|
|
counterStep,
|
|
deriveKey: deriveKey2,
|
|
counterMode,
|
|
timeout
|
|
});
|
|
} catch (err) {
|
|
return self.postMessage({ error: err });
|
|
}
|
|
self.postMessage(solution);
|
|
}
|
|
};
|
|
}
|
|
async function deriveKey(parameters, salt, password) {
|
|
const { algorithm, keyLength = 32 } = parameters;
|
|
const iterations = Math.max(1, parameters.cost);
|
|
let data = void 0;
|
|
let derivedKey = void 0;
|
|
for (let i = 0; i < iterations; i++) {
|
|
if (i === 0) {
|
|
data = concatBuffers(salt, password);
|
|
} else {
|
|
data = derivedKey;
|
|
}
|
|
derivedKey = new Uint8Array(
|
|
(await crypto.subtle.digest(algorithm, data)).slice(0, keyLength)
|
|
);
|
|
}
|
|
return {
|
|
parameters: {},
|
|
derivedKey
|
|
};
|
|
}
|
|
handler({
|
|
deriveKey
|
|
});
|
|
})();
|