Skip to content

Commit

Permalink
Merge pull request #271 from xmtp/ar/merge-main-frames-signer
Browse files Browse the repository at this point in the history
Merge main frames signer to Beta
  • Loading branch information
alexrisch authored Feb 20, 2024
2 parents d320c0d + bd22509 commit e32b7bf
Show file tree
Hide file tree
Showing 7 changed files with 186 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import org.xmtp.android.library.messages.InvitationV1ContextBuilder
import org.xmtp.android.library.messages.Pagination
import org.xmtp.android.library.messages.PrivateKeyBuilder
import org.xmtp.android.library.messages.Signature
import org.xmtp.android.library.messages.getPublicKeyBundle
import org.xmtp.android.library.push.XMTPPush
import org.xmtp.android.library.toHex
import org.xmtp.proto.keystore.api.v1.Keystore.TopicMap.TopicData
Expand Down Expand Up @@ -257,6 +258,37 @@ class XMTPModule : Module() {
}
}

AsyncFunction("sign") { clientAddress: String, digest: List<Int>, keyType: String, preKeyIndex: Int ->
logV("sign")
val client = clients[clientAddress] ?: throw XMTPException("No client")
val digestBytes =
digest.foldIndexed(ByteArray(digest.size)) { i, a, v ->
a.apply {
set(
i,
v.toByte()
)
}
}
val privateKeyBundle = client.keys
val signedPrivateKey = if (keyType == "prekey") {
privateKeyBundle.preKeysList[preKeyIndex]
} else {
privateKeyBundle.identityKey
}
val signature = runBlocking {
val privateKey = PrivateKeyBuilder.buildFromSignedPrivateKey(signedPrivateKey)
PrivateKeyBuilder(privateKey).sign(digestBytes)
}
signature.toByteArray().map { it.toInt() and 0xFF }
}

AsyncFunction("exportPublicKeyBundle") { clientAddress: String ->
logV("exportPublicKeyBundle")
val client = clients[clientAddress] ?: throw XMTPException("No client")
client.keys.getPublicKeyBundle().toByteArray().map { it.toInt() and 0xFF }
}

