Web Crypto API - importKey or deriveKey not generating correctly


#1

Hello,

I’ve finally gotten the Web Crypto API to generate encrypt and decrpyt PBKDF2 (SHA-256) with HMAC. I’ve also gotten the output to match how Django stores and validates pbkdf2_sha256 passwords.

You can see it working in:
https://jsfiddle.net/webchad/o0f2dwpv/68/

However, in the Worker playground the base64 key is being padded:
https://cloudflareworkers.com/#27b5b93509aa5ccee48b70cdc5568ea1:https://tutorial.cloudflareworkers.com

To highlight the mismatch, when I run JSFiddle using the latest Chrome, I get

generatedKey = oroYqBB8G4iLE0D7R0vYv1hwSIxCn4Pm3dQy7LZX6E4=
expectedKey  = oroYqBB8G4iLE0D7R0vYv1hwSIxCn4Pm3dQy7LZX6E4=

:success: :ohyeah:

In the Cloudflare environment

generatedKey = oroYqAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
expectedKey  = oroYqBB8G4iLE0D7R0vYv1hwSIxCn4Pm3dQy7LZX6E4=

:exploding_head: :sob:

I’ve done a lot of debugging and printing of things to the console. And I have the problem down to one of three crypto api calls within the Cloudflare environment.

  /**
   * No. 1 - crypto.subtle.importKey
   */ 
  const baseKey = await crypto.subtle.importKey(
    'raw',
    key, {
      name: 'PBKDF2'
    },
    false, ['deriveKey'],
  )

  /**
   * No. 2 - crypto.subtle.deriveKey
   */ 
  const dkey = await crypto.subtle.deriveKey({
      name: 'PBKDF2',
      salt,
      iterations,
      hash
    },
    baseKey, {
      name: mode,
      hash: {
        name: hash
      }
    },
    true, ['sign', 'verify'],
  )

  /**
   * No. 3 - crypto.subtle.exportKey
   */ 
  return await crypto.subtle.exportKey('raw', dkey)

Things I know about the three calls

  • crypto.subtle.importKey and crypto.subtle.deriveKey return the correct type, CryptoKey
  • crypto.subtle.exportKey returns an array of bytes, which is correct …
  • crypto.subtle.exportKey exports ArrayBuffer(4) on Cloudflare vs ArrayBuffer(64) in Chrome

Things I don’t know about the three calls

  • In crypto.subtle.importKey and crypto.subtle.deriveKey, I don’t know of a way to verify the contents are correct

Other things I’ve tried

Using Node’s crypto package to use pbkdf2, however, it adds a lot of weight to the script, around 600kb (which is a lot when we have a 1MB limit). But the biggest problem, the polyfill changes the addEventListener. Thus the fetch event never gets handled.

Since I couldn’t use Node’s crypto package, I switched to using the native Web Crypto API. Which was painful. Mostly because:

  • it does things slightly different from Node
  • most articles focused on encrypting/decrypting from a key or a single part, and not how to change the calls together
  • converting from String, Base64, ArrayBuffers made it easy to pass the wrong type
  • or it’s a really old way of storing passwords

Once I gave up developing in my IDE, running webpack to bundle, and copy/paste to the Cloudflare IDE, and testing. And moved to using JSFiddle, it was faster to get things encrypting and verifying.

After I got it working in JSFiddle I pulled it into my IDE and started testing with in the Cloudflare environment. Thus this is where I am at now.

Any help, comments greatly appreciated.


#2

Hi @webchad,

You’ve found a bug in our implementation. When an HMAC key is derived via PBKDF2 and an explicit desired key length is not provided, we default the key length to the block size of the hash algorithm selected (256 bits for SHA-256). However, the default we use is in bytes, but our deriveKey() implementation expects it to be in bits, which is why you are observing that the derived key comes out 8 times too short. Yikes. :frowning:

We’ll have a fix for this rolled out next week (Edit: I originally said this week, but we need to examine how this may affect scripts in production first). In the meantime, you can work around the bug by passing an explicit desired key length:

  const dkey = await crypto.subtle.deriveKey({
      name: 'PBKDF2',
      salt,
      iterations,
      hash
    },
    baseKey, {
      name: mode,
      hash: {
        name: hash
      },
      length: 256,  // desired key length in bits
    },
    true, ['sign', 'verify'],
  )

Thank you very much for reporting this.

Harris


#3

Thanks @harris!

After 3 days of fighting with crypto, it works!


#4

I think the community would really benefit from having this in recipe-exchange!

Doing password hashing properly is not always straightforward and here you replicate the excellent Django password hashing methods using native Web Crypto API, that’s just awesome!

Would you mind making a write-up of this explaining what you’re doing and sharing it as a recipe?


#5

@thomas4 I can take a stab and post something. I’ll try to get to it this week.

As a side note, Cloudflare’s Crypto Week might have made some of the hoops I had to jump through easier.