Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 38 additions & 8 deletions src/keys/PrivateKey.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { bytesToHex, equalBytes } from "@noble/ciphers/utils";

import type { EllipticCurve } from "../config.js";
import { ECIES_CONFIG, type EllipticCurve } from "../config.js";

import {
decodeHex,
Expand All @@ -13,35 +13,54 @@ import {
import { PublicKey } from "./PublicKey.js";

export class PrivateKey {
public static fromHex(hex: string, curve?: EllipticCurve): PrivateKey {
/**
* Creates a `PrivateKey` instance from a hexadecimal string.
* @param hex - The hexadecimal string representing the private key.
* @param curve - (optional) The elliptic curve to use (default: `ECIES_CONFIG.ellipticCurve`).
* @returns A new `PrivateKey` instance.
*/
public static fromHex(
hex: string,
curve: EllipticCurve = ECIES_CONFIG.ellipticCurve
): PrivateKey {
return new PrivateKey(decodeHex(hex), curve);
}

private readonly curve?: EllipticCurve;
private readonly curve: EllipticCurve;
private readonly data: Uint8Array;
public readonly publicKey: PublicKey;

/**
* @description
* In version 0.4.18, `Buffer` is returned when available, otherwise `Uint8Array`.
* From version 0.5.0, `Uint8Array` will be returned instead of `Buffer`.
* From version 0.5.0, `Uint8Array` is returned instead of `Buffer`.
*/
get secret(): Uint8Array {
return this.data;
}

constructor(secret?: Uint8Array, curve?: EllipticCurve) {
/**
* Constructs a `PrivateKey` instance from a byte array or generates a new random private key if no argument is provided.
* @param secret - (optional) The byte array representing the private key. If not provided, a new random private key will be generated.
* @param curve - (optional) The elliptic curve to use (default: `ECIES_CONFIG.ellipticCurve`).
* @throws Will throw an error if the provided `secret` is not a valid private key for the specified curve.
*/
constructor(secret?: Uint8Array, curve: EllipticCurve = ECIES_CONFIG.ellipticCurve) {
this.curve = curve;
if (secret === undefined) {
this.data = getValidSecret(curve);
} else if (isValidPrivateKey(secret, curve)) {
} else if (isValidPrivateKey(curve, secret)) {
this.data = secret;
} else {
throw new Error("Invalid private key");
}
this.publicKey = new PublicKey(getPublicKey(this.data, curve), curve);
this.publicKey = new PublicKey(getPublicKey(curve, this.data), curve);
}

/**
* Converts the private key to a hexadecimal string.
* @returns The private key as a hexadecimal string.
*/
public toHex(): string {
return bytesToHex(this.data);
}
Expand All @@ -67,10 +86,21 @@ export class PrivateKey {
return getSharedKey(senderPoint, sharedPoint);
}

/**
* Multiplies the private key with a public key to derive a shared point.
* @param pk - The public key to multiply with.
* @param compressed - (default: `false`) Whether to use compressed or uncompressed public keys (secp256k1 only).
* @returns The shared point as a Uint8Array.
*/
public multiply(pk: PublicKey, compressed: boolean = false): Uint8Array {
return getSharedPoint(this.data, pk.toBytes(true), compressed, this.curve);
return getSharedPoint(this.curve, this.data, pk.toBytes(true), compressed);
}

/**
* Compares this private key with another for equality.
* @param other - The other `PrivateKey` to compare with.
* @returns `true` if the private keys are equal, `false` otherwise.
*/
public equals(other: PrivateKey): boolean {
return equalBytes(this.data, other.data);
}
Expand Down
41 changes: 35 additions & 6 deletions src/keys/PublicKey.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
import { bytesToHex, equalBytes } from "@noble/ciphers/utils";

import type { EllipticCurve } from "../config.js";
import { ECIES_CONFIG, type EllipticCurve } from "../config.js";

import { convertPublicKeyFormat, getSharedKey, hexToPublicKey } from "../utils/index.js";
import type { PrivateKey } from "./PrivateKey.js";

export class PublicKey {
public static fromHex(hex: string, curve?: EllipticCurve): PublicKey {
return new PublicKey(hexToPublicKey(hex, curve), curve);
/**
* Creates a `PublicKey` instance from a hexadecimal string.
* @param hex - The hexadecimal string representing the public key.
* @param curve - (optional) The elliptic curve to use (default: `ECIES_CONFIG.ellipticCurve`).
* @returns A new `PublicKey` instance.
*/
public static fromHex(
hex: string,
curve: EllipticCurve = ECIES_CONFIG.ellipticCurve
): PublicKey {
return new PublicKey(hexToPublicKey(curve, hex), curve);
}

private readonly data: Uint8Array; // always compressed if secp256k1
Expand All @@ -17,18 +26,33 @@ export class PublicKey {
return this.dataUncompressed !== null ? this.dataUncompressed : this.data;
}

constructor(data: Uint8Array, curve?: EllipticCurve) {
/**
* Constructs a `PublicKey` instance from a byte array.
* @param data - The byte array representing the public key (compressed or uncompressed if secp256k1).
* @param curve - (optional) The elliptic curve to use (default: `ECIES_CONFIG.ellipticCurve`).
*/
constructor(data: Uint8Array, curve: EllipticCurve = ECIES_CONFIG.ellipticCurve) {
// data can be either compressed or uncompressed if secp256k1
const compressed = convertPublicKeyFormat(data, true, curve);
const uncompressed = convertPublicKeyFormat(data, false, curve);
const compressed = convertPublicKeyFormat(curve, data, true);
const uncompressed = convertPublicKeyFormat(curve, data, false);
this.data = compressed;
this.dataUncompressed = compressed.length !== uncompressed.length ? uncompressed : null;
}

/**
* Converts the public key to bytes in compressed or uncompressed format.
* @param compressed - (default: `true`) Whether to return the public key in compressed or uncompressed format (secp256k1 only).
* @returns The public key as a Uint8Array.
*/
public toBytes(compressed: boolean = true): Uint8Array {
return compressed ? this.data : this._uncompressed;
}

/**
* Converts the public key to a hexadecimal string in compressed or uncompressed format.
* @param compressed - (default: `true`) Whether to return the public key in compressed or uncompressed format (secp256k1 only).
* @returns The public key as a hexadecimal string.
*/
public toHex(compressed: boolean = true): string {
return bytesToHex(this.toBytes(compressed));
}
Expand All @@ -48,6 +72,11 @@ export class PublicKey {
return getSharedKey(senderPoint, sharedPoint);
}

/**
* Compares this public key with another for equality.
* @param other - The other `PublicKey` to compare with.
* @returns `true` if the public keys are equal, `false` otherwise.
*/
public equals(other: PublicKey): boolean {
return equalBytes(this.data, other.data);
}
Expand Down
30 changes: 15 additions & 15 deletions src/utils/elliptic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,57 +2,57 @@ import { randomBytes } from "@noble/ciphers/webcrypto";
import { ed25519, x25519 } from "@noble/curves/ed25519";
import { secp256k1 } from "@noble/curves/secp256k1";

import { ECIES_CONFIG, type EllipticCurve } from "../config.js";
import type { EllipticCurve } from "../config.js";
import { ETH_PUBLIC_KEY_SIZE, SECRET_KEY_LENGTH } from "../consts.js";
import { decodeHex } from "./hex.js";

export const getValidSecret = (curve?: EllipticCurve): Uint8Array => {
export const getValidSecret = (curve: EllipticCurve): Uint8Array => {
let key: Uint8Array;
do {
key = randomBytes(SECRET_KEY_LENGTH);
} while (!isValidPrivateKey(key, curve));
} while (!isValidPrivateKey(curve, key));
return key;
};

export const isValidPrivateKey = (secret: Uint8Array, curve?: EllipticCurve): boolean =>
export const isValidPrivateKey = (curve: EllipticCurve, secret: Uint8Array): boolean =>
// on secp256k1: only key ∈ (0, group order) is valid
// on curve25519: any 32-byte key is valid
_exec(
curve || ECIES_CONFIG.ellipticCurve,
curve,
(curve) => curve.utils.isValidSecretKey(secret),
() => true,
() => true
);

export const getPublicKey = (secret: Uint8Array, curve?: EllipticCurve): Uint8Array =>
export const getPublicKey = (curve: EllipticCurve, secret: Uint8Array): Uint8Array =>
_exec(
curve || ECIES_CONFIG.ellipticCurve,
curve,
(curve) => curve.getPublicKey(secret),
(curve) => curve.getPublicKey(secret),
(curve) => curve.getPublicKey(secret)
);

export const getSharedPoint = (
curve: EllipticCurve,
sk: Uint8Array,
pk: Uint8Array,
compressed?: boolean,
curve?: EllipticCurve
compressed?: boolean
): Uint8Array =>
_exec(
curve || ECIES_CONFIG.ellipticCurve,
curve,
(curve) => curve.getSharedSecret(sk, pk, compressed),
(curve) => curve.getSharedSecret(sk, pk),
(curve) => getSharedPointOnEd25519(curve, sk, pk)
);

export const convertPublicKeyFormat = (
curve: EllipticCurve,
pk: Uint8Array,
compressed: boolean,
curve?: EllipticCurve
compressed: boolean
): Uint8Array =>
// only for secp256k1
_exec(
curve || ECIES_CONFIG.ellipticCurve,
curve,
(curve) =>
curve.getSharedSecret(
Uint8Array.from(Array(31).fill(0).concat([1])), // 1 as private key
Expand All @@ -63,10 +63,10 @@ export const convertPublicKeyFormat = (
() => pk
);

export const hexToPublicKey = (hex: string, curve?: EllipticCurve): Uint8Array => {
export const hexToPublicKey = (curve: EllipticCurve, hex: string): Uint8Array => {
const decoded = decodeHex(hex);
return _exec(
curve || ECIES_CONFIG.ellipticCurve,
curve,
() => compatEthPublicKey(decoded),
() => decoded,
() => decoded
Expand Down
8 changes: 4 additions & 4 deletions tests/utils/elliptic.known.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ describe("test known secp256k1", () => {
"c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee51ae168fea63dc339a3c58419466ceaeef7f632653266d0e1236431a950cfe52a";

it("tests known pk", () => {
expect(getPublicKey(hexToBytes(sk), "secp256k1")).toStrictEqual(decodeHex(pk));
expect(getPublicKey("secp256k1", hexToBytes(sk))).toStrictEqual(decodeHex(pk));
});

it("tests hexToPublicKey", () => {
expect(hexToPublicKey(pkUncompressed, "secp256k1")).toStrictEqual(
expect(hexToPublicKey("secp256k1", pkUncompressed)).toStrictEqual(
hexToBytes("04" + pkUncompressed)
);
});
Expand All @@ -35,7 +35,7 @@ describe("test known x25519", () => {
it("tests known pk", () => {
const sk = "a8abababababababababababababababababababababababababababababab6b";
const pk = "e3712d851a0e5d79b831c5e34ab22b41a198171de209b8b8faca23a11c624859";
expect(getPublicKey(hexToBytes(sk), "x25519")).toStrictEqual(decodeHex(pk));
expect(getPublicKey("x25519", hexToBytes(sk))).toStrictEqual(decodeHex(pk));
});

it("tests getSharedPoint", () => {
Expand Down Expand Up @@ -83,6 +83,6 @@ function testGetSharedPoint(
curve: EllipticCurve
) {
expect(
getSharedPoint(decodeHex(sk), hexToPublicKey(pk, curve), compressed, curve)
getSharedPoint(curve, decodeHex(sk), hexToPublicKey(curve, pk), compressed)
).toStrictEqual(decodeHex(shared));
}
4 changes: 2 additions & 2 deletions tests/utils/elliptic.random.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import type { EllipticCurve } from "../../src/config";
import { getValidSecret, isValidPrivateKey } from "../../src/utils";

describe("test random elliptic", () => {
function testRandom(curve?: EllipticCurve) {
function testRandom(curve: EllipticCurve) {
const key = getValidSecret(curve);
expect(isValidPrivateKey(key, curve)).toBe(true);
expect(isValidPrivateKey(curve, key)).toBe(true);
}

it("tests secp256k1", () => {
Expand Down
Loading