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 #9093

Merged
merged 27 commits into from
Apr 19, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
5b43a7b
refactor: encryptor to typescript
gantunesr Mar 3, 2024
5516e4a
refactor: migrate Encryptor to TypeScript and increase PBKDF2 iterati…
gantunesr Mar 3, 2024
8777bac
Merge branch 'main' into refactor/encryptor-class
gantunesr Mar 28, 2024
2e6efc1
Merge branch 'main' into refactor/encryptor-class
gantunesr Apr 7, 2024
8efbf16
test: add return value to crypto.getRandomValues mock
gantunesr Apr 7, 2024
7e40de3
feat: update iteration numbers
gantunesr Apr 8, 2024
a9366e1
Merge branch 'main' into refactor/encryptor-class
gantunesr Apr 8, 2024
7d7db56
Merge branch 'main' into refactor/encryptor-class
gantunesr Apr 15, 2024
d5b1059
chore: update @metamask/eth-keyring-controller patch
gantunesr Apr 15, 2024
0ce3c52
chore: use correct param name
gantunesr Apr 15, 2024
e94f5ec
chore: update constant name
gantunesr Apr 15, 2024
5af397f
chore: revert param name
gantunesr Apr 15, 2024
4ceab3d
Merge branch 'main' into refactor/encryptor-class
gantunesr Apr 15, 2024
40f69b1
Merge branch 'main' into refactor/encryptor-class
gantunesr Apr 16, 2024
886fbe5
Merge branch 'main' of https://github.com/MetaMask/metamask-mobile in…
gantunesr Apr 16, 2024
b878dfc
Merge branch 'refactor/encryptor-class' of https://github.com/MetaMas…
gantunesr Apr 16, 2024
6c477dd
chore: dedupe
gantunesr Apr 16, 2024
4a25027
chore: dedupe
gantunesr Apr 16, 2024
ae71f72
Merge branch 'main' into refactor/encryptor-class
gantunesr Apr 16, 2024
b3ff44f
Merge branch 'main' into refactor/encryptor-class
gantunesr Apr 16, 2024
429d8cd
Merge branch 'main' into refactor/encryptor-class
gantunesr Apr 18, 2024
c861bee
Merge branch 'main' of https://github.com/MetaMask/metamask-mobile in…
gantunesr Apr 18, 2024
e3a4e66
chore: use legacy number of iterations
gantunesr Apr 18, 2024
3a70ab2
Merge branch 'main' into refactor/encryptor-class
gantunesr Apr 18, 2024
2b41822
Revert "chore: use legacy number of iterations"
gantunesr Apr 18, 2024
72bbd42
Merge branch 'refactor/encryptor-class' of https://github.com/MetaMas…
gantunesr Apr 18, 2024
ccf8052
Merge branch 'main' into refactor/encryptor-class
gantunesr Apr 19, 2024
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,
gantunesr marked this conversation as resolved.
Show resolved Hide resolved
},
},
}),
).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', 600000, 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,
gantunesr marked this conversation as resolved.
Show resolved Hide resolved
},
{
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();
});

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 () => {
gantunesr marked this conversation as resolved.
Show resolved Hide resolved
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