Hello everyone, in this article we will set up application initialization and unlocking. We will also look at how data encryption works and create all the functions needed for encryption.
For everything related to encryption, we will use the crypto-js library, which contains all or almost all cryptographic standards.
yarn add crypto-jsData encryption
Before moving on to implementing data encryption, let's see how all of this will work.
First, the data will be encrypted with the AES algorithm, which is a symmetric encryption algorithm. The principle is quite simple: we pass it the data to encrypt and an encryption key, and as output we get encrypted data. Conversely, we pass it the encrypted data and the key, and we get decrypted data back.
In JavaScript with the crypto-js library, it looks like this:
// Encrypt
var ciphertext = CryptoJS.AES.encrypt('my message', 'secret key 123')
// Decrypt
var bytes = CryptoJS.AES.decrypt(ciphertext.toString(), 'secret key 123')
var plaintext = bytes.toString(CryptoJS.enc.Utf8)
console.log(plaintext)In our application, the key used to encrypt data will be derived from the master password defined by the user. To generate the key from the password, we will use the PBKDF2 derivation function. (See PBKDF2 on Wikipedia).
Of course, this key will not be stored in the application and will be generated each time the user unlocks the application.
CryptoHelper
Now that we have seen in broad terms how data encryption will work, we will create a helper that contains all the functions needed for data encryption.
In the common folder, create a file named CryptoHelper.js:
cd common && touch CryptoHelper.jsTo begin, we will add the GenerateKey method:
/*
* @flow
*/
import CryptoJS from 'crypto-js'
export const GenerateKey = (password: string, salt: string): CryptoJS.WordArray => {
const parsedSalt = CryptoJS.enc.Hex.parse(salt)
const key = CryptoJS.PBKDF2(password, parsedSalt, {
keySize: 256 / 32,
iterations: 1000,
})
return key
}Next, we add the function that checks whether the password is correct:
export const IsValidPassword = (verificationToken: string, password: string, salt: string): boolean => {
const currentToken = CryptoJS.SHA512(salt + password)
return currentToken.toString() === verificationToken
}When we initialize the application, we will generate a salt that will be used to generate the key and the verification token; this salt will be saved. Then we will also generate a verification token, which is a hash of the concatenation of the salt and the password; this token will also be saved.
Finally, to check that the password entered by the user when unlocking the application is correct, we retrieve the salt, then generate a new token from the salt and the entered password. If this token is equal to the one saved during initialization, then the password is correct.
Now we will add the Encrypt and Decrypt functions:
export const Encrypt = (data: string, key: CryptoJS.WordArray, iv: string): string => {
const parsedIv = CryptoJS.enc.Hex.parse(iv)
const encrypted = CryptoJS.AES.encrypt(data, key, {
iv: parsedIv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7,
})
return encrypted.ciphertext.toString()
}
export const Decrypt = (data: string, key: CryptoJS.WordArray, iv: string) => {
const parsedIv = CryptoJS.enc.Hex.parse(iv)
const cipherStuff = CryptoJS.lib.CipherParams.create({
key,
iv: parsedIv,
ciphertext: CryptoJS.enc.Hex.parse(data),
})
return CryptoJS.AES.decrypt(cipherStuff, key, {
iv: parsedIv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7,
}).toString(CryptoJS.enc.Utf8)
}Here, as explained above, to encrypt and decrypt we pass the data and the key, and we use crypto-js.
The difference from the example is that here we also pass a parameter named iv. The iv, or initialization vector, is a block of bits combined with the first data block during an encryption operation (see IV on Wikipedia). This iv will also be generated and stored during application initialization. We do this because, when we do not pass it manually as in the first example, crypto-js generates it randomly. However, because the application allows data backups, if we do not store the iv and a user restores their data on another phone, the iv will no longer be the same and the data cannot be decrypted.
To finish, we will add the IntializeData method, which generates and returns all the parameters we have seen so far.
type CryptoParams = {
salt: string,
iv: string,
key: CryptoJS.WordArray,
verificationToken: string,
}
export const InitializeData = (password: string): CryptoParams => {
const salt = CryptoJS.lib.WordArray.random(128 / 8)
const iv = CryptoJS.lib.WordArray.random(128 / 8)
const key = GenerateKey(password, salt.toString())
const verificationToken = CryptoJS.SHA512(salt.toString() + password)
return {
salt: salt.toString(),
iv: iv.toString(),
key,
verificationToken: verificationToken.toString(),
}
}Application initialization
We will move on to application initialization, meaning adding the actions and reducers and modifying the screen.
First add the action types in actions/types.js:
...
/*
**************
* Initialization
**************
*/
export type InitializationSuccessAction = {
type: 'INITIALIZATION_SUCCESS',
salt: string,
iv: string,
verificationToken: string,
};
export type InitializationFailAction = {
type: 'INITIALIZATION_FAIL',
error: string,
};
export type Action =
/** *** Settings **** */
| SetPasswordLengthAction
| SetAutoGenerationAction
/** *** Initialization **** */
| InitializationSuccessAction
| InitializationFailAction;Then create the initialization.js file in the actions folder and add the following code:
/*
* @flow
*/
import type { ThunkAction, Dispatch, InitializationFailAction, InitializationSuccessAction } from './types'
import strings from '../locales/strings'
import { InitializeData } from '../common/CryptoHelper'
/*
*** Actions ***
*/
const initializeApplication = (password: string, confirmation: string, resetRoute: () => void): ThunkAction => (
dispatch: Dispatch
) => {
if (password.length === 0 || confirmation.length === 0) {
dispatch(initializationFail(strings.passwordLenghtError))
} else if (password !== confirmation) {
dispatch(initializationFail(strings.confirmationError))
} else {
const data = InitializeData(password)
dispatch(initializationSuccess(data.salt, data.iv, data.verificationToken))
resetRoute()
}
}
export default initializeApplication
/*
*** Actions Creator ***
*/
const initializationSuccess = (salt: string, iv: string, verificationToken: string): InitializationSuccessAction => ({
type: 'INITIALIZATION_SUCCESS',
salt,
iv,
verificationToken,
})
const initializationFail = (error: string): InitializationFailAction => ({
type: 'INITIALIZATION_FAIL',
error,
})Then add the resources:
fr.js
confirmationError:'Votre mot de passe et la confirmation ne sont pas identiques',
passwordLenghtError:'Vous devez saisir le mot de passe et la confirmation',en.js
confirmationError:'Your password and confirmation are not the same',
passwordLenghtError:'You need to define a password and the confirmation',strings.js
confirmationError:I18n.t('confirmationError'),
passwordLenghtError:I18n.t('passwordLenghtError'),We will add the user reducer, which will contain the parameters we generated. Before that, define the reducer type in reducers/types.js:
...
export type UserState = {
+salt:string,
+verificationToken:string,
+iv:string,
+appInitialized:boolean,
+error:string
}
export type ReduxState = {
+settings: SettingsState,
+user:UserState
};Then add the user.js file:
/*
* @flow
*/
import type { Action } from '../actions/types'
import type { UserState } from './types'
const initialState: UserState = {
appInitialized: false,
iv: '',
salt: '',
verificationToken: '',
error: '',
}
const userState = (state: UserState = initialState, action: Action): UserState => {
switch (action.type) {
case 'INITIALIZATION_SUCCESS':
return {
...state,
appInitialized: true,
iv: action.iv,
salt: action.salt,
verificationToken: action.verificationToken,
error: '',
}
case 'INITIALIZATION_FAIL':
return { ...state, error: action.error }
default:
return state
}
}
export default userStateFinally, add the reducer to rootReducer in index.js:
/*
* @flow
*/
import { combineReducers } from 'redux'
import settings from './settings'
import user from './user'
const rootReducer = combineReducers({
settings,
user,
})
export default rootReducerAll that remains is to modify the initialization screen to integrate redux. Before that, we will modify navigation in the application. Currently the application opens on the initialization screen; however, once the application has been initialized, we want the default screen to be the one that unlocks the application. To handle that, we will define in the StackNavigator that the initial screen is the unlocking screen, and inside that screen we will check whether the application has been initialized. If not, we will send the user to the initialization screen.
Modify the StackNavigator as follows:
StackNavigator(
{
...routes
},
{
initialRouteName: 'Unlock',
},Next, integrate Redux into the unlocking screen. First add the following imports:
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import { NavigationActions } from 'react-navigation'Then create a Props type and add it to the component:
...
type Props = {
navigation: Object,
appInitialized: boolean,
};
class UnlockScreen extends Component<void, Props, State> {
...Then connect redux to the screen:
function mapStateToProps(state: ReduxState) {
return {
appInitialized: state.user.appInitialized,
}
}
export default connect(mapStateToProps, dispatch => ({
actions: bindActionCreators({}, dispatch),
}))(UnlockScreen)For now, because we have not created the actions for unlocking, we pass an empty object to bindActionCreators.
Finally, in the component add:
componentWillMount() {
if (!this.props.appInitialized) {
const resetAction = NavigationActions.reset({
index: 0,
actions: [NavigationActions.navigate({ routeName: 'Setup' })],
});
this.props.navigation.dispatch(resetAction);
}
}Here we check whether the application has been initialized; otherwise we reset navigation to the initialization screen.
Now we will connect the Setup.js screen to redux. Add the following imports and the Props type:
...
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { NavigationActions } from 'react-navigation';
import type { ReduxState } from '../reducers/types';
import initializeApplication from '../actions/initialization';
...
type Props = {
navigation: Object,
error: string,
actions: Object,
};
class SetupScreen extends Component<void, Props, State> {
...Then connect redux:
function mapStateToProps(state: ReduxState) {
return {
error: state.user.error,
}
}
export default connect(mapStateToProps, dispatch => ({
actions: bindActionCreators({ initializeApplication }, dispatch),
}))(SetupScreen)In the render method, replace the message "This text will be replaced when we add redux" with the error prop:
render() {
return (
<ScrollView contentContainerStyle={styles.container}>
...
<Text style={[styles.instruction, { color: DELETE_COLOR }]}>
{this.props.error}
</Text>
...
</ScrollView>
);
}And finally modify the submit function like this:
submit() {
const { password, confirmation } = this.state;
const resetAction = NavigationActions.reset({
index: 0,
actions: [NavigationActions.navigate({ routeName: 'Unlock' })],
});
const reset = () => this.props.navigation.dispatch(resetAction);
this.props.actions.initializeApplication(password, confirmation, reset);
}In this function, we start by retrieving the password and confirmation from the state, then we define the reset action that redirects to the unlocking screen once initialization is complete. Finally, we trigger the initialization action.
Unlocking the application
Now let's move on to unlocking the application. When unlocking the application, we will also need to decrypt the password data. In our application we do not want certain data to be saved, such as the key used for encryption and the plain-text passwords. We will therefore separate this data into two different reducers, then tell redux-persist that some reducers must not be persisted and rehydrated.
We will create two new reducers and their types. The first will be data, containing the list of unencrypted passwords and the key. The second will be cryptedData, containing the list of encrypted passwords.
In the data reducer, the password list will be normalized (see the Redux documentation). Because normalized data always has the same structure, we will create a NormalizedState type in the types folder:
export type NormalizedState = {
byId: Object,
allIds: Array<string>,
}Next, add the types for our two reducers in reducers/types.js:
...
export type DataState = {
+key: CryptoJS.WordArray,
+passwords: NormalizedState,
+error:string,
};
export type CryptedDataState = {
+passwords: string,
};
export type ReduxState = {
+settings: SettingsState,
+user: UserState,
+data: DataState,
+cryptedData: CryptedDataState,
};Then we create the cryptedData reducer. In the reducers folder, add cryptedData.js:
/*
* @flow
*/
import type { Action } from '../actions/types'
import type { CryptedDataState } from './types'
const initialState: CryptedDataState = {
passwords: '',
}
const cryptedDataState = (state: CryptedDataState = initialState, action: Action): CryptedDataState => {
switch (action.type) {
case 'UPDATE_CRYPTED_PASSWORDS':
return {
...state,
passwords: action.cryptedPassword,
}
default:
return state
}
}
export default cryptedDataStateNext, add the UPDATE_CRYPTED_PASSWORD action type in the actions/types.js file:
...
/*
**************
* Passwords
**************
*/
export type UpdateCryptedPasswordsAction = {
type: 'UPDATE_CRYPTED_PASSWORDS',
cryptedPassword: string,
};
export type Action =
/** *** Settings **** */
| SetPasswordLengthAction
| SetAutoGenerationAction
/** *** Initialization **** */
| InitializationSuccessAction
| InitializationFailAction
/** *** Passwords **** */
| UpdateCryptedPasswordsAction;Now we will create the data reducer. In the reducers folder, add data.js:
/*
* @flow
*/
import type { Action } from '../actions/types'
import type { DataState } from './types'
const initialState: DataState = {
passwords: { allIds: [], byId: {} },
key: '',
error: '',
}
const dataState = (state: DataState = initialState, action: Action): DataState => {
switch (action.type) {
case 'UNLOCK_APP':
return {
...state,
passwords: action.passwords,
key: action.key,
error: '',
}
case 'UNLOCK_APP_FAIL':
return {
...state,
error: action.error,
}
default:
return state
}
}
export default dataStateNext, add the action types in actions/types:
...
export type UnlockAppAction = {
type: 'UNLOCK_APP',
passwords: NormalizedState,
key: CryptoJS.WordArray,
};
export type UnlockAppFailAction = {
type: 'UNLOCK_APP_FAIL',
error: string,
};
export type Action =
/** *** Settings **** */
| SetPasswordLengthAction
| SetAutoGenerationAction
/** *** Initialization **** */
| InitializationSuccessAction
| InitializationFailAction
/** *** Passwords **** */
| UpdateCryptedPasswordsAction
| UnlockAppAction
| UnlockAppFailAction;Finally, add our two reducers to the root reducer:
import cryptedData from './cryptedData'
import data from './data'
const rootReducer = combineReducers({
settings,
user,
cryptedData,
data,
})Let's return to the action types we created. First, UpdateCryptedPasswordsAction will be used whenever we want to update the list of encrypted passwords (add/delete/edit).
Next, UnlockAppAction is used when we unlock the application. It updates the complete list of passwords that have just been decrypted, as well as the key used to encrypt the data.
To finish application unlocking, here is what remains to do: first, modify the data initialization action to add encryption of the empty normalized password list ({ allIds: [], byId: {} }). Then create the action that unlocks the application, connect the unlocking screen to redux, and finally remove the data reducer from the data saved by redux-persist.
During application initialization, once the data (salt, verification token, etc.) has been initialized, we will encrypt the { allIds: [], byId: {} } object and add it to the state by dispatching the UpdateCryptedPasswordsAction.
Start by adding the UpdateCryptedPasswordsAction type import and the Encrypt function in actions/initialize:
import type {
ThunkAction,
Dispatch,
InitializationFailAction,
InitializationSuccessAction,
UpdateCryptedPasswordsAction,
} from './types';
import strings from '../locales/strings';
import { InitializeData, Encrypt } from '../common/CryptoHelper';Then add the following action creator:
const updateCryptedPasswords = (cryptedPassword: strings) => ({
type: 'UPDATE_CRYPTED_PASSWORDS',
cryptedPassword,
});Finally, replace the initializeApplication function with this one:
const initializeApplication = (password: string, confirmation: string, resetRoute: () => void): ThunkAction => (
dispatch: Dispatch
) => {
if (password.length === 0 || confirmation.length === 0) {
dispatch(initializationFail(strings.passwordLenghtError))
} else if (password !== confirmation) {
dispatch(initializationFail(strings.confirmationError))
} else {
const data = InitializeData(password)
const emptyPassword = JSON.stringify({ allIds: [], byId: {} })
const cryptedEmptyPasswords = Encrypt(emptyPassword, data.key, data.iv)
dispatch(initializationSuccess(data.salt, data.iv, data.verificationToken))
dispatch(updateCryptedPasswords(cryptedEmptyPasswords))
resetRoute()
}
}Now we will create the action that unlocks the application and connect the screen with redux.
In the actions folder, create an unlock.js file and add the following code:
/*
* @flow
*/
import type { ThunkAction, Dispatch, UnlockAppAction, UnlockAppFailAction } from './types'
import type { NormalizedState } from '../types/NormalizedState'
import strings from '../locales/strings'
import { IsValidPassword, Decrypt, GenerateKey } from '../common/CryptoHelper'
/*
*** Actions ***
*/
const unlockApp = (
password: string,
verificationToken: string,
salt: string,
iv: string,
cryptedPasswords: string,
resetRoute: () => void
): ThunkAction => (dispatch: Dispatch) => {
if (IsValidPassword(verificationToken, password, salt)) {
const key = GenerateKey(password, salt)
const passwords = Decrypt(cryptedPasswords, key, iv)
dispatch(unlockApplication(key, JSON.parse(passwords)))
resetRoute()
} else {
dispatch(unlockAppFail(strings.invalid_password))
}
}
export default unlockApp
/*
*** Actions Creator ***
*/
const unlockApplication = (key: strings, passwords: NormalizedState): UnlockAppAction => ({
type: 'UNLOCK_APP',
key,
passwords,
})
const unlockAppFail = (error: string): UnlockAppFailAction => ({
type: 'UNLOCK_APP_FAIL',
error,
})Then add the resource:
fr.js
invalid_password: 'Votre mot de passe est invalide. Réessayer',en.js
invalid_password: 'Your password is invalid. Try again',strings.js
invalid_password: I18n.t('invalid_password'),Next, in the Unlock.js screen, import the unlockApp action and modify the Props type:
import unlockApp from '../actions/unlock'
type Props = {
navigation: Object,
actions: Object,
appInitialized: boolean,
cryptedPasswords: string,
verificationToken: string,
salt: string,
iv: string,
error: string,
}Then add the new props in mapStateToProps and add the action in bindActionCreators:
function mapStateToProps(state: ReduxState) {
return {
appInitialized: state.user.appInitialized,
cryptedPasswords: state.cryptedData.passwords,
verificationToken: state.user.verificationToken,
salt: state.user.salt,
iv: state.user.iv,
error: state.data.error,
}
}
export default connect(mapStateToProps, dispatch => ({
actions: bindActionCreators({ unlockApp }, dispatch),
}))(UnlockScreen)Finally, all that remains is to replace the text "This text will be replaced when we add redux" with the error prop and modify the submit function:
...
submit() {
const resetAction = NavigationActions.reset({
index: 0,
actions: [NavigationActions.navigate({ routeName: 'App' })],
});
const reset = () => this.props.navigation.dispatch(resetAction);
this.props.actions.unlockApp(
this.state.password,
this.props.verificationToken,
this.props.salt,
this.props.iv,
this.props.cryptedPasswords,
reset,
);
}
render() {
return (
<ScrollView contentContainerStyle={styles.container}>
...
<Text style={styles.error}> {this.props.error}</Text>
...
</ScrollView>
);
}
...To finish this article, we will exclude the data reducer from the data that redux-persist should save. In configureStore.js, simply add the list of reducers we want to exclude in the config object like this:
const config = {
key: 'root', // key is required
storage, // storage is now required
blacklist: ['data'],
}Be careful: if you have already initialized and unlocked the application before excluding the reducer, the data has been saved. You should delete it either with AsyncStorage.clear or from your smartphone/emulator settings so the change is taken into account.
We have now finished application initialization and unlocking. In the next article we will move on to password management.