AsyncFunction("exportKeyBundle") { clientAddress: String ->
logV("exportKeyBundle")
val client = clients[clientAddress] ?: throw XMTPException("No client")
Expand Down
1 change: 1 addition & 0 deletions example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"@react-navigation/native-stack": "^6.9.12",
"@thirdweb-dev/react-native": "^0.6.2",
"@thirdweb-dev/react-native-compat": "^0.6.2",
"@xmtp/frames-client": "^0.3.2",
"ethers": "^5.7.2",
"expo": "~48.0.18",
"expo-document-picker": "^11.5.4",
Expand Down
51 changes: 51 additions & 0 deletions example/src/tests.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { FramesClient } from '@xmtp/frames-client'
import { content } from '@xmtp/proto'
import ReactNativeBlobUtil from 'react-native-blob-util'
import { TextEncoder, TextDecoder } from 'text-encoding'
Expand Down Expand Up @@ -793,6 +794,20 @@ test('canMessage', async () => {
return true
})

test('fetch a public key bundle and sign a digest', async () => {
const bob = await Client.createRandom({ env: 'local' })
const bytes = new Uint8Array([1, 2, 3])
const signature = await bob.sign(bytes, { kind: 'identity' })
if (signature.length === 0) {
throw new Error('signature was not returned')
}
const keyBundle = await bob.exportPublicKeyBundle()
if (keyBundle.length === 0) {
throw new Error('key bundle was not returned')
}
return true
})

test('createFromKeyBundle throws error for non string value', async () => {
try {
const bytes = [1, 2, 3]
Expand Down Expand Up @@ -1614,3 +1629,39 @@ test('correctly handles lowercase addresses', async () => {
}
return true
})

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 framesClient = new FramesClient(client)
const metadata = await framesClient.proxy.readMetadata(frameUrl)
if (!metadata) {
throw new Error('metadata should exist')
}
const signedPayload = await framesClient.signFrameAction({
frameUrl,
buttonIndex: 1,
conversationTopic: 'foo',
participantAccountAddresses: ['amal', 'bola'],
})
const postUrl = metadata.extractedTags['fc:frame:post_url']
const response = await framesClient.proxy.post(postUrl, signedPayload)
if (!response) {
throw new Error('response should exist')
}
if (response.extractedTags['fc:frame'] !== 'vNext') {
throw new Error('response should have expected extractedTags')
}
const imageUrl = response.extractedTags['fc:frame:image']
const mediaUrl = framesClient.proxy.mediaUrl(imageUrl)

const downloadedMedia = await fetch(mediaUrl)
if (!downloadedMedia.ok) {
throw new Error('downloadedMedia should be ok')
}
if (downloadedMedia.headers.get('content-type') !== 'image/png') {
throw new Error('downloadedMedia should be image/png')
}
return true
})
42 changes: 40 additions & 2 deletions example/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4632,6 +4632,11 @@
find-up "^5.0.0"
js-yaml "^4.1.0"

"@fastify/busboy@^2.0.0":
version "2.1.0"
resolved "https://registry.yarnpkg.com/@fastify/busboy/-/busboy-2.1.0.tgz#0709e9f4cb252351c609c6e6d8d6779a8d25edff"
integrity sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA==

"@fastify/cookie@^9.1.0":
version "9.3.0"
resolved "https://registry.npmjs.org/@fastify/cookie/-/cookie-9.3.0.tgz"
Expand Down Expand Up @@ -5075,7 +5080,7 @@
resolved "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz"
integrity sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==

"@noble/[email protected]", "@noble/hashes@^1.3.2", "@noble/hashes@~1.3.0", "@noble/hashes@~1.3.1":
"@noble/[email protected]", "@noble/hashes@^1.3.2", "@noble/hashes@^1.3.3", "@noble/hashes@~1.3.0", "@noble/hashes@~1.3.1":
version "1.3.3"
resolved "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz"
integrity sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==
Expand Down Expand Up @@ -6793,6 +6798,25 @@
resolved "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.7.13.tgz"
integrity sha512-lm2GW5PkosIzccsaZIz7tp8cPADSIlIHWDFTR1N0SzfinhhYgeIQjFMz4rYzanCScr3DqQLeomUDArp6MWKm+g==

"@xmtp/frames-client@^0.3.2":
version "0.3.2"
resolved "https://registry.yarnpkg.com/@xmtp/frames-client/-/frames-client-0.3.2.tgz#6c860e11cbf7a63aa956543b4941ee72738ed211"
integrity sha512-61rxA7YcNUUKndQ9e5X44LNVwWJCrrZR6sBGOWLckfMK00LqRasoiLgom1PlR34c17a59PPZmagxoNQ2QCto7A==
dependencies:
"@noble/hashes" "^1.3.3"
"@xmtp/proto" "3.41.0-beta.5"
long "^5.2.3"

"@xmtp/[email protected]":
version "3.41.0-beta.5"
resolved "https://registry.yarnpkg.com/@xmtp/proto/-/proto-3.41.0-beta.5.tgz#fe6d2f4f0a37e69c18c516ed0796a48fb16574db"
integrity sha512-vx5zqLpAVPjTEdyqY/woXrgvWMKjbTwwco+x9WE+T1iVlv+472yp2DwFJRLpfeQByC1cHl7XQyuO2Q+8t8HL4Q==
dependencies:
long "^5.2.0"
protobufjs "^7.0.0"
rxjs "^7.8.0"
undici "^5.8.1"

JSONStream@^1.3.5:
version "1.3.5"
resolved "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz"
Expand Down Expand Up @@ -11470,7 +11494,7 @@ logkitty@^0.7.1:
dayjs "^1.8.15"
yargs "^15.1.0"

long@^5.0.0:
long@^5.0.0, long@^5.2.0, long@^5.2.3:
version "5.2.3"
resolved "https://registry.npmjs.org/long/-/long-5.2.3.tgz"
integrity sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==
Expand Down Expand Up @@ -13925,6 +13949,13 @@ rxjs@^6.6.3:
dependencies:
tslib "^1.9.0"

rxjs@^7.8.0:
version "7.8.1"
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.1.tgz#6f6f3d99ea8044291efd92e7c7fcf562c4057543"
integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==
dependencies:
tslib "^2.1.0"

safe-array-concat@^1.0.1:
version "1.1.0"
resolved "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.0.tgz"
Expand Down Expand Up @@ -15028,6 +15059,13 @@ undici-types@~5.26.4:
resolved "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz"
integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==

undici@^5.8.1:
version "5.28.3"
resolved "https://registry.yarnpkg.com/undici/-/undici-5.28.3.tgz#a731e0eff2c3fcfd41c1169a869062be222d1e5b"
integrity sha512-3ItfzbrhDlINjaP0duwnNsKpDQk3acHI3gVJ1z4fmwMK31k5G9OVIAMLSIaP6w4FaGkaAkN6zaQO9LUvZ1t7VA==
dependencies:
"@fastify/busboy" "^2.0.0"

unenv@^1.8.0:
version "1.9.0"
resolved "https://registry.npmjs.org/unenv/-/unenv-1.9.0.tgz"
Expand Down
21 changes: 21 additions & 0 deletions ios/XMTPModule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,27 @@ public class XMTPModule: Module {
throw error
}
}

