Skip to content

Pré-dérivation du mot de passe

Lors de l'étape précédente, nous avions intégré le SDK en protégeant l'identité par mot de passe, mais ce mot de passe est aussi utilisé pour l'authentification, ce qui n'est pas sécurisé en l'état, puisque le backend en a connaissance lors de la création de compte et à chaque connexion.

Lors de cette étape, nous ajouterons une "pré-dérivation" du mot de passe pour assurer que le backend n'a jamais accès au mot de passe.

La branche sur laquelle est basée cette étape est 1-quick-start, le résultat final est 2-pre-derivation.

Explication

Usuellement l'authentification est effectuée de la façon suivante :

uml diagram

Le problème réside dans le fait que le backend reçoit dans un tel protocole le password tel quel, et pourrait donc l'utiliser pour déchiffrer l'identité protégée sur ssks.

La solution que nous préconisons est d'effectuer une étape dite de "pré-dérivation" du mot de passe pour empêcher le serveur de connaître le mot de passe tel quel, y compris lors de l'exécution.

uml diagram

Dans le cadre du projet exemple, nous allons implémenter cette pré-dérivation dans le service frontend/services/api.js et dans le fichier frontend/utils/index.js :

Fonction de pre-derivation

La fonction de dérivation choisie est scrypt, mais n'importe quelle fonction de dérivation de mot de passe comme pbkdf2, bcrypt, argon2 ou autre offrirait la même fonctionnalité.

Il faut commencer par installer le paquet scrypt-js :

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

Puis dans le fichier frontend/utils/index.js :

  • on définit encode qui :
    • normalise le mot de passe de façon canonique avec la méthode String#normalize ;
    • transforme le mot de passe normalisé en Buffer ;
  • on définit hashPassword qui encode password et salt et les dérive avec scyrpt-js configuré avec des paramètres robustes, puis retourne le résultat sous forme de 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))

Utilisation de la fonction de pre-derivation

Il faut pré-dériver le mot de passe avant la création de compte et avant la connexion.

En guise de sel, on utilise la concaténation (séparée par |) de :

  • un String arbitraire pour l'application qui sera intégré dans le code du frontend, défini via la variable de configuration APPLICATION_SALT, par défaut à 'sdk-example-project-salt' ;
  • le userId.
Details

Il ne s'agit pas d'un sel aléatoire parce que:

  • le frontend n'a pas la capacité de stocker un sel aléatoire, un utilisateur peut utiliser un nouveau navigateur à tout moment ;
  • le backend est considéré comme malveillant, il n'est donc pas possible de lui faire confiance pour stocker un sel aléatoire pour chaque utilisateur, il pourrait rendre un faux sel arbitraire et obtenir le résultat, et pourrait donc déterminer si deux utilisateurs ont le même mot de passe.
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
  }
  /* ... */
}
/* ... */

Migration côté serveur

Dans le projet exemple, aucune étape de migration n'est prévue, il faut supprimer la base de données et recréer les utilisateurs.

Si l'on souhaite effectuer une migration des mots de passe, il n'est pas possible de le faire hors-ligne puisque le serveur n'est pas censé enregistrer le mot de passe de l'utilisateur, mais une version hachée et salée.

Une stratégie envisageable est donc de forcer la réinitialisation de mot de passe pour tous les utilisateurs. Des stratégies plus fines peuvent être envisagées selon les cas.

Conclusion

Nous avons pu ajouter une étape de pré-derivation du mot de passe pour rendre la protection par mot de passe robuste, puisque dans le projet exemple celui-ci est aussi utilisé pour l'authentification.

Il reste à intégrer avant un passage en production :

  • la persistance de l'identité à l'ouverture d'un nouvel onglet en stockant l'identité en localstorage ;