Skip to content

Commit

Permalink
feat: persist UserStorage e2e content keys using an encrypted keyStore
Browse files Browse the repository at this point in the history
fixes #5128
  • Loading branch information
mirceanis committed Jan 10, 2025
1 parent 280d897 commit be2f7ca
Show file tree
Hide file tree
Showing 8 changed files with 424 additions and 22 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,6 @@ scripts/coverage

# typescript
packages/*/*.tsbuildinfo

# jetbrains IDE local files
/.idea
2 changes: 2 additions & 0 deletions packages/profile-sync-controller/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,9 @@
"@metamask/network-controller": "^22.1.1",
"@metamask/snaps-sdk": "^6.7.0",
"@metamask/snaps-utils": "^8.3.0",
"@metamask/utils": "^11.0.1",
"@noble/ciphers": "^0.5.2",
"@noble/curves": "^1.7.0",
"@noble/hashes": "^1.4.0",
"immer": "^9.0.6",
"loglevel": "^1.8.1",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { HandleSnapRequest } from '@metamask/snaps-controllers';
import type { SnapId } from '@metamask/snaps-sdk';
import type { Eip1024EncryptedData } from '@metamask/utils';

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

Expand All @@ -22,6 +23,22 @@ export function createSnapPublicKeyRequest(): SnapRPCRequest {
};
}

/**
* Constructs Request to Message Signing Snap to get the Encryption Public Key
*
* @returns Snap Encryption Public Key Request
*/
export function createSnapEncryptionPublicKeyRequest(): SnapRPCRequest {
return {
snapId,
origin: '',
handler: 'onRpcRequest' as any,
request: {
method: 'getEncryptionPublicKey',
},
};
}

