Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ed25519 keys support and import keys new flow #126

Merged
merged 26 commits into from
Mar 20, 2024
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
25 changes: 14 additions & 11 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"@toruslabs/http-helpers": "^6.1.0",
"bn.js": "^5.2.1",
"elliptic": "^6.5.5",
"bs58": "^5.0.0",
"ethereum-cryptography": "^2.1.3",
"json-stable-stringify": "^1.1.1",
"loglevel": "^1.9.1"
Expand Down
6 changes: 5 additions & 1 deletion src/Point.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@ class Point {
encode(enc: string): Buffer {
switch (enc) {
case "arr":
return Buffer.concat([Buffer.from("04", "hex"), Buffer.from(this.x.toString("hex"), "hex"), Buffer.from(this.y.toString("hex"), "hex")]);
return Buffer.concat([
Buffer.from("04", "hex"),
Buffer.from(this.x.toString("hex", 64), "hex"),
Buffer.from(this.y.toString("hex", 64), "hex"),
]);
case "elliptic-compressed": {
const key = this.ecCurve.keyFromPublic({ x: this.x.toString("hex", 64), y: this.y.toString("hex", 64) }, "hex");
return Buffer.from(key.getPublic(true, "hex"));
Expand Down
4 changes: 2 additions & 2 deletions src/Share.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ class Share {

toJSON(): StringifiedType {
return {
share: this.share.toString("hex"),
shareIndex: this.shareIndex.toString("hex"),
share: this.share.toString("hex", 64),
shareIndex: this.shareIndex.toString("hex", 64),
};
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export const JRPC_METHODS = {
GET_OR_SET_KEY: "GetPubKeyOrKeyAssign",
COMMITMENT_REQUEST: "CommitmentRequest",
IMPORT_SHARE: "ImportShare",
IMPORT_SHARES: "ImportShares",
GET_SHARE_OR_KEY_ASSIGN: "GetShareOrKeyAssign",
};
23 changes: 22 additions & 1 deletion src/helpers/common.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,22 @@
import { Ecies } from "@toruslabs/eccrypto";
import { BN } from "bn.js";
import { ec as EC } from "elliptic";
import JsonStringify from "json-stable-stringify";

import { EciesHex, LegacyVerifierLookupResponse, VerifierLookupResponse } from "../interfaces";
import { EciesHex, KeyType, LegacyVerifierLookupResponse, VerifierLookupResponse } from "../interfaces";
import { keccak256 } from ".";

export const ed25519Curve = new EC("ed25519");
export const secp256k1Curve = new EC("secp256k1");

export const getKeyCurve = (keyType: KeyType) => {
if (keyType === "ed25519") {
return ed25519Curve;
} else if (keyType === "secp256k1") {
return secp256k1Curve;
}
throw new Error(`Invalid keyType: ${keyType}`);
};
// this function normalizes the result from nodes before passing the result to threshold check function
// For ex: some fields returns by nodes might be different from each other
// like created_at field might vary and nonce_data might not be returned by all nodes because
Expand Down Expand Up @@ -105,6 +119,13 @@ export function encParamsHexToBuf(eciesData: Omit<EciesHex, "ciphertext">): Omit
};
}

export function getProxyCoordinatorEndpointIndex(endpoints: string[], verifier: string, verifierId: string) {
const verifierIdStr = `${verifier}${verifierId}`;
const hashedVerifierId = keccak256(Buffer.from(verifierIdStr, "utf8")).slice(2);
const proxyEndpointNum = new BN(hashedVerifierId, "hex").mod(new BN(endpoints.length)).toNumber();
return proxyEndpointNum;
}

export function calculateMedian(arr: number[]): number {
const arrSize = arr.length;

Expand Down
225 changes: 215 additions & 10 deletions src/helpers/keyUtils.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,27 @@
import { INodePub } from "@toruslabs/constants";
import { Ecies, encrypt } from "@toruslabs/eccrypto";
import BN from "bn.js";
import { ec } from "elliptic";
import base58 from "bs58";
import { curve, ec as EC } from "elliptic";
import { keccak256 as keccakHash } from "ethereum-cryptography/keccak";
import { sha512 } from "ethereum-cryptography/sha512";
import stringify from "json-stable-stringify";
import log from "loglevel";

import { EncryptedSeed, ImportedShare, KeyType, PrivateKeyData } from "../interfaces";
import { ed25519Curve, encParamsBufToHex, getKeyCurve, secp256k1Curve } from "./common";
import { generateRandomPolynomial } from "./langrangeInterpolatePoly";
import { generateNonceMetadataParams } from "./metadataUtils";

export function keccak256(a: Buffer): string {
const hash = Buffer.from(keccakHash(a)).toString("hex");
return `0x${hash}`;
}

export const generatePrivateKey = (ecCurve: EC, buf: typeof Buffer): Buffer => {
return ecCurve.genKeyPair().getPrivate().toArrayLike(buf);
};

export function stripHexPrefix(str: string): string {
return str.startsWith("0x") ? str.slice(2) : str;
}
Expand All @@ -29,22 +44,212 @@ export function toChecksumAddress(hexAddress: string): string {
return ret;
}

export function generateAddressFromPrivKey(ecCurve: ec, privateKey: BN): string {
function adjustScalarBytes(bytes: Buffer): Buffer {
// Section 5: For X25519, in order to decode 32 random bytes as an integer scalar,
// set the three least significant bits of the first byte
bytes[0] &= 248; // 0b1111_1000
// and the most significant bit of the last to zero,
bytes[31] &= 127; // 0b0111_1111
// set the second most significant bit of the last byte to 1
bytes[31] |= 64; // 0b0100_0000
return bytes;
}

/** Convenience method that creates public key and other stuff. RFC8032 5.1.5 */
export function getEd25519ExtendedPublicKey(keyHex: BN): {
scalar: BN;
point: curve.base.BasePoint;
} {
const len = 32;
const G = ed25519Curve.g;
const N = ed25519Curve.n;
const keyBuffer = keyHex.toArrayLike(Buffer);

if (keyBuffer.length !== 32) {
log.error("Invalid seed for ed25519 key derivation", keyBuffer.length);
throw new Error("Invalid seed for ed25519 key derivation");
}
// Hash private key with curve's hash function to produce uniformingly random input
// Check byte lengths: ensure(64, h(ensure(32, key)))
const hashed = sha512(keyBuffer);
if (hashed.length !== 64) {
throw new Error("Invalid hash length for ed25519 seed");
}
const head = new BN(adjustScalarBytes(Buffer.from(hashed.slice(0, len))), "le");
const scalar = new BN(head.umod(N), "le"); // The actual private scalar
const point = G.mul(scalar) as curve.base.BasePoint; // Point on Edwards curve aka public key
return { scalar, point };
}

export const getSecpKeyFromEd25519 = (
ed25519Scalar: BN
): {
scalar: BN;
point: curve.base.BasePoint;
} => {
const ed25519Key = ed25519Scalar.toString("hex", 64);
const keyHash = keccakHash(Buffer.from(ed25519Key, "hex"));
const secpKey = new BN(keyHash).umod(secp256k1Curve.curve.n);
const secpKeyPair = secp256k1Curve.keyFromPrivate(secpKey.toString("hex", 64));
return {
scalar: secpKeyPair.getPrivate(),
point: secpKeyPair.getPublic(),
};
};

export function encodeEd25519Point(point: curve.base.BasePoint) {
const encodingLength = Math.ceil(ed25519Curve.n.bitLength() / 8);
const enc = point.getY().toArrayLike(Buffer, "le", encodingLength);
enc[encodingLength - 1] |= point.getX().isOdd() ? 0x80 : 0;
return enc;
}

export const generateEd25519KeyData = async (ed25519Seed: BN): Promise<PrivateKeyData> => {
const finalEd25519Key = getEd25519ExtendedPublicKey(ed25519Seed);
const encryptionKey = getSecpKeyFromEd25519(finalEd25519Key.scalar);
const encryptedSeed = await encrypt(Buffer.from(encryptionKey.point.encodeCompressed("hex"), "hex"), ed25519Seed.toArrayLike(Buffer));
const encData: EncryptedSeed = {
enc_text: encryptedSeed.ciphertext.toString("hex"),
metadata: encParamsBufToHex(encryptedSeed),
public_key: encodeEd25519Point(finalEd25519Key.point).toString("hex"),
};

const encDataBase64 = Buffer.from(JSON.stringify(encData), "utf-8").toString("base64");
const metadataPrivNonce = ed25519Curve.genKeyPair().getPrivate();
const oauthKey = finalEd25519Key.scalar.sub(metadataPrivNonce).umod(ed25519Curve.n);
const oauthKeyPair = ed25519Curve.keyFromPrivate(oauthKey.toArrayLike(Buffer));
return {
oAuthKeyScalar: oauthKeyPair.getPrivate(),
oAuthPubX: oauthKeyPair.getPublic().getX(),
oAuthPubY: oauthKeyPair.getPublic().getY(),
SigningPubX: encryptionKey.point.getX(),
SigningPubY: encryptionKey.point.getY(),
metadataNonce: metadataPrivNonce,
metadataSigningKey: encryptionKey.scalar,
encryptedSeed: encDataBase64,
finalUserPubKeyPoint: finalEd25519Key.point,
};
};

export const generateSecp256k1KeyData = async (scalar: BN): Promise<PrivateKeyData> => {
const randomNonce = new BN(generatePrivateKey(secp256k1Curve, Buffer));
const oAuthKey = scalar.sub(randomNonce).umod(secp256k1Curve.curve.n);
const oAuthKeyPair = secp256k1Curve.keyFromPrivate(oAuthKey.toString("hex").padStart(64, "0"));
const oAuthPubKey = oAuthKeyPair.getPublic();

const finalUserKeyPair = secp256k1Curve.keyFromPrivate(scalar.toString("hex", 64));

return {
oAuthKeyScalar: oAuthKeyPair.getPrivate(),
oAuthPubX: oAuthPubKey.getX(),
oAuthPubY: oAuthPubKey.getY(),
SigningPubX: oAuthPubKey.getX(),
SigningPubY: oAuthPubKey.getY(),
metadataNonce: randomNonce,
encryptedSeed: "",
metadataSigningKey: oAuthKeyPair.getPrivate(),
finalUserPubKeyPoint: finalUserKeyPair.getPublic(),
};
};

export function generateAddressFromPrivKey(keyType: KeyType, privateKey: BN): string {
const ecCurve = getKeyCurve(keyType);
const key = ecCurve.keyFromPrivate(privateKey.toString("hex", 64), "hex");
const publicKey = key.getPublic().encode("hex", false).slice(2);
const evmAddressLower = `0x${keccak256(Buffer.from(publicKey, "hex")).slice(64 - 38)}`;
return toChecksumAddress(evmAddressLower);
if (keyType === "secp256k1") {
const publicKey = key.getPublic().encode("hex", false).slice(2);
const evmAddressLower = `0x${keccak256(Buffer.from(publicKey, "hex")).slice(64 - 38)}`;
return toChecksumAddress(evmAddressLower);
} else if (keyType === "ed25519") {
const publicKey = encodeEd25519Point(key.getPublic());
const address = base58.encode(publicKey);
return address;
}
throw new Error(`Invalid keyType: ${keyType}`);
}

export function generateAddressFromPubKey(ecCurve: ec, publicKeyX: BN, publicKeyY: BN): string {
export function generateAddressFromPubKey(keyType: KeyType, publicKeyX: BN, publicKeyY: BN): string {
const ecCurve = getKeyCurve(keyType);
const key = ecCurve.keyFromPublic({ x: publicKeyX.toString("hex", 64), y: publicKeyY.toString("hex", 64) });
const publicKey = key.getPublic().encode("hex", false).slice(2);
const evmAddressLower = `0x${keccak256(Buffer.from(publicKey, "hex")).slice(64 - 38)}`;
return toChecksumAddress(evmAddressLower);
if (keyType === "secp256k1") {
const publicKey = key.getPublic().encode("hex", false).slice(2);
const evmAddressLower = `0x${keccak256(Buffer.from(publicKey, "hex")).slice(64 - 38)}`;
return toChecksumAddress(evmAddressLower);
} else if (keyType === "ed25519") {
const publicKey = encodeEd25519Point(key.getPublic());
const address = base58.encode(publicKey);
return address;
}
throw new Error(`Invalid keyType: ${keyType}`);
}

export function getPostboxKeyFrom1OutOf1(ecCurve: ec, privKey: string, nonce: string): string {
export function getPostboxKeyFrom1OutOf1(ecCurve: EC, privKey: string, nonce: string): string {
const privKeyBN = new BN(privKey, 16);
const nonceBN = new BN(nonce, 16);
return privKeyBN.sub(nonceBN).umod(ecCurve.curve.n).toString("hex");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

some users / libraries may expect the hex to be 32 bytes long (64 characters). ideally should use a global function to encode to bytes / hex, so that we don't have to do and check this manually every time. (even better might be to create a package that can be used across libraries.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will address these suggestions to make test cases modular in other PRs, thanks for suggesting these improvement

}

export function derivePubKey(ecCurve: EC, sk: BN): curve.base.BasePoint {
const skHex = sk.toString(16, 64);
return ecCurve.keyFromPrivate(skHex).getPublic();
}

export const encryptionEC = new EC("secp256k1");

export const generateShares = async (
ecCurve: EC,
keyType: KeyType,
serverTimeOffset: number,
nodeIndexes: number[],
nodePubkeys: INodePub[],
privKey: BN
) => {
const keyData = keyType === "ed25519" ? await generateEd25519KeyData(privKey) : await generateSecp256k1KeyData(privKey);
const { metadataNonce, oAuthKeyScalar: oAuthKey, encryptedSeed, metadataSigningKey } = keyData;
const threshold = ~~(nodePubkeys.length / 2) + 1;
const degree = threshold - 1;
const nodeIndexesBn: BN[] = [];

for (const nodeIndex of nodeIndexes) {
nodeIndexesBn.push(new BN(nodeIndex));
}
const oAuthPubKey = ecCurve.keyFromPrivate(oAuthKey.toString("hex").padStart(64, "0")).getPublic();
const poly = generateRandomPolynomial(ecCurve, degree, oAuthKey);
const shares = poly.generateShares(nodeIndexesBn);
const nonceParams = generateNonceMetadataParams(serverTimeOffset, "getOrSetNonce", metadataSigningKey, keyType, metadataNonce, encryptedSeed);
const nonceData = Buffer.from(stringify(nonceParams.set_data), "utf8").toString("base64");
const sharesData: ImportedShare[] = [];
const encPromises: Promise<Ecies>[] = [];
for (let i = 0; i < nodeIndexesBn.length; i++) {
const shareJson = shares[nodeIndexesBn[i].toString("hex", 64)].toJSON() as Record<string, string>;
if (!nodePubkeys[i]) {
throw new Error(`Missing node pub key for node index: ${nodeIndexesBn[i].toString("hex", 64)}`);
}
const nodePubKey = encryptionEC.keyFromPublic({ x: nodePubkeys[i].X, y: nodePubkeys[i].Y });
encPromises.push(
encrypt(Buffer.from(nodePubKey.getPublic().encodeCompressed("hex"), "hex"), Buffer.from(shareJson.share.padStart(64, "0"), "hex"))
);
}
const encShares = await Promise.all(encPromises);
for (let i = 0; i < nodeIndexesBn.length; i++) {
const shareJson = shares[nodeIndexesBn[i].toString("hex", 64)].toJSON() as Record<string, string>;
const encParams = encShares[i];
const encParamsMetadata = encParamsBufToHex(encParams);
const shareData: ImportedShare = {
encrypted_seed: keyData.encryptedSeed,
final_user_point: keyData.finalUserPubKeyPoint,
oauth_pub_key_x: oAuthPubKey.getX().toString("hex"),
oauth_pub_key_y: oAuthPubKey.getY().toString("hex"),
signing_pub_key_x: keyData.SigningPubX.toString("hex"),
signing_pub_key_y: keyData.SigningPubY.toString("hex"),
encrypted_share: encParamsMetadata.ciphertext,
encrypted_share_metadata: encParamsMetadata,
node_index: Number.parseInt(shareJson.shareIndex, 16),
key_type: keyType,
nonce_data: nonceData,
nonce_signature: nonceParams.signature,
};
sharesData.push(shareData);
}

return sharesData;
};
Loading
Loading