Skip to content

Commit

Permalink
feat: verify consent proof signature
Browse files Browse the repository at this point in the history
Verified consent proofs
Added tests for consent proofs
  • Loading branch information
Alex Risch authored and Alex Risch committed Apr 24, 2024
1 parent d71b473 commit 22ce2f8
Show file tree
Hide file tree
Showing 3 changed files with 124 additions and 14 deletions.
2 changes: 2 additions & 0 deletions src/Invitation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export class InvitationV1 implements invitation.InvitationV1 {
topic,
context,
aes256GcmHkdfSha256,
consentProof,
}: invitation.InvitationV1) {
if (!topic || !topic.length) {
throw new Error('Missing topic')
Expand All @@ -40,6 +41,7 @@ export class InvitationV1 implements invitation.InvitationV1 {
this.topic = topic
this.context = context
this.aes256GcmHkdfSha256 = aes256GcmHkdfSha256
this.consentProof = consentProof
}

static createRandom(
Expand Down
55 changes: 46 additions & 9 deletions src/conversations/Conversations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@ import type {
messageApi,
} from '@xmtp/proto'
import Long from 'long'
import { hashMessage, hexToBytes } from 'viem'
import { SortDirection, type OnConnectionLostCallback } from '@/ApiClient'
import type { ListMessagesOptions } from '@/Client'
import type Client from '@/Client'
import {
PublicKeyBundle,
SignedPublicKeyBundle,
} from '@/crypto/PublicKeyBundle'
import { ecdsaSignerKey, WalletSigner } from '@/crypto/Signature'
import { splitSignature } from '@/crypto/utils'
import type { InvitationContext } from '@/Invitation'
import { DecodedMessage, MessageV1 } from '@/Message'
import Stream from '@/Stream'
Expand Down Expand Up @@ -157,6 +160,43 @@ export default class Conversations<ContentTypes = any> {
return this.decodeInvites(envelopes)
}

private async validateConsentSignature(
signature: `0x${string}`,
timestamp: number,
peerAddress: string
): Promise<boolean> {
const signatureData = splitSignature(signature)
const message = WalletSigner.consentProofRequestText(peerAddress, timestamp)
const digest = hexToBytes(hashMessage(message))
// Recover public key
const publicKey = ecdsaSignerKey(digest, signatureData)
if (!publicKey) {
return false
}
console.log('Recovered public key: ', typeof publicKey)
console.log('here1116', publicKey.getEthereumAddress())
return publicKey.getEthereumAddress() === this.client.address
}

private async handleConsentProof(
consentProof: invitation.ConsentProofPayload,
peerAddress: string
): Promise<void> {
const { signature, timestamp } = consentProof
const isValid = await this.validateConsentSignature(
signature as `0x${string}`,
Number(timestamp),
peerAddress
)
if (!isValid) {
return
}
const consentState = await this.client.contacts.consentState(peerAddress)
if (consentState === 'unknown') {
this.client.contacts.allow([peerAddress])
}
}

private async decodeInvites(
envelopes: messageApi.Envelope[],
shouldThrow = false
Expand All @@ -174,13 +214,12 @@ export default class Conversations<ContentTypes = any> {
const out: ConversationV2<ContentTypes>[] = []
for (const response of responses) {
try {
console.log(
'here11113',
!!response.result?.conversation?.consentProofPayload
)
// if (response.result?.conversation?.consentProofPayload) {

// }
if (response.result?.conversation?.consentProofPayload) {
this.handleConsentProof(
response.result.conversation.consentProofPayload,
response.result.conversation.peerAddress
)
}
out.push(this.saveInviteResponseToConversation(response))
} catch (e) {
console.warn('Error saving invite response to conversation: ', e)
Expand Down Expand Up @@ -498,7 +537,6 @@ export default class Conversations<ContentTypes = any> {
if (contact instanceof PublicKeyBundle && !context?.conversationId) {
return new ConversationV1(this.client, peerAddress, new Date())
}

// If no conversationId, check and see if we have an existing V1 conversation
if (!context?.conversationId) {
const v1Convos = await this.listV1Conversations()
Expand Down Expand Up @@ -548,7 +586,6 @@ export default class Conversations<ContentTypes = any> {
if (newItemMatch) {
return newItemMatch
}

return this.createV2Convo(
contact as SignedPublicKeyBundle,
context,
Expand Down
81 changes: 76 additions & 5 deletions test/conversations/Conversations.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { invitation } from '@xmtp/proto'
import Client from '@/Client'
import { ConversationV1, ConversationV2 } from '@/conversations/Conversation'
// import { PrivateKey, SignedPrivateKey } from '@/crypto/PrivateKey'
// import { PublicKey, SignedPublicKey } from '@/crypto/PublicKey'
import { WalletSigner } from '@/crypto/Signature'
import { sleep } from '@/utils/async'
import { buildDirectMessageTopic, buildUserIntroTopic } from '@/utils/topic'
Expand Down Expand Up @@ -290,30 +288,103 @@ describe('conversations', () => {
const wallet = newWallet()
const keySigner = new WalletSigner(wallet)
const alixAddress = await keySigner.wallet.getAddress()
const alix = await Client.create(wallet, {
env: 'local',
})
const timestamp = Date.now()
const consentMessage = WalletSigner.consentProofRequestText(
bob.address,
bo.address,
timestamp
)
const signedMessage = await keySigner.wallet.signMessage(consentMessage)
const consentProofPayload = invitation.ConsentProofPayload.fromPartial({
signature: signedMessage,
timestamp,
payloadVersion:
invitation.ConsentProofPayloadVersion.CONSENT_PROOF_PAYLOAD_VERSION_1,
})
const boConvo = await bo.conversations.newConversation(
alixAddress,
undefined,
consentProofPayload
)
const conversations = await alix.conversations.list()
const convo = conversations.find((c) => c.topic === boConvo.topic)
expect(convo).toBeTruthy()
await alix.contacts.refreshConsentList()
const isApproved = await alix.contacts.isAllowed(bo.address)
expect(isApproved).toBeTruthy()
await alix.close()
await bo.close()
})

it('consent proof yields to network consent', async () => {
const bo = await newLocalHostClient()
const wallet = newWallet()
const keySigner = new WalletSigner(wallet)
const alixAddress = await keySigner.wallet.getAddress()
const alix = await Client.create(wallet, {
env: 'local',
})
const timestamp = Date.now()
const consentMessage = WalletSigner.consentProofRequestText(
bo.address,
timestamp
)
const signedMessage = await keySigner.wallet.signMessage(consentMessage)
const consentProofPayload = invitation.ConsentProofPayload.fromPartial({
signature: signedMessage,
timestamp,
payloadVersion:
invitation.ConsentProofPayloadVersion.CONSENT_PROOF_PAYLOAD_VERSION_1,
})
const boConvo = await bo.conversations.newConversation(
alixAddress,
undefined,
consentProofPayload
)
await alix.contacts.deny([bo.address])
const conversations = await alix.conversations.list()
const convo = conversations.find((c) => c.topic === boConvo.topic)
expect(convo).toBeTruthy()
await alix.contacts.refreshConsentList()
const isDenied = await alix.contacts.isDenied(bo.address)
expect(isDenied).toBeTruthy()
await alix.close()
await bo.close()
})

it('consent proof correctly validates', async () => {
const bo = await newLocalHostClient()
const wallet = newWallet()
const keySigner = new WalletSigner(wallet)
const alixAddress = await keySigner.wallet.getAddress()
const alix = await Client.create(wallet, {
env: 'local',
})
const timestamp = Date.now() + 1
const consentMessage = WalletSigner.consentProofRequestText(
bo.address,
timestamp
)
const signedMessage = await keySigner.wallet.signMessage(consentMessage)
const consentProofPayload = invitation.ConsentProofPayload.fromPartial({
signature: signedMessage,
timestamp,
payloadVersion:
invitation.ConsentProofPayloadVersion.CONSENT_PROOF_PAYLOAD_VERSION_1,
})
const boConvo = await bo.conversations.newConversation(
alixAddress,
undefined,
consentProofPayload
)
const conversations = await alix.conversations.list()
const convo = conversations.find((c) => c.topic === boConvo.topic)
expect(convo).toBeTruthy()
await alix.contacts.refreshConsentList()
const isApproved = await alix.contacts.isAllowed(bob.address)
expect(isApproved).toBeTruthy()
const isAllowed = await alix.contacts.isAllowed(bo.address)
expect(isAllowed).toBeFalsy()

Check failure on line 387 in test/conversations/Conversations.test.ts

View workflow job for this annotation

GitHub Actions / happy-dom

test/conversations/Conversations.test.ts > conversations > newConversation > consent proof correctly validates

AssertionError: expected true to be falsy - Expected + Received - true + false ❯ test/conversations/Conversations.test.ts:387:25

Check failure on line 387 in test/conversations/Conversations.test.ts

View workflow job for this annotation

GitHub Actions / node

test/conversations/Conversations.test.ts > conversations > newConversation > consent proof correctly validates

AssertionError: expected true to be falsy - Expected + Received - true + false ❯ test/conversations/Conversations.test.ts:387:25
await alix.close()
await bo.close()
})
Expand Down

0 comments on commit 22ce2f8

Please sign in to comment.