Skip to content

Commit 62a9880

Browse files
committed
feat: persist UserStorage e2e content keys using an encrypted keyStore
fixes #5128
1 parent 280d897 commit 62a9880

File tree

10 files changed

+434
-22
lines changed

10 files changed

+434
-22
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,6 @@ scripts/coverage
3535

3636
# typescript
3737
packages/*/*.tsbuildinfo
38+
39+
# jetbrains IDE local files
40+
/.idea

packages/profile-sync-controller/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,9 @@
106106
"@metamask/network-controller": "^22.1.1",
107107
"@metamask/snaps-sdk": "^6.7.0",
108108
"@metamask/snaps-utils": "^8.3.0",
109+
"@metamask/utils": "^11.0.1",
109110
"@noble/ciphers": "^0.5.2",
111+
"@noble/curves": "^1.7.0",
110112
"@noble/hashes": "^1.4.0",
111113
"immer": "^9.0.6",
112114
"loglevel": "^1.8.1",

packages/profile-sync-controller/src/controllers/authentication/auth-snap-requests.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/* eslint-disable @typescript-eslint/no-explicit-any */
22
import type { HandleSnapRequest } from '@metamask/snaps-controllers';
33
import type { SnapId } from '@metamask/snaps-sdk';
4+
import type { Eip1024EncryptedData } from '@metamask/utils';
45

56
type SnapRPCRequest = Parameters<HandleSnapRequest['handler']>[0];
67

@@ -22,6 +23,22 @@ export function createSnapPublicKeyRequest(): SnapRPCRequest {
2223
};
2324
}
2425

26+
/**
27+
* Constructs Request to Message Signing Snap to get the Encryption Public Key
28+
*
29+
* @returns Snap Encryption Public Key Request
30+
*/
31+
export function createSnapEncryptionPublicKeyRequest(): SnapRPCRequest {
32+
return {
33+
snapId,
34+
origin: '',
35+
handler: 'onRpcRequest' as any,
36+
request: {
37+
method: 'getEncryptionPublicKey',
38+
},
39+
};
40+
}
41+
2542
/**
2643
* Constructs Request to get Message Signing Snap to sign a message.
2744
*
@@ -41,3 +58,23 @@ export function createSnapSignMessageRequest(
4158
},
4259
};
4360
}
61+
62+
/**
63+
* Constructs Request to get Message Signing Snap to decrypt a message.
64+
*
65+
* @param data - message to decrypt
66+
* @returns Snap Sign Message Request
67+
*/
68+
export function createSnapDecryptMessageRequest(
69+
data: Eip1024EncryptedData,
70+
): SnapRPCRequest {
71+
return {
72+
snapId,
73+
origin: '',
74+
handler: 'onRpcRequest' as any,
75+
request: {
76+
method: 'decryptMessage',
77+
params: { data },
78+
},
79+
};
80+
}

packages/profile-sync-controller/src/controllers/user-storage/UserStorageController.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ describe('user-storage/user-storage-controller - performGetStorage() tests', ()
109109
isAccountSyncingInProgress: false,
110110
hasAccountSyncingSyncedAtLeastOnce: false,
111111
isAccountSyncingReadyToBeDispatched: false,
112+
encryptedContentKeys: {},
112113
},
113114
});
114115

@@ -191,6 +192,7 @@ describe('user-storage/user-storage-controller - performGetStorageAllFeatureEntr
191192
hasAccountSyncingSyncedAtLeastOnce: false,
192193
isAccountSyncingReadyToBeDispatched: false,
193194
isAccountSyncingInProgress: false,
195+
encryptedContentKeys: {},
194196
},
195197
});
196198

@@ -273,6 +275,7 @@ describe('user-storage/user-storage-controller - performSetStorage() tests', ()
273275
hasAccountSyncingSyncedAtLeastOnce: false,
274276
isAccountSyncingReadyToBeDispatched: false,
275277
isAccountSyncingInProgress: false,
278+
encryptedContentKeys: {},
276279
},
277280
});
278281

