Skip to content

Commit

Permalink
fix(identity)!: update type of private key (#803)
Browse files Browse the repository at this point in the history
* fix(identity)!: update type of private key

The new types support buffers and text, so that there is less ambiguity on strings.

* refactor(identity): use base64 as encoding for exported priv keys

re #799

* Update packages/identity/src/index.ts

Co-authored-by: Andrew Twyman <artwyman@users.noreply.github.com>

* test(identity): add more tests

re #799

---------

Co-authored-by: Andrew Twyman <artwyman@users.noreply.github.com>
  • Loading branch information
cedoor and artwyman committed Jun 18, 2024
1 parent 2830d3d commit 3572f44
Show file tree
Hide file tree
Showing 5 changed files with 161 additions and 62 deletions.
22 changes: 22 additions & 0 deletions packages/identity/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,28 @@ const { privateKey, publicKey, commitment } = new Identity()
const identity = new Identity("your-private-key")
```

\# **identity.export**(): _string_

```typescript
import { Identity } from "@semaphore-protocol/identity"

const identity = new Identity()

const privateKey = identity.export()
```

\# **identity.import**(privateKey: _string_): _Identity_

```typescript
import { Identity } from "@semaphore-protocol/identity"

const identity = new Identity()

const privateKey = identity.export()

const identity2 = Identity.import(privateKey)
```

\# **identity.signMessage**(message: _BigNumberish_): _Signature\<string>_

```typescript
Expand Down
4 changes: 2 additions & 2 deletions packages/identity/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@
},
"dependencies": {
"@zk-kit/baby-jubjub": "1.0.1",
"@zk-kit/eddsa-poseidon": "1.0.1",
"@zk-kit/utils": "1.0.0",
"@zk-kit/eddsa-poseidon": "1.0.2",
"@zk-kit/utils": "1.2.0",
"poseidon-lite": "0.2.0"
}
}
68 changes: 34 additions & 34 deletions packages/identity/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import type { Point } from "@zk-kit/baby-jubjub"
import { EdDSAPoseidon, Signature, signMessage, verifySignature } from "@zk-kit/eddsa-poseidon"
import type { BigNumberish } from "@zk-kit/utils"
import { hexadecimalToBuffer } from "@zk-kit/utils/conversions"
import { requireString } from "@zk-kit/utils/error-handlers"
import { isHexadecimal } from "@zk-kit/utils/type-checks"
import { base64ToBuffer, bufferToBase64, textToBase64 } from "@zk-kit/utils/conversions"
import { isString } from "@zk-kit/utils/type-checks"
import { poseidon2 } from "poseidon-lite/poseidon2"

