diff --git a/app/manifest/v2/chrome.json b/app/manifest/v2/chrome.json index a152130d8..10de9fdf6 100644 --- a/app/manifest/v2/chrome.json +++ b/app/manifest/v2/chrome.json @@ -1,5 +1,5 @@ { - "content_security_policy": "frame-ancestors 'none'; script-src 'self'; object-src 'self'", + "content_security_policy": "frame-ancestors 'none'; script-src 'self' 'wasm-unsafe-eval'; object-src 'self'", "externally_connectable": { "matches": ["https://metamask.io/*"], "ids": ["*"] diff --git a/app/scripts/lib/indexed-db-backend.test.ts b/app/scripts/lib/indexed-db-backend.test.ts new file mode 100644 index 000000000..3aca51959 --- /dev/null +++ b/app/scripts/lib/indexed-db-backend.test.ts @@ -0,0 +1,93 @@ +import 'fake-indexeddb/auto'; + +import { IndexedDBPPOMStorage } from './indexed-db-backend'; + +Object.defineProperty(globalThis, 'crypto', { + value: { + subtle: { + digest: () => new ArrayBuffer(12), + }, + }, +}); + +const enc = new TextEncoder(); +const dec = new TextDecoder('utf-8'); + +describe('IndexedDBPPOMStorage', () => { + it('should be able to initialise correctly', () => { + const indexDBBackend = new IndexedDBPPOMStorage('PPOMDB', 1); + expect(indexDBBackend).toBeDefined(); + }); + + it('should be able to write and read file data if checksum matches', async () => { + const indexDBBackend = new IndexedDBPPOMStorage('PPOMDB', 1); + await indexDBBackend.write( + { name: 'fake_name', chainId: '5' }, + enc.encode('fake_data'), + '000000000000000000000000', + ); + const file = await indexDBBackend.read( + { name: 'fake_name', chainId: '5' }, + '000000000000000000000000', + ); + expect(dec.decode(file)).toStrictEqual('fake_data'); + }); + + it('should fail to write if checksum does not match', async () => { + const indexDBBackend = new IndexedDBPPOMStorage('PPOMDB', 1); + await expect(async () => { + await indexDBBackend.write( + { name: 'fake_name', chainId: '5' }, + enc.encode('fake_data'), + 'XXX', + ); + }).rejects.toThrow('Checksum mismatch'); + }); + + it('should fail to read if checksum does not match', async () => { + const indexDBBackend = new IndexedDBPPOMStorage('PPOMDB', 1); + await expect(async () => { + await indexDBBackend.write( + { name: 'fake_name', chainId: '5' }, + enc.encode('fake_data'), + '000000000000000000000000', + ); + await indexDBBackend.read({ name: 'fake_name', chainId: '5' }, 'XXX'); + }).rejects.toThrow('Checksum mismatch'); + }); + + it('should delete a file when delete method is called', async () => { + const indexDBBackend = new IndexedDBPPOMStorage('PPOMDB', 1); + await indexDBBackend.write( + { name: 'fake_name', chainId: '5' }, + enc.encode('fake_data'), + '000000000000000000000000', + ); + await indexDBBackend.delete({ name: 'fake_name', chainId: '5' }); + const result = await indexDBBackend.read( + { name: 'fake_name', chainId: '5' }, + '000000000000000000000000', + ); + expect(result).toBeUndefined(); + }); + + it('should list all keys when dir is called', async () => { + const keys = [ + { chainId: '5', name: 'fake_name_1' }, + { chainId: '1', name: 'fake_name_2' }, + ]; + const indexDBBackend = new IndexedDBPPOMStorage('PPOMDB', 1); + await indexDBBackend.write( + keys[0], + enc.encode('fake_data_1'), + '000000000000000000000000', + ); + await indexDBBackend.write( + keys[1], + enc.encode('fake_data_2'), + '000000000000000000000000', + ); + const result = await indexDBBackend.dir(); + expect(result).toStrictEqual(keys); + }); +}); diff --git a/app/scripts/lib/indexed-db-backend.ts b/app/scripts/lib/indexed-db-backend.ts new file mode 100644 index 000000000..41c5af262 --- /dev/null +++ b/app/scripts/lib/indexed-db-backend.ts @@ -0,0 +1,127 @@ +import { StorageBackend } from '@metamask/ppom-validator'; + +type StorageKey = { + name: string; + chainId: string; +}; + +const validateChecksum = async ( + key: StorageKey, + data: ArrayBuffer, + checksum: string, +) => { + const hash = await crypto.subtle.digest('SHA-256', data); + const hashString = Array.from(new Uint8Array(hash)) + .map((b) => b.toString(16).padStart(2, '0')) + .join(''); + + if (hashString !== checksum) { + throw new Error(`Checksum mismatch for key ${key}`); + } +}; + +export class IndexedDBPPOMStorage implements StorageBackend { + private storeName: string; + + private dbVersion: number; + + constructor(storeName: string, dbVersion: number) { + this.storeName = storeName; + this.dbVersion = dbVersion; + } + + #getObjectStore(mode: IDBTransactionMode): Promise { + return new Promise((resolve, reject) => { + const request = indexedDB.open(this.storeName, this.dbVersion); + + request.onerror = (event: Event) => { + reject( + new Error( + `Failed to open database ${this.storeName}: ${ + (event.target as any)?.error + }`, + ), + ); + }; + + request.onupgradeneeded = (event) => { + const db = (event.target as IDBOpenDBRequest).result; + + if (!db.objectStoreNames.contains(this.storeName)) { + db.createObjectStore(this.storeName, { + keyPath: ['name', 'chainId'], + }); + } + }; + + request.onsuccess = (event) => { + const db = (event.target as IDBOpenDBRequest).result; + const transaction = db.transaction([this.storeName], mode); + const objectStore = transaction.objectStore(this.storeName); + resolve(objectStore); + }; + }); + } + + private async objectStoreAction( + method: 'get' | 'delete' | 'put' | 'getAllKeys', + args?: any, + mode: IDBTransactionMode = 'readonly', + ): Promise { + return new Promise((resolve, reject) => { + this.#getObjectStore(mode) + .then((objectStore) => { + const request = objectStore[method](args); + + request.onsuccess = async (event) => { + resolve(event); + }; + + request.onerror = (event) => { + reject( + new Error( + `Error in indexDB operation ${method}: ${ + (event.target as any)?.error + }`, + ), + ); + }; + }) + .catch((error) => { + reject(error); + }); + }); + } + + async read(key: StorageKey, checksum: string): Promise { + const event = await this.objectStoreAction('get', [key.name, key.chainId]); + const data = (event.target as any)?.result?.data; + await validateChecksum(key, data, checksum); + return data; + } + + async write( + key: StorageKey, + data: ArrayBuffer, + checksum: string, + ): Promise { + await validateChecksum(key, data, checksum); + await this.objectStoreAction('put', { ...key, data }, 'readwrite'); + } + + async delete(key: StorageKey): Promise { + await this.objectStoreAction( + 'delete', + [key.name, key.chainId], + 'readwrite', + ); + } + + async dir(): Promise { + const event = await this.objectStoreAction('getAllKeys'); + return (event.target as any)?.result.map(([name, chainId]: string[]) => ({ + name, + chainId, + })); + } +} diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 018e39aff..17b8d98dd 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -76,6 +76,9 @@ import { CustodyController } from '@metamask-institutional/custody-controller'; import { TransactionUpdateController } from '@metamask-institutional/transaction-update'; ///: END:ONLY_INCLUDE_IN import { SignatureController } from '@metamask/signature-controller'; +///: BEGIN:ONLY_INCLUDE_IN(blockaid) +import { PPOMController, createPPOMMiddleware } from '@metamask/ppom-validator'; +///: END:ONLY_INCLUDE_IN ///: BEGIN:ONLY_INCLUDE_IN(desktop) // eslint-disable-next-line import/order @@ -210,6 +213,9 @@ import { } from './controllers/permissions'; import createRPCMethodTrackingMiddleware from './lib/createRPCMethodTrackingMiddleware'; import { securityProviderCheck } from './lib/security-provider-helpers'; +///: BEGIN:ONLY_INCLUDE_IN(blockaid) +import { IndexedDBPPOMStorage } from './lib/indexed-db-backend'; +///: END:ONLY_INCLUDE_IN import { updateCurrentLocale } from './translate'; export const METAMASK_CONTROLLER_EVENTS = { @@ -630,6 +636,22 @@ export default class MetamaskController extends EventEmitter { this.phishingController.setStalelistRefreshInterval(30 * SECOND); } + ///: BEGIN:ONLY_INCLUDE_IN(blockaid) + this.ppomController = new PPOMController({ + messenger: this.controllerMessenger.getRestricted({ + name: 'PPOMController', + }), + storageBackend: new IndexedDBPPOMStorage('PPOMDB', 1), + provider: this.provider, + state: initState.PPOMController, + chainId: this.networkController.state.providerConfig.chainId, + onNetworkChange: networkControllerMessenger.subscribe.bind( + networkControllerMessenger, + 'NetworkController:stateChange', + ), + }); + ///: END:ONLY_INCLUDE_IN + const announcementMessenger = this.controllerMessenger.getRestricted({ name: 'AnnouncementController', }); @@ -1529,6 +1551,9 @@ export default class MetamaskController extends EventEmitter { SwapsController: this.swapsController.store, EnsController: this.ensController.store, ApprovalController: this.approvalController, + ///: BEGIN:ONLY_INCLUDE_IN(blockaid) + PPOMController: this.ppomController, + ///: END:ONLY_INCLUDE_IN }; this.store.updateStructure({ @@ -1633,6 +1658,9 @@ export default class MetamaskController extends EventEmitter { this.swapsController.resetState, this.ensController.resetState, this.approvalController.clear.bind(this.approvalController), + ///: BEGIN:ONLY_INCLUDE_IN(blockaid) + this.ppomController.clear.bind(this.ppomController), + ///: END:ONLY_INCLUDE_IN // WE SHOULD ADD TokenListController.resetState here too. But it's not implemented yet. ]; @@ -3910,6 +3938,10 @@ export default class MetamaskController extends EventEmitter { engine.push(createLoggerMiddleware({ origin })); engine.push(this.permissionLogController.createMiddleware()); + ///: BEGIN:ONLY_INCLUDE_IN(blockaid) + engine.push(createPPOMMiddleware(this.ppomController)); + ///: END:ONLY_INCLUDE_IN + engine.push( createRPCMethodTrackingMiddleware({ trackEvent: this.metaMetricsController.trackEvent.bind( diff --git a/builds.yml b/builds.yml index 99311a32d..4ea5e4eb7 100644 --- a/builds.yml +++ b/builds.yml @@ -116,6 +116,7 @@ features: - DISABLE_WEB_SOCKET_ENCRYPTION: false - SKIP_OTP_PAIRING_FLOW: false - WEB_SOCKET_PORT: null + blockaid: ### # Build Type code extensions. Things like different support links, warning pages, banners diff --git a/development/build/static.js b/development/build/static.js index 644105c87..29e6a7ab7 100644 --- a/development/build/static.js +++ b/development/build/static.js @@ -22,19 +22,20 @@ module.exports = function createStaticAssetTasks({ const copyTargetsProds = {}; const copyTargetsDevs = {}; + const buildConfig = loadBuildTypesConfig(); + + const activeFeatures = buildConfig.buildTypes[buildType].features ?? []; + browserPlatforms.forEach((browser) => { const [copyTargetsProd, copyTargetsDev] = getCopyTargets( shouldIncludeLockdown, shouldIncludeSnow, + activeFeatures, ); copyTargetsProds[browser] = copyTargetsProd; copyTargetsDevs[browser] = copyTargetsDev; }); - const buildConfig = loadBuildTypesConfig(); - - const activeFeatures = buildConfig.buildTypes[buildType].features ?? []; - const additionalAssets = activeFeatures.flatMap( (feature) => buildConfig.features[feature].assets?.filter( @@ -108,7 +109,11 @@ module.exports = function createStaticAssetTasks({ } }; -function getCopyTargets(shouldIncludeLockdown, shouldIncludeSnow) { +function getCopyTargets( + shouldIncludeLockdown, + shouldIncludeSnow, + activeFeatures, +) { const allCopyTargets = [ { src: `./app/_locales/`, @@ -198,6 +203,14 @@ function getCopyTargets(shouldIncludeLockdown, shouldIncludeSnow) { }, ]; + if (activeFeatures.includes('blockaid')) { + allCopyTargets.push({ + src: getPathInsideNodeModules('@metamask/ppom-validator', 'dist/'), + pattern: '*.wasm', + dest: '', + }); + } + const languageTags = new Set(); for (const locale of locales) { const { code } = locale; diff --git a/package.json b/package.json index 88754b540..4abcb2bf1 100644 --- a/package.json +++ b/package.json @@ -253,6 +253,7 @@ "@metamask/permission-controller": "^4.0.0", "@metamask/phishing-controller": "^3.0.0", "@metamask/post-message-stream": "^6.0.0", + "@metamask/ppom-validator": "^0.0.1", "@metamask/providers": "^11.1.0", "@metamask/rate-limit-controller": "^3.0.0", "@metamask/rpc-methods": "^1.0.0-prerelease.1", @@ -464,6 +465,7 @@ "eslint-plugin-react": "^7.23.1", "eslint-plugin-react-hooks": "^4.2.0", "eslint-plugin-storybook": "^0.6.12", + "fake-indexeddb": "^4.0.1", "fancy-log": "^1.3.3", "fast-glob": "^3.2.2", "fs-extra": "^8.1.0", diff --git a/yarn.lock b/yarn.lock index 70031d4d3..4db3f92c8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1619,6 +1619,13 @@ __metadata: languageName: node linkType: hard +"@blockaid/ppom-mock@npm:^1.0.0": + version: 1.0.0 + resolution: "@blockaid/ppom-mock@npm:1.0.0" + checksum: 297efc29210aae5fb258bbecefcd742645966041bd9af6f256aa80c671920d5e7d9e669c4d1e34795f8556997663abc42422bfafc511ab8379134ce1c8ac324e + languageName: node + linkType: hard + "@chainsafe/as-sha256@npm:^0.3.1": version: 0.3.1 resolution: "@chainsafe/as-sha256@npm:0.3.1" @@ -4551,6 +4558,18 @@ __metadata: languageName: node linkType: hard +"@metamask/ppom-validator@npm:^0.0.1": + version: 0.0.1 + resolution: "@metamask/ppom-validator@npm:0.0.1" + dependencies: + "@blockaid/ppom-mock": ^1.0.0 + "@metamask/base-controller": ^3.0.0 + "@metamask/controller-utils": ^4.0.0 + await-semaphore: ^0.1.3 + checksum: a94edcd618f670b392a84caa236bbc951a6a99100d8a5fa7bd89b78747c3b06b289738b42aee433659b647441eab0a8741e1951a0e29ef6aa98ffa10a3f33f5b + languageName: node + linkType: hard + "@metamask/preferences-controller@npm:^4.1.0": version: 4.1.0 resolution: "@metamask/preferences-controller@npm:4.1.0" @@ -10703,6 +10722,13 @@ __metadata: languageName: node linkType: hard +"base64-arraybuffer-es6@npm:^0.7.0": + version: 0.7.0 + resolution: "base64-arraybuffer-es6@npm:0.7.0" + checksum: 6d2fd114df49201b476cea5d470504e5d4e8c4cd42544152b312c9bdcb824313086fe83f1ffc34262e9e276b82d46aefc6e63bb85553f016932061137b355cdf + languageName: node + linkType: hard + "base64-arraybuffer@npm:^0.1.5": version: 0.1.5 resolution: "base64-arraybuffer@npm:0.1.5" @@ -14689,10 +14715,12 @@ __metadata: languageName: node linkType: hard -"domexception@npm:^1.0.0": - version: 1.0.0 - resolution: "domexception@npm:1.0.0" - checksum: a580e233689e9dcd5e5322f4b58da618bdbf9c4b96532bd11065903b43e81acbe6ec936ceacc57ce2014b695f778ac6368eb079fe3efb1276073a363d59500a8 +"domexception@npm:^1.0.0, domexception@npm:^1.0.1": + version: 1.0.1 + resolution: "domexception@npm:1.0.1" + dependencies: + webidl-conversions: ^4.0.2 + checksum: f564a9c0915dcb83ceefea49df14aaed106b1468fbe505119e8bcb0b77e242534f3aba861978537c0fc9dc6f35b176d0ffc77b3e342820fb27a8f215e7ae4d52 languageName: node linkType: hard @@ -17083,6 +17111,15 @@ __metadata: languageName: node linkType: hard +"fake-indexeddb@npm:^4.0.1": + version: 4.0.1 + resolution: "fake-indexeddb@npm:4.0.1" + dependencies: + realistic-structured-clone: ^3.0.0 + checksum: dd1c82111e3b97c262a647a29dc012209f8c3bed0fbe7ae9630927772842fe8d3276794ff196d0021a5e60563a25a4323eca622a6a7bc6575b62e074328a0c90 + languageName: node + linkType: hard + "fake-merkle-patricia-tree@npm:^1.0.1": version: 1.0.1 resolution: "fake-merkle-patricia-tree@npm:1.0.1" @@ -23714,7 +23751,7 @@ __metadata: languageName: node linkType: hard -"lodash@npm:^4.13.1, lodash@npm:^4.16.4, lodash@npm:^4.17.11, lodash@npm:^4.17.14, lodash@npm:^4.17.15, lodash@npm:^4.17.19, lodash@npm:^4.17.20, lodash@npm:^4.17.21, lodash@npm:^4.17.4": +"lodash@npm:^4.13.1, lodash@npm:^4.16.4, lodash@npm:^4.17.11, lodash@npm:^4.17.14, lodash@npm:^4.17.15, lodash@npm:^4.17.19, lodash@npm:^4.17.20, lodash@npm:^4.17.21, lodash@npm:^4.17.4, lodash@npm:^4.7.0": version: 4.17.21 resolution: "lodash@npm:4.17.21" checksum: eb835a2e51d381e561e508ce932ea50a8e5a68f4ebdd771ea240d3048244a8d13658acbd502cd4829768c56f2e16bdd4340b9ea141297d472517b83868e677f7 @@ -24631,6 +24668,7 @@ __metadata: "@metamask/phishing-controller": ^3.0.0 "@metamask/phishing-warning": ^2.1.0 "@metamask/post-message-stream": ^6.0.0 + "@metamask/ppom-validator": ^0.0.1 "@metamask/providers": ^11.1.0 "@metamask/rate-limit-controller": ^3.0.0 "@metamask/rpc-methods": ^1.0.0-prerelease.1 @@ -24769,6 +24807,7 @@ __metadata: ethjs-contract: ^0.2.3 ethjs-query: ^0.3.4 extension-port-stream: ^2.0.0 + fake-indexeddb: ^4.0.1 fancy-log: ^1.3.3 fast-glob: ^3.2.2 fast-json-patch: ^3.1.1 @@ -29794,6 +29833,17 @@ __metadata: languageName: node linkType: hard +"realistic-structured-clone@npm:^3.0.0": + version: 3.0.0 + resolution: "realistic-structured-clone@npm:3.0.0" + dependencies: + domexception: ^1.0.1 + typeson: ^6.1.0 + typeson-registry: ^1.0.0-alpha.20 + checksum: b4521b299c8dc320a5e3ef44678f80a92b0f1837901a5fbd1c7be06808110fb0b591b417114306ec55b44ef47fd17968aacca079afc9665afbe1c528026295ec + languageName: node + linkType: hard + "recast@npm:^0.21.0": version: 0.21.5 resolution: "recast@npm:0.21.5" @@ -33580,6 +33630,15 @@ __metadata: languageName: node linkType: hard +"tr46@npm:^2.1.0": + version: 2.1.0 + resolution: "tr46@npm:2.1.0" + dependencies: + punycode: ^2.1.1 + checksum: ffe6049b9dca3ae329b059aada7f515b0f0064c611b39b51ff6b53897e954650f6f63d9319c6c008d36ead477c7b55e5f64c9dc60588ddc91ff720d64eb710b3 + languageName: node + linkType: hard + "tr46@npm:^3.0.0": version: 3.0.0 resolution: "tr46@npm:3.0.0" @@ -34052,6 +34111,24 @@ __metadata: languageName: node linkType: hard +"typeson-registry@npm:^1.0.0-alpha.20": + version: 1.0.0-alpha.39 + resolution: "typeson-registry@npm:1.0.0-alpha.39" + dependencies: + base64-arraybuffer-es6: ^0.7.0 + typeson: ^6.0.0 + whatwg-url: ^8.4.0 + checksum: c6b629697acf4652aecfff7be760356d764600afc9beca253278bbfc44fae0fe635b7619201b83e497cdc30645cbce7614d12a04b5726d9b8b505f73e6a3fc2a + languageName: node + linkType: hard + +"typeson@npm:^6.0.0, typeson@npm:^6.1.0": + version: 6.1.0 + resolution: "typeson@npm:6.1.0" + checksum: 00a77b03ac8f704acb103307bad9295fe47d6b304c386297f078ec3be63875c0b81e022a4815edb9dc2c7da0a72a431345411d35c755a8510af4a420e9e46cdc + languageName: node + linkType: hard + "uglify-js@npm:^3.1.4": version: 3.17.0 resolution: "uglify-js@npm:3.17.0" @@ -35404,6 +35481,13 @@ __metadata: languageName: node linkType: hard +"webidl-conversions@npm:^6.1.0": + version: 6.1.0 + resolution: "webidl-conversions@npm:6.1.0" + checksum: 1f526507aa491f972a0c1409d07f8444e1d28778dfa269a9971f2e157182f3d496dc33296e4ed45b157fdb3bf535bb90c90bf10c50dcf1dd6caacb2a34cc84fb + languageName: node + linkType: hard + "webidl-conversions@npm:^7.0.0": version: 7.0.0 resolution: "webidl-conversions@npm:7.0.0" @@ -35578,6 +35662,17 @@ __metadata: languageName: node linkType: hard +"whatwg-url@npm:^8.4.0": + version: 8.7.0 + resolution: "whatwg-url@npm:8.7.0" + dependencies: + lodash: ^4.7.0 + tr46: ^2.1.0 + webidl-conversions: ^6.1.0 + checksum: a87abcc6cefcece5311eb642858c8fdb234e51ec74196bfacf8def2edae1bfbffdf6acb251646ed6301f8cee44262642d8769c707256125a91387e33f405dd1e + languageName: node + linkType: hard + "which-boxed-primitive@npm:^1.0.2": version: 1.0.2 resolution: "which-boxed-primitive@npm:1.0.2"