Skip to content

Commit 7342010

Browse files
committed
(wip) keyring: add support for envelope encryption
1 parent fff64c8 commit 7342010

File tree

2 files changed

+122
-22
lines changed

2 files changed

+122
-22
lines changed

packages/keyring-controller/src/KeyringController.ts

Lines changed: 121 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,11 @@ export type KeyringControllerState = {
100100
/**
101101
* The salt used to derive the encryption key from the password.
102102
*/
103-
encryptionSalt?: string;
103+
encryptionSalt?: string; // TODO why is this stored here?
104+
/**
105+
* The encrypted encryption key. If set, envelope encryption is used.
106+
*/
107+
encryptedEncryptionKey?: string;
104108
};
105109

106110
export type KeyringControllerMemState = Omit<
@@ -327,6 +331,7 @@ export type SerializedKeyring = {
327331
type SessionState = {
328332
keyrings: SerializedKeyring[];
329333
password?: string;
334+
encryptedEncryptionKey?: string;
330335
};
331336

332337
/**
@@ -543,6 +548,20 @@ function assertIsValidPassword(password: unknown): asserts password is string {
543548
}
544549
}
545550

551+
/**
552+
* Assert that the provided cacheEncryptionKey is true.
553+
*
554+
* @param cacheEncryptionKey - The cacheEncryptionKey to check.
555+
* @throws If the cacheEncryptionKey is not true.
556+
*/
557+
function assertIsCacheEncryptionKeyTrue(
558+
cacheEncryptionKey: boolean,
559+
): asserts cacheEncryptionKey is true {
560+
if (!cacheEncryptionKey) {
561+
throw new Error(KeyringControllerError.CacheEncryptionKeyDisabled);
562+
}
563+
}
564+
546565
/**
547566
* Checks if the provided value is a serialized keyrings array.
548567
*
@@ -679,6 +698,7 @@ export class KeyringController extends BaseController<
679698
keyrings: { persist: false, anonymous: false },
680699
encryptionKey: { persist: false, anonymous: false },
681700
encryptionSalt: { persist: false, anonymous: false },
701+
encryptedEncryptionKey: { persist: true, anonymous: false },
682702
},
683703
messenger,
684704
state: {
@@ -816,19 +836,26 @@ export class KeyringController extends BaseController<
816836
/**
817837
* Create a new vault and primary keyring.
818838
*
819-
* This only works if keyrings are empty. If there is a pre-existing unlocked vault, calling this will have no effect.
820-
* If there is a pre-existing locked vault, it will be replaced.
839+
* This only works if keyrings are empty. If there is a pre-existing unlocked
840+
* vault, calling this will have no effect. If there is a pre-existing locked
841+
* vault, it will be replaced.
821842
*
822843
* @param password - Password to unlock the new vault.
844+
* @param encryptionKey - Optional encryption key to encrypt the new vault. If
845+
* set, envelope encryption will be used.
823846
* @returns Promise resolving when the operation ends successfully.
824847
*/
825-
async createNewVaultAndKeychain(password: string) {
848+
async createNewVaultAndKeychain(password: string, encryptionKey?: string) {
826849
return this.#persistOrRollback(async () => {
827850
const accounts = await this.#getAccountsFromKeyrings();
828851
if (!accounts.length) {
829-
await this.#createNewVaultWithKeyring(password, {
830-
type: KeyringTypes.hd,
831-
});
852+
await this.#createNewVaultWithKeyring(
853+
password,
854+
{
855+
type: KeyringTypes.hd,
856+
},
857+
encryptionKey,
858+
);
832859
}
833860
});
834861
}
@@ -1415,6 +1442,21 @@ export class KeyringController extends BaseController<
14151442
* @returns Promise resolving when the operation completes.
14161443
*/
14171444
changePassword(password: string): Promise<void> {
1445+
return this.changePasswordAndEncryptionKey(password);
1446+
}
1447+
1448+
/**
1449+
* Changes the password and encryption key used to encrypt the vault.
1450+
*
1451+
* @param password - The new password.
1452+
* @param encryptionKey - The new encryption key. If omitted, the encryption
1453+
* key will not be changed.
1454+
* @returns Promise resolving when the operation completes.
1455+
*/
1456+
changePasswordAndEncryptionKey(
1457+
password: string,
1458+
encryptionKey?: string,
1459+
): Promise<void> {
14181460
this.#assertIsUnlocked();
14191461

14201462
return this.#persistOrRollback(async () => {
@@ -1430,9 +1472,27 @@ export class KeyringController extends BaseController<
14301472
delete state.encryptionSalt;
14311473
});
14321474
}
1475+
1476+
await this.#updateEncryptedEncryptionKey(password, encryptionKey);
14331477
});
14341478
}
14351479