/**
Expand All @@ -13,12 +12,11 @@ import { poseidon2 } from "poseidon-lite/poseidon2"
* and {@link https://www.poseidon-hash.info | Poseidon} for signatures.
* In addition, the commitment, i.e. the hash of the public key, is used to represent
* Semaphore identities in groups, adding an additional layer of privacy and security.
* The private key of the identity is stored as a hexadecimal string or text.
* The other attributes are stored as stringified bigint.
* The private key of the identity can be exported as a base64 string.
*/
export class Identity {
// The EdDSA private key, passed as a parameter or generated randomly.
private _privateKey: string
private _privateKey: string | Buffer | Uint8Array
// The secret scalar derived from the private key.
// It is used in circuits to derive the public key.
private _secretScalar: bigint
Expand All @@ -28,9 +26,8 @@ export class Identity {
private _commitment: bigint

/**
* Initializes the class attributes based on a given private key, which must be a hexadecimal string or a text.
* Hexadecimal strings must not start with '0x' or '0X'.
* If the private key is not passed as a parameter, a random hexadecimal key will be generated.
* Initializes the class attributes based on a given private key, which must be text or a buffer.
* If the private key is not passed as a parameter, a random private key will be generated.
* The EdDSAPoseidon class is used to generate the secret scalar and the public key.
* Additionally, the constructor computes a commitment of the public key using a hash function (Poseidon).
*
Expand All @@ -43,35 +40,20 @@ export class Identity {
*
* @param privateKey The private key used to derive the public key (hexadecimal or string).
*/
constructor(privateKey?: string) {
let eddsa: EdDSAPoseidon

if (privateKey) {
requireString(privateKey, "privateKey")

this._privateKey = privateKey

if (isHexadecimal(privateKey, false)) {
eddsa = new EdDSAPoseidon(hexadecimalToBuffer(privateKey))
} else {
eddsa = new EdDSAPoseidon(privateKey)
}
} else {
eddsa = new EdDSAPoseidon()

this._privateKey = eddsa.privateKey as string
}
constructor(privateKey?: string | Buffer | Uint8Array) {
const eddsa = new EdDSAPoseidon(privateKey)

this._privateKey = eddsa.privateKey
this._secretScalar = eddsa.secretScalar
this._publicKey = eddsa.publicKey
this._commitment = poseidon2(this._publicKey)
}

/**
* Returns the private key.
* @returns The private key as a string (hexadecimal or text).
* @returns The private key as a buffer or text.
*/
public get privateKey(): string {
public get privateKey(): string | Buffer | Uint8Array {
return this._privateKey
}

Expand Down Expand Up @@ -99,6 +81,28 @@ export class Identity {
return this._commitment
}

/**
* Returns the private key encoded as a base64 string.
* @returns The private key as a base64 string.
*/
public export(): string {
if (isString(this._privateKey)) {
return textToBase64(this._privateKey as string)
}

return bufferToBase64(this.privateKey as Buffer | Uint8Array)
}

/**
* Returns a Semaphore identity based on a private key encoded as a base64 string.
* The private key will be converted to a buffer, regardless of its original type.
* @param privateKey The private key as a base64 string.
* @returns The Semaphore identity.
*/
static import(privateKey: string): Identity {
return new Identity(base64ToBuffer(privateKey))
}

/**
* Generates a signature for a given message using the private key.
* This method demonstrates how to sign a message and could be used
Expand All @@ -112,11 +116,7 @@ export class Identity {
* @returns A {@link https://zkkit.pse.dev/types/_zk_kit_eddsa_poseidon.Signature.html | Signature} object containing the signature components.
*/
public signMessage(message: BigNumberish): Signature<bigint> {
const privateKey = isHexadecimal(this.privateKey, false)
? hexadecimalToBuffer(this.privateKey)
: this.privateKey

return signMessage(privateKey, message)
return signMessage(this.privateKey, message)
}

/**
Expand Down
108 changes: 88 additions & 20 deletions packages/identity/tests/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,111 @@
import { derivePublicKey, deriveSecretScalar } from "@zk-kit/eddsa-poseidon"
import { poseidon2 } from "poseidon-lite"
import { Identity } from "../src"

describe("Identity", () => {
const privateKeyText = "secret"
const privateKeyHexadecimal = "dd998334940df8931b76d899fdb189415f7ff4280599f03a7574725a166aad7d"
const privateKeyBuffer = Buffer.from("another secret")

describe("# Identity", () => {
it("Should create an identity with a random secret (private key)", () => {
const identity = new Identity()
const privateKey = Buffer.from(identity.privateKey)

expect(typeof identity.privateKey).toBe("string")
expect(identity.privateKey).toHaveLength(64)
expect(typeof identity.secretScalar).toBe("bigint")
expect(identity.publicKey).toHaveLength(2)
expect(typeof identity.commitment).toBe("bigint")
expect(identity.privateKey).toHaveLength(32)
expect(identity.secretScalar).toBe(deriveSecretScalar(privateKey))
expect(identity.publicKey).toStrictEqual(derivePublicKey(privateKey))
expect(identity.commitment).toBe(poseidon2(identity.publicKey))
})

it("Should create deterministic identities from a secret text (private key)", () => {
it("Should create a deterministic identity from a secret text (private key)", () => {
const identity = new Identity(privateKeyText)

expect(typeof identity.privateKey).toBe("string")
expect(typeof identity.secretScalar).toBe("bigint")
expect(identity.publicKey).toHaveLength(2)
expect(typeof identity.commitment).toBe("bigint")
expect(identity.secretScalar).toBe(deriveSecretScalar(privateKeyText))
expect(identity.publicKey).toStrictEqual(derivePublicKey(privateKeyText))
expect(identity.commitment).toBe(poseidon2(identity.publicKey))
})

it("Should create deterministic identities from a secret hexadecimal (private key)", () => {
const identity = new Identity(privateKeyHexadecimal)
it("Should create a deterministic identity from a secret buffer (private key)", () => {
const identity = new Identity(privateKeyBuffer)

expect(typeof identity.privateKey).toBe("string")
expect(identity.privateKey).toHaveLength(64)
expect(typeof identity.secretScalar).toBe("bigint")
expect(identity.publicKey).toHaveLength(2)
expect(typeof identity.commitment).toBe("bigint")
expect(identity.secretScalar).toBe(deriveSecretScalar(privateKeyBuffer))
expect(identity.publicKey).toStrictEqual(derivePublicKey(privateKeyBuffer))
expect(identity.commitment).toBe(poseidon2(identity.publicKey))
})

it("Should create the same identity if the private key is the same", () => {
const identity = new Identity()

const identity2 = new Identity(identity.privateKey)

expect(identity.privateKey).toStrictEqual(identity2.privateKey)
expect(identity.secretScalar).toBe(identity2.secretScalar)
expect(identity.publicKey).toStrictEqual(identity2.publicKey)
expect(identity.commitment).toBe(identity2.commitment)
})

it("Should throw an error if the private key is not a string", () => {
const fun = () => new Identity(32 as any)

expect(fun).toThrow("Parameter 'privateKey' is not a string, received type: number")
expect(fun).toThrow("Parameter 'privateKey' is none of the following types: Buffer, Uint8Array, string")
})
})

describe("# export", () => {
it("Should export an identity where the private key is a buffer", () => {
const identity = new Identity(privateKeyBuffer)

const privateKey = identity.export()

expect(typeof privateKey).toBe("string")
expect(Buffer.from(privateKey, "base64")).toStrictEqual(privateKeyBuffer)
})

it("Should export an identity where the private key is text", () => {
const identity = new Identity(privateKeyBuffer)

const privateKey = identity.export()

expect(typeof privateKey).toBe("string")
expect(Buffer.from(privateKey, "base64")).toStrictEqual(privateKeyBuffer)
})
})

describe("# import", () => {
it("Should import an identity with a private key of buffer type", () => {
const identity = new Identity(privateKeyBuffer)
const privateKey = identity.export()

const identity2 = Identity.import(privateKey)

expect(identity2.privateKey).toStrictEqual(identity.privateKey)
expect(identity2.secretScalar).toBe(identity.secretScalar)
expect(identity2.publicKey).toStrictEqual(identity.publicKey)
expect(identity2.commitment).toBe(identity.commitment)
})

it("Should import an identity with a private key of text type", () => {
const identity = new Identity(privateKeyText)
const privateKey = identity.export()

const identity2 = Identity.import(privateKey)

expect(identity2.privateKey).toStrictEqual(Buffer.from(identity.privateKey))
expect(identity2.secretScalar).toBe(identity.secretScalar)
expect(identity2.publicKey).toStrictEqual(identity.publicKey)
expect(identity2.commitment).toBe(identity.commitment)
})

it("Should import an identity generated from a random private key", () => {
const identity = new Identity()
const privateKey = identity.export()

const identity2 = Identity.import(privateKey)

expect(identity2.privateKey).toStrictEqual(Buffer.from(identity.privateKey))
expect(identity2.secretScalar).toBe(identity.secretScalar)
expect(identity2.publicKey).toStrictEqual(identity.publicKey)
expect(identity2.commitment).toBe(identity.commitment)
})
})

Expand All @@ -63,7 +131,7 @@ describe("Identity", () => {
})

it("Should verify a signature with hexadecimal private key", () => {
const identity = new Identity(privateKeyHexadecimal)
const identity = new Identity(privateKeyBuffer)

const signature = identity.signMessage("message")

Expand Down
21 changes: 15 additions & 6 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6394,8 +6394,8 @@ __metadata:
"@rollup/plugin-node-resolve": "npm:^15.2.3"
"@rollup/plugin-typescript": "npm:^11.1.6"
"@zk-kit/baby-jubjub": "npm:1.0.1"
"@zk-kit/eddsa-poseidon": "npm:1.0.1"
"@zk-kit/utils": "npm:1.0.0"
"@zk-kit/eddsa-poseidon": "npm:1.0.2"
"@zk-kit/utils": "npm:1.2.0"
poseidon-lite: "npm:0.2.0"
rimraf: "npm:^5.0.5"
rollup: "npm:^4.12.0"
Expand Down Expand Up @@ -8487,14 +8487,14 @@ __metadata:
languageName: node
linkType: hard

"@zk-kit/eddsa-poseidon@npm:1.0.1":
version: 1.0.1
resolution: "@zk-kit/eddsa-poseidon@npm:1.0.1"
"@zk-kit/eddsa-poseidon@npm:1.0.2":
version: 1.0.2
resolution: "@zk-kit/eddsa-poseidon@npm:1.0.2"
dependencies:
"@zk-kit/baby-jubjub": "npm:1.0.1"
"@zk-kit/utils": "npm:1.0.0"
buffer: "npm:6.0.3"
checksum: 10/337db4a73bd58680e462b6a82f95321406b3cffce7dce0413f7c857159798b8d85120750de5f5e4c7f2bcc102a02a5e3145302aeb9110a3b6172e6bd3bc29f43
checksum: 10/4b4e984a96c5dbc95a8cf36ceb8b3712e37291c1473d01b0e22c15a33311e9cf88c86175d878dacda1852cc905cd2074aaa76defeaed33490be3964ca7a53372
languageName: node
linkType: hard

Expand Down Expand Up @@ -8525,6 +8525,15 @@ __metadata:
languageName: node
linkType: hard

"@zk-kit/utils@npm:1.2.0":
version: 1.2.0
resolution: "@zk-kit/utils@npm:1.2.0"
dependencies:
buffer: "npm:^6.0.3"
checksum: 10/4c0b37d64b28a6cc33c901a0c59325b1fe9c31e6519eaefe4aa6028ae9cb85e97f047976942875face030a1835d5c955ea546d7dc4fcf9d35df79192ee2502f3
languageName: node
linkType: hard

"JSONStream@npm:1.3.2":
version: 1.3.2
resolution: "JSONStream@npm:1.3.2"
Expand Down

0 comments on commit 3572f44

Please sign in to comment.