@0xpolygonid/js-sdk
Version:
SDK to work with Polygon ID
190 lines (189 loc) • 8.38 kB
JavaScript
import { getRandomBytes } from '@iden3/js-crypto';
import { AuthMethod } from '../types';
import { mergeObjects } from '../../utils';
import { getUnixTimestamp } from '@iden3/js-iden3-core';
import { AcceptJwsAlgorithms, defaultAcceptProfile, MediaType } from '../constants';
import { ethers } from 'ethers';
import { packZkpProof, prepareZkpProof } from '../../storage/blockchain/common';
/**
* Groups the ZeroKnowledgeProofRequest objects based on their groupId.
* Returns a Map where the key is the groupId and the value is an object containing the query and linkNonce.
*
* @param requestScope - An array of ZeroKnowledgeProofRequest objects.
* @returns A Map<number, { query: ZeroKnowledgeProofQuery; linkNonce: number }> representing the grouped queries.
*/
const getGroupedQueries = (requestScope) => requestScope.reduce((acc, proofReq) => {
const groupId = proofReq.query.groupId;
if (!groupId) {
return acc;
}
const existedData = acc.get(groupId);
if (!existedData) {
const seed = getRandomBytes(12);
const dataView = new DataView(seed.buffer);
const linkNonce = dataView.getUint32(0);
acc.set(groupId, { query: proofReq.query, linkNonce });
return acc;
}
const credentialSubject = mergeObjects(existedData.query.credentialSubject, proofReq.query.credentialSubject);
acc.set(groupId, {
...existedData,
query: {
skipClaimRevocationCheck: existedData.query.skipClaimRevocationCheck || proofReq.query.skipClaimRevocationCheck,
...existedData.query,
credentialSubject
}
});
return acc;
}, new Map());
/**
* Processes zero knowledge proof requests.
*
* @param to - The identifier of the recipient.
* @param requests - An array of zero knowledge proof requests.
* @param from - The identifier of the sender.
* @param proofService - The proof service.
* @param opts - Additional options for processing the requests.
* @returns A promise that resolves to an array of zero knowledge proof responses.
*/
export const processZeroKnowledgeProofRequests = async (to, requests, from, proofService, opts) => {
const requestScope = requests ?? [];
const combinedQueries = getGroupedQueries(requestScope);
const groupedCredentialsCache = new Map();
const zkpResponses = [];
for (const proofReq of requestScope) {
if (!opts.supportedCircuits.includes(proofReq.circuitId)) {
throw new Error(`Circuit ${proofReq.circuitId} is not allowed`);
}
const query = proofReq.query;
const groupId = query.groupId;
const combinedQueryData = combinedQueries.get(groupId);
if (groupId) {
if (!combinedQueryData) {
throw new Error(`Invalid group id ${query.groupId}`);
}
const combinedQuery = combinedQueryData.query;
if (!groupedCredentialsCache.has(groupId)) {
const credWithRevStatus = await proofService.findCredentialByProofQuery(to, combinedQueryData.query);
if (!credWithRevStatus.cred) {
throw new Error(`Credential not found for query ${JSON.stringify(combinedQuery)}`);
}
groupedCredentialsCache.set(groupId, credWithRevStatus);
}
}
const credWithRevStatus = groupedCredentialsCache.get(groupId);
const zkpRes = await proofService.generateProof(proofReq, to, {
verifierDid: from,
challenge: opts.challenge,
skipRevocation: Boolean(query.skipClaimRevocationCheck),
credential: credWithRevStatus?.cred,
credentialRevocationStatus: credWithRevStatus?.revStatus,
linkNonce: combinedQueryData?.linkNonce ? BigInt(combinedQueryData.linkNonce) : undefined
});
zkpResponses.push(zkpRes);
}
return zkpResponses;
};
/**
* Processes auth proof requests.
*
* @param to - The identifier of the recipient.
* @param proofService - The proof service.
* @param opts - Additional options for processing the requests.
* @returns A promise that resolves to an auth proof response.
*/
export const processProofAuth = async (to, proofService, opts) => {
if (!opts.acceptProfile) {
opts.acceptProfile = defaultAcceptProfile;
}
switch (opts.acceptProfile.env) {
case MediaType.ZKPMessage:
if (!opts.acceptProfile.circuits) {
throw new Error('Circuit not specified in accept profile');
}
for (const circuitId of opts.acceptProfile.circuits) {
if (!opts.supportedCircuits.includes(circuitId)) {
throw new Error(`Circuit ${circuitId} is not supported`);
}
if (!opts.senderAddress) {
throw new Error('Sender address is not provided');
}
const challengeAuth = calcChallengeAuthV2(opts.senderAddress, opts.zkpResponses);
const zkpRes = await proofService.generateAuthProof(circuitId, to, { challenge: challengeAuth });
return {
authProof: {
authMethod: AuthMethod.AUTHV2,
zkp: zkpRes
}
};
}
throw new Error(`Auth method is not supported`);
case MediaType.SignedMessage:
if (!opts.acceptProfile.alg || opts.acceptProfile.alg.length === 0) {
throw new Error('Algorithm not specified');
}
if (opts.acceptProfile.alg[0] === AcceptJwsAlgorithms.ES256KR) {
return {
authProof: {
authMethod: AuthMethod.ETH_IDENTITY,
userDid: to
}
};
}
throw new Error(`Algorithm ${opts.acceptProfile.alg[0]} not supported`);
default:
throw new Error('Accept env not supported');
}
};
/**
* Processes a ZeroKnowledgeProofResponse object and prepares it for further use.
* @param zkProof - The ZeroKnowledgeProofResponse object containing the proof data.
* @returns An object containing the requestId, zkProofEncoded, and metadata.
*/
export const processProofResponse = (zkProof) => {
const requestId = zkProof.id;
const inputs = zkProof.pub_signals;
const emptyBytes = '0x';
if (inputs.length === 0) {
return { requestId, zkProofEncoded: emptyBytes, metadata: emptyBytes };
}
const preparedZkpProof = prepareZkpProof(zkProof.proof);
const zkProofEncoded = packZkpProof(inputs, preparedZkpProof.a, preparedZkpProof.b, preparedZkpProof.c);
const metadata = emptyBytes;
return { requestId, zkProofEncoded, metadata };
};
/**
* Calculates the challenge authentication V2 value.
* @param senderAddress - The address of the sender.
* @param zkpResponses - An array of ZeroKnowledgeProofResponse objects.
* @returns A bigint representing the challenge authentication value.
*/
export const calcChallengeAuthV2 = (senderAddress, zkpResponses) => {
const responses = zkpResponses.map((zkpResponse) => {
const response = processProofResponse(zkpResponse);
return {
requestId: response.requestId,
proof: response.zkProofEncoded,
metadata: response.metadata
};
});
return (BigInt(ethers.keccak256(new ethers.AbiCoder().encode(['address', '(uint256 requestId,bytes proof,bytes metadata)[]'], [senderAddress, responses]))) & BigInt('0x0fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'));
};
/**
* Packs metadata into a string format suitable for encoding in a transaction.
* @param metas - An array of objects containing key-value pairs to be packed.
* @returns A string representing the packed metadata.
*/
export const packMetadatas = (metas) => {
return new ethers.AbiCoder().encode(['tuple(' + 'string key,' + 'bytes value' + ')[]'], [metas]);
};
/**
* Verifies that the expires_time field of a message is not in the past. Throws an error if it is.
*
* @param message - Basic message to verify.
*/
export const verifyExpiresTime = (message) => {
if (message?.expires_time && message.expires_time < getUnixTimestamp(new Date())) {
throw new Error('Message expired');
}
};