Conversation
17aedf0 to
aeff2b4
Compare
PBKDF2 iterationsPBKDF2 iterations
src/index.ts
Outdated
| password: string, | ||
| salt: string, | ||
| exportable = false, | ||
| iterations = 900_000, |
There was a problem hiding this comment.
Great idea, this will allow clients who can handle more iterations to increase this number without harming those which cannot.
There was a problem hiding this comment.
This also gives the chance to a client to decrypt a vault with a weaker key to then re-encrypt it with a more secure one
There was a problem hiding this comment.
iterations = 900_000,has been changed to:
opts: KeyDerivationOptions = {
algorithm: 'PBKDF2',
params: {
iterations: 900_000,
exportable: false,
},
},|
LGTM! |
test/index.spec.ts
Outdated
| salt: 'sysHvNRoWykN/JVUSpBwXhmp0llTMQabfY7zucEfAJg=', | ||
| }; | ||
|
|
||
| test('encryptor:decrypt encrypted data with key derived with different number of iterations', async ({ |
There was a problem hiding this comment.
nit: for extra clarity
| test('encryptor:decrypt encrypted data with key derived with different number of iterations', async ({ | |
| test('encryptor:decrypt encrypted data using key derived with smaller number of iterations than the default configuration', async ({ |
There was a problem hiding this comment.
I ended up removing this test case, and instead repeating all test cases for both types of encryption results (old and new), to better test backward compatibility
54abac4
There was a problem hiding this comment.
I had to dismiss all approvals as we need a way to know how the key was derived in some scenarios.
For this reason, in most of the functions, instead of accepting / returning a CryptoKey we'll return an EncryptionKey object, which has this shape:
export type EncryptionKey = {
key: CryptoKey; // this might change in the future for other, more generic, types
derivationFunction: KeyDerivationOptions;
};
export type KeyDerivationOptions = {
algorithm: 'PBKDF2'; // this can easily become an enum in the future
params: PBKDF2Params; // ..and this a union type, depending on `algorithm`
};
export type PBKDF2Params = {
iterations?: number;
exportable?: boolean;
};And keyMetadata field (which reports the same object of derivationFunction) has been added also to the encryption result object:
export type EncryptionResult = {
data: string;
iv: string;
salt?: string;
// old encryption results will not have this
keyMetadata?: KeyDerivationOptions;
};Finally, instead of adding a single iterations argument to keyFromPassword function, the function has now this signature:
keyFromPassword(
password: string,
salt: string,
opts: KeyDerivationOptions = {
algorithm: 'PBKDF2',
params: {
iterations: 900_000,
exportable: false,
},
},
): Promise<EncryptionKey>;This allows:
- The client to choose the preferred algorithm and parameters
- To easily extend this in the future with other algorithms and parameters
mikesposito
left a comment
There was a problem hiding this comment.
Changes in f10e866 are needed to carry derivation function metadata in the exported key and to expect them (in new cases) when importing it.
|
This PR will also close #1 |
NEllusion
left a comment
There was a problem hiding this comment.
While I do acknowledge the benefit of having easy backwards compatibility, I am worried about the longer term technical debt this may incur. Given we are adding additional guards, conditions, and migrated tests to support two kinds of inputs for each operation, many of the the functions now require additional context on the various support cases to be modified effectively in the future.
IMO this is a case where I think verbosity may be better than the neatness of minimizing bundle size, as software bugs here can result in a bit more severe behaviour. Especially since a majority of the conditions are only relevant on that first migration.
What are your thoughts on this:
- We denote this as being a
breaking changeand define a migration guide for developers who wish to update to this breaking change version. - Alongside the guide, we provide a
transformationfunction which will take in the encrypted data, and transform it into the format we expect. Notably, adding thekeyargument, and adding the previous parameters that we used to use for thePBKDF2function. This function will be a no-op if the vault is already migrated. - Consumers of this package will simply call the transformation function after they've loaded their vault, but before they interact with the updated interface.
In the MetaMask extension, this can be done with a migration, in other clients, they can use their own strategy, or simply call this each time their vault is loaded as it will later get saved with the new updated parameters.
This will remove all the additional complexity from these functions leaving them simple to understand and build on in the future without worry of breaking vault compatibility.
| export type PBKDF2Params = { | ||
| iterations?: number; | ||
| exportable?: boolean; | ||
| }; | ||
|
|
||
| export type KeyDerivationOptions = { | ||
| algorithm: 'PBKDF2'; | ||
| params: PBKDF2Params; | ||
| }; |
There was a problem hiding this comment.
Nice! This also makes it pretty easy to expand to other algorithms one day doing something like:
export type KeyDerivationOptions = { algorithm: 'PBKDF2'; params: PBKDF2Params;} | { algorithm: 'OtherAlgo'; params: OtherAlgoParams;}
src/index.ts
Outdated
| const key = await keyFromPassword(password, salt, { | ||
| algorithm: 'PBKDF2', | ||
| params: { | ||
| exportable: true, | ||
| }, | ||
| }); |
There was a problem hiding this comment.
Don't we want to allow the client to specify its iterations here?
There was a problem hiding this comment.
Good idea! this has been added as an optional argument, with DEFAULT_DERIVATION_PARAMS as the default value in 9d06954
src/index.ts
Outdated
| if ( | ||
| !(encryptionKey instanceof CryptoKey) && | ||
| encryptionKey.derivationFunction | ||
| ) { | ||
| encryptionResult.keyMetadata = encryptionKey.derivationFunction; | ||
| } | ||
|
|
||
| return encryptionResult; |
There was a problem hiding this comment.
Why do we only conditionally add the metadata to the encryption result?
There was a problem hiding this comment.
Because if the user encrypts data using an arbitrary CryptoKey we can't assume that it was derived with this library, thus we should leave the metadata empty
src/index.ts
Outdated
| if ( | ||
| !(encryptionKey instanceof CryptoKey) && | ||
| encryptionKey.derivationFunction | ||
| ) { | ||
| encryptionResult.keyMetadata = encryptionKey.derivationFunction; | ||
| } | ||
|
|
||
| return encryptionResult; |
There was a problem hiding this comment.
Why do we only conditionally add the metadata to the encryption result?
There was a problem hiding this comment.
Because if the user encrypts data using an arbitrary CryptoKey we can't assume that it was derived with this library, thus we should leave the metadata empty
|
didnt read thread yet but want to add that this doesn't have to be breaking if we record the iterations and assume lack of iterations means the present old default |
|
Thank you for this, I've wanted to do this since first writing the module. I suspect there's a way to do this without breaking, and allowing for a smooth migration path for users of the library:
This way, even a consumer who is unaware of this issue could have their vaults silently upgraded to a more secure number of iterations. |
|
@kumavis @danfinlay Thanks for taking a look into this! There are two reasons why making this non-breaking or assuming lower iterations might be erroneous:
Also, when encrypting new vaults, if we don't want to stop accepting arbitrary Please let me know if this makes sense, and if it would make sense to stop accepting arbitrary (with no metadata) keys when encrypting new vaults - given also that what @NicholasEllul wrote is correct: keeping compatibility with older versions increases complexity |
Perhaps I'm missing something, but I thought we only used these parameters to create the key. So if we have the key already, it doesn't matter how many iterations were used to create it. |
mcmire
left a comment
There was a problem hiding this comment.
I have some other comments besides this but I will save that for another comment so it is more easily quotable. A lot of these comments are just about the tests.
PBKDF2 iterationsPBKDF2 iterations
PBKDF2 iterations
src/index.ts
Outdated
| /** | ||
| * Updates the provided vault, re-encrypting | ||
| * data with a safer algorithm if one is available. | ||
| * | ||
| * If the provided vault is already using the latest available encryption method, | ||
| * it is returned as is. | ||
| * | ||
| * @param vault - The vault to update. | ||
| * @param password - The password to use for encryption. | ||
| * @returns A promise resolving to the updated vault. | ||
| */ | ||
| export async function updateVault( | ||
| vault: string, | ||
| password: string, | ||
| ): Promise<string> { | ||
| if (isVaultUpdated(vault)) { | ||
| return vault; | ||
| } | ||
|
|
||
| return encrypt(password, await decrypt(password, vault)); | ||
| } |
There was a problem hiding this comment.
This function can be used by the extension and other clients to migrate their vaults.
As this PR is non-breaking, this is not mandatory, as the library would silently use the new method when encrypting the vault again. But with this helper, the library gets the responsibility of knowing whether the vault is using the latest safe configuration available, resulting in a no-op if the vault is already up-to-date
mcmire
left a comment
There was a problem hiding this comment.
One final thing, but this otherwise looks good to me.
65a9009 to
c50ede4
Compare
c50ede4 to
e1fe87e
Compare
| /** | ||
| * Updates the provided vault and exported key, re-encrypting | ||
| * data with a safer algorithm if one is available. | ||
| * | ||
| * If the provided vault is already using the latest available encryption method, | ||
| * it is returned as is. | ||
| * | ||
| * @param encryptionResult - The encrypted data to update. | ||
| * @param password - The password to use for encryption. | ||
| * @returns A promise resolving to the updated encrypted data and exported key. | ||
| */ | ||
| export async function updateVaultWithDetail( | ||
| encryptionResult: DetailedEncryptionResult, | ||
| password: string, | ||
| ): Promise<DetailedEncryptionResult> { | ||
| if (isVaultUpdated(encryptionResult.vault)) { | ||
| return encryptionResult; | ||
| } | ||
|
|
||
| return encryptWithDetail( | ||
| password, | ||
| await decrypt(password, encryptionResult.vault), | ||
| ); | ||
| } |
There was a problem hiding this comment.
On @metamask/eth-keyring-controller, we use detailed encryption / decryption as we have to cache the encryption key derived from the password. Without this function, when running the extension in MV3-compatible mode, we'll not be able to cache the encryption key if the vault gets updated - the cached encryption key will become invalid/outdated in case the vault gets updated, so we need to return the exported key in these cases.
This PR adds support for multiple key derivation algorithms and parameters, by modifying the shape of the vault (encrypted data) and the encryption key (also when exported), in order to hold some (nullable) metadata related to how the key was derived in the first place.
This is mainly needed to increase key derivation iterations, while keeping old vaults decryption possible. With this PR, PBKDF2 default iterations has been set to 900.000 - but when the old shape of the vault or key is detected (missing derivation metadata), the old number of iterations is used.
To keep compatibility with older formats, methods exported by this library will accept both old and new formats.
Changes
ADDED:
EncryptionKeytype to hold aCryptoKeyalong with its derivation parametersADDED:
ExportedEncryptionKeytype to hold aJsonWebKeyalong with its derivation parametersADDED: Optional
keyMetadataproperty of typeKeyDerivationOptionstoEncryptionResultADDED: Optional
optsargument tokeyFromPasswordto specify algorithm and parameters to be used in the key derivation. Defaults toPBKDF2at 10.000 iterationsADDED: Added
iterationsargument tokeyFromPasswordfunctionADDED: Optional
keyDerivationOptionsargument toencryptto specify algorithm and parameters to be used in the key derivation. Defaults toPBKDF2at 900.000 iterationsADDED: Optional
keyDerivationOptionsargument toencryptWithDetailto specify algorithm and parameters to be used in the key derivation. Defaults toPBKDF2at 900.000 iterationsADDED:
updateVaultWithDetailfunction to update existing vault and exported key with a safer encryption method if availableADDED:
updateVaultfunction to update existing vault string with a safer encryption method if availableCHANGED:
encryptmethod accepts bothEncryptionKeyandCryptoKeytypes askeyargumentCHANGED:
encryptWithKeymethod accepts bothEncryptionKeyandCryptoKeytypes askeyargumentCHANGED:
decryptmethod accepts bothEncryptionKeyandCryptoKeytypes askeyargumentCHANGED:
decryptWithKeymethod accepts bothEncryptionKeyandCryptoKeytypes askeyargumentCHANGED:
importKeymethod returns aCryptoKeywhen a JWK string is passed, or anEncryptionKeywhen anExportedEncryptionKeystring is passed.CHANGED:
exportKeymethod accepts bothEncryptionKeyandCryptoKeytypes askeyargument, and returns anExportedEncryptionKeyfor the former and aJsonWebKeyfor the latter.