keeweb/kdbxweb

Painful integration of argon2 in browser

mstoecklein opened this issue · 9 comments

My experience using the argon2 algorithm in kdbxweb in the browser was a bit tricky.

When I use your argon2-asm.min.js from your test, I can create my derived key and everything is fine. The output file also works in my keepass client. That's nice. Unfortunately in the browser the JS is polluting my window object and I had issues with that lately. I also tried NPM libraries, but they don't worked without a bigger effort in setting up my bundler.

What I did to work around that issue:

I don't like this solution, but it was the easiest way to continue using my current bundler settings without using any additional compiler steps.

It would be a great improvement if you add the choice of an additional bundle with the baked-in argon2 algorithm.

Hi, not sure this is the best way to do (maybe I'm lacking something, but here's how I went for my VueJS project):

1 - Install argon2-browser (also by Antelle) and base64-loader libraries:
npm install --save argon2-browser base64-loader

2 - Load the .wasm file of argon2-browser in your project. More details here: https://github.com/antelle/argon2-browser#usage
For my VueJS project, I only need to add this in my vue.config.js file:

module.exports = {
  configureWebpack: {
    ...
    module: {
      noParse: /\.wasm$/,
      rules: [
        {
          test: /\.wasm$/,
          loaders: ['base64-loader'],
          type: 'javascript/auto',
        },
      ],
    },
  },
  ...
}

3 - Create an argon2 implementation file using argon2-browser (in my case file is named argon2Impl.ts):

/**
 * argon2 implementation using argon2-browser: https://github.com/antelle/argon2-browser
 */

import { Argon2Type, Argon2Version } from 'kdbxweb/dist/types/crypto/crypto-engine';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const argon2Browser = require('argon2-browser');

export function argon2(
  password: ArrayBuffer,
  salt: ArrayBuffer,
  memory: number,
  iterations: number,
  length: number,
  parallelism: number,
  type: Argon2Type,
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  version: Argon2Version // maybe I missed a thing here, because it ain't used in argon2-browser?
): Promise<Uint8Array> {
  return argon2Browser
    .hash({
      // required
      pass: Buffer.from(new Uint8Array(password)),
      salt: Buffer.from(new Uint8Array(salt)),
      // optional
      time: iterations, // the number of iterations
      mem: memory, // used memory, in KiB
      hashLen: length, // desired hash length
      parallelism: parallelism, // desired parallelism (it won't be computed in parallel, however)
      //secret: new Uint8Array([...]), // optional secret data - not sure of how to use this?
      //ad: new Uint8Array([...]), // optional associated data - not sure of how to use this?
      type: type, // Argon2d, Argon2i, Argon2id
    })
    .then((res: { hash: Uint8Array; hashHex: string; encoded: string }) => {
      //res.hash; // hash as Uint8Array
      //res.hashHex; // hash as hex-string
      //res.encoded; // encoded hash, as required by argon2
      return res.hash;
    })
    .catch((err: { message: string; code: number }) => {
      //err.message; // error message as string, if available
      //err.code; // numeric error code
      throw err;
    });
}

4 - Set argon2 implementation before loading your keepass database:

// import your implementation file here based on argon2-browser
import { argon2 } from '@/utils/argon2Impl';

// required for KDBX 4 argon2 implementation
kdbxweb.CryptoEngine.setArgon2Impl(argon2);

const fileData: ArrayBuffer = xxxYOUR_FILE_AS_ARRAYBUFFERxxx;
const credentials = new kdbxweb.Credentials(kdbxweb.ProtectedValue.fromString(password || ''));
const db = await kdbxweb.Kdbx.load(fileData, credentials);

This is a nice solution. Thanks for sharing.

For me it was a proof of concept and it worked as I described above. I didn't want to spend a lot of time going into the details and setting up a proper bundler. My goal was to get the exported kdbx file working with keepassXC. Quick & dirty at its best!

It would be nice to expand the documentation to this point, because it's not really intuitive for people who don't have a deeper understanding of cryptography or don't have the time to deal with it.

yo guys, i have a problem with

import { Argon2Type, Argon2Version } from 'kdbxweb/dist/types/crypto/crypto-engine';

