diff --git a/packages/snaps-controllers/coverage.json b/packages/snaps-controllers/coverage.json index 5620fbae4d..c10c3a0f6a 100644 --- a/packages/snaps-controllers/coverage.json +++ b/packages/snaps-controllers/coverage.json @@ -1,6 +1,6 @@ { - "branches": 92.67, + "branches": 92.73, "functions": 96.65, - "lines": 97.98, - "statements": 97.68 + "lines": 97.99, + "statements": 97.69 } diff --git a/packages/snaps-controllers/src/snaps/SnapController.ts b/packages/snaps-controllers/src/snaps/SnapController.ts index a29da71243..f7e24db998 100644 --- a/packages/snaps-controllers/src/snaps/SnapController.ts +++ b/packages/snaps-controllers/src/snaps/SnapController.ts @@ -87,7 +87,6 @@ import { NpmSnapFileNames, OnNameLookupResponseStruct, getLocalizedSnapManifest, - parseJson, MAX_FILE_SIZE, } from '@metamask/snaps-utils'; import type { Json, NonEmptyArray, SemVerRange } from '@metamask/utils'; @@ -101,7 +100,6 @@ import { hasProperty, inMilliseconds, isNonEmptyArray, - isValidJson, isValidSemVerRange, satisfiesVersionRange, timeSince, @@ -1749,6 +1747,17 @@ export class SnapController extends BaseController< return { key: encryptionKey, salt }; } + /** + * Check if a given Snap has a cached encryption key stored in the runtime. + * + * @param snapId - The Snap ID. + * @returns True if the Snap has a cached encryption key, otherwise false. + */ + #hasCachedEncryptionKey(snapId: SnapId) { + const runtime = this.#getRuntimeExpect(snapId); + return runtime.encryptionKey !== null && runtime.encryptionSalt !== null; + } + /** * Decrypt the encrypted state for a given Snap. * @@ -1759,9 +1768,15 @@ export class SnapController extends BaseController< */ async #decryptSnapState(snapId: SnapId, state: string) { try { - const parsed = parseJson(state); + // We assume that the state string here is valid JSON since we control serialization. + // This lets us skip JSON validation. + const parsed = JSON.parse(state) as EncryptionResult; const { salt, keyMetadata } = parsed; - const useCache = this.#encryptor.isVaultUpdated(state); + + // We only cache encryption keys if they are already cached or if the encryption key is using the latest key derivation params. + const useCache = + this.#hasCachedEncryptionKey(snapId) || + this.#encryptor.isVaultUpdated(state); const { key } = await this.#getSnapEncryptionKey({ snapId, salt, @@ -1772,8 +1787,7 @@ export class SnapController extends BaseController< }); const decryptedState = await this.#encryptor.decryptWithKey(key, parsed); - assert(isValidJson(decryptedState)); - + // We assume this to be valid JSON, since all RPC requests from a Snap are validated and sanitized. return decryptedState as Record; } catch { throw rpcErrors.internal({ @@ -1864,7 +1878,8 @@ export class SnapController extends BaseController< } if (!encrypted) { - return parseJson(state); + // For performance reasons, we do not validate that the state is JSON, since we control serialization. + return JSON.parse(state); } const decrypted = await this.#decryptSnapState(snapId, state);