Repository navigation
perf(profile-sync-controller): Replace JS AES implementation with cryptography package #10621
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
Changes from all commits
7452189
fe4ce64
9bf8c61
836aad0
8165e14
9f91c23
fd045a6
d6c135f
ee15675
5cc0186
0a3cbc4
8e2bd03
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,13 @@ | ||
| import { gcm } from '@noble/ciphers/aes'; | ||
| import { randomBytes } from '@noble/ciphers/webcrypto'; | ||
| import { decrypt, encrypt, IV_LENGTH } from '@metamask/cryptography/aes-gcm'; | ||
| import { | ||
| getErrorMessage, | ||
| stringToBytes, | ||
| bytesToHex, | ||
| concatBytes, | ||
| remove0x, | ||
| } from '@metamask/utils'; | ||
| import { scryptAsync } from '@noble/hashes/scrypt'; | ||
| import { sha256 } from '@noble/hashes/sha256'; | ||
| import { utf8ToBytes, concatBytes, bytesToHex } from '@noble/hashes/utils'; | ||
|
|
||
| import type { NativeScrypt } from '../types/encryption.js'; | ||
| import { | ||
|
|
@@ -12,7 +17,6 @@ import { | |
| } from './cache.js'; | ||
| import { | ||
| ALGORITHM_KEY_SIZE, | ||
| ALGORITHM_NONCE_SIZE, | ||
| MAX_KDF_PROMISE_CACHE_SIZE, | ||
| SCRYPT_N, | ||
| SCRYPT_p, | ||
|
|
@@ -53,7 +57,7 @@ class EncryptorDecryptor { | |
| // Promise cache for ongoing KDF operations to prevent duplicate work | ||
| readonly #kdfPromiseCache = new Map< | ||
| string, | ||
| Promise<{ key: Uint8Array; salt: Uint8Array }> | ||
| Promise<{ key: Uint8Array<ArrayBuffer>; salt: Uint8Array<ArrayBuffer> }> | ||
| >(); | ||
|
|
||
| async encryptString( | ||
|
|
@@ -68,8 +72,7 @@ class EncryptorDecryptor { | |
| nativeScryptCrypto, | ||
| ); | ||
| } catch (e) { | ||
| const errorMessage = e instanceof Error ? e.message : JSON.stringify(e); | ||
| throw new Error(`Unable to encrypt string - ${errorMessage}`); | ||
| throw new Error(`Unable to encrypt string - ${getErrorMessage(e)}`); | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -93,8 +96,7 @@ class EncryptorDecryptor { | |
| `Unsupported encrypted data payload - ${encryptedDataStr}`, | ||
| ); | ||
| } catch (e) { | ||
| const errorMessage = e instanceof Error ? e.message : JSON.stringify(e); | ||
| throw new Error(`Unable to decrypt string - ${errorMessage}`); | ||
| throw new Error(`Unable to decrypt string - ${getErrorMessage(e)}`); | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -116,11 +118,11 @@ class EncryptorDecryptor { | |
| ); | ||
|
|
||
| // Encrypt and prepend salt. | ||
| const plaintextRaw = utf8ToBytes(plaintext); | ||
| const ciphertextAndNonceAndSalt = concatBytes( | ||
| const plaintextRaw = stringToBytes(plaintext); | ||
| const ciphertextAndNonceAndSalt = concatBytes([ | ||
| salt, | ||
| this.#encrypt(plaintextRaw, key), | ||
| ); | ||
| await this.#encrypt(plaintextRaw, key), | ||
| ]); | ||
|
|
||
| // Convert to Base64 | ||
| const encryptedData = byteArrayToBase64(ciphertextAndNonceAndSalt); | ||
|
|
@@ -174,7 +176,7 @@ class EncryptorDecryptor { | |
| ); | ||
|
|
||
| // Decrypt and return result. | ||
| return bytesToUtf8(this.#decrypt(ciphertextAndNonce, key)); | ||
| return bytesToUtf8(await this.#decrypt(ciphertextAndNonce, key)); | ||
| } | ||
|
|
||
| getSalt(encryptedDataStr: string) { | ||
|
|
@@ -198,8 +200,7 @@ class EncryptorDecryptor { | |
| `Unsupported encrypted data payload - ${encryptedDataStr}`, | ||
| ); | ||
| } catch (e) { | ||
| const errorMessage = e instanceof Error ? e.message : JSON.stringify(e); | ||
| throw new Error(`Unable to get salt - ${errorMessage}`); | ||
| throw new Error(`Unable to get salt - ${getErrorMessage(e)}`); | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -218,33 +219,37 @@ class EncryptorDecryptor { | |
| return strSet.size === salts.length; | ||
| } | ||
|
|
||
| #encrypt(plaintext: Uint8Array, key: Uint8Array): Uint8Array { | ||
| const nonce = randomBytes(ALGORITHM_NONCE_SIZE); | ||
|
|
||
| async #encrypt( | ||
| plaintext: Uint8Array<ArrayBuffer>, | ||
| key: Uint8Array<ArrayBuffer>, | ||
| ): Promise<Uint8Array<ArrayBuffer>> { | ||
| // Encrypt and prepend nonce. | ||
| const ciphertext = gcm(key, nonce).encrypt(plaintext); | ||
| const { ciphertext, iv } = await encrypt(key, plaintext); | ||
|
|
||
| return concatBytes(nonce, ciphertext); | ||
| return concatBytes([iv, ciphertext]); | ||
| } | ||
|
|
||
| #decrypt(ciphertextAndNonce: Uint8Array, key: Uint8Array): Uint8Array { | ||
| async #decrypt( | ||
| ciphertextAndNonce: Uint8Array<ArrayBuffer>, | ||
| key: Uint8Array<ArrayBuffer>, | ||
| ): Promise<Uint8Array<ArrayBuffer>> { | ||
| // Create buffers of nonce and ciphertext. | ||
| const nonce = ciphertextAndNonce.slice(0, ALGORITHM_NONCE_SIZE); | ||
| const nonce = ciphertextAndNonce.slice(0, IV_LENGTH); | ||
| const ciphertext = ciphertextAndNonce.slice( | ||
| ALGORITHM_NONCE_SIZE, | ||
| IV_LENGTH, | ||
| ciphertextAndNonce.length, | ||
| ); | ||
|
|
||
| // Decrypt and return result. | ||
| return gcm(key, nonce).decrypt(ciphertext); | ||
| return decrypt(key, nonce, ciphertext); | ||
|
Comment on lines
+233
to
+244
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: But I think we can use the right wording now, I'd go with
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. They are interchangeable tbh, do you feel strongly about this?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not that much 😄 I usually see IV rather than nonce in the context of AES, but that's not incorrect neither |
||
| } | ||
|
|
||
| async #getOrGenerateScryptKey( | ||
| password: string, | ||
| o: EncryptedPayload['o'], | ||
| salt?: Uint8Array, | ||
| salt?: Uint8Array<ArrayBuffer>, | ||
| nativeScryptCrypto?: NativeScrypt, | ||
| ) { | ||
| ): Promise<{ key: Uint8Array<ArrayBuffer>; salt: Uint8Array<ArrayBuffer> }> { | ||
| const hashedPassword = createSHA256Hash(password); | ||
|
|
||
| // Check if we already have the key cached | ||
|
|
@@ -318,11 +323,11 @@ class EncryptorDecryptor { | |
| async #performKdfOperation( | ||
| password: string, | ||
| o: EncryptedPayload['o'], | ||
| salt: Uint8Array, | ||
| salt: Uint8Array<ArrayBuffer>, | ||
| hashedPassword: string, | ||
| nativeScryptCrypto?: NativeScrypt, | ||
| ): Promise<{ key: Uint8Array; salt: Uint8Array }> { | ||
| let newKey: Uint8Array; | ||
| ): Promise<{ key: Uint8Array<ArrayBuffer>; salt: Uint8Array<ArrayBuffer> }> { | ||
| let newKey: Uint8Array<ArrayBuffer>; | ||
|
|
||
| if (nativeScryptCrypto) { | ||
| newKey = await nativeScryptCrypto( | ||
|
|
@@ -334,12 +339,12 @@ class EncryptorDecryptor { | |
| o.dkLen, | ||
| ); | ||
| } else { | ||
| newKey = await scryptAsync(password, salt, { | ||
| newKey = (await scryptAsync(password, salt, { | ||
| N: o.N, | ||
| r: o.r, | ||
| p: o.p, | ||
| dkLen: o.dkLen, | ||
| }); | ||
| })) as Uint8Array<ArrayBuffer>; | ||
| } | ||
|
|
||
| setCachedKey(hashedPassword, salt, newKey); | ||
|
|
@@ -362,5 +367,5 @@ export default encryption; | |
| */ | ||
| export function createSHA256Hash(data: string): string { | ||
| const hashedData = sha256(data); | ||
| return bytesToHex(hashedData); | ||
| return remove0x(bytesToHex(hashedData)); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,9 @@ | |
| { | ||
| "path": "../base-controller" | ||
| }, | ||
| { | ||
| "path": "../cryptography/" | ||
| }, | ||
| { | ||
| "path": "../controller-utils" | ||
| }, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -25,6 +25,9 @@ | |
| }, | ||
| { | ||
| "path": "../controller-utils/tsconfig.lint.json" | ||
| }, | ||
| { | ||
| "path": "../cryptography/tsconfig.lint.json" | ||
| } | ||
| ] | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm really not an expert on those matters, so please bear with me here. Genuine question: wasn't
ALGORITHM_NONCE_SIZEof importance here?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@metamask/cryptographyuses a 12-byte nonce/IV as well! It is equivalent, just generated within the encryption function: https://github.com/MetaMask/core/blob/main/packages/cryptography/src/aes-gcm.ts#L5There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if we could have exported this constant? E.g we could have re-used it while decoding the bytes on the other part of the code?
So we avoid having drifiting values for
ALGORITHM_NONCE_SIZEandAES_GCM_IV_LENGTHThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, makes sense, thanks! But then, if it changes one day in
@metamask/cryptography, wouldn't we start having problems here:The fact that rely both on a "silent" external constant AND a local one is making me a bit uncomfortable. WDYT?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's fair, we can export it and re-use!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Though tbf this shouldn't change, the defacto standard is 12 bytes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yep that's right, I feel like it still helps with the code here (especially for the encoding/decoding part)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree and am inclined to approve as-is. But for the sake of future readers, this asymmetry should probably be fixed in a follow up PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
0a3cbc4