@@ -379,6 +382,7 @@ describe('user-storage/user-storage-controller - performBatchSetStorage() tests'
379382
hasAccountSyncingSyncedAtLeastOnce: false,
380383
isAccountSyncingReadyToBeDispatched: false,
381384
isAccountSyncingInProgress: false,
385+
encryptedContentKeys: {},
382386
},
383387
});
384388

@@ -482,6 +486,7 @@ describe('user-storage/user-storage-controller - performBatchDeleteStorage() tes
482486
hasAccountSyncingSyncedAtLeastOnce: false,
483487
isAccountSyncingReadyToBeDispatched: false,
484488
isAccountSyncingInProgress: false,
489+
encryptedContentKeys: {},
485490
},
486491
});
487492

@@ -586,6 +591,7 @@ describe('user-storage/user-storage-controller - performDeleteStorage() tests',
586591
hasAccountSyncingSyncedAtLeastOnce: false,
587592
isAccountSyncingReadyToBeDispatched: false,
588593
isAccountSyncingInProgress: false,
594+
encryptedContentKeys: {},
589595
},
590596
});
591597

@@ -687,6 +693,7 @@ describe('user-storage/user-storage-controller - performDeleteStorageAllFeatureE
687693
hasAccountSyncingSyncedAtLeastOnce: false,
688694
isAccountSyncingReadyToBeDispatched: false,
689695
isAccountSyncingInProgress: false,
696+
encryptedContentKeys: {},
690697
},
691698
});
692699

@@ -780,6 +787,7 @@ describe('user-storage/user-storage-controller - getStorageKey() tests', () => {
780787
hasAccountSyncingSyncedAtLeastOnce: false,
781788
isAccountSyncingReadyToBeDispatched: false,
782789
isAccountSyncingInProgress: false,
790+
encryptedContentKeys: {},
783791
},
784792
});
785793

@@ -827,6 +835,7 @@ describe('user-storage/user-storage-controller - enableProfileSyncing() tests',
827835
hasAccountSyncingSyncedAtLeastOnce: false,
828836
isAccountSyncingReadyToBeDispatched: false,
829837
isAccountSyncingInProgress: false,
838+
encryptedContentKeys: {},
830839
},
831840
});
832841

packages/profile-sync-controller/src/controllers/user-storage/UserStorageController.ts

