From 0c741e5b69cdbc4d94c746998e1e065ecb46d0bc Mon Sep 17 00:00:00 2001 From: himanshu Date: Fri, 1 Nov 2024 13:06:55 +0530 Subject: [PATCH 1/4] passkey connector apis --- src/TorusUtilsExtraParams.ts | 13 +- src/constants.ts | 5 + src/helpers/nodeUtils.ts | 82 +++++++--- src/helpers/passkeyConnectorUtils.ts | 221 +++++++++++++++++++++++++++ src/interfaces.ts | 5 + src/passkeyConnectorInterfaces.ts | 41 +++++ src/torus.ts | 14 +- 7 files changed, 350 insertions(+), 31 deletions(-) create mode 100644 src/helpers/passkeyConnectorUtils.ts create mode 100644 src/passkeyConnectorInterfaces.ts diff --git a/src/TorusUtilsExtraParams.ts b/src/TorusUtilsExtraParams.ts index e9dc942..34192a8 100644 --- a/src/TorusUtilsExtraParams.ts +++ b/src/TorusUtilsExtraParams.ts @@ -1,8 +1,4 @@ -export interface TorusUtilsExtraParams { - nonce?: string; // farcaster - - message?: string; // farcaster - +export interface TorusUtilsPasskeyExtraParams { signature?: string; // farcaster, passkey, webauthn clientDataJson?: string; // passkey, webauthn @@ -16,6 +12,13 @@ export interface TorusUtilsExtraParams { rpOrigin?: string; // passkey, webauthn rpId?: string; // passkey, webauthn +} +export interface TorusUtilsExtraParams extends TorusUtilsPasskeyExtraParams { + nonce?: string; // farcaster + + message?: string; // farcaster + + signature?: string; // farcaster, passkey, webauthn session_token_exp_second?: number; diff --git a/src/constants.ts b/src/constants.ts index 1f449aa..70cf24e 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -4,6 +4,11 @@ export const JRPC_METHODS = { COMMITMENT_REQUEST: "CommitmentRequest", IMPORT_SHARES: "ImportShares", GET_SHARE_OR_KEY_ASSIGN: "GetShareOrKeyAssign", + RETRIEVE_SHARES_WITH_LINKED_PASSKEY: "RetrieveSharesWithLinkedPasskey", + GENERATE_AUTH_MESSAGE: "GenerateAuthMessage", + LINK_PASSKEY: "LinkPasskey", + UNLINK_PASSKEY: "UnlinkPasskey", + GET_LINKED_PASSKEYS: "GetLinkedPasskeys", }; export const SAPPHIRE_METADATA_URL = "https://node-1.node.web3auth.io/metadata"; diff --git a/src/helpers/nodeUtils.ts b/src/helpers/nodeUtils.ts index feb291b..fdcfa9e 100644 --- a/src/helpers/nodeUtils.ts +++ b/src/helpers/nodeUtils.ts @@ -28,7 +28,7 @@ import { } from "../interfaces"; import log from "../loglevel"; import { Some } from "../some"; -import { TorusUtilsExtraParams } from "../TorusUtilsExtraParams"; +import { TorusUtilsExtraParams, TorusUtilsPasskeyExtraParams } from "../TorusUtilsExtraParams"; import { calculateMedian, generatePrivateKey, @@ -368,6 +368,7 @@ export async function retrieveOrImportShare(params: { extraParams: TorusUtilsExtraParams; newImportedShares?: ImportedShare[]; checkCommitment?: boolean; + useLinkedPasskey?: boolean; }): Promise { const { legacyMetadataHost, @@ -389,6 +390,7 @@ export async function retrieveOrImportShare(params: { useDkg = true, serverTimeOffset, checkCommitment = true, + useLinkedPasskey = false, } = params; await get( allowHost, @@ -517,33 +519,63 @@ export async function retrieveOrImportShare(params: { promiseArrRequest.push(p); } else { for (let i = 0; i < endpoints.length; i += 1) { - const p = post>( - endpoints[i], - generateJsonRPCObject(JRPC_METHODS.GET_SHARE_OR_KEY_ASSIGN, { - encrypted: "yes", - use_temp: true, - key_type: keyType, - distributed_metadata: true, - verifieridentifier: verifier, - temppubx: nodeSigs.length === 0 && !checkCommitment ? sessionPubX : "", // send session pub key x only if node signatures are not available (Ie. in non commitment flow) - temppuby: nodeSigs.length === 0 && !checkCommitment ? sessionPubY : "", // send session pub key y only if node signatures are not available (Ie. in non commitment flow) - item: [ - { + if (useLinkedPasskey) { + const passkeyExtraParams = { ...extraParams } as TorusUtilsPasskeyExtraParams; + const p = post>( + endpoints[i], + generateJsonRPCObject(JRPC_METHODS.RETRIEVE_SHARES_WITH_LINKED_PASSKEY, { + encrypted: "yes", + use_temp: true, + key_type: keyType, + distributed_metadata: true, + verifier, + passkey_pub_key: verifierParams.verifier_id, + temp_pub_x: nodeSigs.length === 0 && !checkCommitment ? sessionPubX : "", // send session pub key x only if node signatures are not available (Ie. in non commitment flow) + temppuby: nodeSigs.length === 0 && !checkCommitment ? sessionPubY : "", // send session pub key y only if node signatures are not available (Ie. in non commitment flow) + passkey_auth_data: { ...verifierParams, - idtoken: idToken, + id_token: idToken, key_type: keyType, - nodesignatures: nodeSigs, - verifieridentifier: verifier, - ...extraParams, + node_signatures: nodeSigs, + verifier, + ...passkeyExtraParams, }, - ], - client_time: Math.floor(Date.now() / 1000).toString(), - one_key_flow: true, - }), - {}, - { logTracingHeader: config.logRequestTracing } - ); - promiseArrRequest.push(p); + client_time: Math.floor(Date.now() / 1000).toString(), + one_key_flow: true, + }), + {}, + { logTracingHeader: config.logRequestTracing } + ); + promiseArrRequest.push(p); + } else { + const p = post>( + endpoints[i], + generateJsonRPCObject(JRPC_METHODS.GET_SHARE_OR_KEY_ASSIGN, { + encrypted: "yes", + use_temp: true, + key_type: keyType, + distributed_metadata: true, + verifieridentifier: verifier, + temppubx: nodeSigs.length === 0 && !checkCommitment ? sessionPubX : "", // send session pub key x only if node signatures are not available (Ie. in non commitment flow) + temppuby: nodeSigs.length === 0 && !checkCommitment ? sessionPubY : "", // send session pub key y only if node signatures are not available (Ie. in non commitment flow) + item: [ + { + ...verifierParams, + idtoken: idToken, + key_type: keyType, + nodesignatures: nodeSigs, + verifieridentifier: verifier, + ...extraParams, + }, + ], + client_time: Math.floor(Date.now() / 1000).toString(), + one_key_flow: true, + }), + {}, + { logTracingHeader: config.logRequestTracing } + ); + promiseArrRequest.push(p); + } } } return Some< diff --git a/src/helpers/passkeyConnectorUtils.ts b/src/helpers/passkeyConnectorUtils.ts new file mode 100644 index 0000000..11219a6 --- /dev/null +++ b/src/helpers/passkeyConnectorUtils.ts @@ -0,0 +1,221 @@ +import { generateJsonRPCObject, post } from "@toruslabs/http-helpers"; + +import { config } from "../config"; +import { JRPC_METHODS } from "../constants"; +import { AuthMessageRequestResult, JRPCResponse } from "../interfaces"; +import { + GetAuthMessageFromNodesParams, + LinkPasskeyParams, + ListLinkedPasskeysParams, + ListLinkedPasskeysResponse, + PasskeyListItem, + UnLinkPasskeyParams, +} from "../passkeyConnectorInterfaces"; +import { Some } from "../some"; +export const getAuthMessageFromNodes = (params: GetAuthMessageFromNodesParams) => { + const { verifier, verifierId, passkeyPubKey, endpoints } = params; + const threeFourthsThreshold = ~~((endpoints.length * 3) / 4) + 1; + + if (!verifierId && !passkeyPubKey) { + throw new Error("Verifier ID or passkey pub key is required"); + } + const promiseArr: Promise>[] = []; + for (let i = 0; i < endpoints.length; i++) { + const p = post>( + endpoints[i], + generateJsonRPCObject(JRPC_METHODS.GENERATE_AUTH_MESSAGE, { + verifier, + verifier_id: verifierId, + passkey_pub_key: passkeyPubKey, + }), + {}, + { logTracingHeader: config.logRequestTracing } + ); + promiseArr.push(p); + } + + return new Promise[]>((resolve, reject) => { + Some, (null | JRPCResponse)[]>(promiseArr, (resultArr) => { + const completedRequests = resultArr.filter((x) => { + if (!x || typeof x !== "object") { + return false; + } + if (x.error) { + return false; + } + return true; + }); + if (completedRequests.length >= threeFourthsThreshold) { + return Promise.resolve(completedRequests); + } + return Promise.reject(new Error("Failed to get auth message from threshold number of nodes")); + }) + .then((resultArr: JRPCResponse[]) => { + return resolve(resultArr); + }) + .catch(reject); + }); +}; + +export const linkPasskey = async (params: LinkPasskeyParams) => { + const { endpoints, message, label, passkeyPubKey, oAuthKeySignature, keyType, passkeyAuthData } = params; + const halfThreshold = ~~(endpoints.length / 2) + 1; + + if (!endpoints || endpoints.length === 0) { + throw new Error("Endpoints are required"); + } + + const promiseArr: Promise>>[] = []; + for (let i = 0; i < endpoints.length; i++) { + const p = post>>( + endpoints[i], + generateJsonRPCObject(JRPC_METHODS.LINK_PASSKEY, { + message, + label, + passkey_pub_key: passkeyPubKey, + verifier_account_signature: oAuthKeySignature, + key_type: keyType, + passkey_auth_data: passkeyAuthData, + }), + {}, + { logTracingHeader: config.logRequestTracing } + ); + promiseArr.push(p); + } + + return new Promise>[]>((resolve, reject) => { + Some>, (null | JRPCResponse>)[]>(promiseArr, (resultArr) => { + const completedRequests = resultArr.filter((x) => { + if (!x || typeof x !== "object") { + return false; + } + if (x.error) { + return false; + } + return true; + }); + if (completedRequests.length >= halfThreshold) { + return Promise.resolve(completedRequests); + } + return Promise.reject(new Error("Failed to get auth message from threshold number of nodes")); + }) + .then((resultArr: JRPCResponse>[]) => { + return resolve(resultArr); + }) + .catch(reject); + }); +}; + +export const UnlinkPasskey = async (params: UnLinkPasskeyParams) => { + const { endpoints, message, passkeyPubKey, oAuthKeySignature, keyType } = params; + const halfThreshold = ~~(endpoints.length / 2) + 1; + + if (!endpoints || endpoints.length === 0) { + throw new Error("Endpoints are required"); + } + + const promiseArr: Promise>>[] = []; + for (let i = 0; i < endpoints.length; i++) { + const p = post>>( + endpoints[i], + generateJsonRPCObject(JRPC_METHODS.UNLINK_PASSKEY, { + message, + passkey_pub_key: passkeyPubKey, + verifier_account_signature: oAuthKeySignature, + key_type: keyType, + }), + {}, + { logTracingHeader: config.logRequestTracing } + ); + promiseArr.push(p); + } + + return new Promise>[]>((resolve, reject) => { + Some>, (null | JRPCResponse>)[]>(promiseArr, (resultArr) => { + const completedRequests = resultArr.filter((x) => { + if (!x || typeof x !== "object") { + return false; + } + if (x.error) { + return false; + } + return true; + }); + if (completedRequests.length >= halfThreshold) { + return Promise.resolve(completedRequests); + } + return Promise.reject(new Error("Failed to get auth message from threshold number of nodes")); + }) + .then((resultArr: JRPCResponse>[]) => { + return resolve(resultArr); + }) + .catch(reject); + }); +}; + +export const ListLinkedPasskey = async (params: ListLinkedPasskeysParams) => { + const { endpoints, message, oAuthKeySignature, keyType } = params; + const halfThreshold = ~~(endpoints.length / 2) + 1; + + if (!endpoints || endpoints.length === 0) { + throw new Error("Endpoints are required"); + } + + const promiseArr: Promise>[] = []; + for (let i = 0; i < endpoints.length; i++) { + const p = post>( + endpoints[i], + generateJsonRPCObject(JRPC_METHODS.GET_LINKED_PASSKEYS, { + message, + verifier_account_signature: oAuthKeySignature, + key_type: keyType, + }), + {}, + { logTracingHeader: config.logRequestTracing } + ); + promiseArr.push(p); + } + + return new Promise((resolve, reject) => { + Some, PasskeyListItem[]>(promiseArr, (resultArr) => { + const completedRequests = resultArr.filter((x) => { + if (!x || typeof x !== "object") { + return false; + } + if (x.error) { + return false; + } + return true; + }); + if (completedRequests.length >= halfThreshold) { + // find all passkeys object which have same passkey_pub_key inside each complated request passkeys array object + // Find passkeys that appear in at least halfThreshold number of responses + const passkeyMap = new Map(); + + // Count occurrences of each passkey by pub_key + completedRequests.forEach((request) => { + request.result.passkeys.forEach((passkey) => { + const existing = passkeyMap.get(passkey.passkey_pub_key); + if (existing) { + existing.count++; + } else { + passkeyMap.set(passkey.passkey_pub_key, { count: 1, passkey }); + } + }); + }); + + // Filter passkeys that meet threshold requirement + const result = Array.from(passkeyMap.values()) + .filter((item) => item.count >= halfThreshold) + .map((item) => item.passkey); + + return Promise.resolve(result); + } + return Promise.reject(new Error("Failed to get auth message from threshold number of nodes")); + }) + .then((resultArr: PasskeyListItem[]) => { + return resolve(resultArr); + }) + .catch(reject); + }); +}; diff --git a/src/interfaces.ts b/src/interfaces.ts index 9e9e2d2..2fd5fed 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -89,6 +89,10 @@ export interface CommitmentRequestResult { nodeindex: string; pub_key_x: string; } + +export interface AuthMessageRequestResult { + message: string; +} export interface JRPCResponse { id: number; jsonrpc: "2.0"; @@ -291,6 +295,7 @@ export interface RetrieveSharesParams { idToken: string; nodePubkeys: INodePub[]; extraParams?: TorusUtilsExtraParams; + useLinkedPasskey?: boolean; useDkg?: boolean; checkCommitment?: boolean; } diff --git a/src/passkeyConnectorInterfaces.ts b/src/passkeyConnectorInterfaces.ts new file mode 100644 index 0000000..e70c70f --- /dev/null +++ b/src/passkeyConnectorInterfaces.ts @@ -0,0 +1,41 @@ +import { TorusUtilsPasskeyExtraParams } from "./TorusUtilsExtraParams"; + +export type GetAuthMessageFromNodesParams = { endpoints: string[]; verifier: string; verifierId?: string; passkeyPubKey?: string }; + +export type PasskeyAuthData = { + verifier: string; + verifier_id: string; + id_token: string; + key_type: KeyType; + node_signatures: string[]; +} & TorusUtilsPasskeyExtraParams; + +// passkey auth data is required only when relinking a existing passkey +export type LinkPasskeyParams = { + endpoints: string[]; + passkeyPubKey: string; + message: string; + label: string; + oAuthKeySignature: string; + keyType: KeyType; + passkeyAuthData?: PasskeyAuthData; +}; + +export type UnLinkPasskeyParams = { + endpoints: string[]; + passkeyPubKey: string; + message: string; + oAuthKeySignature: string; + keyType: KeyType; +}; +export type ListLinkedPasskeysParams = { + endpoints: string[]; + message: string; + oAuthKeySignature: string; + keyType: KeyType; +}; + +export type PasskeyListItem = { label: string; verifier: string; verifier_id: string; passkey_pub_key: string }; +export type ListLinkedPasskeysResponse = { + passkeys: PasskeyListItem[]; +}; diff --git a/src/torus.ts b/src/torus.ts index 11e54f3..a15b287 100644 --- a/src/torus.ts +++ b/src/torus.ts @@ -112,7 +112,18 @@ class Torus { } async retrieveShares(params: RetrieveSharesParams): Promise { - const { verifier, verifierParams, idToken, nodePubkeys, indexes, endpoints, useDkg, extraParams = {}, checkCommitment = true } = params; + const { + verifier, + verifierParams, + idToken, + nodePubkeys, + indexes, + endpoints, + useDkg, + useLinkedPasskey, + extraParams = {}, + checkCommitment = true, + } = params; if (nodePubkeys.length === 0) { throw new Error("nodePubkeys param is required"); } @@ -165,6 +176,7 @@ class Torus { nodePubkeys, extraParams, checkCommitment, + useLinkedPasskey, }); } From 1f4777edce4cc6d3867278398d0d4665c4ac8731 Mon Sep 17 00:00:00 2001 From: himanshu Date: Wed, 6 Nov 2024 09:48:43 +0530 Subject: [PATCH 2/4] bug fixes in passkey link --- src/helpers/index.ts | 1 + src/helpers/passkeyConnectorUtils.ts | 30 ++++++++++++++-------------- src/index.ts | 1 + src/interfaces.ts | 1 + src/passkeyConnectorInterfaces.ts | 13 ++++++------ 5 files changed, 25 insertions(+), 21 deletions(-) diff --git a/src/helpers/index.ts b/src/helpers/index.ts index 92a0751..23036e0 100644 --- a/src/helpers/index.ts +++ b/src/helpers/index.ts @@ -4,4 +4,5 @@ export * from "./keyUtils"; export * from "./langrangeInterpolatePoly"; export * from "./metadataUtils"; export * from "./nodeUtils"; +export * from "./passkeyConnectorUtils"; export * from "./tssPubKeyUtils"; diff --git a/src/helpers/passkeyConnectorUtils.ts b/src/helpers/passkeyConnectorUtils.ts index 11219a6..9a35d80 100644 --- a/src/helpers/passkeyConnectorUtils.ts +++ b/src/helpers/passkeyConnectorUtils.ts @@ -58,11 +58,11 @@ export const getAuthMessageFromNodes = (params: GetAuthMessageFromNodesParams) = }; export const linkPasskey = async (params: LinkPasskeyParams) => { - const { endpoints, message, label, passkeyPubKey, oAuthKeySignature, keyType, passkeyAuthData } = params; + const { endpoints, messages, label, passkeyPubKey, oAuthKeySignatures, keyType, passkeyAuthData } = params; const halfThreshold = ~~(endpoints.length / 2) + 1; - if (!endpoints || endpoints.length === 0) { - throw new Error("Endpoints are required"); + if (!endpoints || endpoints.length < halfThreshold) { + throw new Error(`minimum ${halfThreshold} endpoints are required`); } const promiseArr: Promise>>[] = []; @@ -70,10 +70,10 @@ export const linkPasskey = async (params: LinkPasskeyParams) => { const p = post>>( endpoints[i], generateJsonRPCObject(JRPC_METHODS.LINK_PASSKEY, { - message, + message: messages[i], label, passkey_pub_key: passkeyPubKey, - verifier_account_signature: oAuthKeySignature, + verifier_account_signature: oAuthKeySignatures[i], key_type: keyType, passkey_auth_data: passkeyAuthData, }), @@ -107,11 +107,11 @@ export const linkPasskey = async (params: LinkPasskeyParams) => { }; export const UnlinkPasskey = async (params: UnLinkPasskeyParams) => { - const { endpoints, message, passkeyPubKey, oAuthKeySignature, keyType } = params; + const { endpoints, messages, passkeyPubKey, oAuthKeySignatures, keyType } = params; const halfThreshold = ~~(endpoints.length / 2) + 1; - if (!endpoints || endpoints.length === 0) { - throw new Error("Endpoints are required"); + if (!endpoints || endpoints.length < halfThreshold) { + throw new Error(`minimum ${halfThreshold} endpoints are required`); } const promiseArr: Promise>>[] = []; @@ -119,9 +119,9 @@ export const UnlinkPasskey = async (params: UnLinkPasskeyParams) => { const p = post>>( endpoints[i], generateJsonRPCObject(JRPC_METHODS.UNLINK_PASSKEY, { - message, + message: messages[i], passkey_pub_key: passkeyPubKey, - verifier_account_signature: oAuthKeySignature, + verifier_account_signature: oAuthKeySignatures[i], key_type: keyType, }), {}, @@ -154,11 +154,11 @@ export const UnlinkPasskey = async (params: UnLinkPasskeyParams) => { }; export const ListLinkedPasskey = async (params: ListLinkedPasskeysParams) => { - const { endpoints, message, oAuthKeySignature, keyType } = params; + const { endpoints, messages, oAuthKeySignatures, keyType } = params; const halfThreshold = ~~(endpoints.length / 2) + 1; - if (!endpoints || endpoints.length === 0) { - throw new Error("Endpoints are required"); + if (!endpoints || endpoints.length < halfThreshold) { + throw new Error(`minimum ${halfThreshold} endpoints are required`); } const promiseArr: Promise>[] = []; @@ -166,8 +166,8 @@ export const ListLinkedPasskey = async (params: ListLinkedPasskeysParams) => { const p = post>( endpoints[i], generateJsonRPCObject(JRPC_METHODS.GET_LINKED_PASSKEYS, { - message, - verifier_account_signature: oAuthKeySignature, + message: messages[i], + verifier_account_signature: oAuthKeySignatures[i], key_type: keyType, }), {}, diff --git a/src/index.ts b/src/index.ts index a7e6297..be5c551 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,7 @@ export * from "./constants"; export * from "./helpers"; export * from "./interfaces"; +export * from "./passkeyConnectorInterfaces"; export { default as Point } from "./Point"; export { default as Polynomial } from "./Polynomial"; export { default as Share } from "./Share"; diff --git a/src/interfaces.ts b/src/interfaces.ts index 2fd5fed..428712b 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -92,6 +92,7 @@ export interface CommitmentRequestResult { export interface AuthMessageRequestResult { message: string; + node_index: number; } export interface JRPCResponse { id: number; diff --git a/src/passkeyConnectorInterfaces.ts b/src/passkeyConnectorInterfaces.ts index e70c70f..5483f28 100644 --- a/src/passkeyConnectorInterfaces.ts +++ b/src/passkeyConnectorInterfaces.ts @@ -1,3 +1,4 @@ +import { KeyType } from "./interfaces"; import { TorusUtilsPasskeyExtraParams } from "./TorusUtilsExtraParams"; export type GetAuthMessageFromNodesParams = { endpoints: string[]; verifier: string; verifierId?: string; passkeyPubKey?: string }; @@ -14,9 +15,9 @@ export type PasskeyAuthData = { export type LinkPasskeyParams = { endpoints: string[]; passkeyPubKey: string; - message: string; + messages: string[]; label: string; - oAuthKeySignature: string; + oAuthKeySignatures: string[]; keyType: KeyType; passkeyAuthData?: PasskeyAuthData; }; @@ -24,14 +25,14 @@ export type LinkPasskeyParams = { export type UnLinkPasskeyParams = { endpoints: string[]; passkeyPubKey: string; - message: string; - oAuthKeySignature: string; + messages: string[]; + oAuthKeySignatures: string[]; keyType: KeyType; }; export type ListLinkedPasskeysParams = { endpoints: string[]; - message: string; - oAuthKeySignature: string; + messages: string[]; + oAuthKeySignatures: string[]; keyType: KeyType; }; From 007aaef016f25db2c52f7b988687f7a1378ce788 Mon Sep 17 00:00:00 2001 From: himanshu Date: Fri, 8 Nov 2024 13:37:35 +0530 Subject: [PATCH 3/4] tests added for passkey connector apis --- src/helpers/passkeyConnectorUtils.ts | 28 ++++--- src/interfaces.ts | 4 - src/passkeyConnectorInterfaces.ts | 9 +++ test/sapphire_devnet.test.ts | 108 ++++++++++++++++++++++++++- 4 files changed, 133 insertions(+), 16 deletions(-) diff --git a/src/helpers/passkeyConnectorUtils.ts b/src/helpers/passkeyConnectorUtils.ts index 9a35d80..0619839 100644 --- a/src/helpers/passkeyConnectorUtils.ts +++ b/src/helpers/passkeyConnectorUtils.ts @@ -2,8 +2,10 @@ import { generateJsonRPCObject, post } from "@toruslabs/http-helpers"; import { config } from "../config"; import { JRPC_METHODS } from "../constants"; -import { AuthMessageRequestResult, JRPCResponse } from "../interfaces"; +import { JRPCResponse } from "../interfaces"; import { + AuthMessageData, + AuthMessageRequestJRPCResult, GetAuthMessageFromNodesParams, LinkPasskeyParams, ListLinkedPasskeysParams, @@ -19,9 +21,9 @@ export const getAuthMessageFromNodes = (params: GetAuthMessageFromNodesParams) = if (!verifierId && !passkeyPubKey) { throw new Error("Verifier ID or passkey pub key is required"); } - const promiseArr: Promise>[] = []; + const promiseArr: Promise>[] = []; for (let i = 0; i < endpoints.length; i++) { - const p = post>( + const p = post>( endpoints[i], generateJsonRPCObject(JRPC_METHODS.GENERATE_AUTH_MESSAGE, { verifier, @@ -34,8 +36,8 @@ export const getAuthMessageFromNodes = (params: GetAuthMessageFromNodesParams) = promiseArr.push(p); } - return new Promise[]>((resolve, reject) => { - Some, (null | JRPCResponse)[]>(promiseArr, (resultArr) => { + return new Promise((resolve, reject) => { + Some, (null | JRPCResponse)[]>(promiseArr, (resultArr) => { const completedRequests = resultArr.filter((x) => { if (!x || typeof x !== "object") { return false; @@ -50,8 +52,12 @@ export const getAuthMessageFromNodes = (params: GetAuthMessageFromNodesParams) = } return Promise.reject(new Error("Failed to get auth message from threshold number of nodes")); }) - .then((resultArr: JRPCResponse[]) => { - return resolve(resultArr); + .then((resultArr: JRPCResponse[]) => { + const authMessageData: AuthMessageData[] = resultArr.map((x) => ({ + message: x.result.message, + nodeIndex: x.result.node_index, + })); + return resolve(authMessageData); }) .catch(reject); }); @@ -106,7 +112,7 @@ export const linkPasskey = async (params: LinkPasskeyParams) => { }); }; -export const UnlinkPasskey = async (params: UnLinkPasskeyParams) => { +export const unlinkPasskey = async (params: UnLinkPasskeyParams) => { const { endpoints, messages, passkeyPubKey, oAuthKeySignatures, keyType } = params; const halfThreshold = ~~(endpoints.length / 2) + 1; @@ -153,7 +159,7 @@ export const UnlinkPasskey = async (params: UnLinkPasskeyParams) => { }); }; -export const ListLinkedPasskey = async (params: ListLinkedPasskeysParams) => { +export const listLinkedPasskey = async (params: ListLinkedPasskeysParams) => { const { endpoints, messages, oAuthKeySignatures, keyType } = params; const halfThreshold = ~~(endpoints.length / 2) + 1; @@ -194,7 +200,8 @@ export const ListLinkedPasskey = async (params: ListLinkedPasskeysParams) => { // Count occurrences of each passkey by pub_key completedRequests.forEach((request) => { - request.result.passkeys.forEach((passkey) => { + const passkeys = request.result.passkeys || []; + passkeys.forEach((passkey) => { const existing = passkeyMap.get(passkey.passkey_pub_key); if (existing) { existing.count++; @@ -211,7 +218,6 @@ export const ListLinkedPasskey = async (params: ListLinkedPasskeysParams) => { return Promise.resolve(result); } - return Promise.reject(new Error("Failed to get auth message from threshold number of nodes")); }) .then((resultArr: PasskeyListItem[]) => { return resolve(resultArr); diff --git a/src/interfaces.ts b/src/interfaces.ts index 428712b..c8d7863 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -90,10 +90,6 @@ export interface CommitmentRequestResult { pub_key_x: string; } -export interface AuthMessageRequestResult { - message: string; - node_index: number; -} export interface JRPCResponse { id: number; jsonrpc: "2.0"; diff --git a/src/passkeyConnectorInterfaces.ts b/src/passkeyConnectorInterfaces.ts index 5483f28..b3ff73e 100644 --- a/src/passkeyConnectorInterfaces.ts +++ b/src/passkeyConnectorInterfaces.ts @@ -3,6 +3,10 @@ import { TorusUtilsPasskeyExtraParams } from "./TorusUtilsExtraParams"; export type GetAuthMessageFromNodesParams = { endpoints: string[]; verifier: string; verifierId?: string; passkeyPubKey?: string }; +export interface AuthMessageRequestJRPCResult { + message: string; + node_index: number; +} export type PasskeyAuthData = { verifier: string; verifier_id: string; @@ -40,3 +44,8 @@ export type PasskeyListItem = { label: string; verifier: string; verifier_id: st export type ListLinkedPasskeysResponse = { passkeys: PasskeyListItem[]; }; + +export type AuthMessageData = { + message: string; + nodeIndex: number; +}; diff --git a/test/sapphire_devnet.test.ts b/test/sapphire_devnet.test.ts index 8cc2e8f..a59e4ec 100644 --- a/test/sapphire_devnet.test.ts +++ b/test/sapphire_devnet.test.ts @@ -5,7 +5,7 @@ import BN from "bn.js"; import { expect } from "chai"; import { ec as EC } from "elliptic"; -import { generatePrivateKey, keccak256 } from "../src"; +import { generatePrivateKey, getAuthMessageFromNodes, getKeyCurve, keccak256, linkPasskey, listLinkedPasskey, unlinkPasskey } from "../src"; import TorusUtils from "../src/torus"; import { generateIdToken, getImportKeyParams, getRetrieveSharesParams, lookupVerifier } from "./helpers"; @@ -956,4 +956,110 @@ describe("torus utils sapphire devnet", function () { const set = new Set(addresses); expect(set.size).to.equal(6); }); + + it("should be able to get auth messages", async function () { + const verifierDetails = { verifier: TORUS_TEST_VERIFIER, verifierId: TORUS_TEST_EMAIL }; + const nodeDetails = await TORUS_NODE_MANAGER.getNodeDetails(verifierDetails); + const torusNodeEndpoints = nodeDetails.torusNodeSSSEndpoints; + const result = await getAuthMessageFromNodes({ + endpoints: torusNodeEndpoints, + verifier: TORUS_TEST_VERIFIER, + verifierId: TORUS_TEST_EMAIL, + }); + expect(result.length).to.greaterThanOrEqual(4); + }); + + it.skip("should be able to link passkey", async function () { + const verifierDetails = { verifier: TORUS_TEST_VERIFIER, verifierId: TORUS_TEST_EMAIL }; + const nodeDetails = await TORUS_NODE_MANAGER.getNodeDetails(verifierDetails); + const torusNodeEndpoints = nodeDetails.torusNodeSSSEndpoints; + + async function getAuthMessagesData(nodeEndpoints: string[], key: EC.KeyPair) { + const authMessages = await getAuthMessageFromNodes({ + endpoints: nodeEndpoints, + verifier: TORUS_TEST_VERIFIER, + verifierId: TORUS_TEST_EMAIL, + }); + const sigs = authMessages.map((msgData) => { + // const rawMessage = msgData.message.split("\u0017").join(""); + const hash = keccak256(Buffer.from(msgData.message, "utf8")).slice(2); + const sig = key.sign(hash); + return Buffer.from(`${sig.r.toString(16, 64) + sig.s.toString(16, 64)}00`, "hex").toString("hex"); + }); + + const validEndpoints = authMessages.map(({ nodeIndex }) => torusNodeEndpoints[nodeIndex - 1]); + + return { authMessages, validEndpoints, sigs }; + } + + const token = generateIdToken(TORUS_TEST_EMAIL, "ES256"); + const loginResult = await torus.retrieveShares( + getRetrieveSharesParams( + torusNodeEndpoints, + nodeDetails.torusIndexes, + TORUS_TEST_VERIFIER, + { verifier_id: TORUS_TEST_EMAIL }, + token, + nodeDetails.torusNodePub + ) + ); + + const postboxKey = loginResult.postboxKeyData.privKey.padStart(64, "0"); + const key = getKeyCurve("secp256k1").keyFromPrivate(postboxKey, "hex"); + + const { authMessages, sigs, validEndpoints } = await getAuthMessagesData(torusNodeEndpoints, key); + + // random credential pubkey + const credentialPubKey = "pQECAyYgASFYIJn3UuEVxjb8w4sztWIV3SL615NeU0Wps0D7sSuwWMwPIlggt_5cIKbXbvMtvvXLYT"; + await linkPasskey({ + endpoints: validEndpoints, + passkeyPubKey: credentialPubKey, + messages: authMessages.map(({ message }) => message), + label: "test", + oAuthKeySignatures: sigs, + keyType: "secp256k1", + }); + + const { + sigs: listingSigs, + validEndpoints: validEndpointsForList, + authMessages: authMessagesForList, + } = await getAuthMessagesData(torusNodeEndpoints, key); + + const linkedPasskeys = await listLinkedPasskey({ + endpoints: validEndpointsForList, + messages: authMessagesForList.map(({ message }) => message), + oAuthKeySignatures: listingSigs, + keyType: "secp256k1", + }); + expect(linkedPasskeys.length).to.equal(1); + + const { + authMessages: unlinkAuthMsgs, + sigs: unlinkSigs, + validEndpoints: unlinkValidEndpoints, + } = await getAuthMessagesData(torusNodeEndpoints, key); + + await unlinkPasskey({ + endpoints: unlinkValidEndpoints, + passkeyPubKey: credentialPubKey, + messages: unlinkAuthMsgs.map(({ message }) => message), + oAuthKeySignatures: unlinkSigs, + keyType: "secp256k1", + }); + + const { + authMessages: relistAuthMsgs, + sigs: relistSigs, + validEndpoints: relistValidEndpoints, + } = await getAuthMessagesData(torusNodeEndpoints, key); + + const passkeysAfterUnlink = await listLinkedPasskey({ + endpoints: relistValidEndpoints, + messages: relistAuthMsgs.map(({ message }) => message), + oAuthKeySignatures: relistSigs, + keyType: "secp256k1", + }); + expect(passkeysAfterUnlink.length).to.equal(0); + }); }); From 135a53c652d7f14eeca706397ee01852de6642ac Mon Sep 17 00:00:00 2001 From: himanshu Date: Fri, 29 Nov 2024 12:48:07 +0530 Subject: [PATCH 4/4] cleanup and bug fixes --- src/helpers/nodeUtils.ts | 463 ++++++++++++++------------- src/helpers/passkeyConnectorUtils.ts | 99 +++++- src/interfaces.ts | 1 - src/passkeyConnectorInterfaces.ts | 22 +- src/torus.ts | 52 ++- 5 files changed, 394 insertions(+), 243 deletions(-) diff --git a/src/helpers/nodeUtils.ts b/src/helpers/nodeUtils.ts index fdcfa9e..d8a939f 100644 --- a/src/helpers/nodeUtils.ts +++ b/src/helpers/nodeUtils.ts @@ -28,7 +28,7 @@ import { } from "../interfaces"; import log from "../loglevel"; import { Some } from "../some"; -import { TorusUtilsExtraParams, TorusUtilsPasskeyExtraParams } from "../TorusUtilsExtraParams"; +import { TorusUtilsExtraParams } from "../TorusUtilsExtraParams"; import { calculateMedian, generatePrivateKey, @@ -260,7 +260,7 @@ const commitmentRequest = async (params: { verifieridentifier: verifier, verifier_id: verifierParams.verifier_id, extended_verifier_id: verifierParams.extended_verifier_id, - is_import_key_flow: true, + is_import_key_flow: finalImportedShares.length > 0, }), {}, { logTracingHeader: config.logRequestTracing } @@ -348,236 +348,38 @@ const commitmentRequest = async (params: { .catch(reject); }); }; -export async function retrieveOrImportShare(params: { - legacyMetadataHost: string; - serverTimeOffset: number; - enableOneKey: boolean; - ecCurve: ec; - keyType: KeyType; - allowHost: string; - network: TORUS_NETWORK_TYPE; - clientId: string; - endpoints: string[]; - indexes: number[]; - verifier: string; - verifierParams: VerifierParams; - idToken: string; - useDkg: boolean; - overrideExistingKey: boolean; - nodePubkeys: INodePub[]; - extraParams: TorusUtilsExtraParams; - newImportedShares?: ImportedShare[]; - checkCommitment?: boolean; - useLinkedPasskey?: boolean; -}): Promise { + +export async function processShareResponse( + params: { + legacyMetadataHost: string; + serverTimeOffset: number; + sessionAuthKey: Buffer; + enableOneKey: boolean; + ecCurve: ec; + keyType: KeyType; + network: TORUS_NETWORK_TYPE; + verifierParams: VerifierParams; + endpoints: string[]; + isImportedShares: boolean; + verifier?: string; // not required for passkey linked flow + }, + promiseArrRequest: Promise | JRPCResponse>[] +) { const { legacyMetadataHost, + serverTimeOffset, + sessionAuthKey, enableOneKey, ecCurve, keyType, - allowHost, network, - clientId, - endpoints, - nodePubkeys, - indexes, - verifier, verifierParams, - idToken, - overrideExistingKey, - newImportedShares, - extraParams, - useDkg = true, - serverTimeOffset, - checkCommitment = true, - useLinkedPasskey = false, + verifier, + endpoints, + isImportedShares, } = params; - await get( - allowHost, - { - headers: { - verifier, - verifierid: verifierParams.verifier_id, - network, - clientid: clientId, - enablegating: "true", - }, - }, - { useAPIKey: true } - ); - - // generate temporary private and public key that is used to secure receive shares - const sessionAuthKey = generatePrivate(); - const pubKey = getPublic(sessionAuthKey).toString("hex"); - const sessionPubX = pubKey.slice(2, 66); - const sessionPubY = pubKey.slice(66); - let finalImportedShares: ImportedShare[] = []; const halfThreshold = ~~(endpoints.length / 2) + 1; - if (newImportedShares?.length > 0) { - if (newImportedShares.length !== endpoints.length) { - throw new Error("Invalid imported shares length"); - } - finalImportedShares = newImportedShares; - } else if (!useDkg) { - const bufferKey = keyType === KEY_TYPE.SECP256K1 ? generatePrivateKey(ecCurve, Buffer) : await getRandomBytes(32); - const generatedShares = await generateShares(ecCurve, keyType, serverTimeOffset, indexes, nodePubkeys, Buffer.from(bufferKey)); - finalImportedShares = [...finalImportedShares, ...generatedShares]; - } - - let commitmentRequestResult: (void | JRPCResponse)[] = []; - let isExistingKey: boolean; - const nodeSigs: CommitmentRequestResult[] = []; - if (checkCommitment) { - commitmentRequestResult = await commitmentRequest({ - idToken, - endpoints, - indexes, - keyType, - verifier, - verifierParams, - pubKeyX: sessionPubX, - pubKeyY: sessionPubY, - finalImportedShares, - overrideExistingKey, - }); - for (let i = 0; i < commitmentRequestResult.length; i += 1) { - const x = commitmentRequestResult[i]; - if (!x || typeof x !== "object" || x.error) { - continue; - } - if (x) nodeSigs.push((x as JRPCResponse).result); - } - // if user's account already - isExistingKey = !!thresholdSame( - nodeSigs.map((x) => x && x.pub_key_x), - halfThreshold - ); - } else if (!checkCommitment && finalImportedShares.length > 0) { - // in case not allowed to override existing key for import request - // check if key exists - if (!overrideExistingKey) { - const keyLookupResult = await VerifierLookupRequest({ endpoints, verifier, verifierId: verifierParams.verifier_id, keyType }); - if ( - keyLookupResult.errorResult && - !(keyLookupResult.errorResult?.data as string)?.includes("Verifier + VerifierID has not yet been assigned") - ) { - throw new Error( - `node results do not match at first lookup ${JSON.stringify(keyLookupResult.keyResult || {})}, ${JSON.stringify(keyLookupResult.errorResult || {})}` - ); - } - if (keyLookupResult.keyResult?.keys?.length > 0) { - isExistingKey = !!keyLookupResult.keyResult.keys[0]; - } - } - } - const promiseArrRequest = []; - - const canImportedShares = overrideExistingKey || (!useDkg && !isExistingKey); - if (canImportedShares) { - const proxyEndpointNum = getProxyCoordinatorEndpointIndex(endpoints, verifier, verifierParams.verifier_id); - const items: Record[] = []; - for (let i = 0; i < endpoints.length; i += 1) { - const importedShare = finalImportedShares[i]; - if (!importedShare) { - throw new Error(`invalid imported share at index ${i}`); - } - items.push({ - ...verifierParams, - idtoken: idToken, - nodesignatures: nodeSigs, - verifieridentifier: verifier, - pub_key_x: importedShare.oauth_pub_key_x, - pub_key_y: importedShare.oauth_pub_key_y, - signing_pub_key_x: importedShare.signing_pub_key_x, - signing_pub_key_y: importedShare.signing_pub_key_y, - encrypted_share: importedShare.encrypted_share, - encrypted_share_metadata: importedShare.encrypted_share_metadata, - node_index: importedShare.node_index, - key_type: importedShare.key_type, - nonce_data: importedShare.nonce_data, - nonce_signature: importedShare.nonce_signature, - sss_endpoint: endpoints[i], - ...extraParams, - }); - } - const p = post>( - endpoints[proxyEndpointNum], - generateJsonRPCObject(JRPC_METHODS.IMPORT_SHARES, { - encrypted: "yes", - use_temp: true, - verifieridentifier: verifier, - temppubx: nodeSigs.length === 0 && !checkCommitment ? sessionPubX : "", // send session pub key x only if node signatures are not available (Ie. in non commitment flow) - temppuby: nodeSigs.length === 0 && !checkCommitment ? sessionPubY : "", // send session pub key y only if node signatures are not available (Ie. in non commitment flow) - item: items, - key_type: keyType, - one_key_flow: true, - }), - {}, - { logTracingHeader: config.logRequestTracing } - ).catch((err) => log.error("share req", err)); - promiseArrRequest.push(p); - } else { - for (let i = 0; i < endpoints.length; i += 1) { - if (useLinkedPasskey) { - const passkeyExtraParams = { ...extraParams } as TorusUtilsPasskeyExtraParams; - const p = post>( - endpoints[i], - generateJsonRPCObject(JRPC_METHODS.RETRIEVE_SHARES_WITH_LINKED_PASSKEY, { - encrypted: "yes", - use_temp: true, - key_type: keyType, - distributed_metadata: true, - verifier, - passkey_pub_key: verifierParams.verifier_id, - temp_pub_x: nodeSigs.length === 0 && !checkCommitment ? sessionPubX : "", // send session pub key x only if node signatures are not available (Ie. in non commitment flow) - temppuby: nodeSigs.length === 0 && !checkCommitment ? sessionPubY : "", // send session pub key y only if node signatures are not available (Ie. in non commitment flow) - passkey_auth_data: { - ...verifierParams, - id_token: idToken, - key_type: keyType, - node_signatures: nodeSigs, - verifier, - ...passkeyExtraParams, - }, - client_time: Math.floor(Date.now() / 1000).toString(), - one_key_flow: true, - }), - {}, - { logTracingHeader: config.logRequestTracing } - ); - promiseArrRequest.push(p); - } else { - const p = post>( - endpoints[i], - generateJsonRPCObject(JRPC_METHODS.GET_SHARE_OR_KEY_ASSIGN, { - encrypted: "yes", - use_temp: true, - key_type: keyType, - distributed_metadata: true, - verifieridentifier: verifier, - temppubx: nodeSigs.length === 0 && !checkCommitment ? sessionPubX : "", // send session pub key x only if node signatures are not available (Ie. in non commitment flow) - temppuby: nodeSigs.length === 0 && !checkCommitment ? sessionPubY : "", // send session pub key y only if node signatures are not available (Ie. in non commitment flow) - item: [ - { - ...verifierParams, - idtoken: idToken, - key_type: keyType, - nodesignatures: nodeSigs, - verifieridentifier: verifier, - ...extraParams, - }, - ], - client_time: Math.floor(Date.now() / 1000).toString(), - one_key_flow: true, - }), - {}, - { logTracingHeader: config.logRequestTracing } - ); - promiseArrRequest.push(p); - } - } - } return Some< void | JRPCResponse | JRPCResponse, | { @@ -642,7 +444,7 @@ export async function retrieveOrImportShare(params: { } }); - const thresholdReqCount = canImportedShares ? endpoints.length : halfThreshold; + const thresholdReqCount = isImportedShares ? endpoints.length : halfThreshold; // optimistically run lagrange interpolation once threshold number of shares have been received // this is matched against the user public key to ensure that shares are consistent // Note: no need of thresholdMetadataNonce for extended_verifier_id key @@ -976,3 +778,218 @@ export async function retrieveOrImportShare(params: { } as TorusKey; }); } +export async function retrieveOrImportShare(params: { + legacyMetadataHost: string; + serverTimeOffset: number; + enableOneKey: boolean; + ecCurve: ec; + keyType: KeyType; + allowHost: string; + network: TORUS_NETWORK_TYPE; + clientId: string; + endpoints: string[]; + indexes: number[]; + verifier: string; + verifierParams: VerifierParams; + idToken: string; + useDkg: boolean; + overrideExistingKey: boolean; + nodePubkeys: INodePub[]; + extraParams: TorusUtilsExtraParams; + newImportedShares?: ImportedShare[]; + checkCommitment?: boolean; +}): Promise { + const { + legacyMetadataHost, + enableOneKey, + ecCurve, + keyType, + allowHost, + network, + clientId, + endpoints, + nodePubkeys, + indexes, + verifier, + verifierParams, + idToken, + overrideExistingKey, + newImportedShares, + extraParams, + useDkg = true, + serverTimeOffset, + checkCommitment = true, + } = params; + await get( + allowHost, + { + headers: { + verifier, + verifierid: verifierParams.verifier_id, + network, + clientid: clientId, + enablegating: "true", + }, + }, + { useAPIKey: true } + ); + + // generate temporary private and public key that is used to secure receive shares + const sessionAuthKey = generatePrivate(); + const pubKey = getPublic(sessionAuthKey).toString("hex"); + const sessionPubX = pubKey.slice(2, 66); + const sessionPubY = pubKey.slice(66); + let finalImportedShares: ImportedShare[] = []; + const halfThreshold = ~~(endpoints.length / 2) + 1; + + if (newImportedShares?.length > 0) { + if (newImportedShares.length !== endpoints.length) { + throw new Error("Invalid imported shares length"); + } + finalImportedShares = newImportedShares; + } else if (!useDkg) { + const bufferKey = keyType === KEY_TYPE.SECP256K1 ? generatePrivateKey(ecCurve, Buffer) : await getRandomBytes(32); + const generatedShares = await generateShares(ecCurve, keyType, serverTimeOffset, indexes, nodePubkeys, Buffer.from(bufferKey)); + finalImportedShares = [...finalImportedShares, ...generatedShares]; + } + + let commitmentRequestResult: (void | JRPCResponse)[] = []; + let isExistingKey: boolean; + const nodeSigs: CommitmentRequestResult[] = []; + if (checkCommitment) { + commitmentRequestResult = await commitmentRequest({ + idToken, + endpoints, + indexes, + keyType, + verifier, + verifierParams, + pubKeyX: sessionPubX, + pubKeyY: sessionPubY, + finalImportedShares, + overrideExistingKey, + }); + for (let i = 0; i < commitmentRequestResult.length; i += 1) { + const x = commitmentRequestResult[i]; + if (!x || typeof x !== "object" || x.error) { + continue; + } + if (x) nodeSigs.push((x as JRPCResponse).result); + } + // if user's account already + isExistingKey = !!thresholdSame( + nodeSigs.map((x) => x && x.pub_key_x), + halfThreshold + ); + } else if (!checkCommitment && finalImportedShares.length > 0) { + // in case not allowed to override existing key for import request + // check if key exists + if (!overrideExistingKey) { + const keyLookupResult = await VerifierLookupRequest({ endpoints, verifier, verifierId: verifierParams.verifier_id, keyType }); + if ( + keyLookupResult.errorResult && + !(keyLookupResult.errorResult?.data as string)?.includes("Verifier + VerifierID has not yet been assigned") + ) { + throw new Error( + `node results do not match at first lookup ${JSON.stringify(keyLookupResult.keyResult || {})}, ${JSON.stringify(keyLookupResult.errorResult || {})}` + ); + } + if (keyLookupResult.keyResult?.keys?.length > 0) { + isExistingKey = !!keyLookupResult.keyResult.keys[0]; + } + } + } + const promiseArrRequest: Promise | JRPCResponse>[] = []; + + const canImportedShares = overrideExistingKey || (!useDkg && !isExistingKey); + if (canImportedShares) { + const proxyEndpointNum = getProxyCoordinatorEndpointIndex(endpoints, verifier, verifierParams.verifier_id); + const items: Record[] = []; + for (let i = 0; i < endpoints.length; i += 1) { + const importedShare = finalImportedShares[i]; + if (!importedShare) { + throw new Error(`invalid imported share at index ${i}`); + } + items.push({ + ...verifierParams, + idtoken: idToken, + nodesignatures: nodeSigs, + verifieridentifier: verifier, + pub_key_x: importedShare.oauth_pub_key_x, + pub_key_y: importedShare.oauth_pub_key_y, + signing_pub_key_x: importedShare.signing_pub_key_x, + signing_pub_key_y: importedShare.signing_pub_key_y, + encrypted_share: importedShare.encrypted_share, + encrypted_share_metadata: importedShare.encrypted_share_metadata, + node_index: importedShare.node_index, + key_type: importedShare.key_type, + nonce_data: importedShare.nonce_data, + nonce_signature: importedShare.nonce_signature, + sss_endpoint: endpoints[i], + ...extraParams, + }); + } + const p = post>( + endpoints[proxyEndpointNum], + generateJsonRPCObject(JRPC_METHODS.IMPORT_SHARES, { + encrypted: "yes", + use_temp: true, + verifieridentifier: verifier, + temppubx: nodeSigs.length === 0 && !checkCommitment ? sessionPubX : "", // send session pub key x only if node signatures are not available (Ie. in non commitment flow) + temppuby: nodeSigs.length === 0 && !checkCommitment ? sessionPubY : "", // send session pub key y only if node signatures are not available (Ie. in non commitment flow) + item: items, + key_type: keyType, + one_key_flow: true, + }), + {}, + { logTracingHeader: config.logRequestTracing } + ).catch((err) => log.error("share req", err)); + promiseArrRequest.push(p); + } else { + for (let i = 0; i < endpoints.length; i += 1) { + const p = post>( + endpoints[i], + generateJsonRPCObject(JRPC_METHODS.GET_SHARE_OR_KEY_ASSIGN, { + encrypted: "yes", + use_temp: true, + key_type: keyType, + distributed_metadata: true, + verifieridentifier: verifier, + temppubx: nodeSigs.length === 0 && !checkCommitment ? sessionPubX : "", // send session pub key x only if node signatures are not available (Ie. in non commitment flow) + temppuby: nodeSigs.length === 0 && !checkCommitment ? sessionPubY : "", // send session pub key y only if node signatures are not available (Ie. in non commitment flow) + item: [ + { + ...verifierParams, + idtoken: idToken, + key_type: keyType, + nodesignatures: nodeSigs, + verifieridentifier: verifier, + ...extraParams, + }, + ], + client_time: Math.floor(Date.now() / 1000).toString(), + one_key_flow: true, + }), + {}, + { logTracingHeader: config.logRequestTracing } + ); + promiseArrRequest.push(p); + } + } + return processShareResponse( + { + legacyMetadataHost, + serverTimeOffset, + sessionAuthKey, + enableOneKey, + ecCurve, + keyType, + network, + verifier, + verifierParams, + endpoints, + isImportedShares: canImportedShares, + }, + promiseArrRequest + ); +} diff --git a/src/helpers/passkeyConnectorUtils.ts b/src/helpers/passkeyConnectorUtils.ts index 0619839..7d5f04c 100644 --- a/src/helpers/passkeyConnectorUtils.ts +++ b/src/helpers/passkeyConnectorUtils.ts @@ -1,8 +1,11 @@ +import { INodePub, TORUS_NETWORK_TYPE } from "@toruslabs/constants"; +import { generatePrivate, getPublic } from "@toruslabs/eccrypto"; import { generateJsonRPCObject, post } from "@toruslabs/http-helpers"; +import { ec } from "elliptic"; import { config } from "../config"; import { JRPC_METHODS } from "../constants"; -import { JRPCResponse } from "../interfaces"; +import { JRPCResponse, KeyType, ShareRequestResult } from "../interfaces"; import { AuthMessageData, AuthMessageRequestJRPCResult, @@ -14,8 +17,10 @@ import { UnLinkPasskeyParams, } from "../passkeyConnectorInterfaces"; import { Some } from "../some"; +import { TorusUtilsPasskeyExtraParams } from "../TorusUtilsExtraParams"; +import { processShareResponse } from "./nodeUtils"; export const getAuthMessageFromNodes = (params: GetAuthMessageFromNodesParams) => { - const { verifier, verifierId, passkeyPubKey, endpoints } = params; + const { verifier, verifierId, passkeyPubKey, endpoints, requiredNodeIndexes } = params; const threeFourthsThreshold = ~~((endpoints.length * 3) / 4) + 1; if (!verifierId && !passkeyPubKey) { @@ -48,7 +53,22 @@ export const getAuthMessageFromNodes = (params: GetAuthMessageFromNodesParams) = return true; }); if (completedRequests.length >= threeFourthsThreshold) { - return Promise.resolve(completedRequests); + // wait for all required node indexes to be resolved + if (requiredNodeIndexes.length > 0) { + const retrievedNodeIndexes: Record = {}; + completedRequests.forEach((x) => { + retrievedNodeIndexes[x.result.node_index] = true; + }); + const pendingNodeIndexes = requiredNodeIndexes.filter((x) => { + if (!retrievedNodeIndexes[x]) return x; + return false; + }); + if (pendingNodeIndexes.length === 0) { + return Promise.resolve(completedRequests); + } + } else { + return Promise.resolve(completedRequests); + } } return Promise.reject(new Error("Failed to get auth message from threshold number of nodes")); }) @@ -64,7 +84,7 @@ export const getAuthMessageFromNodes = (params: GetAuthMessageFromNodesParams) = }; export const linkPasskey = async (params: LinkPasskeyParams) => { - const { endpoints, messages, label, passkeyPubKey, oAuthKeySignatures, keyType, passkeyAuthData } = params; + const { endpoints, messages, label, passkeyPubKey, oAuthKeySignatures, keyType, sessionData, passkeyAuthData } = params; const halfThreshold = ~~(endpoints.length / 2) + 1; if (!endpoints || endpoints.length < halfThreshold) { @@ -82,6 +102,7 @@ export const linkPasskey = async (params: LinkPasskeyParams) => { verifier_account_signature: oAuthKeySignatures[i], key_type: keyType, passkey_auth_data: passkeyAuthData, + session_data: sessionData, }), {}, { logTracingHeader: config.logRequestTracing } @@ -113,7 +134,7 @@ export const linkPasskey = async (params: LinkPasskeyParams) => { }; export const unlinkPasskey = async (params: UnLinkPasskeyParams) => { - const { endpoints, messages, passkeyPubKey, oAuthKeySignatures, keyType } = params; + const { endpoints, messages, passkeyPubKey, oAuthKeySignatures, sessionData, keyType } = params; const halfThreshold = ~~(endpoints.length / 2) + 1; if (!endpoints || endpoints.length < halfThreshold) { @@ -129,6 +150,7 @@ export const unlinkPasskey = async (params: UnLinkPasskeyParams) => { passkey_pub_key: passkeyPubKey, verifier_account_signature: oAuthKeySignatures[i], key_type: keyType, + session_data: sessionData, }), {}, { logTracingHeader: config.logRequestTracing } @@ -225,3 +247,70 @@ export const listLinkedPasskey = async (params: ListLinkedPasskeysParams) => { .catch(reject); }); }; + +export async function _linkedPasskeyRetrieveShares(params: { + serverTimeOffset: number; + ecCurve: ec; + keyType: KeyType; + allowHost: string; + network: TORUS_NETWORK_TYPE; + clientId: string; + endpoints: string[]; + nodePubkeys: INodePub[]; + indexes: number[]; + passkeyPublicKey: string; + passkeyVerifierID: string; + idToken: string; + sessionExpSecond: number; + extraParams: TorusUtilsPasskeyExtraParams; +}) { + const { endpoints, passkeyPublicKey, passkeyVerifierID, idToken, keyType, sessionExpSecond, extraParams, serverTimeOffset, ecCurve, network } = + params; + + // generate temporary private and public key that is used to secure receive shares + const sessionAuthKey = generatePrivate(); + const pubKey = getPublic(sessionAuthKey).toString("hex"); + const sessionPubX = pubKey.slice(2, 66); + const sessionPubY = pubKey.slice(66); + const promiseArrRequest: Promise>[] = []; + + const passkeyExtraParams = { ...extraParams } as TorusUtilsPasskeyExtraParams; + for (let i = 0; i < endpoints.length; i += 1) { + const p = post>( + endpoints[i], + generateJsonRPCObject(JRPC_METHODS.RETRIEVE_SHARES_WITH_LINKED_PASSKEY, { + encrypted: "yes", + key_type: keyType, + passkey_pub_key: passkeyPublicKey, + temp_pub_x: sessionPubX, + temp_pub_y: sessionPubY, + passkey_auth_data: { + verifier_id: passkeyVerifierID, + idtoken: idToken, + key_type: keyType, + session_token_exp_second: sessionExpSecond, + ...passkeyExtraParams, + }, + client_time: Math.floor(Date.now() / 1000).toString(), + }), + {}, + { logTracingHeader: config.logRequestTracing } + ); + promiseArrRequest.push(p); + } + return processShareResponse( + { + legacyMetadataHost: "", // this method only works for sapphire + serverTimeOffset, + sessionAuthKey, + enableOneKey: true, + ecCurve, + keyType, + network, + verifierParams: { verifier_id: passkeyPublicKey }, + endpoints, + isImportedShares: false, + }, + promiseArrRequest + ); +} diff --git a/src/interfaces.ts b/src/interfaces.ts index c8d7863..2a65e56 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -292,7 +292,6 @@ export interface RetrieveSharesParams { idToken: string; nodePubkeys: INodePub[]; extraParams?: TorusUtilsExtraParams; - useLinkedPasskey?: boolean; useDkg?: boolean; checkCommitment?: boolean; } diff --git a/src/passkeyConnectorInterfaces.ts b/src/passkeyConnectorInterfaces.ts index b3ff73e..4dd8eda 100644 --- a/src/passkeyConnectorInterfaces.ts +++ b/src/passkeyConnectorInterfaces.ts @@ -1,7 +1,15 @@ +import { INodePub } from "@toruslabs/constants"; + import { KeyType } from "./interfaces"; import { TorusUtilsPasskeyExtraParams } from "./TorusUtilsExtraParams"; -export type GetAuthMessageFromNodesParams = { endpoints: string[]; verifier: string; verifierId?: string; passkeyPubKey?: string }; +export type GetAuthMessageFromNodesParams = { + endpoints: string[]; + verifier: string; + verifierId?: string; + passkeyPubKey?: string; + requiredNodeIndexes?: number[]; +}; export interface AuthMessageRequestJRPCResult { message: string; @@ -23,6 +31,7 @@ export type LinkPasskeyParams = { label: string; oAuthKeySignatures: string[]; keyType: KeyType; + sessionData: string[]; passkeyAuthData?: PasskeyAuthData; }; @@ -31,6 +40,7 @@ export type UnLinkPasskeyParams = { passkeyPubKey: string; messages: string[]; oAuthKeySignatures: string[]; + sessionData: string[]; keyType: KeyType; }; export type ListLinkedPasskeysParams = { @@ -49,3 +59,13 @@ export type AuthMessageData = { message: string; nodeIndex: number; }; + +export interface RetrieveSharesWithLinkedPasskeyParams { + endpoints: string[]; + indexes: number[]; + passkeyPublicKey: string; + passkeyVerifierID: string; + idToken: string; + nodePubkeys: INodePub[]; + extraParams?: TorusUtilsPasskeyExtraParams; +} diff --git a/src/torus.ts b/src/torus.ts index a15b287..0ff27fc 100644 --- a/src/torus.ts +++ b/src/torus.ts @@ -13,6 +13,7 @@ import { curve, ec as EC } from "elliptic"; import { config } from "./config"; import { + _linkedPasskeyRetrieveShares, encodeEd25519Point, generateAddressFromPubKey, generateShares, @@ -35,6 +36,7 @@ import { v2NonceResultType, } from "./interfaces"; import log from "./loglevel"; +import { RetrieveSharesWithLinkedPasskeyParams } from "./passkeyConnectorInterfaces"; // Implement threshold logic wrappers around public APIs // of Torus nodes to handle malicious node responses @@ -112,18 +114,7 @@ class Torus { } async retrieveShares(params: RetrieveSharesParams): Promise { - const { - verifier, - verifierParams, - idToken, - nodePubkeys, - indexes, - endpoints, - useDkg, - useLinkedPasskey, - extraParams = {}, - checkCommitment = true, - } = params; + const { verifier, verifierParams, idToken, nodePubkeys, indexes, endpoints, useDkg, extraParams = {}, checkCommitment = true } = params; if (nodePubkeys.length === 0) { throw new Error("nodePubkeys param is required"); } @@ -176,7 +167,42 @@ class Torus { nodePubkeys, extraParams, checkCommitment, - useLinkedPasskey, + }); + } + + async retrieveSharesWithLinkedPasskey(params: RetrieveSharesWithLinkedPasskeyParams): Promise { + const { passkeyPublicKey, idToken, nodePubkeys, indexes, endpoints, extraParams = {}, passkeyVerifierID } = params; + if (nodePubkeys.length === 0) { + throw new Error("nodePubkeys param is required"); + } + + if (nodePubkeys.length !== indexes.length) { + throw new Error("nodePubkeys length must be same as indexes length"); + } + + if (nodePubkeys.length !== endpoints.length) { + throw new Error("nodePubkeys length must be same as endpoints length"); + } + + if (LEGACY_NETWORKS_ROUTE_MAP[this.network as TORUS_LEGACY_NETWORK_TYPE]) { + throw new Error(`retrieveSharesWithLinkedPasskey is not supported by legacy network; ${this.network}`); + } + + return _linkedPasskeyRetrieveShares({ + serverTimeOffset: this.serverTimeOffset, + ecCurve: this.ec, + keyType: this.keyType, + allowHost: this.allowHost, + network: this.network, + clientId: this.clientId, + endpoints, + indexes, + nodePubkeys, + idToken, + passkeyPublicKey, + passkeyVerifierID, + extraParams, + sessionExpSecond: Torus.sessionTime, }); }