antelle / argon2-browser

Argon2 library compiled for browser runtime
https://antelle.net/argon2-browser
MIT License
363 stars 78 forks source link

Is there a way to make it non-blocking? #96

Open VityaSchel opened 4 months ago

VityaSchel commented 4 months ago

When using crypto_pwhash_MEMLIMIT_MODERATE/1024 memory (256 MiB), 3 iterations, it takes more than a second to hash and while it hashing, render loop is blocked and page is frozen I'm using service worker anyway, maybe use that here?

VityaSchel commented 4 months ago

I resolved this using custom service worker with WASM.

First, you need webpack or other bundler to bundle everything into single js file in service worker. Then install argon2-browser and use this code:

import argon2 from 'argon2-browser'

const crypto_pwhash_SALTBYTES = 16
const crypto_pwhash_MEMLIMIT_MODERATE = 268435456
const crypto_pwhash_OPSLIMIT_MODERATE = 3
const crypto_aead_xchacha20poly1305_ietf_KEYBYTES = 32

const channel = new BroadcastChannel('sw-messages')

self.addEventListener('message', async event => {
  if(typeof event.data === 'object') {
    if (event.data.type === 'hash' && 'plain' in event.data) {
      const OLD_ENC_SALT = new Uint8Array(crypto_pwhash_SALTBYTES)
      const result = await argon2.hash({
        pass: event.data.plain,
        salt: OLD_ENC_SALT,
        time: crypto_pwhash_OPSLIMIT_MODERATE,
        mem: crypto_pwhash_MEMLIMIT_MODERATE / 1024,
        hashLen: crypto_aead_xchacha20poly1305_ietf_KEYBYTES,
        parallelism: 1,
        type: argon2.ArgonType.Argon2id
      })
      channel.postMessage({ type: 'hash_result', result, plain: event.data.plain })
    }
  }
})

Then build it into single file, import with service worker and you can use it like this on your website:

const hashChannel = new BroadcastChannel('sw-messages')

const plain = 'what you want to hash goes here'
const sw = navigator.serviceWorker.controller
      if (!sw) return console.error('Service Worker not ready')
      sw.postMessage({ type: 'hash', plain })
      const subscription = (event: MessageEvent<{ type: 'hash_result', result: import('argon2-browser').Argon2BrowserHashResult, plain: string }>) => {
        if (
          typeof event.data === 'object' 
          && event.data.type === 'hash_result' 
          && 'result' in event.data
          && event.data.plain === plain
        ) {
          hashChannel.removeEventListener('message', subscription)
          resolve(event.data.result.hash)
        }
      }
      hashChannel.addEventListener('message', subscription)
VityaSchel commented 4 months ago

Oh and this is the config I used for webpack to correctly bundle wasm:

/* eslint-disable @typescript-eslint/no-var-requires */
const path = require('path')
const { IgnorePlugin } = require('webpack')

/** @type {import('webpack').Configuration} */
const config = {
  mode: 'production',
  entry: __dirname + '/index.ts',
  output: {
    path: path.resolve(__dirname, '../public'),
    filename: 'custom-worker.js'
  },
  module: {
    rules: [
      {
        test: /\.wasm$/,
        type: 'javascript/auto',
        use: [{
          loader: 'base64-loader'
        }]
      }
    ],
    noParse: /\.wasm$/
  },
  plugins: [
    new IgnorePlugin({ resourceRegExp: /\/__tests__\// })
  ],
  resolve: {
    fallback: {
      path: require.resolve('path-browserify'),
      fs: false
    }
  }
}
module.exports = config