Lines changed: 107 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import type {
2+
AccountsControllerAccountAddedEvent,
3+
AccountsControllerAccountRenamedEvent,
24
AccountsControllerListAccountsAction,
35
AccountsControllerUpdateAccountMetadataAction,
4-
AccountsControllerAccountRenamedEvent,
5-
AccountsControllerAccountAddedEvent,
66
} from '@metamask/accounts-controller';
77
import type {
88
ControllerGetStateAction,
@@ -12,10 +12,10 @@ import type {
1212
} from '@metamask/base-controller';
1313
import { BaseController } from '@metamask/base-controller';
1414
import {
15+
type KeyringControllerAddNewAccountAction,
1516
type KeyringControllerGetStateAction,
1617
type KeyringControllerLockEvent,
1718
type KeyringControllerUnlockEvent,
18-
type KeyringControllerAddNewAccountAction,
1919
} from '@metamask/keyring-controller';
2020
import type { InternalAccount } from '@metamask/keyring-internal-api';
2121
import type {
@@ -26,22 +26,29 @@ import type {
2626
NetworkControllerUpdateNetworkAction,
2727
} from '@metamask/network-controller';
2828
import type { HandleSnapRequest } from '@metamask/snaps-controllers';
29+
import type { Eip1024EncryptedData } from '@metamask/utils';
2930

3031
import { createSHA256Hash } from '../../shared/encryption';
32+
import type { KeyStore } from '../../shared/encryption/key-storage';
33+
import { ERC1024WrappedKeyStore } from '../../shared/encryption/key-storage';
3134
import type { UserStorageFeatureKeys } from '../../shared/storage-schema';
3235
import {
3336
type UserStoragePathWithFeatureAndKey,
3437
type UserStoragePathWithFeatureOnly,
3538
} from '../../shared/storage-schema';
3639
import type { NativeScrypt } from '../../shared/types/encryption';
37-
import { createSnapSignMessageRequest } from '../authentication/auth-snap-requests';
40+
import {
41+
createSnapDecryptMessageRequest,
42+
createSnapEncryptionPublicKeyRequest,
43+
createSnapSignMessageRequest,
44+
} from '../authentication/auth-snap-requests';
3845
import type {
3946
AuthenticationControllerGetBearerToken,
4047
AuthenticationControllerGetSessionProfile,
4148
AuthenticationControllerIsSignedIn,
4249
AuthenticationControllerPerformSignIn,
4350
AuthenticationControllerPerformSignOut,
44-
} from '../authentication/AuthenticationController';
51+
} from '../authentication';
4552
import {
4653
saveInternalAccountToUserStorage,
4754
syncInternalAccountsWithUserStorage,
@@ -102,6 +109,10 @@ export type UserStorageControllerState = {
102109
* Condition used to ensure that we do not perform any network sync mutations until we have synced at least once
103110
*/
104111
hasNetworkSyncingSyncedAtLeastOnce?: boolean;
112+
/**
113+
* Content keys used to encrypt/decrypt user storage content. These are wrapped while at rest.
114+
*/
115+
encryptedContentKeys: Record<string, string>;
105116
};
106117

107118
export const defaultState: UserStorageControllerState = {
@@ -110,6 +121,7 @@ export const defaultState: UserStorageControllerState = {
110121
hasAccountSyncingSyncedAtLeastOnce: false,
111122
isAccountSyncingReadyToBeDispatched: false,
112123
isAccountSyncingInProgress: false,
124+
encryptedContentKeys: {},
113125
};
114126

115127
const metadata: StateMetadata<UserStorageControllerState> = {
@@ -137,6 +149,10 @@ const metadata: StateMetadata<UserStorageControllerState> = {
137149
persist: true,
138150
anonymous: false,
139151
},
152+
encryptedContentKeys: {
153+
persist: true,
154+
anonymous: false,
155+
},
140156
};
141157

142158
type ControllerConfig = {
@@ -313,6 +329,79 @@ export default class UserStorageController extends BaseController<
313329
isNetworkSyncingEnabled: false,
314330
};
315331

332+
#_snapPublicKeyCache: string | null = null;
333+
334+
#keyWrapping = {
335+
/**
336+
* Returns the snap Encryption public key.
337+
*
338+
* @returns The snap Encryption public key.
339+
*/
340+
snapGetEncryptionPublicKey: async (): Promise<string> => {
341+
if (this.#_snapPublicKeyCache) {
342+
return this.#_snapPublicKeyCache;
343+
}
344+
345+
if (!this.#isUnlocked) {
346+
throw new Error(
347+
'#snapGetEncryptionPublicKey - unable to call snap, wallet is locked',
348+
);
349+
}
350+
351+
const result = (await this.messagingSystem.call(
352+
'SnapController:handleRequest',
353+
createSnapEncryptionPublicKeyRequest(),
354+
)) as string;
355+
356+
this.#_snapPublicKeyCache = result;
357+
358+
return result;
359+
},
360+
361+
/**
362+
* Decrypts a message using the message signing snap.
363+
*
364+
* @param data - Eip1024EncryptedData - The encrypted data.
365+
* @returns The decrypted message, if it was intended for this wallet, null otherwise. TODO: check error scenarios
366+
*/
367+
snapDecryptMessage: async (data: Eip1024EncryptedData): Promise<string> => {
368+
if (!this.#isUnlocked) {
369+
throw new Error(
370+
'#snapDecryptMessage - unable to call snap, wallet is locked',
371+
);
372+
}
373+
374+
const result = (await this.messagingSystem.call(
375+
'SnapController:handleRequest',
376+
createSnapDecryptMessageRequest(data),
377+
)) as string;
378+
379+
return result;
380+
},
381+
382+
loadWrappedKey: async (keyRef: string): Promise<string | null> => {
383+
return this.state.encryptedContentKeys[keyRef] ?? null;
384+
},
385+
386+
storeWrappedKey: (keyRef: string, wrappedKey: string): Promise<void> => {
387+
return new Promise((resolve) => {
388+
this.update((state) => {
389+
state.encryptedContentKeys[keyRef] = wrappedKey;
390+
resolve();
391+
});
392+
});
393+
},
394+
395+
getWrappedKeyStore: (): KeyStore => {
396+
return new ERC1024WrappedKeyStore({
397+
decryptMessage: this.#keyWrapping.snapDecryptMessage,
398+
getPublicKey: this.#keyWrapping.snapGetEncryptionPublicKey,
399+
getItem: this.#keyWrapping.loadWrappedKey,
400+
setItem: this.#keyWrapping.storeWrappedKey,
401+
});
402+
},
403+
};
404+
316405
#auth = {
317406
getBearerToken: async () => {
318407
return await this.messagingSystem.call(
@@ -374,7 +463,9 @@ export default class UserStorageController extends BaseController<
374463
},
375464
};
376465

377-
#nativeScryptCrypto: NativeScrypt | undefined = undefined;
466+
#nativeScryptCrypto: NativeScrypt | undefined;
467+
468+
#keyStore: KeyStore | undefined;
378469

379470
getMetaMetricsState: () => boolean;
380471

@@ -385,6 +476,7 @@ export default class UserStorageController extends BaseController<
385476
config,
386477
getMetaMetricsState,
387478
nativeScryptCrypto,
479+
keyStore,
388480
}: {
389481
messenger: UserStorageControllerMessenger;
390482
state?: UserStorageControllerState;
@@ -395,6 +487,7 @@ export default class UserStorageController extends BaseController<
395487
};
396488
getMetaMetricsState: () => boolean;
397489
nativeScryptCrypto?: NativeScrypt;
490+
keyStore?: KeyStore;
398491
}) {
399492
super({
400493
messenger,
@@ -432,6 +525,8 @@ export default class UserStorageController extends BaseController<
432525
!this.state.hasNetworkSyncingSyncedAtLeastOnce,
433526
});
434527
}
528+
529+
this.#keyStore = keyStore ?? this.#keyWrapping.getWrappedKeyStore();
435530
}
436531

