Skip to content

Commit 13afba8

Browse files
authored
refactor: migrate Encryptor to TypeScript and increase PBKDF2 iterations number (#9093)
This change introduces the following modifications - `@metamask/keyring-controller` bump from `v8.1.0` to `v9.0.0` - Refactor the Encryptor class from JS to TS - Adds unit tests to the Encryptor class - Increases the number of iterations for PBKDF2 from 5.000 to 600.000 Co-authored-by: gantunesr <gantunesr@users.noreply.github.com>
1 parent 4f64f83 commit 13afba8

File tree

14 files changed

+696
-171
lines changed

14 files changed

+696
-171
lines changed

app/core/Encryptor.js

Lines changed: 0 additions & 77 deletions
This file was deleted.
Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
import { NativeModules } from 'react-native';
2+
import { Encryptor } from './Encryptor';
3+
import {
4+
ENCRYPTION_LIBRARY,
5+
DERIVATION_PARAMS,
6+
KeyDerivationIteration,
7+
} from './constants';
8+
9+
const Aes = NativeModules.Aes;
10+
const AesForked = NativeModules.AesForked;
11+
12+
describe('Encryptor', () => {
13+
let encryptor: Encryptor;
14+
15+
beforeEach(() => {
16+
encryptor = new Encryptor({ derivationParams: DERIVATION_PARAMS });
17+
});
18+
19+
describe('constructor', () => {
20+
it('throws an error if the provided iterations do not meet the minimum required', () => {
21+
expect(
22+
() =>
23+
new Encryptor({
24+
derivationParams: {
25+
algorithm: 'PBKDF2',
26+
params: {
27+
iterations: 100,
28+
},
29+
},
30+
}),
31+
).toThrowError(
32+
`Invalid key derivation iterations: 100. Recommended number of iterations is ${KeyDerivationIteration.Default}. Minimum required is ${KeyDerivationIteration.Minimum}.`,
33+
);
34+
});
35+
});
36+
37+
describe('encrypt', () => {
38+
afterEach(() => {
39+
jest.clearAllMocks();
40+
});
41+
42+
it('should encrypt an object correctly', async () => {
43+
const password = 'testPassword';
44+
const objectToEncrypt = { key: 'value' };
45+
46+
const encryptedString = await encryptor.encrypt(
47+
password,
48+
objectToEncrypt,
49+
);
50+
const encryptedObject = JSON.parse(encryptedString);
51+
52+
expect(encryptedObject).toHaveProperty('cipher');
53+
expect(encryptedObject).toHaveProperty('iv');
54+
expect(encryptedObject).toHaveProperty('salt');
55+
expect(encryptedObject).toHaveProperty('lib', 'original');
56+
});
57+
});
58+
59+
describe('decrypt', () => {
60+
let decryptAesSpy: jest.SpyInstance,
61+
pbkdf2AesSpy: jest.SpyInstance,
62+
decryptAesForkedSpy: jest.SpyInstance,
63+
pbkdf2AesForkedSpy: jest.SpyInstance;
64+
65+
beforeEach(() => {
66+
decryptAesSpy = jest
67+
.spyOn(Aes, 'decrypt')
68+
.mockResolvedValue('{"mockData": "mockedPlainText"}');
69+
pbkdf2AesSpy = jest
70+
.spyOn(Aes, 'pbkdf2')
71+
.mockResolvedValue('mockedAesKey');
72+
decryptAesForkedSpy = jest
73+
.spyOn(AesForked, 'decrypt')
74+
.mockResolvedValue('{"mockData": "mockedPlainText"}');
75+
pbkdf2AesForkedSpy = jest
76+
.spyOn(AesForked, 'pbkdf2')
77+
.mockResolvedValue('mockedAesForkedKey');
78+
});
79+
80+
afterEach(() => {
81+
decryptAesSpy.mockRestore();
82+
pbkdf2AesSpy.mockRestore();
83+
decryptAesForkedSpy.mockRestore();
84+
pbkdf2AesForkedSpy.mockRestore();
85+
});
86+
87+
it.each([
88+
{
89+
lib: ENCRYPTION_LIBRARY.original,
90+
expectedKey: 'mockedAesKey',
91+
expectedPBKDF2Args: ['testPassword', 'mockedSalt', 600000, 256],
92+
description:
93+
'with original library and default iterations number for key generation',
94+
keyMetadata: DERIVATION_PARAMS,
95+
},
96+
{
97+
lib: ENCRYPTION_LIBRARY.original,
98+
expectedKey: 'mockedAesKey',
99+
expectedPBKDF2Args: ['testPassword', 'mockedSalt', 5000, 256],
100+
description:
101+
'with original library and old iterations number for key generation',
102+
},
103+
{
104+
lib: 'random-lib', // Assuming not using "original" should lead to AesForked
105+
expectedKey: 'mockedAesForkedKey',
106+
expectedPBKDF2Args: ['testPassword', 'mockedSalt'],
107+
description:
108+
'with library different to "original" and default iterations number for key generation',
109+
keyMetadata: DERIVATION_PARAMS,
110+
},
111+
{
112+
lib: 'random-lib', // Assuming not using "original" should lead to AesForked
113+
expectedKey: 'mockedAesForkedKey',
114+
expectedPBKDF2Args: ['testPassword', 'mockedSalt'],
115+
description:
116+
'with library different to "original" and old iterations number for key generation',
117+
},
118+
])(
119+
'decrypts a string correctly $description',
120+
async ({ lib, expectedKey, expectedPBKDF2Args, keyMetadata }) => {
121+
const password = 'testPassword';
122+
const mockVault = {
123+
cipher: 'mockedCipher',
124+
iv: 'mockedIV',
125+
salt: 'mockedSalt',
126+
lib,
127+
};
128+
129+
const decryptedObject = await encryptor.decrypt(
130+
password,
131+
JSON.stringify(
132+
keyMetadata !== undefined
133+
? { ...mockVault, keyMetadata }
134+
: mockVault,
135+
),
136+
);
137+
138+
expect(decryptedObject).toEqual(expect.any(Object));
139+
expect(
140+
lib === ENCRYPTION_LIBRARY.original
141+
? decryptAesSpy
142+
: decryptAesForkedSpy,
143+
).toHaveBeenCalledWith(mockVault.cipher, expectedKey, mockVault.iv);
144+
expect(
145+
lib === ENCRYPTION_LIBRARY.original
146+
? pbkdf2AesSpy
147+
: pbkdf2AesForkedSpy,
148+
).toHaveBeenCalledWith(...expectedPBKDF2Args);
149+
},
150+
);
151+
});
152+
153+
describe('isVaultUpdated', () => {
154+
it('returns true if a vault has the correct format', () => {
155+
expect(
156+
encryptor.isVaultUpdated(
157+
JSON.stringify({
158+
cipher: 'mockedCipher',
159+
iv: 'mockedIV',
160+
salt: 'mockedSalt',
161+
lib: 'original',
162+
keyMetadata: DERIVATION_PARAMS,
163+
}),
164+
),
165+
).toBe(true);
166+
});
167+
168+
it('returns false if a vault has the incorrect format', () => {
169+
expect(
170+
encryptor.isVaultUpdated(
171+
JSON.stringify({
172+
cipher: 'mockedCipher',
173+
iv: 'mockedIV',
174+
salt: 'mockedSalt',
175+
lib: 'original',
176+
}),
177+
),
178+
).toBe(false);
179+
});
180+
});
181+
182+
describe('updateVault', () => {
183+
let encryptSpy: jest.SpyInstance, decryptSpy: jest.SpyInstance;
184+
const expectedKeyMetadata = DERIVATION_PARAMS;
185+
186+
beforeEach(() => {
187+
encryptSpy = jest
188+
.spyOn(Aes, 'encrypt')
189+
.mockResolvedValue(() => Promise.resolve('mockedCipher'));
190+
decryptSpy = jest
191+
.spyOn(Aes, 'decrypt')
192+
.mockResolvedValue('{"mockData": "mockedPlainText"}');
193+
});
194+
195+
afterEach(() => {
196+
encryptSpy.mockRestore();
197+
decryptSpy.mockRestore();
198+
});
199+
200+
it('updates a vault correctly if keyMetadata is not present', async () => {
201+
const mockVault = {
202+
cipher: 'mockedCipher',
203+
iv: 'mockedIV',
204+
salt: 'mockedSalt',
205+
lib: 'original',
206+
};
207+
208+
const updatedVault = await encryptor.updateVault(
209+
JSON.stringify(mockVault),
210+
'mockPassword',
211+
);
212+
213+
const vault = JSON.parse(updatedVault);
214+
215+
expect(encryptSpy).toBeCalledTimes(1);
216+
expect(decryptSpy).toBeCalledTimes(1);
217+
expect(vault).toHaveProperty('keyMetadata');
218+
expect(vault.keyMetadata).toStrictEqual(expectedKeyMetadata);
219+
});
220+
221+
it('does not update a vault if algorithm is PBKDF2 and the number of iterations is 900000', async () => {
222+
const mockVault = {
223+
cipher: 'mockedCipher',
224+
iv: 'mockedIV',
225+
salt: 'mockedSalt',
226+
lib: 'original',
227+
keyMetadata: DERIVATION_PARAMS,
228+
};
229+
230+
const updatedVault = await encryptor.updateVault(
231+
JSON.stringify(mockVault),
232+
'mockPassword',
233+
);
234+
235+
const vault = JSON.parse(updatedVault);
236+
237+
expect(encryptSpy).toBeCalledTimes(0);
238+
expect(decryptSpy).toBeCalledTimes(0);
239+
expect(vault).toHaveProperty('keyMetadata');
240+
expect(vault.keyMetadata).toStrictEqual(expectedKeyMetadata);
241+
});
242+
});
243+
});

0 commit comments

Comments
 (0)