@zkpassport/sdk 0.3.4 → 0.4.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/dist/cjs/assets/abi/ZKPassportVerifier.json +14 -14
- package/dist/cjs/index.d.ts +10 -5
- package/dist/cjs/index.js +77 -104
- package/dist/esm/assets/abi/ZKPassportVerifier.json +14 -14
- package/dist/esm/index.d.ts +10 -5
- package/dist/esm/index.js +78 -105
- package/package.json +3 -2
- package/src/assets/abi/ZKPassportVerifier.json +14 -14
- package/src/index.ts +86 -119
- package/src/encryption.ts +0 -45
- package/src/json-rpc.ts +0 -61
- package/src/mobile.ts +0 -186
- package/src/websocket.ts +0 -16
package/src/index.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { randomBytes } from "crypto"
|
|
2
1
|
import { Alpha3Code, getAlpha3Code, registerLocale } from "i18n-iso-countries"
|
|
3
2
|
import {
|
|
4
3
|
type DisclosableIDCredential,
|
|
@@ -57,11 +56,9 @@ import {
|
|
|
57
56
|
ProofData,
|
|
58
57
|
getScopeFromOuterProof,
|
|
59
58
|
getSubscopeFromOuterProof,
|
|
59
|
+
getServiceScopeHash,
|
|
60
60
|
} from "@zkpassport/utils"
|
|
61
61
|
import { bytesToHex } from "@noble/ciphers/utils"
|
|
62
|
-
import { getWebSocketClient, WebSocketClient } from "./websocket"
|
|
63
|
-
import { createEncryptedJsonRpcRequest } from "./json-rpc"
|
|
64
|
-
import { decrypt, generateECDHKeyPair, getSharedSecret } from "./encryption"
|
|
65
62
|
import { noLogger as logger } from "./logger"
|
|
66
63
|
import { inflate } from "pako"
|
|
67
64
|
import i18en from "i18n-iso-countries/langs/en.json"
|
|
@@ -70,6 +67,7 @@ import { sha256 } from "@noble/hashes/sha2"
|
|
|
70
67
|
import { hexToBytes } from "@noble/hashes/utils"
|
|
71
68
|
import ZKPassportVerifierAbi from "./assets/abi/ZKPassportVerifier.json"
|
|
72
69
|
import { RegistryClient } from "@zkpassport/registry"
|
|
70
|
+
import { Bridge, BridgeInterface } from "@obsidion/bridge"
|
|
73
71
|
|
|
74
72
|
const DEFAULT_DATE_VALUE = new Date(1111, 10, 11)
|
|
75
73
|
|
|
@@ -124,6 +122,24 @@ export type SolidityVerifierParameters = {
|
|
|
124
122
|
|
|
125
123
|
export type EVMChain = "ethereum_sepolia" | "local_anvil"
|
|
126
124
|
|
|
125
|
+
function getChainIdFromEVMChain(chain: EVMChain): number {
|
|
126
|
+
if (chain === "ethereum_sepolia") {
|
|
127
|
+
return 11155111
|
|
128
|
+
} else if (chain === "local_anvil") {
|
|
129
|
+
return 31337
|
|
130
|
+
}
|
|
131
|
+
throw new Error(`Unsupported chain: ${chain}`)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function getEVMChainFromChainId(chainId: number): EVMChain {
|
|
135
|
+
if (chainId === 11155111) {
|
|
136
|
+
return "ethereum_sepolia"
|
|
137
|
+
} else if (chainId === 31337) {
|
|
138
|
+
return "local_anvil"
|
|
139
|
+
}
|
|
140
|
+
throw new Error(`Unsupported chain ID: ${chainId}`)
|
|
141
|
+
}
|
|
142
|
+
|
|
127
143
|
registerLocale(i18en)
|
|
128
144
|
|
|
129
145
|
function hasRequestedAccessToField(credentialsRequest: Query, field: IDCredential): boolean {
|
|
@@ -351,13 +367,12 @@ export class ZKPassport {
|
|
|
351
367
|
devMode: boolean
|
|
352
368
|
}
|
|
353
369
|
> = {}
|
|
354
|
-
private
|
|
355
|
-
private
|
|
356
|
-
private topicToSharedSecret: Record<string, Uint8Array> = {}
|
|
370
|
+
private topicToPublicKey: Record<string, string> = {}
|
|
371
|
+
private topicToBridge: Record<string, BridgeInterface> = {}
|
|
357
372
|
private topicToRequestReceived: Record<string, boolean> = {}
|
|
358
373
|
private topicToService: Record<
|
|
359
374
|
string,
|
|
360
|
-
{ name: string; logo: string; purpose: string; scope?: string }
|
|
375
|
+
{ name: string; logo: string; purpose: string; scope?: string; chainId?: number }
|
|
361
376
|
> = {}
|
|
362
377
|
private topicToProofs: Record<string, Array<ProofResult>> = {}
|
|
363
378
|
private topicToExpectedProofCount: Record<string, number> = {}
|
|
@@ -400,6 +415,9 @@ export class ZKPassport {
|
|
|
400
415
|
queryResult: result,
|
|
401
416
|
validity: this.topicToLocalConfig[topic]?.validity,
|
|
402
417
|
scope: this.topicToService[topic]?.scope,
|
|
418
|
+
evmChain: this.topicToService[topic]?.chainId
|
|
419
|
+
? getEVMChainFromChainId(this.topicToService[topic]?.chainId)
|
|
420
|
+
: undefined,
|
|
403
421
|
devMode: this.topicToLocalConfig[topic]?.devMode,
|
|
404
422
|
})
|
|
405
423
|
delete this.topicToProofs[topic]
|
|
@@ -498,11 +516,7 @@ export class ZKPassport {
|
|
|
498
516
|
* @param request The request.
|
|
499
517
|
* @param outerRequest The outer request.
|
|
500
518
|
*/
|
|
501
|
-
private async handleEncryptedMessage(
|
|
502
|
-
topic: string,
|
|
503
|
-
request: JsonRpcRequest,
|
|
504
|
-
outerRequest: JsonRpcRequest,
|
|
505
|
-
) {
|
|
519
|
+
private async handleEncryptedMessage(topic: string, request: JsonRpcRequest) {
|
|
506
520
|
logger.debug("Received encrypted message:", request)
|
|
507
521
|
if (request.method === "accept") {
|
|
508
522
|
logger.debug(`User accepted the request and is generating a proof`)
|
|
@@ -512,31 +526,9 @@ export class ZKPassport {
|
|
|
512
526
|
await Promise.all(this.onRejectCallbacks[topic].map((callback) => callback()))
|
|
513
527
|
} else if (request.method === "proof") {
|
|
514
528
|
logger.debug(`User generated proof`)
|
|
515
|
-
|
|
516
|
-
const bytesProof = Buffer.from(request.params.proof, "base64")
|
|
517
|
-
const bytesCommittedInputs = request.params.committedInputs
|
|
518
|
-
? Buffer.from(request.params.committedInputs, "base64")
|
|
519
|
-
: null
|
|
520
|
-
const uncompressedProof = inflate(bytesProof)
|
|
521
|
-
const uncompressedCommittedInputs = bytesCommittedInputs
|
|
522
|
-
? inflate(bytesCommittedInputs)
|
|
523
|
-
: null
|
|
524
|
-
// The gzip lib in the app compress the proof as ASCII
|
|
525
|
-
// and since the app passes the proof as a hex string, we can
|
|
526
|
-
// just decode the bytes as hex characters using the TextDecoder
|
|
527
|
-
const hexProof = new TextDecoder().decode(uncompressedProof)
|
|
528
|
-
const processedProof: ProofResult = {
|
|
529
|
-
proof: hexProof,
|
|
530
|
-
vkeyHash: request.params.vkeyHash,
|
|
531
|
-
name: request.params.name,
|
|
532
|
-
version: request.params.version,
|
|
533
|
-
committedInputs: uncompressedCommittedInputs
|
|
534
|
-
? JSON.parse(new TextDecoder().decode(uncompressedCommittedInputs))
|
|
535
|
-
: undefined,
|
|
536
|
-
}
|
|
537
|
-
this.topicToProofs[topic].push(processedProof)
|
|
529
|
+
this.topicToProofs[topic].push(request.params)
|
|
538
530
|
await Promise.all(
|
|
539
|
-
this.onProofGeneratedCallbacks[topic].map((callback) => callback(
|
|
531
|
+
this.onProofGeneratedCallbacks[topic].map((callback) => callback(request.params)),
|
|
540
532
|
)
|
|
541
533
|
// If the results were received before all the proofs were generated,
|
|
542
534
|
// we can handle the result now
|
|
@@ -651,7 +643,7 @@ export class ZKPassport {
|
|
|
651
643
|
const base64Service = Buffer.from(JSON.stringify(this.topicToService[topic])).toString(
|
|
652
644
|
"base64",
|
|
653
645
|
)
|
|
654
|
-
const pubkey =
|
|
646
|
+
const pubkey = this.topicToPublicKey[topic]
|
|
655
647
|
this.setExpectedProofCount(topic)
|
|
656
648
|
return {
|
|
657
649
|
url: `https://zkpassport.id/r?d=${this.domain}&t=${topic}&c=${base64Config}&s=${base64Service}&p=${pubkey}&m=${this.topicToLocalConfig[topic].mode}`,
|
|
@@ -675,7 +667,7 @@ export class ZKPassport {
|
|
|
675
667
|
onReject: (callback: () => void) => this.onRejectCallbacks[topic].push(callback),
|
|
676
668
|
onError: (callback: (error: string) => void) =>
|
|
677
669
|
this.onErrorCallbacks[topic].push(callback),
|
|
678
|
-
isBridgeConnected: () => this.
|
|
670
|
+
isBridgeConnected: () => this.topicToBridge[topic].isBridgeConnected(),
|
|
679
671
|
requestReceived: () => this.topicToRequestReceived[topic] === true,
|
|
680
672
|
}
|
|
681
673
|
},
|
|
@@ -690,6 +682,7 @@ export class ZKPassport {
|
|
|
690
682
|
* @param scope Scope this request to a specific use case
|
|
691
683
|
* @param validity How many days ago should have the ID been last scanned by the user?
|
|
692
684
|
* @param devMode Whether to enable dev mode. This will allow you to verify mock proofs (i.e. from ZKR)
|
|
685
|
+
* @param evmChain The EVM chain to use for the request (if using the proof onchain)
|
|
693
686
|
* @returns The query builder object.
|
|
694
687
|
*/
|
|
695
688
|
public async request({
|
|
@@ -698,6 +691,7 @@ export class ZKPassport {
|
|
|
698
691
|
purpose,
|
|
699
692
|
scope,
|
|
700
693
|
mode,
|
|
694
|
+
evmChain,
|
|
701
695
|
validity,
|
|
702
696
|
devMode,
|
|
703
697
|
topicOverride,
|
|
@@ -708,21 +702,27 @@ export class ZKPassport {
|
|
|
708
702
|
purpose: string
|
|
709
703
|
scope?: string
|
|
710
704
|
mode?: ProofMode
|
|
705
|
+
evmChain?: EVMChain
|
|
711
706
|
validity?: number
|
|
712
707
|
devMode?: boolean
|
|
713
708
|
topicOverride?: string
|
|
714
709
|
keyPairOverride?: { privateKey: Uint8Array; publicKey: Uint8Array }
|
|
715
710
|
}): Promise<QueryBuilder> {
|
|
716
|
-
const
|
|
711
|
+
const bridge = await Bridge.create({
|
|
712
|
+
keyPair: keyPairOverride,
|
|
713
|
+
bridgeId: topicOverride,
|
|
714
|
+
})
|
|
717
715
|
|
|
718
|
-
const
|
|
719
|
-
this.topicToKeyPair[topic] = {
|
|
720
|
-
privateKey: keyPair.privateKey,
|
|
721
|
-
publicKey: keyPair.publicKey,
|
|
722
|
-
}
|
|
716
|
+
const topic = bridge.connection.getBridgeId()
|
|
723
717
|
|
|
724
718
|
this.topicToConfig[topic] = {}
|
|
725
|
-
this.topicToService[topic] = {
|
|
719
|
+
this.topicToService[topic] = {
|
|
720
|
+
name,
|
|
721
|
+
logo,
|
|
722
|
+
purpose,
|
|
723
|
+
scope,
|
|
724
|
+
chainId: evmChain ? getChainIdFromEVMChain(evmChain) : undefined,
|
|
725
|
+
}
|
|
726
726
|
this.topicToProofs[topic] = []
|
|
727
727
|
this.topicToExpectedProofCount[topic] = 0
|
|
728
728
|
this.topicToLocalConfig[topic] = {
|
|
@@ -740,68 +740,22 @@ export class ZKPassport {
|
|
|
740
740
|
this.onRejectCallbacks[topic] = []
|
|
741
741
|
this.onErrorCallbacks[topic] = []
|
|
742
742
|
|
|
743
|
-
|
|
744
|
-
this.topicToWebSocketClient[topic] = wsClient
|
|
745
|
-
wsClient.onopen = async () => {
|
|
746
|
-
logger.info("[frontend] WebSocket connection established")
|
|
747
|
-
await Promise.all(this.onBridgeConnectCallbacks[topic].map((callback) => callback()))
|
|
748
|
-
}
|
|
749
|
-
wsClient.addEventListener("message", async (event: any) => {
|
|
750
|
-
logger.debug("[frontend] Received message:", event.data)
|
|
751
|
-
try {
|
|
752
|
-
const data: JsonRpcRequest = JSON.parse(event.data)
|
|
753
|
-
// Handshake happens when the mobile app scans the QR code and connects to the bridge
|
|
754
|
-
if (data.method === "handshake") {
|
|
755
|
-
logger.debug("[frontend] Received handshake:", event.data)
|
|
743
|
+
this.topicToPublicKey[topic] = bridge.getPublicKey()
|
|
756
744
|
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
this.topicToSharedSecret[topic],
|
|
771
|
-
topic,
|
|
772
|
-
)
|
|
773
|
-
logger.debug("[frontend] Sending encrypted message:", encryptedMessage)
|
|
774
|
-
wsClient.send(JSON.stringify(encryptedMessage))
|
|
775
|
-
|
|
776
|
-
await Promise.all(this.onRequestReceivedCallbacks[topic].map((callback) => callback()))
|
|
777
|
-
return
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
// Handle encrypted messages
|
|
781
|
-
if (data.method === "encryptedMessage") {
|
|
782
|
-
// Decode the payload from base64 to Uint8Array
|
|
783
|
-
const payload = new Uint8Array(
|
|
784
|
-
atob(data.params.payload)
|
|
785
|
-
.split("")
|
|
786
|
-
.map((c) => c.charCodeAt(0)),
|
|
787
|
-
)
|
|
788
|
-
try {
|
|
789
|
-
// Decrypt the payload using the shared secret
|
|
790
|
-
const decrypted = await decrypt(payload, this.topicToSharedSecret[topic], topic)
|
|
791
|
-
const decryptedJson: JsonRpcRequest = JSON.parse(decrypted)
|
|
792
|
-
this.handleEncryptedMessage(topic, decryptedJson, data)
|
|
793
|
-
} catch (error) {
|
|
794
|
-
logger.error("[frontend] Error decrypting message:", error)
|
|
795
|
-
}
|
|
796
|
-
return
|
|
797
|
-
}
|
|
798
|
-
} catch (error) {
|
|
799
|
-
logger.error("[frontend] Error:", error)
|
|
800
|
-
}
|
|
745
|
+
this.topicToBridge[topic] = bridge
|
|
746
|
+
bridge.onConnect(async (reconnection: boolean) => {
|
|
747
|
+
logger.debug("Bridge connected")
|
|
748
|
+
logger.debug("Is reconnection:", reconnection)
|
|
749
|
+
await Promise.all(this.onBridgeConnectCallbacks[topic].map((callback) => callback()))
|
|
750
|
+
})
|
|
751
|
+
bridge.onSecureChannelEstablished(async () => {
|
|
752
|
+
logger.debug("Secure channel established")
|
|
753
|
+
await Promise.all(this.onRequestReceivedCallbacks[topic].map((callback) => callback()))
|
|
754
|
+
})
|
|
755
|
+
bridge.onSecureMessage(async (message: any) => {
|
|
756
|
+
logger.debug("Received message:", message)
|
|
757
|
+
this.handleEncryptedMessage(topic, message)
|
|
801
758
|
})
|
|
802
|
-
wsClient.onerror = (error: Event) => {
|
|
803
|
-
logger.error("[frontend] WebSocket error:", error)
|
|
804
|
-
}
|
|
805
759
|
return this.getZkPassportRequest(topic)
|
|
806
760
|
}
|
|
807
761
|
|
|
@@ -1780,13 +1734,17 @@ export class ZKPassport {
|
|
|
1780
1734
|
queryResultErrors: QueryResultErrors,
|
|
1781
1735
|
key: string,
|
|
1782
1736
|
scope?: string,
|
|
1737
|
+
chainId?: number,
|
|
1783
1738
|
) {
|
|
1784
1739
|
let isCorrect = true
|
|
1785
|
-
if (
|
|
1740
|
+
if (
|
|
1741
|
+
this.domain &&
|
|
1742
|
+
getServiceScopeHash(this.domain, chainId) !== BigInt(proofData.publicInputs[1])
|
|
1743
|
+
) {
|
|
1786
1744
|
console.warn("The proof comes from a different domain than the one expected")
|
|
1787
1745
|
isCorrect = false
|
|
1788
1746
|
queryResultErrors[key as keyof QueryResultErrors].scope = {
|
|
1789
|
-
expected: `Scope: ${
|
|
1747
|
+
expected: `Scope: ${getServiceScopeHash(this.domain, chainId).toString()}`,
|
|
1790
1748
|
received: `Scope: ${BigInt(proofData.publicInputs[1]).toString()}`,
|
|
1791
1749
|
message: "The proof comes from a different domain than the one expected",
|
|
1792
1750
|
}
|
|
@@ -1840,6 +1798,7 @@ export class ZKPassport {
|
|
|
1840
1798
|
queryResult: QueryResult,
|
|
1841
1799
|
validity?: number,
|
|
1842
1800
|
scope?: string,
|
|
1801
|
+
chainId?: number,
|
|
1843
1802
|
) {
|
|
1844
1803
|
let commitmentIn: bigint | undefined
|
|
1845
1804
|
let commitmentOut: bigint | undefined
|
|
@@ -1945,11 +1904,14 @@ export class ZKPassport {
|
|
|
1945
1904
|
message: "The proof does not verify all the requested conditions and information",
|
|
1946
1905
|
}
|
|
1947
1906
|
}
|
|
1948
|
-
if (
|
|
1907
|
+
if (
|
|
1908
|
+
this.domain &&
|
|
1909
|
+
getServiceScopeHash(this.domain, chainId) !== getScopeFromOuterProof(proofData)
|
|
1910
|
+
) {
|
|
1949
1911
|
console.warn("The proof comes from a different domain than the one expected")
|
|
1950
1912
|
isCorrect = false
|
|
1951
1913
|
queryResultErrors.outer.scope = {
|
|
1952
|
-
expected: `Scope: ${
|
|
1914
|
+
expected: `Scope: ${getServiceScopeHash(this.domain, chainId).toString()}`,
|
|
1953
1915
|
received: `Scope: ${getScopeFromOuterProof(proofData).toString()}`,
|
|
1954
1916
|
message: "The proof comes from a different domain than the one expected",
|
|
1955
1917
|
}
|
|
@@ -2646,6 +2608,9 @@ export class ZKPassport {
|
|
|
2646
2608
|
* @param proofs The proofs to verify.
|
|
2647
2609
|
* @param queryResult The query result to verify against
|
|
2648
2610
|
* @param validity How many days ago should have the ID been last scanned by the user?
|
|
2611
|
+
* @param scope Scope this request to a specific use case
|
|
2612
|
+
* @param evmChain The EVM chain to use for the verification (if using the proof onchain)
|
|
2613
|
+
* @param devMode Whether to enable dev mode. This will allow you to verify mock proofs (i.e. from ZKR)
|
|
2649
2614
|
* @returns An object containing the unique identifier associated to the user
|
|
2650
2615
|
* and a boolean indicating whether the proofs were successfully verified.
|
|
2651
2616
|
*/
|
|
@@ -2654,12 +2619,14 @@ export class ZKPassport {
|
|
|
2654
2619
|
queryResult,
|
|
2655
2620
|
validity,
|
|
2656
2621
|
scope,
|
|
2622
|
+
evmChain,
|
|
2657
2623
|
devMode = false,
|
|
2658
2624
|
}: {
|
|
2659
2625
|
proofs: Array<ProofResult>
|
|
2660
2626
|
queryResult: QueryResult
|
|
2661
2627
|
validity?: number
|
|
2662
2628
|
scope?: string
|
|
2629
|
+
evmChain?: EVMChain
|
|
2663
2630
|
devMode?: boolean
|
|
2664
2631
|
}): Promise<{
|
|
2665
2632
|
uniqueIdentifier: string | undefined
|
|
@@ -2692,11 +2659,12 @@ export class ZKPassport {
|
|
|
2692
2659
|
let verified = true
|
|
2693
2660
|
let uniqueIdentifier: string | undefined
|
|
2694
2661
|
let queryResultErrors: QueryResultErrors | undefined
|
|
2662
|
+
const chainId = evmChain ? getChainIdFromEVMChain(evmChain) : undefined
|
|
2695
2663
|
const {
|
|
2696
2664
|
isCorrect,
|
|
2697
2665
|
uniqueIdentifier: uniqueIdentifierFromPublicInputs,
|
|
2698
2666
|
queryResultErrors: queryResultErrorsFromPublicInputs,
|
|
2699
|
-
} = await this.checkPublicInputs(proofs, formattedResult, validity, scope)
|
|
2667
|
+
} = await this.checkPublicInputs(proofs, formattedResult, validity, scope, chainId)
|
|
2700
2668
|
uniqueIdentifier = uniqueIdentifierFromPublicInputs
|
|
2701
2669
|
verified = isCorrect
|
|
2702
2670
|
queryResultErrors = isCorrect ? undefined : queryResultErrorsFromPublicInputs
|
|
@@ -2790,7 +2758,7 @@ export class ZKPassport {
|
|
|
2790
2758
|
if (network === "ethereum_sepolia") {
|
|
2791
2759
|
return {
|
|
2792
2760
|
...baseConfig,
|
|
2793
|
-
address: "
|
|
2761
|
+
address: "0xDfE02DFd5c208854884B58bFf6522De5c42F73E3",
|
|
2794
2762
|
}
|
|
2795
2763
|
} else if (network === "local_anvil") {
|
|
2796
2764
|
return {
|
|
@@ -2959,7 +2927,7 @@ export class ZKPassport {
|
|
|
2959
2927
|
* @returns The URL of the request.
|
|
2960
2928
|
*/
|
|
2961
2929
|
public getUrl(requestId: string) {
|
|
2962
|
-
const pubkey =
|
|
2930
|
+
const pubkey = this.topicToPublicKey[requestId]
|
|
2963
2931
|
const base64Config = Buffer.from(JSON.stringify(this.topicToConfig[requestId])).toString(
|
|
2964
2932
|
"base64",
|
|
2965
2933
|
)
|
|
@@ -2974,14 +2942,13 @@ export class ZKPassport {
|
|
|
2974
2942
|
* @param requestId The request ID.
|
|
2975
2943
|
*/
|
|
2976
2944
|
public cancelRequest(requestId: string) {
|
|
2977
|
-
if (this.
|
|
2978
|
-
this.
|
|
2979
|
-
delete this.
|
|
2945
|
+
if (this.topicToBridge[requestId]) {
|
|
2946
|
+
this.topicToBridge[requestId].close()
|
|
2947
|
+
delete this.topicToBridge[requestId]
|
|
2980
2948
|
}
|
|
2981
|
-
delete this.
|
|
2949
|
+
delete this.topicToPublicKey[requestId]
|
|
2982
2950
|
delete this.topicToConfig[requestId]
|
|
2983
2951
|
delete this.topicToLocalConfig[requestId]
|
|
2984
|
-
delete this.topicToSharedSecret[requestId]
|
|
2985
2952
|
delete this.topicToProofs[requestId]
|
|
2986
2953
|
delete this.topicToExpectedProofCount[requestId]
|
|
2987
2954
|
delete this.topicToFailedProofCount[requestId]
|
|
@@ -2998,7 +2965,7 @@ export class ZKPassport {
|
|
|
2998
2965
|
* @notice Clears all requests.
|
|
2999
2966
|
*/
|
|
3000
2967
|
public clearAllRequests() {
|
|
3001
|
-
for (const requestId in this.
|
|
2968
|
+
for (const requestId in this.topicToBridge) {
|
|
3002
2969
|
this.cancelRequest(requestId)
|
|
3003
2970
|
}
|
|
3004
2971
|
}
|
package/src/encryption.ts
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import { gcm } from "@noble/ciphers/aes"
|
|
2
|
-
import { utf8ToBytes } from "@noble/ciphers/utils"
|
|
3
|
-
|
|
4
|
-
async function sha256Truncate(topic: string): Promise<Uint8Array> {
|
|
5
|
-
const encoder = new TextEncoder()
|
|
6
|
-
const data = encoder.encode(topic)
|
|
7
|
-
const hashBuffer = await crypto.subtle.digest("SHA-256", data)
|
|
8
|
-
const fullHashArray = new Uint8Array(hashBuffer)
|
|
9
|
-
const truncatedHashArray = fullHashArray.slice(0, 12)
|
|
10
|
-
return truncatedHashArray
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export async function generateECDHKeyPair() {
|
|
14
|
-
const secp256k1 = await import("@noble/secp256k1")
|
|
15
|
-
const privKey = secp256k1.utils.randomPrivateKey()
|
|
16
|
-
const pubKey = secp256k1.getPublicKey(privKey)
|
|
17
|
-
return {
|
|
18
|
-
privateKey: privKey,
|
|
19
|
-
publicKey: pubKey,
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export async function getSharedSecret(privateKey: string, publicKey: string) {
|
|
24
|
-
const secp256k1 = await import("@noble/secp256k1")
|
|
25
|
-
const sharedSecret = secp256k1.getSharedSecret(privateKey, publicKey)
|
|
26
|
-
return sharedSecret.slice(0, 32)
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export async function encrypt(message: string, sharedSecret: Uint8Array, topic: string) {
|
|
30
|
-
// Nonce must be 12 bytes
|
|
31
|
-
const nonce = await sha256Truncate(topic)
|
|
32
|
-
const aes = gcm(sharedSecret, nonce)
|
|
33
|
-
const data = utf8ToBytes(message)
|
|
34
|
-
const ciphertext = aes.encrypt(data)
|
|
35
|
-
return ciphertext
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export async function decrypt(ciphertext: Uint8Array, sharedSecret: Uint8Array, topic: string) {
|
|
39
|
-
// Nonce must be 12 bytes
|
|
40
|
-
const nonce = await sha256Truncate(topic)
|
|
41
|
-
const aes = gcm(sharedSecret, nonce)
|
|
42
|
-
const data = aes.decrypt(ciphertext)
|
|
43
|
-
const dataString = new TextDecoder().decode(data)
|
|
44
|
-
return dataString
|
|
45
|
-
}
|
package/src/json-rpc.ts
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import { randomBytes } from "crypto"
|
|
2
|
-
import type { JsonRpcRequest, JsonRpcResponse } from "@zkpassport/utils"
|
|
3
|
-
import { encrypt } from "./encryption"
|
|
4
|
-
import { WebSocketClient } from "./websocket"
|
|
5
|
-
import { noLogger as 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(
|
|
23
|
-
JSON.stringify({ method, params: params || {} }),
|
|
24
|
-
sharedSecret,
|
|
25
|
-
topic,
|
|
26
|
-
)
|
|
27
|
-
return createJsonRpcRequest("encryptedMessage", {
|
|
28
|
-
payload: Buffer.from(encryptedMessage).toString("base64"),
|
|
29
|
-
})
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export async function sendEncryptedJsonRpcRequest(
|
|
33
|
-
method: string,
|
|
34
|
-
params: any,
|
|
35
|
-
sharedSecret: Uint8Array,
|
|
36
|
-
topic: string,
|
|
37
|
-
wsClient: WebSocketClient,
|
|
38
|
-
): Promise<boolean> {
|
|
39
|
-
try {
|
|
40
|
-
const message = { method, params: params || {} }
|
|
41
|
-
const encryptedMessage = await encrypt(JSON.stringify(message), sharedSecret, topic)
|
|
42
|
-
const request = createJsonRpcRequest("encryptedMessage", {
|
|
43
|
-
payload: Buffer.from(encryptedMessage).toString("base64"),
|
|
44
|
-
})
|
|
45
|
-
logger.debug("Sending encrypted message (original):", message)
|
|
46
|
-
logger.debug("Sending encrypted message (encrypted):", request)
|
|
47
|
-
wsClient.send(JSON.stringify(request))
|
|
48
|
-
return true
|
|
49
|
-
} catch (error) {
|
|
50
|
-
logger.error("Error sending encrypted message:", error)
|
|
51
|
-
return false
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function createJsonRpcResponse(id: string, result: any): JsonRpcResponse {
|
|
56
|
-
return {
|
|
57
|
-
jsonrpc: "2.0",
|
|
58
|
-
id,
|
|
59
|
-
result,
|
|
60
|
-
}
|
|
61
|
-
}
|
package/src/mobile.ts
DELETED
|
@@ -1,186 +0,0 @@
|
|
|
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 type { JsonRpcRequest } from "@zkpassport/utils"
|
|
6
|
-
import { noLogger as 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(
|
|
28
|
-
topic: string,
|
|
29
|
-
request: JsonRpcRequest,
|
|
30
|
-
outerRequest: JsonRpcRequest,
|
|
31
|
-
) {
|
|
32
|
-
logger.debug("Received encrypted message:", request)
|
|
33
|
-
if (request.method === "hello") {
|
|
34
|
-
logger.info(`Successfully verified origin domain name: ${outerRequest.origin}`)
|
|
35
|
-
this.topicToRemoteDomainVerified[topic] = true
|
|
36
|
-
await Promise.all(this.onDomainVerifiedCallbacks[topic].map((callback) => callback()))
|
|
37
|
-
} else if (request.method === "closed_page") {
|
|
38
|
-
// TODO: Implement
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* @notice Scan a credentirequest QR code.
|
|
44
|
-
* @returns
|
|
45
|
-
*/
|
|
46
|
-
public async scan(
|
|
47
|
-
url: string,
|
|
48
|
-
{
|
|
49
|
-
keyPairOverride,
|
|
50
|
-
}: {
|
|
51
|
-
keyPairOverride?: { privateKey: Uint8Array; publicKey: Uint8Array }
|
|
52
|
-
} = {},
|
|
53
|
-
) {
|
|
54
|
-
const parsedUrl = new URL(url)
|
|
55
|
-
const domain = parsedUrl.searchParams.get("d")
|
|
56
|
-
const topic = parsedUrl.searchParams.get("t")
|
|
57
|
-
const pubkeyHex = parsedUrl.searchParams.get("p")
|
|
58
|
-
|
|
59
|
-
if (!domain || !topic || !pubkeyHex) {
|
|
60
|
-
throw new Error("Invalid URL: missing required parameters")
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const pubkey = new Uint8Array(Buffer.from(pubkeyHex, "hex"))
|
|
64
|
-
|
|
65
|
-
this.domain = domain
|
|
66
|
-
const keyPair = keyPairOverride || (await generateECDHKeyPair())
|
|
67
|
-
|
|
68
|
-
this.topicToKeyPair[topic] = {
|
|
69
|
-
privateKey: keyPair.privateKey,
|
|
70
|
-
publicKey: keyPair.publicKey,
|
|
71
|
-
}
|
|
72
|
-
this.topicToRemotePublicKey[topic] = pubkey
|
|
73
|
-
this.topicToSharedSecret[topic] = await getSharedSecret(
|
|
74
|
-
bytesToHex(keyPair.privateKey),
|
|
75
|
-
bytesToHex(pubkey),
|
|
76
|
-
)
|
|
77
|
-
this.topicToRemoteDomainVerified[topic] = false
|
|
78
|
-
this.onDomainVerifiedCallbacks[topic] = []
|
|
79
|
-
this.onBridgeConnectCallbacks[topic] = []
|
|
80
|
-
|
|
81
|
-
// Set up WebSocket connection
|
|
82
|
-
const wsClient = getWebSocketClient(
|
|
83
|
-
`wss://bridge.zkpassport.id?topic=${topic}&pubkey=${bytesToHex(keyPair.publicKey)}`,
|
|
84
|
-
)
|
|
85
|
-
this.topicToWebSocketClient[topic] = wsClient
|
|
86
|
-
|
|
87
|
-
wsClient.onopen = async () => {
|
|
88
|
-
logger.info("[mobile] WebSocket connection established")
|
|
89
|
-
await Promise.all(this.onBridgeConnectCallbacks[topic].map((callback) => callback()))
|
|
90
|
-
// Server sends handshake automatically (when it sees a pubkey in websocket URI)
|
|
91
|
-
// wsClient.send(
|
|
92
|
-
// JSON.stringify(
|
|
93
|
-
// createJsonRpcRequest('handshake', {
|
|
94
|
-
// pubkey: bytesToHex(keyPair.publicKey),
|
|
95
|
-
// }),
|
|
96
|
-
// ),
|
|
97
|
-
// )
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
wsClient.addEventListener("message", async (event: any) => {
|
|
101
|
-
logger.info("[mobile] Received message:", event.data)
|
|
102
|
-
|
|
103
|
-
try {
|
|
104
|
-
const data: JsonRpcRequest = JSON.parse(event.data)
|
|
105
|
-
const originDomain = data.origin ? new URL(data.origin).hostname : undefined
|
|
106
|
-
// Origin domain must match domain in QR code
|
|
107
|
-
if (originDomain !== this.domain) {
|
|
108
|
-
logger.warn(
|
|
109
|
-
`[mobile] Origin does not match domain in QR code. Expected ${this.domain} but got ${originDomain}`,
|
|
110
|
-
)
|
|
111
|
-
return
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
if (data.method === "encryptedMessage") {
|
|
115
|
-
// Decode the payload from base64 to Uint8Array
|
|
116
|
-
const payload = new Uint8Array(
|
|
117
|
-
atob(data.params.payload)
|
|
118
|
-
.split("")
|
|
119
|
-
.map((c) => c.charCodeAt(0)),
|
|
120
|
-
)
|
|
121
|
-
try {
|
|
122
|
-
// Decrypt the payload using the shared secret
|
|
123
|
-
const decrypted = await decrypt(payload, this.topicToSharedSecret[topic], topic)
|
|
124
|
-
const decryptedJson: JsonRpcRequest = JSON.parse(decrypted)
|
|
125
|
-
await this.handleEncryptedMessage(topic, decryptedJson, data)
|
|
126
|
-
} catch (error) {
|
|
127
|
-
logger.error("[mobile] Error decrypting message:", error)
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
} catch (error) {
|
|
131
|
-
logger.error("[mobile] Error:", error)
|
|
132
|
-
}
|
|
133
|
-
})
|
|
134
|
-
|
|
135
|
-
wsClient.onerror = (error: Event) => {
|
|
136
|
-
logger.error("[mobile] WebSocket error:", error)
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
return {
|
|
140
|
-
domain: this.domain,
|
|
141
|
-
requestId: topic,
|
|
142
|
-
isBridgeConnected: () => this.topicToWebSocketClient[topic].readyState === WebSocket.OPEN,
|
|
143
|
-
isDomainVerified: () => this.topicToRemoteDomainVerified[topic] === true,
|
|
144
|
-
onDomainVerified: (callback: () => void) =>
|
|
145
|
-
this.onDomainVerifiedCallbacks[topic].push(callback),
|
|
146
|
-
onBridgeConnect: (callback: () => void) =>
|
|
147
|
-
this.onBridgeConnectCallbacks[topic].push(callback),
|
|
148
|
-
notifyReject: async () => {
|
|
149
|
-
await sendEncryptedJsonRpcRequest(
|
|
150
|
-
"reject",
|
|
151
|
-
null,
|
|
152
|
-
this.topicToSharedSecret[topic],
|
|
153
|
-
topic,
|
|
154
|
-
this.topicToWebSocketClient[topic],
|
|
155
|
-
)
|
|
156
|
-
},
|
|
157
|
-
notifyAccept: async () => {
|
|
158
|
-
await sendEncryptedJsonRpcRequest(
|
|
159
|
-
"accept",
|
|
160
|
-
null,
|
|
161
|
-
this.topicToSharedSecret[topic],
|
|
162
|
-
topic,
|
|
163
|
-
this.topicToWebSocketClient[topic],
|
|
164
|
-
)
|
|
165
|
-
},
|
|
166
|
-
notifyDone: async (proof: any) => {
|
|
167
|
-
await sendEncryptedJsonRpcRequest(
|
|
168
|
-
"done",
|
|
169
|
-
{ proof },
|
|
170
|
-
this.topicToSharedSecret[topic],
|
|
171
|
-
topic,
|
|
172
|
-
this.topicToWebSocketClient[topic],
|
|
173
|
-
)
|
|
174
|
-
},
|
|
175
|
-
notifyError: async (error: string) => {
|
|
176
|
-
await sendEncryptedJsonRpcRequest(
|
|
177
|
-
"error",
|
|
178
|
-
{ error },
|
|
179
|
-
this.topicToSharedSecret[topic],
|
|
180
|
-
topic,
|
|
181
|
-
this.topicToWebSocketClient[topic],
|
|
182
|
-
)
|
|
183
|
-
},
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
}
|