-
-
Notifications
You must be signed in to change notification settings - Fork 232
Keyring Controller: Add method to export vault key #5984
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
base: main
Are you sure you want to change the base?
Changes from all commits
75b1905
36fbc02
2c120f5
6ac970b
d68e568
f783761
f2ef812
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 |
---|---|---|
|
@@ -1434,16 +1434,17 @@ export class KeyringController extends BaseController< | |
} | ||
|
||
/** | ||
* Attempts to decrypt the current vault and load its keyrings, | ||
* using the given encryption key and salt. | ||
* Attempts to decrypt the current vault and load its keyrings, using the | ||
* given encryption key and salt. The optional salt can be used to check for | ||
* consistency with the vault salt. | ||
* | ||
* @param encryptionKey - Key to unlock the keychain. | ||
* @param encryptionSalt - Salt to unlock the keychain. | ||
* @param encryptionSalt - Optional salt to unlock the keychain. | ||
* @returns Promise resolving when the operation completes. | ||
*/ | ||
async submitEncryptionKey( | ||
encryptionKey: string, | ||
encryptionSalt: string, | ||
encryptionSalt?: string, | ||
): Promise<void> { | ||
const { newMetadata } = await this.#withRollback(async () => { | ||
const result = await this.#unlockKeyrings( | ||
|
@@ -1470,6 +1471,44 @@ export class KeyringController extends BaseController< | |
} | ||
} | ||
|
||
/** | ||
* Exports the vault encryption key. | ||
* | ||
* @returns The vault encryption key. | ||
*/ | ||
async exportEncryptionKey(): Promise<string> { | ||
this.#assertIsUnlocked(); | ||
|
||
return await this.#withControllerLock(async () => { | ||
// There is a case where the controller is unlocked but the encryption key | ||
// is not set, even when #cacheEncryptionKey is true. This happens when | ||
// calling changePassword with the existing password. In this case, the | ||
// encryption key is deleted, but the state is not recreated, because the | ||
// session state does not change in this case, and #updateVault is not | ||
// called in #persistOrRollback. | ||
if (!this.state.encryptionKey) { | ||
return await this.#withVaultLock(async () => { | ||
assertIsExportableKeyEncryptor(this.#encryptor); | ||
assertIsValidPassword(this.#password); | ||
const result = await this.#encryptor.decryptWithDetail( | ||
this.#password, | ||
// Ignoring undefined. Assuming vault is set when unlocked. | ||
this.state.vault as string, | ||
); | ||
if (this.#cacheEncryptionKey) { | ||
this.update((state) => { | ||
state.encryptionKey = result.exportedKeyString; | ||
state.encryptionSalt = result.salt; | ||
}); | ||
} | ||
Comment on lines
+1498
to
+1503
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. Curious: why are we updating this state here if we already used the salt from the vault, and the key is presumably the same? |
||
return result.exportedKeyString; | ||
}); | ||
} | ||
Comment on lines
+1483
to
+1506
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. Can't we just make Thus, the encryption key won't be re-generated and Also, another option would be to put the encryption key as part of the session state I guess? 🤔 This way, we would detect it has changed and we would re-trigger Any thoughts on that @mikesposito? 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. I think that making |
||
|
||
return this.state.encryptionKey; | ||
}); | ||
} | ||
|
||
/** | ||
* Attempts to decrypt the current vault and load its keyrings, | ||
* using the given password. | ||
|
@@ -2279,8 +2318,10 @@ export class KeyringController extends BaseController< | |
} else { | ||
const parsedEncryptedVault = JSON.parse(encryptedVault); | ||
|
||
if (encryptionSalt !== parsedEncryptedVault.salt) { | ||
if (encryptionSalt && encryptionSalt !== parsedEncryptedVault.salt) { | ||
throw new Error(KeyringControllerError.ExpiredCredentials); | ||
} else { | ||
encryptionSalt = parsedEncryptedVault.salt as string; | ||
} | ||
|
||
if (typeof encryptionKey !== 'string') { | ||
|
@@ -2296,10 +2337,7 @@ export class KeyringController extends BaseController< | |
// This call is required on the first call because encryptionKey | ||
// is not yet inside the memStore | ||
updatedState.encryptionKey = encryptionKey; | ||
// we can safely assume that encryptionSalt is defined here | ||
// because we compare it with the salt from the vault | ||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion | ||
updatedState.encryptionSalt = encryptionSalt!; | ||
updatedState.encryptionSalt = encryptionSalt; | ||
} | ||
} else { | ||
if (typeof password !== 'string') { | ||
|
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.
Not sure how often this will be called, but this call takes
Key Derivation time + Decryption time
, and theDecryption time
is all thrown away - which is not ideal. We could use#encryptor.keyFromPassword
instead, although we'd need to:keyFromPassword
toExportableKeyEncryptor
keyMetadata
andsalt
from the vaultI think that adding
keyFromPassword
and using the salt from the vault is trivial, but dealing withkeyMetadata
may be trickier or even unwanted (because it heavily depends on the encryptor injected by the client). Perhaps the ideal solution would be to have an additional method on the encryptor interface to derive a key from a password, using salt and keyMetadata from an existing vault (without decrypting it).This would mean having to add a method in
@metamask/browser-passworder
and one in the mobile encryptor, and it may require more time to ship this feature. If we are ok with the performance hit for now, we can do it later.