export function argon2(
  password: ArrayBuffer,
  salt: ArrayBuffer,
  memory: number,
  iterations: number,
  length: number,
  parallelism: number,
  type: Argon2Type,
  version: Argon2Version
): Promise<ArrayBuffer> {
  let Module = require('./argon2-asm.min');
  if (Module.default) {
    Module = Module.default;
  }
  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 = <number>Module.allocate(new Array(length), 'i8', Module.ALLOC_NORMAL);
  const encodedLen = 512;
  const encoded = Module.allocate(new Array(encodedLen), 'i8', Module.ALLOC_NORMAL);
  try {
    const res = Module._argon2_hash(
      iterations,
      memory,
      parallelism,
      password,
      passwordLen,
      salt,
      saltLen,
      hash,
      length,
      encoded,
      encodedLen,
      type,
      version
    );
    if (res) {
      return Promise.reject(`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 Promise.resolve(hashArr);
  } catch (e) {
    return Promise.reject(e);
  }
}

in one of my projects it just wont load the min file somehow and im lossing my mind about it.
If i try the argon2-browser approach it wont work either :D
Do you have any suggestion for me ? @fchretienow @mstoecklein @Valodim

Where is it not working? Do you have any error at all?

Where is it not working? Do you have any error at all?

The code works for me in 3 of 4 cases.
I got a proof of concept app written in typescript, which works fine running with ts-node and after compiling it works too. After I got the PoC ready, I wanted to implement the logic into my other TS-app, it works there too. But not after compiling with tsc. The tsconfig.json is nearly the same, with the only difference beeing the "resolveJsonModule": true, flag.

The config looks like:

{
  "compilerOptions": {
    "module": "commonjs",
    "target": "es6",
    "lib": ["ES6", "DOM"],
    "sourceMap": true,
    "strict": true,
    "noImplicitAny": false,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "outDir": "build",
    "resolveJsonModule": true,
    "downlevelIteration": true,
    "types": ["node"]
  },
  "exclude": ["node_modules"],
  "include": ["src/**/*"],
}

and class:

import * as kdbxweb from 'kdbxweb';
import * as fs from 'fs';
import { argon2 } from '../crypto/argon2';

export default class IAPVault {
  vdiName: string;
  vault: kdbxweb.Kdbx | null = null;
  constructor(vdiName: string) {
    this.vdiName = vdiName;
    kdbxweb.CryptoEngine.setArgon2Impl((password, salt, memory, iterations, length, parallelism, type, version) => {
      return Promise.resolve(argon2(password, salt, memory, iterations, length, parallelism, type, version));
    });
  }

  loadKeyFile(keyFilePath: string, vaultPassword: string) {
    console.log('KeyFile loading...');
    const fileData = fs.readFileSync(keyFilePath);
    console.log('KeyFile loaded!');
    return new kdbxweb.Credentials(kdbxweb.ProtectedValue.fromString(vaultPassword), Buffer.from(fileData));
  }

  async loadKDBXFromFile(kdbxFilePath: string, keyFilePath: string, vaultPassword: string) {
    const fileData = fs.readFileSync(kdbxFilePath);
    const buffer = Buffer.from(fileData);
    console.log('KDBXFile loaded!');
    this.vault = await kdbxweb.Kdbx.load(buffer.buffer, this.loadKeyFile(keyFilePath, vaultPassword));
    if (this.vault) {
      console.log('KDBX data loaded from file successfully');
      return;
    } else {
      console.log('KDBX not loaded');
      return;
    }
  }

  private async save(filePath: string, kdbx: kdbxweb.Kdbx) {
    const dataAsArrayBuffer = await kdbx.save();
    fs.writeFileSync(filePath, Buffer.from(dataAsArrayBuffer), 'binary');
  }

  async addBotEntryToGroup(kdbx: kdbxweb.Kdbx, botName: string, passwordName: string, password: string, outFilePath: string) {
    const defaultGroup = kdbx.getDefaultGroup();
    const iapBotsGroup = defaultGroup.groups[1];
    const entry = kdbx.createEntry(iapBotsGroup);
    entry.fields.set('Title', botName + passwordName);
    entry.fields.set('Password', kdbxweb.ProtectedValue.fromString(password));
    entry.times.update();
    entry.pushHistory();
    this.save(outFilePath, kdbx);
  }
}

Error i get is:

[2023-10-18T17:24:38.871] [ERROR] default - Caught exception: KdbxError: Error BadSignature
    at KdbxHeader.readSignature (C:\Users\arag0re\Developer\ts-assistant\node_modules\kdbxweb\dist\kdbxweb.js:3049:19)
    at KdbxHeader.read (C:\Users\arag0re\Developer\ts-assistant\node_modules\kdbxweb\dist\kdbxweb.js:3498:16)
    at C:\Users\arag0re\Developer\ts-assistant\node_modules\kdbxweb\dist\kdbxweb.js:2305:57
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5) {
  code: 'BadSignature'
}

and i figured that the argon2-asm.min.js isnt in the build folder after compilation, so fixed that with a copy-cmd after build, but it wont get loaded.
The weird thing is that in my PoC it works as expected.

this is the PoC:
https://github.com/arag0re/keepass-vault/tree/main

I used nodejs 18.18.2 on win10_x64 in all cases.

Fixed it now, the problem was:
kdbxweb.CryptoEngine.setArgon2Impl((password, salt, memory, iterations, length, parallelism, type, version) => { return Promise.resolve(argon2(password, salt, memory, iterations, length, parallelism, type, version)); });

I changed it to:
kdbxweb.CryptoEngine.setArgon2Impl(argon2);

now it works.

The thing I dont understand is, why it worked everywhere else with all these arguments, but in my one project it didn't. Hate when I dont understand something xD

For anyone who have problems with vite + argon2 and arrived here.

This is the fast solution
(Maybe someone can add it to the docs)

npm i argon2-wasm-esm

And add this to your code

// @ts-ignore
import argon2 from "argon2-wasm-esm";

kdbxweb.CryptoEngine.argon2 = async (
  password: ArrayBuffer,
  salt: ArrayBuffer,
  memory: number,
  iterations: number,
  length: number,
  parallelism: number,
  type: argon2.ArgonType,
  version: argon2.ArgonVersion
) => {
  return argon2
    .hash({
      pass: new Uint8Array(password),
      salt: new Uint8Array(salt),
      time: iterations,
      mem: memory,
      hashLen: length,
      parallelism,
      type,
      version,
    })
    .then(({ hash }: { hash: Uint8Array }) => hash);
};

Try using @noble/hashes. This is working well:

import { argon2d, argon2id } from '@noble/hashes/argon2';
import kdbxweb from 'kdbxweb';

kdbxweb.CryptoEngine.setArgon2Impl((password, salt, memory, iterations, length, parallelism, type, version) => {
  const argon2 = type === 0 ? argon2d : argon2id;

  const bytes = argon2(
    new Uint8Array(password),
    new Uint8Array(salt),
    {
      t: iterations,
      m: memory,
      p: parallelism,
      dkLen: length,
      version,
    },
  );

  return Promise.resolve(bytes.buffer);
});