import kdbxweb from 'kdbxweb'; import { Logger } from 'util/logger'; import { Features } from 'util/features'; import { Launcher } from 'comp/launcher'; import { AppSettingsModel } from 'models/app-settings-model'; const logger = new Logger('argon2'); const KdbxwebInit = { init() { kdbxweb.CryptoEngine.argon2 = (...args) => this.argon2(...args); }, argon2(password, salt, memory, iterations, length, parallelism, type, version) { const args = { password, salt, memory, iterations, length, parallelism, type, version }; return this.loadRuntime(memory).then((runtime) => { const ts = logger.ts(); return runtime.hash(args).then((hash) => { logger.debug('Hash computed', logger.ts(ts)); return hash; }); }); }, loadRuntime(requiredMemory) { if (this.runtimeModule) { return Promise.resolve(this.runtimeModule); } if (!global.WebAssembly) { return Promise.reject('WebAssembly is not supported'); } if (Launcher && AppSettingsModel.nativeArgon2) { const ts = logger.ts(); const argon2 = Launcher.reqNative('argon2'); logger.debug('Native argon2 runtime loaded (main thread)', logger.ts(ts)); this.runtimeModule = { hash(args) { return new Promise((resolve, reject) => { const ts = logger.ts(); argon2.hash( Buffer.from(args.password), Buffer.from(args.salt), { type: args.type, version: args.version, hashLength: args.length, saltLength: args.salt.length, timeCost: args.iterations, parallelism: args.parallelism, memoryCost: args.memory }, (err, res) => { if (err) { logger.error('Argon2 error', err); return reject(err); } logger.debug('Argon2 hash calculated', logger.ts(ts)); resolve(res); } ); }); } }; return Promise.resolve(this.runtimeModule); } return new Promise((resolve, reject) => { const loadTimeout = setTimeout(() => reject('timeout'), 5000); try { const ts = logger.ts(); const argon2LoaderCode = require('argon2').default; const wasmBinaryBase64 = require('argon2-wasm'); const KB = 1024 * 1024; const MB = 1024 * KB; const GB = 1024 * MB; const WASM_PAGE_SIZE = 64 * 1024; const totalMemory = (2 * GB - 64 * KB) / 1024 / WASM_PAGE_SIZE; const initialMemory = Math.min( Math.max(Math.ceil((requiredMemory * 1024) / WASM_PAGE_SIZE), 256) + 256, totalMemory ); if (Features.canUseWasmInWebWorker) { const memoryDecl = `var wasmMemory=new WebAssembly.Memory({initial:${initialMemory},maximum:${totalMemory}});`; const moduleDecl = 'var Module={' + 'wasmJSMethod: "native-wasm",' + 'wasmBinary: Uint8Array.from(atob("' + wasmBinaryBase64 + '"), c => c.charCodeAt(0)),' + 'print(...args) { postMessage({op:"log",args}) },' + 'printErr(...args) { postMessage({op:"log",args}) },' + 'postRun:' + this.workerPostRun.toString() + ',' + 'calcHash:' + this.calcHash.toString() + ',' + 'wasmMemory:wasmMemory,' + 'buffer:wasmMemory.buffer,' + 'TOTAL_MEMORY:' + initialMemory * WASM_PAGE_SIZE + '}'; const script = argon2LoaderCode.replace( /^var Module.*?}/, memoryDecl + moduleDecl ); const blob = new Blob([script], { type: 'application/javascript' }); const objectUrl = URL.createObjectURL(blob); const worker = new Worker(objectUrl); const onMessage = (e) => { switch (e.data.op) { case 'log': logger.debug(...e.data.args); break; case 'postRun': logger.debug( 'WebAssembly runtime loaded (web worker)', logger.ts(ts) ); URL.revokeObjectURL(objectUrl); clearTimeout(loadTimeout); worker.removeEventListener('message', onMessage); this.runtimeModule = { hash(args) { return new Promise((resolve, reject) => { worker.postMessage(args); const onHashMessage = (e) => { worker.removeEventListener( 'message', onHashMessage ); worker.terminate(); KdbxwebInit.runtimeModule = null; if (!e.data || e.data.error || !e.data.hash) { const ex = (e.data && e.data.error) || 'unexpected error'; logger.error('Worker error', ex); reject(ex); } else { resolve(e.data.hash); } }; worker.addEventListener('message', onHashMessage); }); } }; resolve(this.runtimeModule); break; default: logger.error('Unknown message', e.data); URL.revokeObjectURL(objectUrl); reject('Load error'); } }; worker.addEventListener('message', onMessage); } else { // Chrome and Electron crash if we use WASM in WebWorker // see https://github.com/keeweb/keeweb/issues/1263 const wasmMemory = new WebAssembly.Memory({ initial: initialMemory, maximum: totalMemory }); global.Module = { wasmJSMethod: 'native-wasm', wasmBinary: Uint8Array.from(atob(wasmBinaryBase64), (c) => c.charCodeAt(0)), print(...args) { logger.debug(...args); }, printErr(...args) { logger.debug(...args); }, postRun: () => { logger.debug('WebAssembly runtime loaded (main thread)', logger.ts(ts)); clearTimeout(loadTimeout); resolve({ hash: (args) => { const hash = this.calcHash(global.Module, args); global.Module.unloadRuntime(); global.Module = undefined; return Promise.resolve(hash); } }); }, wasmMemory, buffer: wasmMemory.buffer, TOTAL_MEMORY: initialMemory * WASM_PAGE_SIZE }; // eslint-disable-next-line no-eval eval(argon2LoaderCode); } } catch (err) { reject(err); } }).catch((err) => { logger.warn('WebAssembly error', err); throw new Error('WebAssembly error'); }); }, // eslint-disable-next-line object-shorthand workerPostRun: function () { self.postMessage({ op: 'postRun' }); self.onmessage = (e) => { try { /* eslint-disable-next-line no-undef */ const hash = Module.calcHash(Module, e.data); self.postMessage({ hash }); } catch (e) { self.postMessage({ error: e.toString() }); } }; }, // eslint-disable-next-line object-shorthand calcHash: function (Module, args) { let { password, salt } = args; const { memory, iterations, length, parallelism, type, version } = args; const passwordLen = password.byteLength; password = Module.allocate(new Uint8Array(password), 'i8', Module.ALLOC_NORMAL); const saltLen = salt.byteLength; salt = Module.allocate(new Uint8Array(salt), 'i8', Module.ALLOC_NORMAL); const hash = Module.allocate(new Array(length), 'i8', Module.ALLOC_NORMAL); const encodedLen = 512; const encoded = Module.allocate(new Array(encodedLen), 'i8', Module.ALLOC_NORMAL); const res = Module._argon2_hash( iterations, memory, parallelism, password, passwordLen, salt, saltLen, hash, length, encoded, encodedLen, type, version ); if (res) { throw new Error('Argon2 error ' + res); } const hashArr = new Uint8Array(length); for (let i = 0; i < length; i++) { hashArr[i] = Module.HEAP8[hash + i]; } Module._free(password); Module._free(salt); Module._free(hash); Module._free(encoded); return hashArr; } }; export { KdbxwebInit };