diff --git a/packages/identity/src/identity.ts b/packages/identity/src/identity.ts index fe2b678..38f5625 100644 --- a/packages/identity/src/identity.ts +++ b/packages/identity/src/identity.ts @@ -1,62 +1,70 @@ import { Identity, SerializedIdentity } from "@zk-kit/types" -import * as bigintConversion from "bigint-conversion" -import * as ciromlibjs from "circomlibjs" -import { genIdentityFromMessage, genRandomIdentity } from "./strategies" -import { Fq } from "./utils" - -const poseidonHash = (data: Array): bigint => ciromlibjs.poseidon(data) - -export enum Strategy { - RANDOM, - MESSAGE, - SERIALIZED -} +import { hexToBigint } from "bigint-conversion" +import { poseidon } from "circomlibjs" +import { genIdentityFromMessage, genRandomIdentity, Strategy } from "./strategies" +import { Fq, parseSerializedIdentity } from "./utils" export enum SecretType { - GENERIC, // generic secret, composed of identityNullifier and identityTrapdoor - MULTIPART_SECRET // multipart secret, composed from multiple parts dependent on the spam threshold + GENERIC, // Generic secret, composed of identityNullifier and identityTrapdoor. + MULTIPART_SECRET // Multipart secret, composed from multiple parts dependent on the spam threshold. } -class ZkIdentity { - private identityTrapdoor: bigint - private identityNullifier: bigint - private secret: Array = [] - private multipartSecret: Array = [] +export default class ZkIdentity { + private _identityTrapdoor: bigint + private _identityNullifier: bigint + + private _secret: bigint[] = [] + private _multipartSecret: bigint[] = [] + /** - * Generates new ZkIdentity - * @param strategy strategy for identity generation - * @param metadata additional data needed to create identity for given strategy + * Generates new ZkIdentity. + * @param strategy strategy for identity generation. + * @param metadata additional data needed to create identity for given strategy. * @returns */ constructor(strategy: Strategy = Strategy.RANDOM, metadata?: string | SerializedIdentity) { - if (strategy === Strategy.RANDOM) { - const { identityTrapdoor, identityNullifier } = genRandomIdentity() - this.identityTrapdoor = identityTrapdoor - this.identityNullifier = identityNullifier - this.genSecret() - this.genMultipartSecret() - } else if (strategy === Strategy.MESSAGE) { - const { identityTrapdoor, identityNullifier } = genIdentityFromMessage(metadata as string) - this.identityTrapdoor = identityTrapdoor - this.identityNullifier = identityNullifier - this.genSecret() - this.genMultipartSecret() - } else if (strategy === Strategy.SERIALIZED) { - const { identityNullifier, identityTrapdoor, secret, multipartSecret } = metadata as SerializedIdentity - this.identityNullifier = bigintConversion.hexToBigint(identityNullifier) - this.identityTrapdoor = bigintConversion.hexToBigint(identityTrapdoor) - this.secret = secret.map((item) => bigintConversion.hexToBigint(item)) - this.multipartSecret = multipartSecret.map((item) => bigintConversion.hexToBigint(item)) - } else throw new Error("provided strategy is not supported") - } + switch (strategy) { + case Strategy.RANDOM: { + const { identityTrapdoor, identityNullifier } = genRandomIdentity() - // Secret and identity generation + this._identityTrapdoor = identityTrapdoor + this._identityNullifier = identityNullifier + this._secret = [this._identityNullifier, this._identityTrapdoor] + this._genMultipartSecret() - /** - * Generate generic secret. To be used by Semaphore related apps. - */ - genSecret(): void { - this.secret = [this.identityNullifier, this.identityTrapdoor] + break + } + case Strategy.MESSAGE: { + const { identityTrapdoor, identityNullifier } = genIdentityFromMessage(metadata as string) + + this._identityTrapdoor = identityTrapdoor + this._identityNullifier = identityNullifier + this._secret = [this._identityNullifier, this._identityTrapdoor] + this._genMultipartSecret() + + break + } + case Strategy.SERIALIZED: { + if (!metadata) { + throw new Error("Metadata is not defined") + } + + if (typeof metadata === "string") { + metadata = parseSerializedIdentity(metadata) + } + + const { identityNullifier, identityTrapdoor, secret, multipartSecret } = metadata + + this._identityNullifier = hexToBigint(identityNullifier) + this._identityTrapdoor = hexToBigint(identityTrapdoor) + this._secret = secret.map((item) => hexToBigint(item)) + this._multipartSecret = multipartSecret.map((item) => hexToBigint(item)) + + break + } + default: + throw new Error("Provided strategy is not supported") + } } /** @@ -64,99 +72,77 @@ class ZkIdentity { * @param parts The number of parts that the secret should be composed of, * corresponding to the spam threshold of the protocol */ - genMultipartSecret(parts = 2): void { + private _genMultipartSecret(parts = 2): void { if (parts < 2) throw new Error("Invalid number of parts") - const initialComponent = Fq.pow(this.identityTrapdoor, this.identityNullifier) - this.multipartSecret = [initialComponent] + const initialComponent = Fq.pow(this._identityTrapdoor, this._identityNullifier) + + this._multipartSecret = [initialComponent] + for (let i = 1; i < parts; i += 1) { - this.multipartSecret.push(Fq.pow(initialComponent, BigInt(i + 1))) + this._multipartSecret.push(Fq.pow(initialComponent, BigInt(i + 1))) } } + /** + * Return the raw user identity, composed of identityNullifier and identityTrapdoor. + * @returns Identity + */ + public getIdentity(): Identity { + return { + identityNullifier: this._identityNullifier, + identityTrapdoor: this._identityTrapdoor + } + } + + public getNullifier(): bigint { + return this._identityNullifier + } + + public getSecret(): bigint[] { + return this._secret + } + + public getMultipartSecret(): bigint[] { + return this._multipartSecret + } + + public getSecretHash(): bigint { + return poseidon(this._secret) + } + + public getMultipartSecretHash(): bigint { + return poseidon(this._multipartSecret) + } + /** * Generate commitment from secret * @param secretType The secret type for which to generate identity commitment * @returns identity commitment */ - genIdentityCommitment(secretType: SecretType = SecretType.GENERIC): bigint { - let secretHash = this.getSecretHash() - if (secretType === SecretType.MULTIPART_SECRET) { - secretHash = this.getMultipartSecretHash() + public genIdentityCommitment(secretType: SecretType = SecretType.GENERIC): bigint { + switch (secretType) { + case SecretType.GENERIC: + return poseidon([this.getSecretHash()]) + case SecretType.MULTIPART_SECRET: + return poseidon([this.getMultipartSecretHash()]) + default: + throw new Error("Provided secret type is not supported") } - return poseidonHash([secretHash]) } - // Serialization - /** * Serializes the `identityNullifier`, `identityTrapdoor` and `secret` from the identity * @returns stringified serialized identity */ - serializeIdentity(): string { + public serializeIdentity(): string { const data: SerializedIdentity = { - identityNullifier: this.identityNullifier.toString(16), - identityTrapdoor: this.identityTrapdoor.toString(16), - secret: this.secret.map((item) => item.toString(16)), - multipartSecret: this.multipartSecret.map((item) => item.toString(16)) + identityNullifier: this._identityNullifier.toString(16), + identityTrapdoor: this._identityTrapdoor.toString(16), + secret: this._secret.map((item) => item.toString(16)), + multipartSecret: this._multipartSecret.map((item) => item.toString(16)) } + return JSON.stringify(data) } - - /** - * Unserialize serialized identity - * @param serialisedIdentity - * @returns - */ - static genFromSerialized(serialisedIdentity: string): ZkIdentity { - const data = JSON.parse(serialisedIdentity) - if ( - !("identityNullifier" in data) || - !("identityTrapdoor" in data) || - !("secret" in data) || - !("multipartSecret" in data) - ) - throw new Error("Wrong input identity") - return new ZkIdentity(Strategy.SERIALIZED, { - identityNullifier: data.identityNullifier, - identityTrapdoor: data.identityTrapdoor, - secret: data.secret, - multipartSecret: data.multipartSecret - }) - } - - // Getters - - /** - * Return the raw user identity, composed of identityNullifier and identityTrapdoor. - * @returns Identity - */ - getIdentity(): Identity { - return { - identityNullifier: this.identityNullifier, - identityTrapdoor: this.identityTrapdoor - } - } - - getNullifier(): bigint { - return this.identityNullifier - } - - getSecret(): Array { - return this.secret - } - - getMultipartSecret(): Array { - return this.multipartSecret - } - - getSecretHash(): bigint { - return poseidonHash(this.secret) - } - - getMultipartSecretHash(): bigint { - return poseidonHash(this.multipartSecret) - } } - -export default ZkIdentity diff --git a/packages/identity/src/index.ts b/packages/identity/src/index.ts index 15ee151..24fbf05 100644 --- a/packages/identity/src/index.ts +++ b/packages/identity/src/index.ts @@ -1,4 +1,5 @@ import { Identity } from "@zk-kit/types" -import ZkIdentity, { Strategy, SecretType } from "./identity" +import ZkIdentity, { SecretType } from "./identity" +import { Strategy } from "./strategies" export { ZkIdentity, Identity, Strategy, SecretType } diff --git a/packages/identity/src/strategies.ts b/packages/identity/src/strategies.ts index 1a66539..4026115 100644 --- a/packages/identity/src/strategies.ts +++ b/packages/identity/src/strategies.ts @@ -1,39 +1,34 @@ -import * as crypto from "crypto" -import * as bigintConversion from "bigint-conversion" -import { sha256 as _sha256 } from "js-sha256" import { Identity } from "@zk-kit/types" +import { hexToBigint } from "bigint-conversion" +import { genRandomNumber, sha256 } from "./utils" -const genRandomNumber = (numBytes = 31): bigint => bigintConversion.bufToBigint(crypto.randomBytes(numBytes)) +export enum Strategy { + RANDOM, + MESSAGE, + SERIALIZED +} /** - * - * @returns Identity + * Generates a random identity. + * @returns Identity The generated identity. */ -const genRandomIdentity = (): Identity => ({ - identityNullifier: genRandomNumber(31), - identityTrapdoor: genRandomNumber(31) -}) - -/** - * - * @param metadata { signedMessage } from which to create identity - * @returns Identity - */ -const genIdentityFromMessage = (message: string): Identity => { - const sha256 = (message: string): string => { - const hash = _sha256.create() - hash.update(message) - return hash.hex() - } - - const messageHash = sha256(message) - const identityNullifier = bigintConversion.hexToBigint(sha256(`${messageHash}identity_nullifier`)) - const identityTrapdoor = bigintConversion.hexToBigint(sha256(`${messageHash}identity_trapdoor`)) - +export function genRandomIdentity(): Identity { return { - identityTrapdoor, - identityNullifier + identityNullifier: genRandomNumber(), + identityTrapdoor: genRandomNumber() } } -export { genRandomIdentity, genIdentityFromMessage, genRandomNumber } +/** + * Generate an deterministic identity from an external message. + * @param message The message from which to create identity. + * @returns Identity The generated identity. + */ +export function genIdentityFromMessage(message: string): Identity { + const messageHash = sha256(message) + + return { + identityNullifier: hexToBigint(sha256(`${messageHash}identity_nullifier`)), + identityTrapdoor: hexToBigint(sha256(`${messageHash}identity_trapdoor`)) + } +} diff --git a/packages/identity/src/utils.ts b/packages/identity/src/utils.ts index 9cf562e..7ff2288 100644 --- a/packages/identity/src/utils.ts +++ b/packages/identity/src/utils.ts @@ -1,5 +1,51 @@ +import { randomBytes } from "@ethersproject/random" +import { SerializedIdentity } from "@zk-kit/types" +import { bufToBigint } from "bigint-conversion" import { ZqField } from "ffjavascript" +import { sha256 as _sha256 } from "js-sha256" -export const SNARK_FIELD_SIZE = BigInt("21888242871839275222246405745257275088548364400416034343698204186575808495617") +const SNARK_FIELD_SIZE = BigInt("21888242871839275222246405745257275088548364400416034343698204186575808495617") export const Fq = new ZqField(SNARK_FIELD_SIZE) + +/** + * Returns an hexadecimal sha256 hash of the message passed as parameter. + * @param message The string to hash. + * @returns The hexadecimal hash of the message. + */ +export function sha256(message: string): string { + const hash = _sha256.create() + + hash.update(message) + + return hash.hex() +} + +/** + * Generates a random big number. + * @param numberOfBytes The number of bytes of the number. + * @returns The generated random number. + */ +export function genRandomNumber(numberOfBytes = 31): bigint { + return bufToBigint(randomBytes(numberOfBytes)) +} + +/** + * Parses a string containing the serialized identity parameters. + * @param serializedIdentity The serialized identity string. + * @returns The serialized identity parameters. + */ +export function parseSerializedIdentity(serializedIdentity: string): SerializedIdentity { + const data = JSON.parse(serializedIdentity) + + if ( + !("identityNullifier" in data) || + !("identityTrapdoor" in data) || + !("secret" in data) || + !("multipartSecret" in data) + ) { + throw new Error("Wrong input identity") + } + + return data +} diff --git a/packages/identity/tests/index.test.ts b/packages/identity/tests/index.test.ts index 464407d..99a6098 100644 --- a/packages/identity/tests/index.test.ts +++ b/packages/identity/tests/index.test.ts @@ -1,58 +1,59 @@ import { Strategy, ZkIdentity } from "../src" -describe("Semaphore identity", () => { +describe("ZK identity", () => { describe("Create identity", () => { - it("Should create a Semaphore identity", async () => { - const identity: ZkIdentity = new ZkIdentity() + it("Should not create a ZK identity if the strategy is wrong", () => { + const fun = () => new ZkIdentity("wrong" as any) - expect(typeof identity).toBe("object") + expect(fun).toThrow("strategy is not supported") }) - it("Should create a Semaphore identity with a message strategy", async () => { - const identity: ZkIdentity = new ZkIdentity(Strategy.MESSAGE, "message") + it("Should create a ZK identity", () => { + const identity = new ZkIdentity() - expect(typeof identity).toBe("object") - }) - - it("Should generate secret from identity", async () => { - const identity: ZkIdentity = new ZkIdentity() - identity.genSecret() const identitySecret = identity.getSecret() + const identityMultipartSecret = identity.getMultipartSecret() expect(identitySecret).toHaveLength(2) expect(typeof identitySecret).toBe("object") + expect(identityMultipartSecret).toHaveLength(2) + expect(typeof identityMultipartSecret).toBe("object") + expect(typeof identity).toBe("object") }) - it("Should generate multipart secret", async () => { - const secretParts = 5 - const identity: ZkIdentity = new ZkIdentity() - identity.genMultipartSecret(secretParts) - const identitySecret = identity.getMultipartSecret() + it("Should create a ZK identity with a message strategy", () => { + const identity = new ZkIdentity(Strategy.MESSAGE, "message") - expect(identitySecret).toHaveLength(5) - expect(typeof identitySecret).toBe("object") + expect(typeof identity).toBe("object") }) - it("Should generate identity commitment from identity", async () => { - const identity: ZkIdentity = new ZkIdentity() - const identityCommitment: bigint = identity.genIdentityCommitment() + it("Should not generate identity commitment if the secret type is wrong", () => { + const identity = new ZkIdentity() + const fun = () => identity.genIdentityCommitment("wrong" as any) + + expect(fun).toThrow("secret type is not supported") + }) + + it("Should generate identity commitment", () => { + const identity = new ZkIdentity() + const identityCommitment = identity.genIdentityCommitment() expect(typeof identityCommitment).toBe("bigint") }) - it("Should serialize identity", async () => { - const identity: ZkIdentity = new ZkIdentity() - const serialized: string = identity.serializeIdentity() + it("Should serialize an identity", () => { + const identity = new ZkIdentity() + const serialized = identity.serializeIdentity() expect(typeof serialized).toBe("string") }) - it("Should unserialize identity", async () => { - const identity: ZkIdentity = new ZkIdentity() - const serialized: string = identity.serializeIdentity() - const unserialized: ZkIdentity = ZkIdentity.genFromSerialized(serialized) + it("Should unserialize an identity", () => { + const identity1 = new ZkIdentity() + const serialized = identity1.serializeIdentity() + const identity2 = new ZkIdentity(Strategy.SERIALIZED, serialized) - expect(unserialized).toStrictEqual(identity) + expect(identity2).toStrictEqual(identity1) }) }) })