Skip to content

Commit 14570e9

Browse files
committed
feat(encryptor): abstract underlying encryption library + fix key parameter
There was also an error in the way we were using the actual key value. We were passing an EncryptionKey to the native library... This would have probably crash with the real library.
1 parent b224271 commit 14570e9

File tree

5 files changed

+146
-40
lines changed

5 files changed

+146
-40
lines changed

app/core/Encryptor/Encryptor.test.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -140,12 +140,6 @@ describe('Encryptor', () => {
140140
lib,
141141
};
142142

143-
const expectedKey: EncryptionKey = {
144-
key: expectedKeyValue,
145-
keyMetadata: keyMetadata ?? KEY_DERIVATION_LEGACY_OPTIONS,
146-
lib,
147-
};
148-
149143
const decryptedObject = await encryptor.decrypt(
150144
password,
151145
JSON.stringify(
@@ -160,7 +154,11 @@ describe('Encryptor', () => {
160154
lib === ENCRYPTION_LIBRARY.original
161155
? decryptAesSpy
162156
: decryptAesForkedSpy,
163-
).toHaveBeenCalledWith(mockVault.cipher, expectedKey, mockVault.iv);
157+
).toHaveBeenCalledWith(
158+
mockVault.cipher,
159+
expectedKeyValue,
160+
mockVault.iv,
161+
);
164162
expect(
165163
lib === ENCRYPTION_LIBRARY.original
166164
? pbkdf2AesSpy

app/core/Encryptor/Encryptor.ts

Lines changed: 14 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
1-
import { NativeModules } from 'react-native';
21
import { hasProperty, isPlainObject, Json } from '@metamask/utils';
32
import {
43
SALT_BYTES_COUNT,
5-
SHA256_DIGEST_LENGTH,
64
ENCRYPTION_LIBRARY,
75
KEY_DERIVATION_LEGACY_OPTIONS,
86
KeyDerivationIteration,
@@ -13,9 +11,7 @@ import type {
1311
EncryptionResult,
1412
KeyDerivationOptions,
1513
} from './types';
16-
17-
const Aes = NativeModules.Aes;
18-
const AesForked = NativeModules.AesForked;
14+
import { getEncryptionLibrary } from './lib';
1915

2016
/**
2117
* Checks if the provided object is a `KeyDerivationOptions`.
@@ -96,17 +92,6 @@ class Encryptor implements WithKeyEncryptor<EncryptionKey, Json> {
9692
return btoa(String.fromCharCode.apply(null, view as unknown as number[]));
9793
};
9894

99-
/**
100-
* Generates a random IV.
101-
*
102-
* @param size - The number of bytes for the IV.
103-
* @returns The generated IV.
104-
*/
105-
private generateIV = async (size: number): Promise<unknown> =>
106-
// Naming isn't perfect here, but this is how the library generates random IV (and encodes it the right way)
107-
// See: https://www.npmjs.com/package/react-native-aes-crypto#example
108-
await Aes.randomKey(size);
109-
11095
/**
11196
* Generate an encryption key from a password and random salt, specifying
11297
* key derivation options.
@@ -123,15 +108,7 @@ class Encryptor implements WithKeyEncryptor<EncryptionKey, Json> {
123108
opts: KeyDerivationOptions,
124109
lib = ENCRYPTION_LIBRARY.original,
125110
): Promise<EncryptionKey> => {
126-
const key =
127-
lib === ENCRYPTION_LIBRARY.original
128-
? await Aes.pbkdf2(
129-
password,
130-
salt,
131-
opts.params.iterations,
132-
SHA256_DIGEST_LENGTH,
133-
)
134-
: await AesForked.pbkdf2(password, salt);
111+
const key = await getEncryptionLibrary(lib).deriveKey(password, salt, opts);
135112

136113
return {
137114
key,
@@ -151,14 +128,18 @@ class Encryptor implements WithKeyEncryptor<EncryptionKey, Json> {
151128
key: EncryptionKey,
152129
data: Json,
153130
): Promise<EncryptionResult> => {
154-
const iv = await this.generateIV(16);
131+
const text = JSON.stringify(data);
155132

156-
return Aes.encrypt(data, key, iv).then((cipher: string) => ({
133+
const lib = getEncryptionLibrary(key.lib);
134+
const iv = await lib.generateIV(16);
135+
const cipher = await lib.encrypt(text, key.key, iv);
136+
137+
return {
157138
cipher,
158139
iv,
159140
keyMetadata: key.keyMetadata,
160141
lib: key.lib,
161-
}));
142+
};
162143
};
163144

164145
/**
@@ -173,10 +154,10 @@ class Encryptor implements WithKeyEncryptor<EncryptionKey, Json> {
173154
payload: EncryptionResult,
174155
): Promise<unknown> => {
175156
// TODO: Check for key and payload compatiblity?
176-
const text =
177-
payload.lib === ENCRYPTION_LIBRARY.original
178-
? await Aes.decrypt(payload.cipher, key, payload.iv)
179-
: await AesForked.decrypt(payload.cipher, key, payload.iv);
157+
158+
// We assume that both `payload.lib` and `key.lib` are the same here!
159+
const lib = getEncryptionLibrary(payload.lib);
160+
const text = await lib.decrypt(payload.cipher, key.key, payload.iv);
180161

181162
return JSON.parse(text);
182163
};
@@ -203,7 +184,7 @@ class Encryptor implements WithKeyEncryptor<EncryptionKey, Json> {
203184
// NOTE: When re-encrypting, we always use the original library and the KDF parameters from
204185
// the encryptor itself. This makes sure we always re-encrypt with the "latest" and "best"
205186
// setup possible.
206-
const result = await this.encryptWithKey(key, JSON.stringify(data));
187+
const result = await this.encryptWithKey(key, data);
207188
result.lib = key.lib; // Use the same library than the one used for key generation!
208189
result.salt = salt;
209190
result.keyMetadata = key.keyMetadata;

app/core/Encryptor/lib.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { getEncryptionLibrary, AesLib, AesForkedLib } from './lib';
2+
import { ENCRYPTION_LIBRARY } from './constants';
3+
4+
describe('lib', () => {
5+
describe('getLib', () => {
6+
it('returns the original library', () => {
7+
const lib = AesLib;
8+
9+
expect(getEncryptionLibrary(ENCRYPTION_LIBRARY.original)).toBe(lib);
10+
});
11+
12+
it('returns the forked library in any other case', () => {
13+
const lib = AesForkedLib;
14+
15+
expect(getEncryptionLibrary('random-lib')).toBe(lib);
16+
// Some older vault might not have the `lib` field, so it is considered `undefined`
17+
expect(getEncryptionLibrary(undefined)).toBe(lib);
18+
});
19+
});
20+
});

app/core/Encryptor/lib.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { NativeModules } from 'react-native';
2+
import { ENCRYPTION_LIBRARY, SHA256_DIGEST_LENGTH } from './constants';
3+
import { EncryptionLibrary, KeyDerivationOptions } from './types';
4+
5+
// Actual native libraries
6+
const Aes = NativeModules.Aes;
7+
const AesForked = NativeModules.AesForked;
8+
9+
class AesEncryptionLibrary implements EncryptionLibrary {
10+
deriveKey = async (
11+
password: string,
12+
salt: string,
13+
opts: KeyDerivationOptions,
14+
): Promise<string> =>
15+
await Aes.pbkdf2(
16+
password,
17+
salt,
18+
opts.params.iterations,
19+
SHA256_DIGEST_LENGTH,
20+
);
21+
22+
generateIV = async (size: number): Promise<string> =>
23+
// Naming isn't perfect here, but this is how the library generates random IV (and encodes it the right way)
24+
// See: https://www.npmjs.com/package/react-native-aes-crypto#example
25+
await Aes.randomKey(size);
26+
27+
encrypt = async (data: string, key: string, iv: unknown): Promise<string> =>
28+
await Aes.encrypt(data, key, iv);
29+
30+
decrypt = async (data: string, key: string, iv: unknown): Promise<string> =>
31+
await Aes.decrypt(data, key, iv);
32+
}
33+
34+
class AesForkedEncryptionLibrary implements EncryptionLibrary {
35+
deriveKey = async (
36+
password: string,
37+
salt: string,
38+
_opts: KeyDerivationOptions,
39+
): Promise<string> => await AesForked.pbkdf2(password, salt);
40+
41+
generateIV = async (size: number): Promise<string> =>
42+
// NOTE: For some reason, we are not using the AesForked one here, so keep the previous behavior!
43+
// Naming isn't perfect here, but this is how the library generates random IV (and encodes it the right way)
44+
// See: https://www.npmjs.com/package/react-native-aes-crypto#example
45+
await Aes.randomKey(size);
46+
47+
encrypt = async (data: string, key: string, iv: unknown): Promise<string> =>
48+
// NOTE: For some reason, we are not using the AesForked one here, so keep the previous behavior!
49+
await Aes.encrypt(data, key, iv);
50+
51+
decrypt = async (data: string, key: string, iv: unknown): Promise<string> =>
52+
await AesForked.decrypt(data, key, iv);
53+
}
54+
55+
// Those wrappers are stateless, we can build them only once!
56+
export const AesLib = new AesEncryptionLibrary();
57+
export const AesForkedLib = new AesForkedEncryptionLibrary();
58+
59+
export function getEncryptionLibrary(
60+
lib: string | undefined,
61+
): EncryptionLibrary {
62+
return lib === ENCRYPTION_LIBRARY.original ? AesLib : AesForkedLib;
63+
}

app/core/Encryptor/types.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,50 @@ import type { KeyDerivationOptions } from '@metamask/browser-passworder';
44
/** Key derivation function options. */
55
export type { KeyDerivationOptions };
66

7+
/**
8+
* Interface that needs to be implemented for the underlying library used by the `Encryptor`.
9+
*/
10+
export interface EncryptionLibrary {
11+
/**
12+
* Derive a key based on a password and some other parameters (KDF).
13+
*
14+
* @param password - The password used to generate the key.
15+
* @param salt - The salt used during key generation.
16+
* @param opts - KDF options used during key generation.
17+
* @return The generated key.
18+
*/
19+
deriveKey(
20+
password: string,
21+
salt: string,
22+
opts: KeyDerivationOptions,
23+
): Promise<string>;
24+
/**
25+
* Generate IV (Initialization Vector) used during encryption/decryption.
26+
*
27+
* @param size - The IV size.
28+
* @return The randomly generated IV.
29+
*/
30+
generateIV(size: number): Promise<string>;
31+
/**
32+
* Encrypts data.
33+
*
34+
* @param data - The data to encrypt.
35+
* @param key - The encryption key (generated by this same library).
36+
* @param iv - The IV.
37+
* @returns The encrypted data.
38+
*/
39+
encrypt(data: string, key: string, iv: string): Promise<string>;
40+
/**
41+
* Decrypts encrypted data.
42+
*
43+
* @param data - The data to decrypt.
44+
* @param key - The encryption key (generated by this same library).
45+
* @param iv - The IV (the same one used during encryption).
46+
* @returns The decrypted original data.
47+
*/
48+
decrypt(data: string, key: string, iv: string): Promise<string>;
49+
}
50+
751
/**
852
* The result of an encryption operation.
953
* @interface EncryptionResult

0 commit comments

Comments
 (0)