437532
/**
@@ -491,6 +586,7 @@ export default class UserStorageController extends BaseController<
491586
storageKey,
492587
bearerToken,
493588
nativeScryptCrypto: this.#nativeScryptCrypto,
589+
keyStore: this.#keyStore,
494590
};
495591
}
496592

@@ -581,6 +677,7 @@ export default class UserStorageController extends BaseController<
581677
bearerToken,
582678
storageKey,
583679
nativeScryptCrypto: this.#nativeScryptCrypto,
680+
keyStore: this.#keyStore,
584681
});
585682

586683
return result;
@@ -606,6 +703,7 @@ export default class UserStorageController extends BaseController<
606703
bearerToken,
607704
storageKey,
608705
nativeScryptCrypto: this.#nativeScryptCrypto,
706+
keyStore: this.#keyStore,
609707
});
610708

611709
return result;
@@ -633,6 +731,7 @@ export default class UserStorageController extends BaseController<
633731
bearerToken,
634732
storageKey,
635733
nativeScryptCrypto: this.#nativeScryptCrypto,
734+
keyStore: this.#keyStore,
636735
});
637736
}
638737

@@ -660,6 +759,7 @@ export default class UserStorageController extends BaseController<
660759
bearerToken,
661760
storageKey,
662761
nativeScryptCrypto: this.#nativeScryptCrypto,
762+
keyStore: this.#keyStore,
663763
});
664764
}
665765

@@ -730,6 +830,7 @@ export default class UserStorageController extends BaseController<
730830
bearerToken,
731831
storageKey,
732832
nativeScryptCrypto: this.#nativeScryptCrypto,
833+
keyStore: this.#keyStore,
733834
});
734835
}
735836

0 commit comments

Comments
 (0)