1480+
async #updateEncryptedEncryptionKey(
1481+
password: string,
1482+
encryptionKey?: string,
1483+
) {
1484+
if (encryptionKey) {
1485+
assertIsCacheEncryptionKeyTrue(this.#cacheEncryptionKey);
1486+
const encryptedEncryptionKey = await this.#encryptor.encrypt(
1487+
password,
1488+
encryptionKey,
1489+
);
1490+
this.update((state) => {
1491+
state.encryptedEncryptionKey = encryptedEncryptionKey;
1492+
});
1493+
}
1494+
}
1495+
14361496
/**
14371497
* Attempts to decrypt the current vault and load its keyrings,
14381498
* using the given encryption key and salt.
@@ -2071,6 +2131,7 @@ export class KeyringController extends BaseController<
20712131
* @param keyring - A object containing the params to instantiate a new keyring.
20722132
* @param keyring.type - The keyring type.
20732133
* @param keyring.opts - Optional parameters required to instantiate the keyring.
2134+
* @param encryptionKey - Optional encryption key to encrypt the vault.
20742135
* @returns A promise that resolves to the state.
20752136
*/
20762137
async #createNewVaultWithKeyring(
@@ -2079,6 +2140,7 @@ export class KeyringController extends BaseController<
20792140
type: string;
20802141
opts?: unknown;
20812142
},
2143+
encryptionKey?: string,
20822144
): Promise<void> {
20832145
this.#assertControllerMutexIsLocked();
20842146

@@ -2093,6 +2155,8 @@ export class KeyringController extends BaseController<
20932155

20942156
this.#password = password;
20952157

2158+
await this.#updateEncryptedEncryptionKey(password, encryptionKey);
2159+
20962160
await this.#clearKeyrings();
20972161
await this.#createKeyringWithFirstAccount(keyring.type, keyring.opts);
20982162
this.#setUnlocked();
@@ -2204,6 +2268,7 @@ export class KeyringController extends BaseController<
22042268
return {
22052269
keyrings: await this.#getSerializedKeyrings(),
22062270
password: this.#password,
2271+
encryptedEncryptionKey: this.state.encryptedEncryptionKey,
22072272
};
22082273
}
22092274

@@ -2255,7 +2320,7 @@ export class KeyringController extends BaseController<
22552320
newMetadata: boolean;
22562321
}> {
22572322
return this.#withVaultLock(async () => {
2258-
const encryptedVault = this.state.vault;
2323+
const { vault: encryptedVault, encryptedEncryptionKey } = this.state;
22592324
if (!encryptedVault) {
22602325
throw new Error(KeyringControllerError.VaultError);
22612326
}
@@ -2267,15 +2332,31 @@ export class KeyringController extends BaseController<
22672332
assertIsExportableKeyEncryptor(this.#encryptor);
22682333

22692334
if (password) {
2270-
const result = await this.#encryptor.decryptWithDetail(
2271-
password,
2272-
encryptedVault,
2273-
);
2274-
vault = result.vault;
2275-
this.#password = password;
2335+
if (encryptedEncryptionKey) {
2336+
const key = (await this.#encryptor.decrypt(
2337+
password,
2338+
encryptedEncryptionKey,
2339+
)) as string;
2340+
2341+
const importedKey = await this.#encryptor.importKey(key);
2342+
2343+
vault = await this.#encryptor.decryptWithKey(
2344+
importedKey,
2345+
encryptedVault,
2346+
);
2347+
this.#password = password;
2348+
updatedState.encryptionKey = key;
2349+
} else {
2350+
const result = await this.#encryptor.decryptWithDetail(
2351+
password,
2352+
encryptedVault,
2353+
);
2354+
vault = result.vault;
2355+
this.#password = password;
22762356

