Skip to content

Commit

Permalink
Merge pull request #232 from xmtp/np/frames-signer
Browse files Browse the repository at this point in the history
Identity signer
  • Loading branch information
alexrisch authored Feb 16, 2024
2 parents 4a4d20f + 9118400 commit 763d037
Show file tree
Hide file tree
Showing 7 changed files with 187 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.suspendCancellableCoroutine
import org.json.JSONObject
import org.xmtp.android.library.Client
Expand All @@ -44,6 +45,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.proto.keystore.api.v1.Keystore.TopicMap.TopicData
import org.xmtp.proto.message.api.v1.MessageApiOuterClass
Expand Down Expand Up @@ -224,6 +226,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 @@ -173,6 +174,20 @@ test('canMessage', async () => {
return canMessage
})

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 @@ -925,3 +940,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.yarnpkg.com/@fastify/cookie/-/cookie-9.3.0.tgz#229d8cfb754a60d815b98215a600a12e7d4b83c1"
Expand Down Expand Up @@ -5075,7 +5080,7 @@
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.1.tgz#8831ef002114670c603c458ab8b11328406953a9"
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.yarnpkg.com/@noble/hashes/-/hashes-1.3.3.tgz#39908da56a4adc270147bb07968bf3b16cfe1699"
integrity sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==
Expand Down Expand Up @@ -6788,6 +6793,25 @@
resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.7.13.tgz#ff34942667a4e19a9f4a0996a76814daac364cf3"
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.yarnpkg.com/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0"
Expand Down Expand Up @@ -11465,7 +11489,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.yarnpkg.com/long/-/long-5.2.3.tgz#a3ba97f3877cf1d778eccbcb048525ebb77499e1"
integrity sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==
Expand Down Expand Up @@ -13920,6 +13944,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.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.1.0.tgz#8d0cae9cb806d6d1c06e08ab13d847293ebe0692"
Expand Down Expand Up @@ -15023,6 +15054,13 @@ undici-types@~5.26.4:
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617"
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.yarnpkg.com/unenv/-/unenv-1.9.0.tgz#469502ae85be1bd3a6aa60f810972b1a904ca312"
Expand Down
21 changes: 21 additions & 0 deletions ios/XMTPModule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,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 @@ -77,6 +77,29 @@ export async function createFromKeyBundle(
)
}

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 @@ -302,6 +302,19 @@ export class Client<ContentTypes> {
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 @@ -408,6 +421,11 @@ export type NetworkOptions = {
appVersion?: string
}

export type KeyType = {
kind: 'identity' | 'prekey'
prekeyIndex?: number
}

export type CallbackOptions = {
preCreateIdentityCallback?: () => Promise<void> | void
preEnableIdentityCallback?: () => Promise<void> | void
Expand Down

0 comments on commit 763d037

Please sign in to comment.