/**
* Constructs Request to get Message Signing Snap to sign a message.
*
Expand All @@ -41,3 +58,23 @@ export function createSnapSignMessageRequest(
},
};
}

/**
* Constructs Request to get Message Signing Snap to decrypt a message.
*
* @param data - message to decrypt
* @returns Snap Sign Message Request
*/
export function createSnapDecryptMessageRequest(
data: Eip1024EncryptedData,
): SnapRPCRequest {
return {
snapId,
origin: '',
handler: 'onRpcRequest' as any,
request: {
method: 'decryptMessage',
params: { data },
},
};
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type {
AccountsControllerAccountAddedEvent,
AccountsControllerAccountRenamedEvent,
AccountsControllerListAccountsAction,
AccountsControllerUpdateAccountMetadataAction,
AccountsControllerAccountRenamedEvent,
AccountsControllerAccountAddedEvent,
} from '@metamask/accounts-controller';
import type {
ControllerGetStateAction,
Expand All @@ -12,10 +12,10 @@ import type {
} from '@metamask/base-controller';
import { BaseController } from '@metamask/base-controller';
import {
type KeyringControllerAddNewAccountAction,
type KeyringControllerGetStateAction,
type KeyringControllerLockEvent,
type KeyringControllerUnlockEvent,
type KeyringControllerAddNewAccountAction,
} from '@metamask/keyring-controller';
import type { InternalAccount } from '@metamask/keyring-internal-api';
import type {
Expand All @@ -26,22 +26,29 @@ import type {
NetworkControllerUpdateNetworkAction,
} from '@metamask/network-controller';
import type { HandleSnapRequest } from '@metamask/snaps-controllers';
import type { Eip1024EncryptedData } from '@metamask/utils';

import { createSHA256Hash } from '../../shared/encryption';
import type { KeyStore } from '../../shared/encryption/key-storage';
import { ERC1024WrappedKeyStore } from '../../shared/encryption/key-storage';
import type { UserStorageFeatureKeys } from '../../shared/storage-schema';
import {
type UserStoragePathWithFeatureAndKey,
type UserStoragePathWithFeatureOnly,
} from '../../shared/storage-schema';
import type { NativeScrypt } from '../../shared/types/encryption';
import { createSnapSignMessageRequest } from '../authentication/auth-snap-requests';
import {
createSnapDecryptMessageRequest,
createSnapEncryptionPublicKeyRequest,
createSnapSignMessageRequest,
} from '../authentication/auth-snap-requests';
import type {
AuthenticationControllerGetBearerToken,
AuthenticationControllerGetSessionProfile,
AuthenticationControllerIsSignedIn,
AuthenticationControllerPerformSignIn,
AuthenticationControllerPerformSignOut,
} from '../authentication/AuthenticationController';
} from '../authentication';
import {
saveInternalAccountToUserStorage,
syncInternalAccountsWithUserStorage,
Expand Down Expand Up @@ -102,6 +109,10 @@ export type UserStorageControllerState = {
* Condition used to ensure that we do not perform any network sync mutations until we have synced at least once
*/
hasNetworkSyncingSyncedAtLeastOnce?: boolean;
/**
* Content keys used to encrypt/decrypt user storage content. These are wrapped while at rest.
*/
encryptedContentKeys: Record<string, string>;
};

export const defaultState: UserStorageControllerState = {
Expand All @@ -110,6 +121,7 @@ export const defaultState: UserStorageControllerState = {
hasAccountSyncingSyncedAtLeastOnce: false,
isAccountSyncingReadyToBeDispatched: false,
isAccountSyncingInProgress: false,
encryptedContentKeys: {},
};

const metadata: StateMetadata<UserStorageControllerState> = {
Expand Down Expand Up @@ -137,6 +149,10 @@ const metadata: StateMetadata<UserStorageControllerState> = {
persist: true,
anonymous: false,
},
encryptedContentKeys: {
persist: true,
anonymous: false,
},
};

type ControllerConfig = {
Expand Down Expand Up @@ -313,6 +329,79 @@ export default class UserStorageController extends BaseController<
isNetworkSyncingEnabled: false,
};

#_snapPublicKeyCache: string | null = null;

#keyWrapping = {
/**
* Returns the snap Encryption public key.
*
* @returns The snap Encryption public key.
*/
snapGetEncryptionPublicKey: async (): Promise<string> => {
if (this.#_snapPublicKeyCache) {
return this.#_snapPublicKeyCache;
}

if (!this.#isUnlocked) {
throw new Error(
'#snapGetEncryptionPublicKey - unable to call snap, wallet is locked',
);
}

const result = (await this.messagingSystem.call(
'SnapController:handleRequest',
createSnapEncryptionPublicKeyRequest(),
)) as string;

this.#_snapPublicKeyCache = result;

return result;
},

/**
* Decrypts a message using the message signing snap.
*
* @param data - Eip1024EncryptedData - The encrypted data.
* @returns The decrypted message, if it was intended for this wallet, null otherwise. TODO: check error scenarios
*/
snapDecryptMessage: async (data: Eip1024EncryptedData): Promise<string> => {
if (!this.#isUnlocked) {
throw new Error(
'#snapDecryptMessage - unable to call snap, wallet is locked',
);
}

const result = (await this.messagingSystem.call(
'SnapController:handleRequest',
createSnapDecryptMessageRequest(data),
)) as string;

return result;
},

loadWrappedKey: async (keyRef: string): Promise<string | null> => {
return this.state.encryptedContentKeys[keyRef] ?? null;
},

storeWrappedKey: (keyRef: string, wrappedKey: string): Promise<void> => {
return new Promise((resolve) => {
this.update((state) => {
state.encryptedContentKeys[keyRef] = wrappedKey;
resolve();
});
});
},

getWrappedKeyStore: (): KeyStore => {
return new ERC1024WrappedKeyStore({
decryptMessage: this.#keyWrapping.snapDecryptMessage,
getPublicKey: this.#keyWrapping.snapGetEncryptionPublicKey,
getItem: this.#keyWrapping.loadWrappedKey,
setItem: this.#keyWrapping.storeWrappedKey,
});
},
};

#auth = {
getBearerToken: async () => {
return await this.messagingSystem.call(
Expand Down Expand Up @@ -374,7 +463,9 @@ export default class UserStorageController extends BaseController<
},
};

#nativeScryptCrypto: NativeScrypt | undefined = undefined;
#nativeScryptCrypto: NativeScrypt | undefined;

#keyStore: KeyStore | undefined;

getMetaMetricsState: () => boolean;

Expand All @@ -385,6 +476,7 @@ export default class UserStorageController extends BaseController<
config,
getMetaMetricsState,
nativeScryptCrypto,
keyStore,
}: {
messenger: UserStorageControllerMessenger;
state?: UserStorageControllerState;
Expand All @@ -395,6 +487,7 @@ export default class UserStorageController extends BaseController<
};
getMetaMetricsState: () => boolean;
nativeScryptCrypto?: NativeScrypt;
keyStore?: KeyStore;
}) {
super({
messenger,
Expand Down Expand Up @@ -432,6 +525,8 @@ export default class UserStorageController extends BaseController<
!this.state.hasNetworkSyncingSyncedAtLeastOnce,
});
}