AsyncFunction("sign") { (clientAddress: String, digest: [UInt8], keyType: String, preKeyIndex: Int) -> [UInt8] in
guard let client = await clientsManager.getClient(key: clientAddress) else {
throw Error.noClient
}
let privateKeyBundle = client.keys
let key = keyType == "prekey" ? privateKeyBundle.preKeys[preKeyIndex] : privateKeyBundle.identityKey

let privateKey = try PrivateKey(key)
let signature = try await privateKey.sign(Data(digest))
let uint = try [UInt8](signature.serializedData())
return uint
}

AsyncFunction("exportPublicKeyBundle") { (clientAddress: String) -> [UInt8] in
guard let client = await clientsManager.getClient(key: clientAddress) else {
throw Error.noClient
}
let bundle = try client.publicKeyBundle.serializedData()
return Array(bundle)
}

// Export the client's serialized key bundle.
AsyncFunction("exportKeyBundle") { (clientAddress: String) -> String in
Expand Down
23 changes: 23 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,29 @@ export async function removeGroupMembers(
return XMTPModule.removeGroupMembers(clientAddress, id, addresses)
}

export async function sign(
clientAddress: string,
digest: Uint8Array,
keyType: string,
preKeyIndex: number = 0
): Promise<Uint8Array> {
const signatureArray = await XMTPModule.sign(
clientAddress,
Array.from(digest),
keyType,
preKeyIndex
)
return new Uint8Array(signatureArray)
}

export async function exportPublicKeyBundle(
clientAddress: string
): Promise<Uint8Array> {
const publicBundleArray =
await XMTPModule.exportPublicKeyBundle(clientAddress)
return new Uint8Array(publicBundleArray)
}

export async function exportKeyBundle(clientAddress: string): Promise<string> {
return await XMTPModule.exportKeyBundle(clientAddress)
}
Expand Down
18 changes: 18 additions & 0 deletions src/lib/Client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,19 @@ export class Client<
this.codecRegistry[id] = contentCodec
}

async sign(digest: Uint8Array, keyType: KeyType): Promise<Uint8Array> {
return XMTPModule.sign(
this.address,
digest,
keyType.kind,
keyType.prekeyIndex
)
}

async exportPublicKeyBundle(): Promise<Uint8Array> {
return XMTPModule.exportPublicKeyBundle(this.address)
}

/**
* Exports the key bundle associated with the current XMTP address.
*
Expand Down Expand Up @@ -416,6 +429,11 @@ export type ClientOptions = {
enableAlphaMls?: boolean
}

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
*
Expand Down

0 comments on commit e32b7bf

Please sign in to comment.