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:
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.
In the context of the example project, we will implement this pre-derivation in the service frontend/services/api.ts
and in the file frontend/utils/index.ts
:
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:
cd frontend
npm install scrypt-js buffer
Then in the file frontend/utils/index.ts
:
- we define
encode
which:- normalizes the password canonically with the method
String#normalize
; - transforms the normalized password into
Buffer
;
- normalizes the password canonically with the method
- we define
hashPassword
which encodespassword
andsalt
and derives them withscyrpt-js
configured with robust parameters, then returns the result asPromise<Buffer>
.
/* frontend/src/utils/index.ts */
import { Buffer } from 'buffer' // don't forget to `npm install buffer`
import scryptJs from 'scrypt-js' // don't forget to `npm install scrypt-js`
export const encode = (password: string): Buffer => Buffer.from(password.normalize('NFKC'), 'utf8')
export const hashPassword = async (password: string, salt: string): Promise<string> =>
// scryptJs returns Uint8Array so we convert it to a proper buffer
// to avoid problems
Buffer.from(await scryptJs.scrypt(encode(password), encode(salt), 16384, 8, 1, 64)).toString('base64')
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
emailAddress
.
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.
/* frontend/src/services/api.ts */
import { hashPassword } from '../utils'
/* ... */
const preDerivePassword = async (password: string, emailAddress: string): Promise<string> => {
const fixedString = await getSetting('APPLICATION_SALT')
return await hashPassword(password, `${fixedString}|${emailAddress}`)
}
/* ... */
export class User {
/* ... */
static async createAccount ({ emailAddress, password, name }: CreateAccountType): Promise<User> {
// 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 }: LoginType): Promise<User> {
// 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;