this.#keyStore = keyStore ?? this.#keyWrapping.getWrappedKeyStore();
}

/**
Expand Down Expand Up @@ -491,6 +586,7 @@ export default class UserStorageController extends BaseController<
storageKey,
bearerToken,
nativeScryptCrypto: this.#nativeScryptCrypto,
keyStore: this.#keyStore,
};
}

Expand Down Expand Up @@ -581,6 +677,7 @@ export default class UserStorageController extends BaseController<
bearerToken,
storageKey,
nativeScryptCrypto: this.#nativeScryptCrypto,
keyStore: this.#keyStore,
});

return result;
Expand All @@ -606,6 +703,7 @@ export default class UserStorageController extends BaseController<
bearerToken,
storageKey,
nativeScryptCrypto: this.#nativeScryptCrypto,
keyStore: this.#keyStore,
});

return result;
Expand Down Expand Up @@ -633,6 +731,7 @@ export default class UserStorageController extends BaseController<
bearerToken,
storageKey,
nativeScryptCrypto: this.#nativeScryptCrypto,
keyStore: this.#keyStore,
});
}

Expand Down Expand Up @@ -660,6 +759,7 @@ export default class UserStorageController extends BaseController<
bearerToken,
storageKey,
nativeScryptCrypto: this.#nativeScryptCrypto,
keyStore: this.#keyStore,
});
}

Expand Down Expand Up @@ -730,6 +830,7 @@ export default class UserStorageController extends BaseController<
bearerToken,
storageKey,
nativeScryptCrypto: this.#nativeScryptCrypto,
keyStore: this.#keyStore,
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import log from 'loglevel';

import encryption, { createSHA256Hash } from '../../shared/encryption';
import { SHARED_SALT } from '../../shared/encryption/constants';
import type { KeyStore } from '../../shared/encryption/key-storage';
import { Env, getEnvUrls } from '../../shared/env';
import type {
UserStoragePathWithFeatureAndKey,
Expand Down Expand Up @@ -36,6 +37,7 @@ export type UserStorageBaseOptions = {
bearerToken: string;
storageKey: string;
nativeScryptCrypto?: NativeScrypt;
keyStore?: KeyStore;
};

export type UserStorageOptions = UserStorageBaseOptions & {
Expand All @@ -58,7 +60,8 @@ export async function getUserStorage(
opts: UserStorageOptions,
): Promise<string | null> {
try {
const { bearerToken, path, storageKey, nativeScryptCrypto } = opts;
const { bearerToken, path, storageKey, nativeScryptCrypto, keyStore } =
opts;

const encryptedPath = createEntryPath(path, storageKey);
const url = new URL(`${USER_STORAGE_ENDPOINT}/${encryptedPath}`);
Expand Down Expand Up @@ -94,6 +97,7 @@ export async function getUserStorage(
encryptedData,
opts.storageKey,
nativeScryptCrypto,
keyStore,
);

// Re-encrypt and re-upload the entry if the salt is random
Expand All @@ -119,7 +123,7 @@ export async function getUserStorageAllFeatureEntries(
opts: UserStorageAllFeatureEntriesOptions,
): Promise<string[] | null> {
try {
const { bearerToken, path, nativeScryptCrypto } = opts;
const { bearerToken, path, nativeScryptCrypto, keyStore } = opts;
const url = new URL(`${USER_STORAGE_ENDPOINT}/${path}`);

const userStorageResponse = await fetch(url.toString(), {
Expand Down Expand Up @@ -161,6 +165,7 @@ export async function getUserStorageAllFeatureEntries(
entry.Data,
opts.storageKey,
nativeScryptCrypto,
keyStore,
);
decryptedData.push(data);

Expand All @@ -173,6 +178,7 @@ export async function getUserStorageAllFeatureEntries(
data,
opts.storageKey,
nativeScryptCrypto,
keyStore,
),
]);
}
Expand Down Expand Up @@ -212,6 +218,7 @@ export async function upsertUserStorage(
data,
opts.storageKey,
nativeScryptCrypto,
opts.keyStore,
);
const encryptedPath = createEntryPath(path, storageKey);
const url = new URL(`${USER_STORAGE_ENDPOINT}/${encryptedPath}`);
Expand Down
Loading

0 comments on commit be2f7ca

Please sign in to comment.