@zkpassport/sdk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +141 -0
- package/dist/constants/index.d.ts +13 -0
- package/dist/constants/index.js +52 -0
- package/dist/encryption.d.ts +7 -0
- package/dist/encryption.js +126 -0
- package/dist/index.d.ts +79 -0
- package/dist/index.js +384 -0
- package/dist/json-rpc.d.ts +6 -0
- package/dist/json-rpc.js +105 -0
- package/dist/logger.d.ts +7 -0
- package/dist/logger.js +72 -0
- package/dist/mobile.d.ts +39 -0
- package/dist/mobile.js +253 -0
- package/dist/types/countries.d.ts +1 -0
- package/dist/types/countries.js +2 -0
- package/dist/types/credentials.d.ts +17 -0
- package/dist/types/credentials.js +2 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/index.js +2 -0
- package/dist/types/json-rpc.d.ts +12 -0
- package/dist/types/json-rpc.js +2 -0
- package/dist/types/query-result.d.ts +46 -0
- package/dist/types/query-result.js +2 -0
- package/dist/websocket.d.ts +2 -0
- package/dist/websocket.js +18 -0
- package/package.json +31 -0
- package/src/circuits/proof_age.json +1 -0
- package/src/constants/index.ts +54 -0
- package/src/encryption.ts +45 -0
- package/src/index.ts +326 -0
- package/src/json-rpc.ts +57 -0
- package/src/logger.ts +44 -0
- package/src/mobile.ts +177 -0
- package/src/types/countries.ts +278 -0
- package/src/types/credentials.ts +40 -0
- package/src/types/index.ts +13 -0
- package/src/types/json-rpc.ts +13 -0
- package/src/types/query-result.ts +49 -0
- package/src/websocket.ts +16 -0
- package/tsconfig.json +20 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import { randomBytes } from 'crypto'
|
|
2
|
+
import { Alpha3Code, getAlpha3Code, registerLocale } from 'i18n-iso-countries'
|
|
3
|
+
import {
|
|
4
|
+
DisclosableIDCredential,
|
|
5
|
+
IDCredential,
|
|
6
|
+
IDCredentialConfig,
|
|
7
|
+
IDCredentialValue,
|
|
8
|
+
NumericalIDCredential,
|
|
9
|
+
} from '@/types/credentials'
|
|
10
|
+
import { ProofResult } from '@/types/query-result'
|
|
11
|
+
import { CountryName } from '@/types/countries'
|
|
12
|
+
//import { UltraHonkBackend, ProofData, CompiledCircuit } from '@noir-lang/backend_barretenberg'
|
|
13
|
+
import { bytesToHex } from '@noble/ciphers/utils'
|
|
14
|
+
import { getWebSocketClient, WebSocketClient } from '@/websocket'
|
|
15
|
+
import { createEncryptedJsonRpcRequest } from '@/json-rpc'
|
|
16
|
+
import { decrypt, generateECDHKeyPair, getSharedSecret } from '@/encryption'
|
|
17
|
+
import { JsonRpcRequest } from '@/types/json-rpc'
|
|
18
|
+
import logger from '@/logger'
|
|
19
|
+
|
|
20
|
+
registerLocale(require('i18n-iso-countries/langs/en.json'))
|
|
21
|
+
|
|
22
|
+
function normalizeCountry(country: CountryName | Alpha3Code) {
|
|
23
|
+
let normalizedCountry: Alpha3Code | undefined
|
|
24
|
+
const alpha3 = getAlpha3Code(country, 'en') as Alpha3Code | undefined
|
|
25
|
+
normalizedCountry = alpha3 || (country as Alpha3Code)
|
|
26
|
+
return normalizedCountry
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function numericalCompare(
|
|
30
|
+
fnName: 'gte' | 'gt' | 'lte' | 'lt',
|
|
31
|
+
key: NumericalIDCredential,
|
|
32
|
+
value: number | Date,
|
|
33
|
+
requestId: string,
|
|
34
|
+
requestIdToConfig: Record<string, Record<string, IDCredentialConfig>>,
|
|
35
|
+
) {
|
|
36
|
+
requestIdToConfig[requestId][key] = {
|
|
37
|
+
...requestIdToConfig[requestId][key],
|
|
38
|
+
[fnName]: value,
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function rangeCompare(
|
|
43
|
+
key: NumericalIDCredential,
|
|
44
|
+
value: [number | Date, number | Date],
|
|
45
|
+
requestId: string,
|
|
46
|
+
requestIdToConfig: Record<string, Record<string, IDCredentialConfig>>,
|
|
47
|
+
) {
|
|
48
|
+
requestIdToConfig[requestId][key] = {
|
|
49
|
+
...requestIdToConfig[requestId][key],
|
|
50
|
+
range: value,
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function generalCompare(
|
|
55
|
+
fnName: 'in' | 'out' | 'eq',
|
|
56
|
+
key: IDCredential,
|
|
57
|
+
value: any,
|
|
58
|
+
requestId: string,
|
|
59
|
+
requestIdToConfig: Record<string, Record<string, IDCredentialConfig>>,
|
|
60
|
+
) {
|
|
61
|
+
requestIdToConfig[requestId][key] = {
|
|
62
|
+
...requestIdToConfig[requestId][key],
|
|
63
|
+
[fnName]: value,
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export * from '@/constants'
|
|
68
|
+
export * from '@/types'
|
|
69
|
+
|
|
70
|
+
export class ZkPassport {
|
|
71
|
+
private domain: string
|
|
72
|
+
private topicToConfig: Record<string, Record<string, IDCredentialConfig>> = {}
|
|
73
|
+
private topicToKeyPair: Record<string, { privateKey: Uint8Array; publicKey: Uint8Array }> = {}
|
|
74
|
+
private topicToWebSocketClient: Record<string, WebSocketClient> = {}
|
|
75
|
+
private topicToSharedSecret: Record<string, Uint8Array> = {}
|
|
76
|
+
private topicToQRCodeScanned: Record<string, boolean> = {}
|
|
77
|
+
|
|
78
|
+
private onQRCodeScannedCallbacks: Record<string, Array<() => void>> = {}
|
|
79
|
+
private onGeneratingProofCallbacks: Record<string, Array<(topic: string) => void>> = {}
|
|
80
|
+
private onBridgeConnectCallbacks: Record<string, Array<() => void>> = {}
|
|
81
|
+
private onProofGeneratedCallbacks: Record<string, Array<(result: ProofResult) => void>> = {}
|
|
82
|
+
private onRejectCallbacks: Record<string, Array<() => void>> = {}
|
|
83
|
+
private onErrorCallbacks: Record<string, Array<(topic: string) => void>> = {}
|
|
84
|
+
private topicToService: Record<string, { name: string; logo: string; purpose: string }> = {}
|
|
85
|
+
|
|
86
|
+
constructor(_domain?: string) {
|
|
87
|
+
if (!_domain && typeof window === 'undefined') {
|
|
88
|
+
throw new Error('Domain argument is required in Node.js environment')
|
|
89
|
+
}
|
|
90
|
+
this.domain = _domain || window.location.hostname
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* @notice Handle an encrypted message.
|
|
95
|
+
* @param request The request.
|
|
96
|
+
* @param outerRequest The outer request.
|
|
97
|
+
*/
|
|
98
|
+
private async handleEncryptedMessage(topic: string, request: JsonRpcRequest, outerRequest: JsonRpcRequest) {
|
|
99
|
+
logger.debug('Received encrypted message:', request)
|
|
100
|
+
if (request.method === 'accept') {
|
|
101
|
+
logger.debug(`User accepted the request and is generating a proof`)
|
|
102
|
+
await Promise.all(this.onGeneratingProofCallbacks[topic].map((callback) => callback(topic)))
|
|
103
|
+
} else if (request.method === 'reject') {
|
|
104
|
+
logger.debug(`User rejected the request`)
|
|
105
|
+
await Promise.all(this.onRejectCallbacks[topic].map((callback) => callback()))
|
|
106
|
+
} else if (request.method === 'done') {
|
|
107
|
+
logger.debug(`User generated proof`)
|
|
108
|
+
await Promise.all(this.onProofGeneratedCallbacks[topic].map((callback) => callback(request.params.result)))
|
|
109
|
+
} else if (request.method === 'error') {
|
|
110
|
+
await Promise.all(this.onErrorCallbacks[topic].map((callback) => callback(request.params.error)))
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private getZkPassportRequest(topic: string) {
|
|
115
|
+
return {
|
|
116
|
+
eq: <T extends IDCredential>(key: T, value: IDCredentialValue<T>) => {
|
|
117
|
+
if (key === 'issuing_country' || key === 'nationality') {
|
|
118
|
+
value = normalizeCountry(value as CountryName) as IDCredentialValue<T>
|
|
119
|
+
}
|
|
120
|
+
generalCompare('eq', key, value, topic, this.topicToConfig)
|
|
121
|
+
return this.getZkPassportRequest(topic)
|
|
122
|
+
},
|
|
123
|
+
gte: <T extends NumericalIDCredential>(key: T, value: IDCredentialValue<T>) => {
|
|
124
|
+
numericalCompare('gte', key, value, topic, this.topicToConfig)
|
|
125
|
+
return this.getZkPassportRequest(topic)
|
|
126
|
+
},
|
|
127
|
+
gt: <T extends NumericalIDCredential>(key: T, value: IDCredentialValue<T>) => {
|
|
128
|
+
numericalCompare('gt', key, value, topic, this.topicToConfig)
|
|
129
|
+
return this.getZkPassportRequest(topic)
|
|
130
|
+
},
|
|
131
|
+
lte: <T extends NumericalIDCredential>(key: T, value: IDCredentialValue<T>) => {
|
|
132
|
+
numericalCompare('lte', key, value, topic, this.topicToConfig)
|
|
133
|
+
return this.getZkPassportRequest(topic)
|
|
134
|
+
},
|
|
135
|
+
lt: <T extends NumericalIDCredential>(key: T, value: IDCredentialValue<T>) => {
|
|
136
|
+
numericalCompare('lt', key, value, topic, this.topicToConfig)
|
|
137
|
+
return this.getZkPassportRequest(topic)
|
|
138
|
+
},
|
|
139
|
+
range: <T extends NumericalIDCredential>(key: T, start: IDCredentialValue<T>, end: IDCredentialValue<T>) => {
|
|
140
|
+
rangeCompare(key, [start, end], topic, this.topicToConfig)
|
|
141
|
+
return this.getZkPassportRequest(topic)
|
|
142
|
+
},
|
|
143
|
+
in: <T extends IDCredential>(key: T, value: IDCredentialValue<T>[]) => {
|
|
144
|
+
if (key === 'issuing_country' || key === 'nationality') {
|
|
145
|
+
value = value.map((v) => normalizeCountry(v as CountryName)) as IDCredentialValue<T>[]
|
|
146
|
+
}
|
|
147
|
+
generalCompare('in', key, value, topic, this.topicToConfig)
|
|
148
|
+
return this.getZkPassportRequest(topic)
|
|
149
|
+
},
|
|
150
|
+
out: <T extends IDCredential>(key: T, value: IDCredentialValue<T>[]) => {
|
|
151
|
+
if (key === 'issuing_country' || key === 'nationality') {
|
|
152
|
+
value = value.map((v) => normalizeCountry(v as CountryName)) as IDCredentialValue<T>[]
|
|
153
|
+
}
|
|
154
|
+
generalCompare('out', key, value, topic, this.topicToConfig)
|
|
155
|
+
return this.getZkPassportRequest(topic)
|
|
156
|
+
},
|
|
157
|
+
disclose: (key: DisclosableIDCredential) => {
|
|
158
|
+
this.topicToConfig[topic][key] = {
|
|
159
|
+
...this.topicToConfig[topic][key],
|
|
160
|
+
disclose: true,
|
|
161
|
+
}
|
|
162
|
+
return this.getZkPassportRequest(topic)
|
|
163
|
+
},
|
|
164
|
+
/*checkAML: (country?: CountryName | Alpha2Code | Alpha3Code) => {
|
|
165
|
+
return this.getZkPassportRequest(topic)
|
|
166
|
+
},*/
|
|
167
|
+
done: () => {
|
|
168
|
+
const base64Config = Buffer.from(JSON.stringify(this.topicToConfig[topic])).toString('base64')
|
|
169
|
+
const base64Service = Buffer.from(JSON.stringify(this.topicToService[topic])).toString('base64')
|
|
170
|
+
const pubkey = bytesToHex(this.topicToKeyPair[topic].publicKey)
|
|
171
|
+
return {
|
|
172
|
+
url: `https://zkpassport.id/r?d=${this.domain}&t=${topic}&c=${base64Config}&s=${base64Service}&p=${pubkey}`,
|
|
173
|
+
requestId: topic,
|
|
174
|
+
onQRCodeScanned: (callback: () => void) => this.onQRCodeScannedCallbacks[topic].push(callback),
|
|
175
|
+
onGeneratingProof: (callback: () => void) => this.onGeneratingProofCallbacks[topic].push(callback),
|
|
176
|
+
onBridgeConnect: (callback: () => void) => this.onBridgeConnectCallbacks[topic].push(callback),
|
|
177
|
+
onProofGenerated: (callback: (result: ProofResult) => void) =>
|
|
178
|
+
this.onProofGeneratedCallbacks[topic].push(callback),
|
|
179
|
+
onReject: (callback: () => void) => this.onRejectCallbacks[topic].push(callback),
|
|
180
|
+
onError: (callback: (error: string) => void) => this.onErrorCallbacks[topic].push(callback),
|
|
181
|
+
isBridgeConnected: () => this.topicToWebSocketClient[topic].readyState === WebSocket.OPEN,
|
|
182
|
+
isQRCodeScanned: () => this.topicToQRCodeScanned[topic] === true,
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* @notice Create a new request.
|
|
190
|
+
* @returns The query builder object.
|
|
191
|
+
*/
|
|
192
|
+
public async request({
|
|
193
|
+
name,
|
|
194
|
+
logo,
|
|
195
|
+
purpose,
|
|
196
|
+
topicOverride,
|
|
197
|
+
keyPairOverride,
|
|
198
|
+
}: {
|
|
199
|
+
name: string
|
|
200
|
+
logo: string
|
|
201
|
+
purpose: string
|
|
202
|
+
topicOverride?: string
|
|
203
|
+
keyPairOverride?: { privateKey: Uint8Array; publicKey: Uint8Array }
|
|
204
|
+
}) {
|
|
205
|
+
const topic = topicOverride || randomBytes(16).toString('hex')
|
|
206
|
+
|
|
207
|
+
const keyPair = keyPairOverride || (await generateECDHKeyPair())
|
|
208
|
+
this.topicToKeyPair[topic] = {
|
|
209
|
+
privateKey: keyPair.privateKey,
|
|
210
|
+
publicKey: keyPair.publicKey,
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
this.topicToConfig[topic] = {}
|
|
214
|
+
this.topicToService[topic] = { name, logo, purpose }
|
|
215
|
+
|
|
216
|
+
this.onQRCodeScannedCallbacks[topic] = []
|
|
217
|
+
this.onGeneratingProofCallbacks[topic] = []
|
|
218
|
+
this.onBridgeConnectCallbacks[topic] = []
|
|
219
|
+
this.onProofGeneratedCallbacks[topic] = []
|
|
220
|
+
this.onRejectCallbacks[topic] = []
|
|
221
|
+
this.onErrorCallbacks[topic] = []
|
|
222
|
+
|
|
223
|
+
const wsClient = getWebSocketClient(`wss://bridge.zkpassport.id?topic=${topic}`, this.domain)
|
|
224
|
+
this.topicToWebSocketClient[topic] = wsClient
|
|
225
|
+
wsClient.onopen = async () => {
|
|
226
|
+
logger.info('[frontend] WebSocket connection established')
|
|
227
|
+
await Promise.all(this.onBridgeConnectCallbacks[topic].map((callback) => callback()))
|
|
228
|
+
}
|
|
229
|
+
wsClient.addEventListener('message', async (event: any) => {
|
|
230
|
+
logger.debug('[frontend] Received message:', event.data)
|
|
231
|
+
try {
|
|
232
|
+
const data: JsonRpcRequest = JSON.parse(event.data)
|
|
233
|
+
// Handshake happens when the mobile app scans the QR code and connects to the bridge
|
|
234
|
+
if (data.method === 'handshake') {
|
|
235
|
+
logger.debug('[frontend] Received handshake:', event.data)
|
|
236
|
+
|
|
237
|
+
this.topicToQRCodeScanned[topic] = true
|
|
238
|
+
this.topicToSharedSecret[topic] = await getSharedSecret(bytesToHex(keyPair.privateKey), data.params.pubkey)
|
|
239
|
+
logger.debug('[frontend] Shared secret:', Buffer.from(this.topicToSharedSecret[topic]).toString('hex'))
|
|
240
|
+
|
|
241
|
+
const encryptedMessage = await createEncryptedJsonRpcRequest(
|
|
242
|
+
'hello',
|
|
243
|
+
null,
|
|
244
|
+
this.topicToSharedSecret[topic],
|
|
245
|
+
topic,
|
|
246
|
+
)
|
|
247
|
+
logger.debug('[frontend] Sending encrypted message:', encryptedMessage)
|
|
248
|
+
wsClient.send(JSON.stringify(encryptedMessage))
|
|
249
|
+
|
|
250
|
+
await Promise.all(this.onQRCodeScannedCallbacks[topic].map((callback) => callback()))
|
|
251
|
+
return
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Handle encrypted messages
|
|
255
|
+
if (data.method === 'encryptedMessage') {
|
|
256
|
+
// Decode the payload from base64 to Uint8Array
|
|
257
|
+
const payload = new Uint8Array(
|
|
258
|
+
atob(data.params.payload)
|
|
259
|
+
.split('')
|
|
260
|
+
.map((c) => c.charCodeAt(0)),
|
|
261
|
+
)
|
|
262
|
+
try {
|
|
263
|
+
// Decrypt the payload using the shared secret
|
|
264
|
+
const decrypted = await decrypt(payload, this.topicToSharedSecret[topic], topic)
|
|
265
|
+
const decryptedJson: JsonRpcRequest = JSON.parse(decrypted)
|
|
266
|
+
this.handleEncryptedMessage(topic, decryptedJson, data)
|
|
267
|
+
} catch (error) {
|
|
268
|
+
logger.error('[frontend] Error decrypting message:', error)
|
|
269
|
+
}
|
|
270
|
+
return
|
|
271
|
+
}
|
|
272
|
+
} catch (error) {
|
|
273
|
+
logger.error('[frontend] Error:', error)
|
|
274
|
+
}
|
|
275
|
+
})
|
|
276
|
+
wsClient.onerror = (error: Event) => {
|
|
277
|
+
logger.error('[frontend] WebSocket error:', error)
|
|
278
|
+
}
|
|
279
|
+
return this.getZkPassportRequest(topic)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* @notice Verifies a proof.
|
|
284
|
+
* @param proof The proof to verify.
|
|
285
|
+
* @returns True if the proof is valid, false otherwise.
|
|
286
|
+
*/
|
|
287
|
+
/*public verify(result: ProofResult) {
|
|
288
|
+
const backend = new UltraHonkBackend(proofOfAgeCircuit as CompiledCircuit)
|
|
289
|
+
const proofData: ProofData = {
|
|
290
|
+
proof: Buffer.from(result.proof as string, 'hex'),
|
|
291
|
+
// TODO: extract the public inputs from the proof
|
|
292
|
+
publicInputs: [],
|
|
293
|
+
}
|
|
294
|
+
return backend.verifyProof(proofData)
|
|
295
|
+
}*/
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* @notice Returns the URL of the request.
|
|
299
|
+
* @param requestId The request ID.
|
|
300
|
+
* @returns The URL of the request.
|
|
301
|
+
*/
|
|
302
|
+
public getUrl(requestId: string) {
|
|
303
|
+
const pubkey = bytesToHex(this.topicToKeyPair[requestId].publicKey)
|
|
304
|
+
const base64Config = Buffer.from(JSON.stringify(this.topicToConfig[requestId])).toString('base64')
|
|
305
|
+
const base64Service = Buffer.from(JSON.stringify(this.topicToService[requestId])).toString('base64')
|
|
306
|
+
return `https://zkpassport.id/r?d=${this.domain}&t=${requestId}&c=${base64Config}&s=${base64Service}&p=${pubkey}`
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* @notice Cancels a request by closing the WebSocket connection and deleting the associated data.
|
|
311
|
+
* @param requestId The request ID.
|
|
312
|
+
*/
|
|
313
|
+
public cancelRequest(requestId: string) {
|
|
314
|
+
this.topicToWebSocketClient[requestId].close()
|
|
315
|
+
delete this.topicToWebSocketClient[requestId]
|
|
316
|
+
delete this.topicToKeyPair[requestId]
|
|
317
|
+
delete this.topicToConfig[requestId]
|
|
318
|
+
delete this.topicToSharedSecret[requestId]
|
|
319
|
+
this.onQRCodeScannedCallbacks[requestId] = []
|
|
320
|
+
this.onGeneratingProofCallbacks[requestId] = []
|
|
321
|
+
this.onBridgeConnectCallbacks[requestId] = []
|
|
322
|
+
this.onProofGeneratedCallbacks[requestId] = []
|
|
323
|
+
this.onRejectCallbacks[requestId] = []
|
|
324
|
+
this.onErrorCallbacks[requestId] = []
|
|
325
|
+
}
|
|
326
|
+
}
|
package/src/json-rpc.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { randomBytes } from 'crypto'
|
|
2
|
+
import { JsonRpcRequest, JsonRpcResponse } from '@/types/json-rpc'
|
|
3
|
+
import { encrypt } from '@/encryption'
|
|
4
|
+
import { WebSocketClient } from '@/websocket'
|
|
5
|
+
import logger from '@/logger'
|
|
6
|
+
|
|
7
|
+
export function createJsonRpcRequest(method: string, params: any): JsonRpcRequest {
|
|
8
|
+
return {
|
|
9
|
+
jsonrpc: '2.0',
|
|
10
|
+
id: randomBytes(16).toString('hex'),
|
|
11
|
+
method,
|
|
12
|
+
params,
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function createEncryptedJsonRpcRequest(
|
|
17
|
+
method: string,
|
|
18
|
+
params: any,
|
|
19
|
+
sharedSecret: Uint8Array,
|
|
20
|
+
topic: string,
|
|
21
|
+
): Promise<JsonRpcRequest> {
|
|
22
|
+
const encryptedMessage = await encrypt(JSON.stringify({ method, params: params || {} }), sharedSecret, topic)
|
|
23
|
+
return createJsonRpcRequest('encryptedMessage', {
|
|
24
|
+
payload: Buffer.from(encryptedMessage).toString('base64'),
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function sendEncryptedJsonRpcRequest(
|
|
29
|
+
method: string,
|
|
30
|
+
params: any,
|
|
31
|
+
sharedSecret: Uint8Array,
|
|
32
|
+
topic: string,
|
|
33
|
+
wsClient: WebSocketClient,
|
|
34
|
+
): Promise<boolean> {
|
|
35
|
+
try {
|
|
36
|
+
const message = { method, params: params || {} }
|
|
37
|
+
const encryptedMessage = await encrypt(JSON.stringify(message), sharedSecret, topic)
|
|
38
|
+
const request = createJsonRpcRequest('encryptedMessage', {
|
|
39
|
+
payload: Buffer.from(encryptedMessage).toString('base64'),
|
|
40
|
+
})
|
|
41
|
+
logger.debug('Sending encrypted message (original):', message)
|
|
42
|
+
logger.debug('Sending encrypted message (encrypted):', request)
|
|
43
|
+
wsClient.send(JSON.stringify(request))
|
|
44
|
+
return true
|
|
45
|
+
} catch (error) {
|
|
46
|
+
logger.error('Error sending encrypted message:', error)
|
|
47
|
+
return false
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function createJsonRpcResponse(id: string, result: any): JsonRpcResponse {
|
|
52
|
+
return {
|
|
53
|
+
jsonrpc: '2.0',
|
|
54
|
+
id,
|
|
55
|
+
result,
|
|
56
|
+
}
|
|
57
|
+
}
|
package/src/logger.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// import { createLogger, transports, format } from 'winston'
|
|
2
|
+
// import colors from 'colors/safe'
|
|
3
|
+
// import util from 'util'
|
|
4
|
+
|
|
5
|
+
// const logger = createLogger({
|
|
6
|
+
// level: 'debug',
|
|
7
|
+
// format: format.combine(
|
|
8
|
+
// format.timestamp({ format: 'HH:mm' }),
|
|
9
|
+
// format.printf(({ timestamp, level, message, additionalInfo }) => {
|
|
10
|
+
// const colorMap = {
|
|
11
|
+
// debug: colors.cyan,
|
|
12
|
+
// info: colors.green,
|
|
13
|
+
// warn: colors.yellow,
|
|
14
|
+
// error: colors.red,
|
|
15
|
+
// } as const
|
|
16
|
+
// const coloredLevel = (colorMap[level as keyof typeof colorMap] || colors.white)(level.toUpperCase())
|
|
17
|
+
|
|
18
|
+
// let logMessage = `${timestamp} [${coloredLevel}] ${message}`
|
|
19
|
+
|
|
20
|
+
// if (additionalInfo && additionalInfo.length > 0) {
|
|
21
|
+
// logMessage += ' ' + util.inspect(additionalInfo, { depth: null, colors: true })
|
|
22
|
+
// }
|
|
23
|
+
|
|
24
|
+
// return logMessage
|
|
25
|
+
// }),
|
|
26
|
+
// ),
|
|
27
|
+
// transports: [new transports.Console()],
|
|
28
|
+
// })
|
|
29
|
+
|
|
30
|
+
// const customLogger = {
|
|
31
|
+
// debug: (message: string, ...args: any[]) => logger.debug(message, { additionalInfo: args }),
|
|
32
|
+
// info: (message: string, ...args: any[]) => logger.info(message, { additionalInfo: args }),
|
|
33
|
+
// warn: (message: string, ...args: any[]) => logger.warn(message, { additionalInfo: args }),
|
|
34
|
+
// error: (message: string, ...args: any[]) => logger.error(message, { additionalInfo: args }),
|
|
35
|
+
// }
|
|
36
|
+
|
|
37
|
+
const customLogger = {
|
|
38
|
+
debug: (message: string, ...args: any[]) => console.debug(message, ...args),
|
|
39
|
+
info: (message: string, ...args: any[]) => console.info(message, ...args),
|
|
40
|
+
warn: (message: string, ...args: any[]) => console.warn(message, ...args),
|
|
41
|
+
error: (message: string, ...args: any[]) => console.error(message, ...args),
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export default customLogger
|
package/src/mobile.ts
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { bytesToHex } from '@noble/ciphers/utils'
|
|
2
|
+
import { getWebSocketClient, WebSocketClient } from '@/websocket'
|
|
3
|
+
import { sendEncryptedJsonRpcRequest } from '@/json-rpc'
|
|
4
|
+
import { decrypt, generateECDHKeyPair, getSharedSecret } from '@/encryption'
|
|
5
|
+
import { JsonRpcRequest } from '@/types/json-rpc'
|
|
6
|
+
import logger from '@/logger'
|
|
7
|
+
|
|
8
|
+
export class ZkPassportProver {
|
|
9
|
+
private domain?: string
|
|
10
|
+
private topicToKeyPair: Record<string, { privateKey: Uint8Array; publicKey: Uint8Array }> = {}
|
|
11
|
+
private topicToWebSocketClient: Record<string, WebSocketClient> = {}
|
|
12
|
+
private topicToRemoteDomainVerified: Record<string, boolean> = {}
|
|
13
|
+
private topicToSharedSecret: Record<string, Uint8Array> = {}
|
|
14
|
+
private topicToRemotePublicKey: Record<string, Uint8Array> = {}
|
|
15
|
+
|
|
16
|
+
private onDomainVerifiedCallbacks: Record<string, Array<() => void>> = {}
|
|
17
|
+
private onBridgeConnectCallbacks: Record<string, Array<() => void>> = {}
|
|
18
|
+
private onWebsiteDomainVerifyFailureCallbacks: Record<string, Array<() => void>> = {}
|
|
19
|
+
|
|
20
|
+
constructor() {}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @notice Handle an encrypted message.
|
|
24
|
+
* @param request The request.
|
|
25
|
+
* @param outerRequest The outer request.
|
|
26
|
+
*/
|
|
27
|
+
private async handleEncryptedMessage(topic: string, request: JsonRpcRequest, outerRequest: JsonRpcRequest) {
|
|
28
|
+
logger.debug('Received encrypted message:', request)
|
|
29
|
+
if (request.method === 'hello') {
|
|
30
|
+
logger.info(`Successfully verified origin domain name: ${outerRequest.origin}`)
|
|
31
|
+
this.topicToRemoteDomainVerified[topic] = true
|
|
32
|
+
await Promise.all(this.onDomainVerifiedCallbacks[topic].map((callback) => callback()))
|
|
33
|
+
} else if (request.method === 'closed_page') {
|
|
34
|
+
// TODO: Implement
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @notice Scan a credentirequest QR code.
|
|
40
|
+
* @returns
|
|
41
|
+
*/
|
|
42
|
+
public async scan(
|
|
43
|
+
url: string,
|
|
44
|
+
{
|
|
45
|
+
keyPairOverride,
|
|
46
|
+
}: {
|
|
47
|
+
keyPairOverride?: { privateKey: Uint8Array; publicKey: Uint8Array }
|
|
48
|
+
} = {},
|
|
49
|
+
) {
|
|
50
|
+
const parsedUrl = new URL(url)
|
|
51
|
+
const domain = parsedUrl.searchParams.get('d')
|
|
52
|
+
const topic = parsedUrl.searchParams.get('t')
|
|
53
|
+
const pubkeyHex = parsedUrl.searchParams.get('p')
|
|
54
|
+
|
|
55
|
+
if (!domain || !topic || !pubkeyHex) {
|
|
56
|
+
throw new Error('Invalid URL: missing required parameters')
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const pubkey = new Uint8Array(Buffer.from(pubkeyHex, 'hex'))
|
|
60
|
+
|
|
61
|
+
this.domain = domain
|
|
62
|
+
const keyPair = keyPairOverride || (await generateECDHKeyPair())
|
|
63
|
+
|
|
64
|
+
this.topicToKeyPair[topic] = {
|
|
65
|
+
privateKey: keyPair.privateKey,
|
|
66
|
+
publicKey: keyPair.publicKey,
|
|
67
|
+
}
|
|
68
|
+
this.topicToRemotePublicKey[topic] = pubkey
|
|
69
|
+
this.topicToSharedSecret[topic] = await getSharedSecret(bytesToHex(keyPair.privateKey), bytesToHex(pubkey))
|
|
70
|
+
this.topicToRemoteDomainVerified[topic] = false
|
|
71
|
+
this.onDomainVerifiedCallbacks[topic] = []
|
|
72
|
+
this.onBridgeConnectCallbacks[topic] = []
|
|
73
|
+
|
|
74
|
+
// Set up WebSocket connection
|
|
75
|
+
const wsClient = getWebSocketClient(
|
|
76
|
+
`wss://bridge.zkpassport.id?topic=${topic}&pubkey=${bytesToHex(keyPair.publicKey)}`,
|
|
77
|
+
)
|
|
78
|
+
this.topicToWebSocketClient[topic] = wsClient
|
|
79
|
+
|
|
80
|
+
wsClient.onopen = async () => {
|
|
81
|
+
logger.info('[mobile] WebSocket connection established')
|
|
82
|
+
await Promise.all(this.onBridgeConnectCallbacks[topic].map((callback) => callback()))
|
|
83
|
+
// Server sends handshake automatically (when it sees a pubkey in websocket URI)
|
|
84
|
+
// wsClient.send(
|
|
85
|
+
// JSON.stringify(
|
|
86
|
+
// createJsonRpcRequest('handshake', {
|
|
87
|
+
// pubkey: bytesToHex(keyPair.publicKey),
|
|
88
|
+
// }),
|
|
89
|
+
// ),
|
|
90
|
+
// )
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
wsClient.addEventListener('message', async (event: any) => {
|
|
94
|
+
logger.info('[mobile] Received message:', event.data)
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
const data: JsonRpcRequest = JSON.parse(event.data)
|
|
98
|
+
const originDomain = data.origin ? new URL(data.origin).hostname : undefined
|
|
99
|
+
// Origin domain must match domain in QR code
|
|
100
|
+
if (originDomain !== this.domain) {
|
|
101
|
+
logger.warn(
|
|
102
|
+
`[mobile] Origin does not match domain in QR code. Expected ${this.domain} but got ${originDomain}`,
|
|
103
|
+
)
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (data.method === 'encryptedMessage') {
|
|
108
|
+
// Decode the payload from base64 to Uint8Array
|
|
109
|
+
const payload = new Uint8Array(
|
|
110
|
+
atob(data.params.payload)
|
|
111
|
+
.split('')
|
|
112
|
+
.map((c) => c.charCodeAt(0)),
|
|
113
|
+
)
|
|
114
|
+
try {
|
|
115
|
+
// Decrypt the payload using the shared secret
|
|
116
|
+
const decrypted = await decrypt(payload, this.topicToSharedSecret[topic], topic)
|
|
117
|
+
const decryptedJson: JsonRpcRequest = JSON.parse(decrypted)
|
|
118
|
+
await this.handleEncryptedMessage(topic, decryptedJson, data)
|
|
119
|
+
} catch (error) {
|
|
120
|
+
logger.error('[mobile] Error decrypting message:', error)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
} catch (error) {
|
|
124
|
+
logger.error('[mobile] Error:', error)
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
wsClient.onerror = (error: Event) => {
|
|
129
|
+
logger.error('[mobile] WebSocket error:', error)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
domain: this.domain,
|
|
134
|
+
requestId: topic,
|
|
135
|
+
isBridgeConnected: () => this.topicToWebSocketClient[topic].readyState === WebSocket.OPEN,
|
|
136
|
+
isDomainVerified: () => this.topicToRemoteDomainVerified[topic] === true,
|
|
137
|
+
onDomainVerified: (callback: () => void) => this.onDomainVerifiedCallbacks[topic].push(callback),
|
|
138
|
+
onBridgeConnect: (callback: () => void) => this.onBridgeConnectCallbacks[topic].push(callback),
|
|
139
|
+
notifyReject: async () => {
|
|
140
|
+
await sendEncryptedJsonRpcRequest(
|
|
141
|
+
'reject',
|
|
142
|
+
null,
|
|
143
|
+
this.topicToSharedSecret[topic],
|
|
144
|
+
topic,
|
|
145
|
+
this.topicToWebSocketClient[topic],
|
|
146
|
+
)
|
|
147
|
+
},
|
|
148
|
+
notifyAccept: async () => {
|
|
149
|
+
await sendEncryptedJsonRpcRequest(
|
|
150
|
+
'accept',
|
|
151
|
+
null,
|
|
152
|
+
this.topicToSharedSecret[topic],
|
|
153
|
+
topic,
|
|
154
|
+
this.topicToWebSocketClient[topic],
|
|
155
|
+
)
|
|
156
|
+
},
|
|
157
|
+
notifyDone: async (proof: any) => {
|
|
158
|
+
await sendEncryptedJsonRpcRequest(
|
|
159
|
+
'done',
|
|
160
|
+
{ proof },
|
|
161
|
+
this.topicToSharedSecret[topic],
|
|
162
|
+
topic,
|
|
163
|
+
this.topicToWebSocketClient[topic],
|
|
164
|
+
)
|
|
165
|
+
},
|
|
166
|
+
notifyError: async (error: string) => {
|
|
167
|
+
await sendEncryptedJsonRpcRequest(
|
|
168
|
+
'error',
|
|
169
|
+
{ error },
|
|
170
|
+
this.topicToSharedSecret[topic],
|
|
171
|
+
topic,
|
|
172
|
+
this.topicToWebSocketClient[topic],
|
|
173
|
+
)
|
|
174
|
+
},
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|