2277-
updatedState.encryptionKey = result.exportedKeyString;
2278-
updatedState.encryptionSalt = result.salt;
2357+
updatedState.encryptionKey = result.exportedKeyString;
2358+
updatedState.encryptionSalt = result.salt;
2359+
}
22792360
} else {
22802361
const parsedEncryptedVault = JSON.parse(encryptedVault);
22812362

@@ -2341,7 +2422,9 @@ export class KeyringController extends BaseController<
23412422
// Ensure no duplicate accounts are persisted.
23422423
await this.#assertNoDuplicateAccounts();
23432424

2344-
const { encryptionKey, encryptionSalt, vault } = this.state;
2425+
const { encryptionKey, encryptionSalt, vault, encryptedEncryptionKey } =
2426+
this.state;
2427+
23452428
// READ THIS CAREFULLY:
23462429
// We do check if the vault is still considered up-to-date, if not, we would not re-use the
23472430
// cached key and we will re-generate a new one (based on the password).
@@ -2379,14 +2462,30 @@ export class KeyringController extends BaseController<
23792462
vaultJSON.salt = encryptionSalt;
23802463
updatedState.vault = JSON.stringify(vaultJSON);
23812464
} else if (this.#password) {
2382-
const { vault: newVault, exportedKeyString } =
2383-
await this.#encryptor.encryptWithDetail(
2465+
if (encryptedEncryptionKey) {
2466+
const decryptedKey = (await this.#encryptor.decrypt(
23842467
this.#password,
2468+
encryptedEncryptionKey,
2469+
)) as string;
2470+
2471+
const importedKey = await this.#encryptor.importKey(decryptedKey);
2472+
2473+
const vaultJSON = await this.#encryptor.encryptWithKey(
2474+
importedKey,
23852475
serializedKeyrings,
23862476
);
2387-
2388-
updatedState.vault = newVault;
2389-
updatedState.encryptionKey = exportedKeyString;
2477+
vaultJSON.salt = encryptionSalt;
2478+
updatedState.vault = JSON.stringify(vaultJSON);
2479+
updatedState.encryptionKey = decryptedKey;
2480+
} else {
2481+
const { vault: newVault, exportedKeyString } =
2482+
await this.#encryptor.encryptWithDetail(
2483+
this.#password,
2484+
serializedKeyrings,
2485+
);
2486+
updatedState.vault = newVault;
2487+
updatedState.encryptionKey = exportedKeyString;
2488+
}
23902489
}
23912490
} else {
23922491
assertIsValidPassword(this.#password);

packages/keyring-controller/src/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export enum KeyringControllerError {
44
UnsafeDirectKeyringAccess = 'KeyringController - Returning keyring instances is unsafe',
55
WrongPasswordType = 'KeyringController - Password must be of type string.',
66
InvalidEmptyPassword = 'KeyringController - Password cannot be empty.',
7+
CacheEncryptionKeyDisabled = 'KeyringController - Cache encryption key is disabled.',
78
NoFirstAccount = 'KeyringController - First Account not found.',
89
DuplicatedAccount = 'KeyringController - The account you are trying to import is a duplicate',
910
VaultError = 'KeyringController - Cannot unlock without a previous vault.',

0 commit comments

Comments
 (0)