Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: migrate Encryptor to TypeScript and increase PBKDF2 iterations number #8828

Closed
wants to merge 18 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 0 additions & 77 deletions app/core/Encryptor.js

This file was deleted.

243 changes: 243 additions & 0 deletions app/core/Encryptor/Encryptor.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
import { NativeModules } from 'react-native';
import { Encryptor } from './Encryptor';
import {
ENCRYPTION_LIBRARY,
DEFAULT_DERIVATION_PARAMS,
KeyDerivationIteration,
} from './constants';

const Aes = NativeModules.Aes;
const AesForked = NativeModules.AesForked;

describe('Encryptor', () => {
let encryptor: Encryptor;

beforeEach(() => {
encryptor = new Encryptor({ derivationParams: DEFAULT_DERIVATION_PARAMS });
});

describe('constructor', () => {
it('throws an error if the provided iterations do not meet the minimum required', () => {
expect(
() =>
new Encryptor({
derivationParams: {
algorithm: 'PBKDF2',
params: {
iterations: 100,
},
},
}),
).toThrowError(
`Invalid key derivation iterations: 100. Recommended number of iterations is ${KeyDerivationIteration.Default}. Minimum required is ${KeyDerivationIteration.Minimum}.`,
);
});
});

describe('encrypt', () => {
afterEach(() => {
jest.clearAllMocks();
});

it('should encrypt an object correctly', async () => {
const password = 'testPassword';
const objectToEncrypt = { key: 'value' };

const encryptedString = await encryptor.encrypt(
password,
objectToEncrypt,
);
const encryptedObject = JSON.parse(encryptedString);

expect(encryptedObject).toHaveProperty('cipher');
expect(encryptedObject).toHaveProperty('iv');
expect(encryptedObject).toHaveProperty('salt');
expect(encryptedObject).toHaveProperty('lib', 'original');
});
});

describe('decrypt', () => {
let decryptAesSpy: jest.SpyInstance,
pbkdf2AesSpy: jest.SpyInstance,
decryptAesForkedSpy: jest.SpyInstance,
pbkdf2AesForkedSpy: jest.SpyInstance;

beforeEach(() => {
decryptAesSpy = jest
.spyOn(Aes, 'decrypt')
.mockResolvedValue('{"mockData": "mockedPlainText"}');
pbkdf2AesSpy = jest
.spyOn(Aes, 'pbkdf2')
.mockResolvedValue('mockedAesKey');
decryptAesForkedSpy = jest
.spyOn(AesForked, 'decrypt')
.mockResolvedValue('{"mockData": "mockedPlainText"}');
pbkdf2AesForkedSpy = jest
.spyOn(AesForked, 'pbkdf2')
.mockResolvedValue('mockedAesForkedKey');
});

afterEach(() => {
decryptAesSpy.mockRestore();
pbkdf2AesSpy.mockRestore();
decryptAesForkedSpy.mockRestore();
pbkdf2AesForkedSpy.mockRestore();
});

it.each([
{
lib: ENCRYPTION_LIBRARY.original,
expectedKey: 'mockedAesKey',
expectedPBKDF2Args: ['testPassword', 'mockedSalt', 900000, 256],
description:
'with original library and default iterations number for key generation',
keyMetadata: DEFAULT_DERIVATION_PARAMS,
},
{
lib: ENCRYPTION_LIBRARY.original,
expectedKey: 'mockedAesKey',
expectedPBKDF2Args: ['testPassword', 'mockedSalt', 5000, 256],
description:
'with original library and old iterations number for key generation',
},
{
lib: 'random-lib', // Assuming not using "original" should lead to AesForked
expectedKey: 'mockedAesForkedKey',
expectedPBKDF2Args: ['testPassword', 'mockedSalt'],
description:
'with library different to "original" and default iterations number for key generation',
keyMetadata: DEFAULT_DERIVATION_PARAMS,
},
{
lib: 'random-lib', // Assuming not using "original" should lead to AesForked
expectedKey: 'mockedAesForkedKey',
expectedPBKDF2Args: ['testPassword', 'mockedSalt'],
description:
'with library different to "original" and old iterations number for key generation',
},
])(
'decrypts a string correctly $description',
async ({ lib, expectedKey, expectedPBKDF2Args, keyMetadata }) => {
const password = 'testPassword';
const mockVault = {
cipher: 'mockedCipher',
iv: 'mockedIV',
salt: 'mockedSalt',
lib,
};

const decryptedObject = await encryptor.decrypt(
password,
JSON.stringify(
keyMetadata !== undefined
? { ...mockVault, keyMetadata }
: mockVault,
),
);

expect(decryptedObject).toEqual(expect.any(Object));
expect(
lib === ENCRYPTION_LIBRARY.original
? decryptAesSpy
: decryptAesForkedSpy,
).toHaveBeenCalledWith(mockVault.cipher, expectedKey, mockVault.iv);
expect(
lib === ENCRYPTION_LIBRARY.original
? pbkdf2AesSpy
: pbkdf2AesForkedSpy,
).toHaveBeenCalledWith(...expectedPBKDF2Args);
},
);
});

describe('isVaultUpdated', () => {
it('returns true if a vault has the correct format', () => {
expect(
encryptor.isVaultUpdated(
JSON.stringify({
cipher: 'mockedCipher',
iv: 'mockedIV',
salt: 'mockedSalt',
lib: 'original',
keyMetadata: DEFAULT_DERIVATION_PARAMS,
}),
),
).toBe(true);
});

it('returns false if a vault has the incorrect format', () => {
expect(
encryptor.isVaultUpdated(
JSON.stringify({
cipher: 'mockedCipher',
iv: 'mockedIV',
salt: 'mockedSalt',
lib: 'original',
}),
),
).toBe(false);
});
});

describe('updateVault', () => {
let encryptSpy: jest.SpyInstance, decryptSpy: jest.SpyInstance;
const expectedKeyMetadata = DEFAULT_DERIVATION_PARAMS;

beforeEach(() => {
encryptSpy = jest
.spyOn(Aes, 'encrypt')
.mockResolvedValue(() => Promise.resolve('mockedCipher'));
decryptSpy = jest
.spyOn(Aes, 'decrypt')
.mockResolvedValue('{"mockData": "mockedPlainText"}');
});

afterEach(() => {
encryptSpy.mockRestore();
decryptSpy.mockRestore();
});

gantunesr marked this conversation as resolved.
Show resolved Hide resolved
it('updates a vault correctly if keyMetadata is not present', async () => {
const mockVault = {
cipher: 'mockedCipher',
iv: 'mockedIV',
salt: 'mockedSalt',
lib: 'original',
};

const updatedVault = await encryptor.updateVault(
JSON.stringify(mockVault),
'mockPassword',
);

const vault = JSON.parse(updatedVault);

expect(encryptSpy).toBeCalledTimes(1);
expect(decryptSpy).toBeCalledTimes(1);
expect(vault).toHaveProperty('keyMetadata');
expect(vault.keyMetadata).toStrictEqual(expectedKeyMetadata);
});

it('does not update a vault if algorithm is PBKDF2 and the number of iterations is 900000', async () => {
const mockVault = {
cipher: 'mockedCipher',
iv: 'mockedIV',
salt: 'mockedSalt',
lib: 'original',
keyMetadata: DEFAULT_DERIVATION_PARAMS,
};

const updatedVault = await encryptor.updateVault(
JSON.stringify(mockVault),
'mockPassword',
);

const vault = JSON.parse(updatedVault);

expect(encryptSpy).toBeCalledTimes(0);
expect(decryptSpy).toBeCalledTimes(0);
expect(vault).toHaveProperty('keyMetadata');
expect(vault.keyMetadata).toStrictEqual(expectedKeyMetadata);
});
});
});
Loading
Loading