I've been working with AdonisJS (4.1) lately on a rebuild of some services and such for my full time job. Today we noticed that password reset tokens were being generated with plusses in them. While this really isn't a major issue, if that token is url-encoded to be emailed to someone, and then url-decoded, those plusses become spaces and subsequently attempting to decrypt that hash fails.

Now, obviously the easiest, quick & dirty solution is to just replace any spaces in the url-decoded token with a + instead, but this made me curious so I dug in a bit to find out what was being used to encrypt tokens, which turned out to be the simple-encryptor npm package. That package offers a couple additional config params that adonis simply ignores:

Options hash - encryptor(opts)

Alternatively you can specify the string key and other options as a hash. The following properties are supported:

key - the string key to derive the crypto key from. Specifically the crypto key will be derived as the SHA-256 hash of this key. This must be specified, there is no default.
hmac - whether or not to calculate the HMAC of the encrypted text and add that to the result. Additionally, if enabled this will verify the HMAC prior to decrypting. Adding HMACs will add 64-bytes to the result of each encryption (32-byte HMAC stored as hex). By default this is true.
reviver - you can pass in a custom reviver function that will be used during decryption. Useful, for example, when your payload contains a date object and you want it to be recreated during decryption.
debug - whether to log errors decrypting, by default this is false.

Adonis merely passes the key, and ignores the reset. With a little bit of poking around it appeared as though if I disabled hmac, resulting tokens did not include those pesky plusses. This made me curious whether I could override the default Encryption provider that's built into adonis core or not (I assumed yes) and how difficult that might be.

Turns out, it's actually fairly straightforward!

To accomplish it, one must simply create their own provider, and then override the Encryption alias to point at their provider instead of the core provider.

Custom provider: /providers/Encryption/Provider.js

'use strict'

const { ServiceProvider } = require('@adonisjs/fold')

class Encryption extends ServiceProvider {
  register() {
    this.app.singleton('MyEncryption', (app) => {
      const Encryption = require('@adonisjs/framework/src/Encryption')

      const options = {
        key: app.use('Adonis/Src/Config').get('app.appKey'),
        hmac: false
      }

      return new Encryption(options)
    })

    this.app.alias('MyEncryption', 'Encryption')
  }
}

module.exports = Encryption

This then gets appended to the end of the providers array in your /start/app.js file:

const providers = [
  '@adonisjs/framework/providers/AppProvider',

  // ... snip ...

  path.join(__dirname, '..', 'providers', 'Encryption/Provider'),
]

That core AppProvider includes several core providers necessary for proper function of Adonis. One could override the whole thing and reference their overridden version instead, but that's silly to just change one of the providers it registers. So the method I've laid out here is probably the more sane approach.

But...

Whether excluding the hmac calculation is a good idea or not is debatable, since it authenticates the validity of the hash in question. Additionally while my observation was that tokens did not include plusses when doing this, I cannot definitively say that's accurate.

Additionally, it seems like the better approach here would be rather than me overriding this for myself, that if it were a worthwhile change it should instead expose those neglected config options that simple-encryptor provides in a config file, and merge that config in within the existing core provider. That would be a relatively painless pull request to put together, but since it's of questionable value, I'm not going to bother.

In any case, it's nice to know that core providers can be easily overridden simply by pointing the alias they define at a different provider instead.