Skip to content

Pre-derivation of the password

In the previous step, we integrated the SDK by protecting the identity with a password, but this password is also used for authentication, which is not secure as it is, since the backend has knowledge of it during the account creation and at each connection.

In this step, we will add a "pre-derivation" of the password to ensure that the backend never has access to the password.

The branch on which this step is based is 1-quick-start, the final result is 2-pre-derivation.

Explanation

Usually the authentication is done as follows:

uml diagram

The problem lies with the fact that the backend receives the password as is in such a protocol, and could therefore use it to decrypt the protected identity on SSKS.

The solution we recommend is to perform a so-called "pre-derivation" step on the password to prevent the server from knowing the password as is, even at runtime.

uml diagram

In the context of the example project, we will implement this pre-derivation in the service frontend/services/api.js and in the file frontend/utils/index.js:

Pre-derivation function

The derivation function chosen is scrypt, but any password derivation function like pbkdf2, bcrypt, argon2 or others would provide the same functionality.

The first thing to do is to install the scrypt-js package:

shell
cd frontend
npm install scrypt-js
cd frontend
npm install scrypt-js

Then in the file frontend/utils/index.js:

  • we define encode which:
    • normalizes the password canonically with the method String#normalize;
    • transforms the normalized password into Buffer;
  • we define hashPassword which encodes password and salt and derives them with scyrpt-js configured with robust parameters, then returns the result as Promise<Buffer>.
js
/* frontend/utils/index.js */
import scryptJs from 'scrypt-js' // don't forget to `npm install scrypt-js`

export const encode = password => Buffer.from(password.normalize('NFKC'), 'utf8')

export const hashPassword = (password, salt) =>
  scryptJs.scrypt(encode(password), encode(salt), 16384, 8, 1, 64)
    // scryptJs returns Uint8Array so we convert it to a proper buffer
    // to avoid problems
    .then(res => Buffer.from(res))
/* frontend/utils/index.js */
import scryptJs from 'scrypt-js' // don't forget to `npm install scrypt-js`

export const encode = password => Buffer.from(password.normalize('NFKC'), 'utf8')

export const hashPassword = (password, salt) =>
  scryptJs.scrypt(encode(password), encode(salt), 16384, 8, 1, 64)
    // scryptJs returns Uint8Array so we convert it to a proper buffer
    // to avoid problems
    .then(res => Buffer.from(res))

Using the pre-derivation function

The password must be pre-derived before the account creation and before the login.

As a salt, we use the concatenation (separated by |) of:

  • an arbitrary String for the application that will be embedded in the frontend code, set via the APPLICATION_SALT configuration variable, defaulting to 'sdk-example-project-salt;
  • the userId.
Details

This is not a random salt because:

  • the frontend does not have the ability to store a random salt, a user can use a new browser at any time;
  • the backend is considered malicious, so it can't be trusted to store a random salt for each user, it could render an arbitrary false salt and get the result, and thus could determine if two users have the same password.
js
/* frontend/services/api.js */
import { hashPassword } from '../utils'
/* ... */
const preDerivePassword = async (password, userId) => {
  const fixedString = await getSetting('APPLICATION_SALT')
  return hashPassword(password, `${fixedString}|${userId}`)
}
/* ... */
export class User {
  /* ... */
  static async createAccount ({ emailAddress, password, name }) {
    // Just before we call the API endpoint to create the account, we preDerive
    // the password, and send the preDerived version instead of the clearText
    // password
    const preDerivedPassword = preDerivePassword(password, emailAddress)
    const { id } = await apiClient.rest.account.create({
      emailAddress,
      password: preDerivedPassword,
      name
    })
    currentUser = new this({
      id,
      emailAddress,
      name
    })
    return currentUser
  }

  static async login ({ emailAddress, password }) {
    // Just before we call the API endpoint to log in, we preDerive the password,
    // using the same parameters and send the preDerived version instead
    // of the clearText password
    const preDerivedPassword = preDerivePassword(password, emailAddress)
    const { id, name } = await apiClient.rest.account.login({
      emailAddress,
      password: preDerivedPassword
    })
    currentUser = new this({
      id,
      emailAddress,
      name
    })
    return currentUser
  }
  /* ... */
}
/* ... */
/* frontend/services/api.js */
import { hashPassword } from '../utils'
/* ... */
const preDerivePassword = async (password, userId) => {
  const fixedString = await getSetting('APPLICATION_SALT')
  return hashPassword(password, `${fixedString}|${userId}`)
}
/* ... */
export class User {
  /* ... */
  static async createAccount ({ emailAddress, password, name }) {
    // Just before we call the API endpoint to create the account, we preDerive
    // the password, and send the preDerived version instead of the clearText
    // password
    const preDerivedPassword = preDerivePassword(password, emailAddress)
    const { id } = await apiClient.rest.account.create({
      emailAddress,
      password: preDerivedPassword,
      name
    })
    currentUser = new this({
      id,
      emailAddress,
      name
    })
    return currentUser
  }

  static async login ({ emailAddress, password }) {
    // Just before we call the API endpoint to log in, we preDerive the password,
    // using the same parameters and send the preDerived version instead
    // of the clearText password
    const preDerivedPassword = preDerivePassword(password, emailAddress)
    const { id, name } = await apiClient.rest.account.login({
      emailAddress,
      password: preDerivedPassword
    })
    currentUser = new this({
      id,
      emailAddress,
      name
    })
    return currentUser
  }
  /* ... */
}
/* ... */

Server side migration

In the example project, no migration step is planned, the database must be deleted and the users recreated.

If we want to perform migration of the passwords, it is not possible to do it offline since the server is not supposed to store the user's password, but a hashed and salted version.

A possible strategy is therefore to force a password reset for all users. More refined strategies can be considered depending on the case.

Conclusion

We were able to add a password pre-derivation step to make the password protection robust, since in the example project it is also used for authentication.

It remains to be integrated before going into production:

  • identity persistence when opening a new tab by storing the identity in localstorage;