diff --git a/watcher/src/utils/isBase64Encoded.ts b/watcher/src/utils/isBase64Encoded.ts new file mode 100644 index 00000000..7f929350 --- /dev/null +++ b/watcher/src/utils/isBase64Encoded.ts @@ -0,0 +1,13 @@ +// This function uses a regex string to check if the input could +// possibly be base64 encoded. +// +// WARNING: There are clear text strings that are NOT base64 encoded +// that will pass this check. +export function isBase64Encoded(input: string): boolean { + const b64Regex = new RegExp('^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$'); + const match = b64Regex.exec(input); + if (match) { + return true; + } + return false; +} diff --git a/watcher/src/watchers/CosmwasmWatcher.ts b/watcher/src/watchers/CosmwasmWatcher.ts index 65d61391..7b069976 100644 --- a/watcher/src/watchers/CosmwasmWatcher.ts +++ b/watcher/src/watchers/CosmwasmWatcher.ts @@ -6,6 +6,7 @@ import { makeBlockKey, makeVaaKey } from '../databases/utils'; import { Watcher } from './Watcher'; import { SHA256 } from 'jscrypto/SHA256'; import { Base64 } from 'jscrypto/Base64'; +import { isBase64Encoded } from '../utils/isBase64Encoded'; export class CosmwasmWatcher extends Watcher { latestBlockTag: string; @@ -112,14 +113,14 @@ export class CosmwasmWatcher extends Watcher { // only care about _contract_address, message.sender and message.sequence const numAttrs = attrs.length; for (let k = 0; k < numAttrs; k++) { - const key = Buffer.from(attrs[k].key, 'base64').toString().toLowerCase(); + const [key, value] = maybeBase64Decode(attrs[k].key, attrs[k].value); this.logger.debug('Encoded Key = ' + attrs[k].key + ', decoded = ' + key); if (key === 'message.sender') { - emitter = Buffer.from(attrs[k].value, 'base64').toString(); + emitter = value; } else if (key === 'message.sequence') { - sequence = Buffer.from(attrs[k].value, 'base64').toString(); + sequence = value; } else if (key === '_contract_address' || key === 'contract_address') { - let addr = Buffer.from(attrs[k].value, 'base64').toString(); + let addr = value; if (addr === address) { coreContract = true; } @@ -275,3 +276,13 @@ type EventsType = { } ]; }; + +export function maybeBase64Decode(key: string, value: string): [string, string] { + if (isBase64Encoded(key)) { + return [ + Buffer.from(key, 'base64').toString().toLowerCase(), + Buffer.from(value, 'base64').toString(), + ]; + } + return [key, value]; +} diff --git a/watcher/src/watchers/__tests__/CosmwasmWatcher.test.ts b/watcher/src/watchers/__tests__/CosmwasmWatcher.test.ts index 7cfcfd64..8eea1ed3 100644 --- a/watcher/src/watchers/__tests__/CosmwasmWatcher.test.ts +++ b/watcher/src/watchers/__tests__/CosmwasmWatcher.test.ts @@ -1,10 +1,11 @@ import { expect, jest, test } from '@jest/globals'; -import { CosmwasmWatcher } from '../CosmwasmWatcher'; +import { CosmwasmWatcher, maybeBase64Decode } from '../CosmwasmWatcher'; import { TerraExplorerWatcher } from '../TerraExplorerWatcher'; import { InjectiveExplorerWatcher } from '../InjectiveExplorerWatcher'; import { SeiExplorerWatcher } from '../SeiExplorerWatcher'; import { WormchainWatcher } from '../WormchainWatcher'; import { INITIAL_DEPLOYMENT_BLOCK_BY_CHAIN } from '@wormhole-foundation/wormhole-monitor-common'; +import { isBase64Encoded } from '../../utils/isBase64Encoded'; jest.setTimeout(60000); @@ -213,3 +214,35 @@ test('getMessagesForBlocks(wormchain)', async () => { '4D861F1BE86325D227FA006CA2745BBC6748AF5B5E0811DE536D02792928472A:3104/aeb534c45c3049d380b9d9b966f9895f53abd4301bfaff407fa09dea8ae7a924/0', ]); }); + +test('isBase64Encoded', async () => { + const msg1: string = 'message.sender'; + const bmsg1: string = Buffer.from(msg1).toString('base64'); + const msg2: string = 'message.sequence'; + const bmsg2: string = Buffer.from(msg1).toString('base64'); + const msg3: string = '_contract.address'; + const bmsg3: string = Buffer.from(msg1).toString('base64'); + const msg4: string = 'contract.address'; + const bmsg4: string = Buffer.from(msg1).toString('base64'); + expect(isBase64Encoded(msg1)).toBe(false); + expect(isBase64Encoded(msg2)).toBe(false); + expect(isBase64Encoded(msg3)).toBe(false); + expect(isBase64Encoded(msg4)).toBe(false); + expect(isBase64Encoded(bmsg1)).toBe(true); + expect(isBase64Encoded(bmsg2)).toBe(true); + expect(isBase64Encoded(bmsg3)).toBe(true); + expect(isBase64Encoded(bmsg4)).toBe(true); + + // This test shows the risk involved with checking for base64 encoding. + // The following is, actually, clear text. But it passes the base64 encoding check. + // So, passing addresses into the check should be done with caution. + const addr: string = 'terra12mrnzvhx3rpej6843uge2yyfppfyd3u9c3uq223q8sl48huz9juqffcnhp'; + expect(isBase64Encoded(addr)).toBe(true); + + const [key1, value1] = maybeBase64Decode(msg1, bmsg1); + expect(key1).toBe(msg1); + expect(value1).toBe(bmsg1); + const [key2, value2] = maybeBase64Decode(bmsg1, bmsg1); + expect(key2).toBe(msg1); + expect(value2).toBe(msg1); +});