From 9ff1528c39f74f7376fd54d19a4f97bb9fd94c9b Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Tue, 23 Apr 2024 21:58:47 -0700 Subject: [PATCH 1/4] fix: make it very explicitly clear that the dbencryption key is required --- src/lib/Client.ts | 60 +++++++++++++++++++++-------------------------- 1 file changed, 27 insertions(+), 33 deletions(-) diff --git a/src/lib/Client.ts b/src/lib/Client.ts index 714c4886c..f8ddeb756 100644 --- a/src/lib/Client.ts +++ b/src/lib/Client.ts @@ -19,11 +19,13 @@ import { DecodedMessage } from '../index' declare const Buffer -export type GetMessageContentTypeFromClient = - C extends Client ? T : never +export type GetMessageContentTypeFromClient = C extends Client + ? T + : never -export type ExtractDecodedType = - C extends XMTPModule.ContentCodec ? T : never +export type ExtractDecodedType = C extends XMTPModule.ContentCodec + ? T + : never export class Client< ContentTypes extends DefaultContentTypes = DefaultContentTypes, @@ -48,9 +50,11 @@ export class Client< ContentCodecs extends DefaultContentTypes = DefaultContentTypes, >( wallet: Signer | WalletClient | null, - opts?: Partial & { codecs?: ContentCodecs } + options: ClientOptions & { codecs?: ContentCodecs } ): Promise> { - const options = defaultOptions(opts) + if (options.dbEncryptionKey.length !== 32) { + throw new Error('The encryption key must be exactly 32 bytes.') + } const { enableSubscription, createSubscription } = this.setupSubscriptions(options) const signer = getSigner(wallet) @@ -96,7 +100,7 @@ export class Client< this.removeSignSubscription() this.removeAuthSubscription() const address = await signer.getAddress() - resolve(new Client(address, opts?.codecs || [])) + resolve(new Client(address, options?.codecs || [])) } ) await XMTPModule.auth( @@ -136,9 +140,11 @@ export class Client< * @returns {Promise} A Promise that resolves to a new Client instance with a random address. */ static async createRandom( - opts?: Partial & { codecs?: ContentTypes } + options: ClientOptions & { codecs?: ContentTypes } ): Promise> { - const options = defaultOptions(opts) + if (options.dbEncryptionKey.length !== 32) { + throw new Error('The encryption key must be exactly 32 bytes.') + } const { enableSubscription, createSubscription } = this.setupSubscriptions(options) const address = await XMTPModule.createRandom( @@ -153,7 +159,7 @@ export class Client< this.removeSubscription(enableSubscription) this.removeSubscription(createSubscription) - return new Client(address, opts?.codecs || []) + return new Client(address, options?.codecs || []) } /** @@ -170,9 +176,11 @@ export class Client< ContentCodecs extends DefaultContentTypes = [], >( keyBundle: string, - opts?: Partial & { codecs?: ContentCodecs } + options: ClientOptions & { codecs?: ContentCodecs } ): Promise> { - const options = defaultOptions(opts) + if (options.dbEncryptionKey.length !== 32) { + throw new Error('The encryption key must be exactly 32 bytes.') + } const address = await XMTPModule.createFromKeyBundle( keyBundle, options.env, @@ -181,7 +189,7 @@ export class Client< options.dbEncryptionKey, options.dbPath ) - return new Client(address, opts?.codecs || []) + return new Client(address, options?.codecs || []) } /** @@ -227,9 +235,11 @@ export class Client< */ static async canMessage( peerAddress: string, - opts?: Partial + options: ClientOptions ): Promise { - const options = defaultOptions(opts) + if (options.dbEncryptionKey.length !== 32) { + throw new Error('The encryption key must be exactly 32 bytes.') + } return await XMTPModule.staticCanMessage( peerAddress, options.env, @@ -439,9 +449,9 @@ export type ClientOptions = { */ enableAlphaMls?: boolean /** - * OPTIONAL specify the encryption key for the database + * REQUIRED specify the encryption key for the database. The encryption key must be exactly 32 bytes. */ - dbEncryptionKey?: Uint8Array + dbEncryptionKey: Uint8Array /** * OPTIONAL specify the XMTP managed database path */ @@ -452,19 +462,3 @@ export type KeyType = { kind: 'identity' | 'prekey' prekeyIndex?: number } - -/** - * Provide a default client configuration. These settings can be used on their own, or as a starting point for custom configurations - * - * @param opts additional options to override the default settings - */ -export function defaultOptions(opts?: Partial): ClientOptions { - const _defaultOptions: ClientOptions = { - env: 'dev', - enableAlphaMls: false, - dbEncryptionKey: undefined, - dbPath: undefined, - } - - return { ..._defaultOptions, ...opts } as ClientOptions -} From 143d8d51a49536a7be2063220cf9bac367c07bf7 Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Wed, 24 Apr 2024 15:30:56 -0700 Subject: [PATCH 2/4] first pass at fixing up all the tests --- example/src/tests/groupTests.ts | 9 ++- example/src/tests/tests.ts | 106 +++++++++----------------------- 2 files changed, 37 insertions(+), 78 deletions(-) diff --git a/example/src/tests/groupTests.ts b/example/src/tests/groupTests.ts index 18f423fad..5b244d12d 100644 --- a/example/src/tests/groupTests.ts +++ b/example/src/tests/groupTests.ts @@ -192,11 +192,17 @@ test('can make a MLS V3 client from bundle', async () => { }) test('production MLS V3 client creation throws error', async () => { + const key = new Uint8Array([ + 233, 120, 198, 96, 154, 65, 132, 17, 132, 96, 250, 40, 103, 35, 125, 64, + 166, 83, 208, 224, 254, 44, 205, 227, 175, 49, 234, 129, 74, 252, 135, 145, + ]) + try { await Client.createRandom({ env: 'production', appVersion: 'Testing/0.0.0', enableAlphaMls: true, + dbEncryptionKey: key, }) // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (error) { @@ -728,8 +734,7 @@ test('can stream all groups and conversations', async () => { }) test('canMessage', async () => { - const bo = await Client.createRandom({ env: 'local' }) - const alix = await Client.createRandom({ env: 'local' }) + const [bo, alix] = await createClients(2) const canMessage = await bo.canMessage(alix.address) if (!canMessage) { diff --git a/example/src/tests/tests.ts b/example/src/tests/tests.ts index 9bb221b4a..92822794f 100644 --- a/example/src/tests/tests.ts +++ b/example/src/tests/tests.ts @@ -175,10 +175,8 @@ function test(name: string, perform: () => Promise) { } test('can make a client', async () => { - const client = await Client.createRandom({ - env: 'local', - appVersion: 'Testing/0.0.0', - }) + const [client] = await createClients(1) + client.register(new RemoteAttachmentCodec()) if (Object.keys(client.codecRegistry).length !== 2) { throw new Error( @@ -260,8 +258,7 @@ test('can load 1995 conversations from dev network "2k lens convos" account', as test('can pass a custom filter date and receive message objects with expected dates', async () => { try { - const bob = await Client.createRandom({ env: 'local' }) - const alice = await Client.createRandom({ env: 'local' }) + const [bob, alice] = await createClients(2) if (bob.address === alice.address) { throw new Error('bob and alice should be different') @@ -326,8 +323,7 @@ test('can pass a custom filter date and receive message objects with expected da }) test('canMessage', async () => { - const bo = await Client.createRandom({ env: 'local' }) - const alix = await Client.createRandom({ env: 'local' }) + const [bo, alix] = await createClients(2) const canMessage = await bo.canMessage(alix.address) if (!canMessage) { @@ -384,8 +380,8 @@ test('createFromKeyBundle throws error for non string value', async () => { }) test('canPrepareMessage', async () => { - const bob = await Client.createRandom({ env: 'local' }) - const alice = await Client.createRandom({ env: 'local' }) + const [bob, alice] = await createClients(2) + await delayToPropogate() const bobConversation = await bob.conversations.newConversation(alice.address) @@ -411,9 +407,7 @@ test('canPrepareMessage', async () => { }) test('can list batch messages', async () => { - const bob = await Client.createRandom({ env: 'local' }) - await delayToPropogate() - const alice = await Client.createRandom({ env: 'local' }) + const [bob, alice] = await createClients(2) await delayToPropogate() if (bob.address === alice.address) { throw new Error('bob and alice should be different') @@ -466,9 +460,7 @@ test('can list batch messages', async () => { }) test('can paginate batch messages', async () => { - const bob = await Client.createRandom({ env: 'local' }) - await delayToPropogate() - const alice = await Client.createRandom({ env: 'local' }) + const [bob, alice] = await createClients(2) await delayToPropogate() if (bob.address === alice.address) { throw new Error('bob and alice should be different') @@ -570,9 +562,7 @@ test('can paginate batch messages', async () => { }) test('can stream messages', async () => { - const bob = await Client.createRandom({ env: 'local' }) - await delayToPropogate() - const alice = await Client.createRandom({ env: 'local' }) + const [bob, alice] = await createClients(2) await delayToPropogate() // Record new conversation stream @@ -669,9 +659,7 @@ test('can stream messages', async () => { }) test('can stream conversations with delay', async () => { - const bo = await Client.createRandom({ env: 'dev' }) - await delayToPropogate() - const alix = await Client.createRandom({ env: 'dev' }) + const [bo, alix] = await createClients(2) await delayToPropogate() const allConvos: Conversation[] = [] @@ -711,14 +699,12 @@ test('can stream conversations with delay', async () => { }) test('remote attachments should work', async () => { - const alice = await Client.createRandom({ - env: 'local', - codecs: [new StaticAttachmentCodec(), new RemoteAttachmentCodec()], - }) - const bob = await Client.createRandom({ - env: 'local', - codecs: [new StaticAttachmentCodec(), new RemoteAttachmentCodec()], - }) + const [bob, alice] = await createClients(2) + alice.register(new StaticAttachmentCodec()) + alice.register(new RemoteAttachmentCodec()) + bob.register(new StaticAttachmentCodec()) + bob.register(new RemoteAttachmentCodec()) + const convo = await alice.conversations.newConversation(bob.address) // Alice is sending Bob a file from her phone. @@ -800,9 +786,7 @@ test('remote attachments should work', async () => { }) test('can send read receipts', async () => { - const bob = await Client.createRandom({ env: 'local' }) - await delayToPropogate() - const alice = await Client.createRandom({ env: 'local' }) + const [bob, alice] = await createClients(2) await delayToPropogate() if (bob.address === alice.address) { throw new Error('bob and alice should be different') @@ -836,9 +820,7 @@ test('can send read receipts', async () => { }) test('can stream all messages', async () => { - const bo = await Client.createRandom({ env: 'local' }) - await delayToPropogate() - const alix = await Client.createRandom({ env: 'local' }) + const [bo, alix, caro] = await createClients(3) await delayToPropogate() // Record message stream across all conversations @@ -862,7 +844,6 @@ test('can stream all messages', async () => { } // Starts a new conversation. - const caro = await Client.createRandom({ env: 'local' }) const caroConvo = await caro.conversations.newConversation(alix.address) await delayToPropogate() for (let i = 0; i < 5; i++) { @@ -892,9 +873,7 @@ test('can stream all messages', async () => { }) test('can stream all msgs with delay', async () => { - const bo = await Client.createRandom({ env: 'dev' }) - await delayToPropogate() - const alix = await Client.createRandom({ env: 'dev' }) + const [bo, alix, caro] = await createClients(3) await delayToPropogate() // Record message stream across all conversations @@ -919,7 +898,6 @@ test('can stream all msgs with delay', async () => { await sleep(LONG_STREAM_DELAY) // Starts a new conversation. - const caro = await Client.createRandom({ env: 'dev' }) const caroConvo = await caro.conversations.newConversation(alix.address) await delayToPropogate() @@ -951,8 +929,7 @@ test('can stream all msgs with delay', async () => { }) test('canManagePreferences', async () => { - const bo = await Client.createRandom({ env: 'local' }) - const alix = await Client.createRandom({ env: 'local' }) + const [bo, alix] = await createClients(2) await delayToPropogate() const alixConversation = await bo.conversations.newConversation(alix.address) @@ -1015,7 +992,7 @@ test('canManagePreferences', async () => { }) test('is address on the XMTP network', async () => { - const alix = await Client.createRandom({ env: 'local' }) + const [alix] = await createClients(1) const notOnNetwork = '0x0000000000000000000000000000000000000000' const isAlixAddressAvailable = await Client.canMessage(alix.address, { @@ -1037,15 +1014,7 @@ test('is address on the XMTP network', async () => { }) test('register and use custom content types', async () => { - const bob = await Client.createRandom({ - env: 'local', - codecs: [new NumberCodec()], - }) - const alice = await Client.createRandom({ - env: 'local', - codecs: [new NumberCodec()], - }) - + const [bob, alice] = await createClients(2) bob.register(new NumberCodec()) alice.register(new NumberCodec()) @@ -1077,14 +1046,7 @@ test('register and use custom content types', async () => { }) test('register and use custom content types when preparing message', async () => { - const bob = await Client.createRandom({ - env: 'local', - codecs: [new NumberCodec()], - }) - const alice = await Client.createRandom({ - env: 'local', - codecs: [new NumberCodec()], - }) + const [bob, alice] = await createClients(2) bob.register(new NumberCodec()) alice.register(new NumberCodec()) @@ -1150,9 +1112,7 @@ test('calls preEnableIdentityCallback when supplied', async () => { }) test('returns keyMaterial for conversations', async () => { - const bob = await Client.createRandom({ env: 'local' }) - await delayToPropogate() - const alice = await Client.createRandom({ env: 'local' }) + const [bob, alice] = await createClients(2) await delayToPropogate() if (bob.address === alice.address) { throw new Error('bob and alice should be different') @@ -1178,9 +1138,7 @@ test('returns keyMaterial for conversations', async () => { }) test('correctly handles lowercase addresses', async () => { - const bob = await Client.createRandom({ env: 'local' }) - await delayToPropogate() - const alice = await Client.createRandom({ env: 'local' }) + const [bob, alice] = await createClients(2) await delayToPropogate() if (bob.address === alice.address) { throw new Error('bob and alice should be different') @@ -1292,7 +1250,7 @@ test('handle fallback types appropriately', async () => { test('instantiate frames client correctly', async () => { const frameUrl = 'https://fc-polls-five.vercel.app/polls/01032f47-e976-42ee-9e3d-3aac1324f4b8' - const client = await Client.createRandom({ env: 'local' }) + const [client] = await createClients(1) const framesClient = new FramesClient(client) const metadata = await framesClient.proxy.readMetadata(frameUrl) if (!metadata) { @@ -1521,12 +1479,12 @@ test('fails to validate HMAC with wrong key', async () => { }) test('get all HMAC keys', async () => { - const alice = await Client.createRandom({ env: 'local' }) + const [alice] = await createClients(1) const conversations: Conversation[] = [] for (let i = 0; i < 5; i++) { - const client = await Client.createRandom({ env: 'local' }) + const [client] = await createClients(1) const convo = await alice.conversations.newConversation(client.address, { conversationID: `https://example.com/${i}`, metadata: { @@ -1597,9 +1555,7 @@ test('get all HMAC keys', async () => { }) test('can handle complex streaming setup', async () => { - const bo = await Client.createRandom({ env: 'dev' }) - await delayToPropogate() - const alix = await Client.createRandom({ env: 'dev' }) + const [bo, alix] = await createClients(2) await delayToPropogate() const allConvos: Conversation[] = [] @@ -1706,9 +1662,7 @@ test('can handle complex streaming setup', async () => { }) test('can handle complex streaming setup with messages from self', async () => { - const bo = await Client.createRandom({ env: 'dev' }) - await delayToPropogate() - const alix = await Client.createRandom({ env: 'dev' }) + const [bo, alix] = await createClients(2) await delayToPropogate() const allConvos: Conversation[] = [] From 34b6698b0aea33db17eefe44357ba4ecc686dbb3 Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Thu, 25 Apr 2024 07:32:05 -0700 Subject: [PATCH 3/4] fix up more of the tests --- example/src/tests/tests.ts | 47 ++++++++++++++++++++++++++++---------- 1 file changed, 35 insertions(+), 12 deletions(-) diff --git a/example/src/tests/tests.ts b/example/src/tests/tests.ts index 92822794f..fe5f1668c 100644 --- a/example/src/tests/tests.ts +++ b/example/src/tests/tests.ts @@ -1,5 +1,5 @@ import { FramesClient } from '@xmtp/frames-client' -import { content } from '@xmtp/proto' +import { content, keystore } from '@xmtp/proto' import { createHmac } from 'crypto' import ReactNativeBlobUtil from 'react-native-blob-util' import Config from 'react-native-config' @@ -213,8 +213,13 @@ test('can load a client from env "2k lens convos" private key', async () => { const signer = convertPrivateKeyAccountToSigner( privateKeyToAccount(privateKeyHex) ) + const key = new Uint8Array([ + 233, 120, 198, 96, 154, 65, 132, 17, 132, 96, 250, 40, 103, 35, 125, 64, + 166, 83, 208, 224, 254, 44, 205, 227, 175, 49, 234, 129, 74, 252, 135, 145, + ]) const xmtpClient = await Client.create(signer, { env: 'local', + dbEncryptionKey: key, }) assert( @@ -230,12 +235,17 @@ test('can load 1995 conversations from dev network "2k lens convos" account', as } const privateKeyHex: `0x${string}` = `0x${Config.TEST_PRIVATE_KEY}` + const key = new Uint8Array([ + 233, 120, 198, 96, 154, 65, 132, 17, 132, 96, 250, 40, 103, 35, 125, 64, + 166, 83, 208, 224, 254, 44, 205, 227, 175, 49, 234, 129, 74, 252, 135, 145, + ]) const signer = convertPrivateKeyAccountToSigner( privateKeyToAccount(privateKeyHex) ) const xmtpClient = await Client.create(signer, { env: 'dev', + dbEncryptionKey: key, }) assert( @@ -354,7 +364,7 @@ test('canMessage', async () => { }) test('fetch a public key bundle and sign a digest', async () => { - const bob = await Client.createRandom({ env: 'local' }) + const [bob] = await createClients(1) const bytes = new Uint8Array([1, 2, 3]) const signature = await bob.sign(bytes, { kind: 'identity' }) if (signature.length === 0) { @@ -368,10 +378,15 @@ test('fetch a public key bundle and sign a digest', async () => { }) test('createFromKeyBundle throws error for non string value', async () => { + const key = new Uint8Array([ + 233, 120, 198, 96, 154, 65, 132, 17, 132, 96, 250, 40, 103, 35, 125, 64, + 166, 83, 208, 224, 254, 44, 205, 227, 175, 49, 234, 129, 74, 252, 135, 145, + ]) try { const bytes = [1, 2, 3] await Client.createFromKeyBundle(JSON.stringify(bytes), { env: 'local', + dbEncryptionKey: key, }) } catch { return true @@ -994,12 +1009,18 @@ test('canManagePreferences', async () => { test('is address on the XMTP network', async () => { const [alix] = await createClients(1) const notOnNetwork = '0x0000000000000000000000000000000000000000' + const key = new Uint8Array([ + 233, 120, 198, 96, 154, 65, 132, 17, 132, 96, 250, 40, 103, 35, 125, 64, + 166, 83, 208, 224, 254, 44, 205, 227, 175, 49, 234, 129, 74, 252, 135, 145, + ]) const isAlixAddressAvailable = await Client.canMessage(alix.address, { env: 'local', + dbEncryptionKey: key, }) const isAddressAvailable = await Client.canMessage(notOnNetwork, { env: 'local', + dbEncryptionKey: key, }) if (!isAlixAddressAvailable) { @@ -1082,9 +1103,14 @@ test('calls preCreateIdentityCallback when supplied', async () => { const preCreateIdentityCallback = () => { isCallbackCalled = true } + const key = new Uint8Array([ + 233, 120, 198, 96, 154, 65, 132, 17, 132, 96, 250, 40, 103, 35, 125, 64, + 166, 83, 208, 224, 254, 44, 205, 227, 175, 49, 234, 129, 74, 252, 135, 145, + ]) await Client.createRandom({ env: 'local', preCreateIdentityCallback, + dbEncryptionKey: key, }) if (!isCallbackCalled) { @@ -1099,9 +1125,14 @@ test('calls preEnableIdentityCallback when supplied', async () => { const preEnableIdentityCallback = () => { isCallbackCalled = true } + const key = new Uint8Array([ + 233, 120, 198, 96, 154, 65, 132, 17, 132, 96, 250, 40, 103, 35, 125, 64, + 166, 83, 208, 224, 254, 44, 205, 227, 175, 49, 234, 129, 74, 252, 135, 145, + ]) await Client.createRandom({ env: 'local', preEnableIdentityCallback, + dbEncryptionKey: key, }) if (!isCallbackCalled) { @@ -1192,18 +1223,10 @@ test('correctly handles lowercase addresses', async () => { }) test('handle fallback types appropriately', async () => { - const bob = await Client.createRandom({ - env: 'local', - codecs: [ - new NumberCodecEmptyFallback(), - new NumberCodecUndefinedFallback(), - ], - }) - const alice = await Client.createRandom({ - env: 'local', - }) + const [bob, alice] = await await createClients(2) bob.register(new NumberCodecEmptyFallback()) bob.register(new NumberCodecUndefinedFallback()) + const bobConvo = await bob.conversations.newConversation(alice.address) const aliceConvo = await alice.conversations.newConversation(bob.address) From ad148beb1c379ef8af7a4ebed8fb009dc866b1eb Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Thu, 25 Apr 2024 07:55:01 -0700 Subject: [PATCH 4/4] fix lint issue --- example/ios/Podfile.lock | 10 +++++----- src/hooks/useClient.ts | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 6d1408b5a..3bb71afc9 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -59,9 +59,9 @@ PODS: - LibXMTP (0.4.4-beta3) - Logging (1.0.0) - MessagePacker (0.4.7) - - MMKV (1.3.4): - - MMKVCore (~> 1.3.4) - - MMKVCore (1.3.4) + - MMKV (1.3.5): + - MMKVCore (~> 1.3.5) + - MMKVCore (1.3.5) - OpenSSL-Universal (1.1.2200) - RCT-Folly (2021.07.22.00): - boost @@ -714,8 +714,8 @@ SPEC CHECKSUMS: LibXMTP: 1422d36d715fe868b5800692f3d9b8a218a41e9d Logging: 9ef4ecb546ad3169398d5a723bc9bea1c46bef26 MessagePacker: ab2fe250e86ea7aedd1a9ee47a37083edd41fd02 - MMKV: ed58ad794b3f88c24d604a5b74f3fba17fcbaf74 - MMKVCore: a67a1cede26175c413176f404a7cedec43f96a0b + MMKV: 506311d0494023c2f7e0b62cc1f31b7370fa3cfb + MMKVCore: 9e2e5fd529b64a9fe15f1a7afb3d73b2e27b4db9 OpenSSL-Universal: 6e1ae0555546e604dbc632a2b9a24a9c46c41ef6 RCT-Folly: 424b8c9a7a0b9ab2886ffe9c3b041ef628fd4fb1 RCTRequired: e9df143e880d0e879e7a498dc06923d728809c79 diff --git a/src/hooks/useClient.ts b/src/hooks/useClient.ts index df6390773..26c0495a4 100644 --- a/src/hooks/useClient.ts +++ b/src/hooks/useClient.ts @@ -7,7 +7,7 @@ import { DefaultContentTypes } from '../lib/types/DefaultContentType' interface InitializeClientOptions { signer: Signer | null - options?: ClientOptions + options: ClientOptions } export const useClient = <