Skip to content

Commit

Permalink
working ed25519 key pairs
Browse files Browse the repository at this point in the history
  • Loading branch information
himanshu committed Mar 15, 2024
1 parent 52d93e9 commit 12fb612
Show file tree
Hide file tree
Showing 9 changed files with 208 additions and 75 deletions.
33 changes: 22 additions & 11 deletions package-lock.json

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

3 changes: 3 additions & 0 deletions src/helpers/common.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
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 { keccak256 } from ".";

export const ed25519Curve = new EC("ed25519");
export const secp256k1Curve = new EC("secp256k1");
// 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
60 changes: 39 additions & 21 deletions src/helpers/keyUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,12 @@ 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 { Point } from "..";
import { ImportedShare, KeyType, PrivateKeyData } from "../interfaces";
import { encParamsBufToHex } from "./common";
import { EncryptedSeed, ImportedShare, KeyType, PrivateKeyData } from "../interfaces";
import { ed25519Curve, encParamsBufToHex, secp256k1Curve } from "./common";
import { generateRandomPolynomial } from "./langrangeInterpolatePoly";
import { generateNonceMetadataParams } from "./metadataUtils";
const ed25519Curve = new EC("ed25519");
const secp256k1Curve = new EC("secp256k1");

export function keccak256(a: Buffer): string {
const hash = Buffer.from(keccakHash(a)).toString("hex");
Expand Down Expand Up @@ -59,14 +57,15 @@ function adjustScalarBytes(bytes: Buffer): Buffer {
/** Convenience method that creates public key and other stuff. RFC8032 5.1.5 */
export function getEd25519ExtendedPublicKey(keyHex: BN): {
scalar: BN;
point: Point;
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
Expand All @@ -75,9 +74,9 @@ export function getEd25519ExtendedPublicKey(keyHex: BN): {
if (hashed.length !== 64) {
throw new Error("Invalid hash length for ed25519 seed");
}
const head = new BN(adjustScalarBytes(Buffer.from(hashed.slice(0, len))));
const scalar = new BN(head.umod(N)); // The actual private scalar
const point = G.mul(scalar) as Point; // Point on Edwards curve aka public key
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 };
}

Expand All @@ -88,7 +87,7 @@ export const getSecpKeyFromEd25519 = (
point: curve.base.BasePoint;
} => {
const ed25519Key = ed25519Scalar.toString("hex", 64);
const keyHash = keccak256(Buffer.from(ed25519Key, "hex"));
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 {
Expand All @@ -97,14 +96,21 @@ export const getSecpKeyFromEd25519 = (
};
};

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 = {
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");
Expand All @@ -115,24 +121,33 @@ export const generateEd25519KeyData = async (ed25519Seed: BN): Promise<PrivateKe
oAuthKeyScalar: oauthKeyPair.getPrivate(),
oAuthPubX: oauthKeyPair.getPublic().getX(),
oAuthPubY: oauthKeyPair.getPublic().getY(),
SigningPubX: encryptionKey.point.getX(),
SigningPubY: encryptionKey.point.getY(),
metadataNonce: metadataPrivNonce,
encryptionScalar: encryptionKey.scalar,
metadataSigningKey: encryptionKey.scalar,
encryptedSeed: encDataBase64,
finalUserPubKeyPoint: finalEd25519Key.point,
};
};

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

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

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

Expand Down Expand Up @@ -172,7 +187,7 @@ export const generateShares = async (
privKey: BN
) => {
const keyData = keyType === "ed25519" ? await generateEd25519KeyData(privKey) : await generateSecp256k1KeyData(privKey);
const { metadataNonce, oAuthKeyScalar: oAuthKey } = keyData;
const { metadataNonce, oAuthKeyScalar: oAuthKey, encryptedSeed, metadataSigningKey } = keyData;
const threshold = ~~(nodePubkeys.length / 2) + 1;
const degree = threshold - 1;
const nodeIndexesBn: BN[] = [];
Expand All @@ -183,7 +198,7 @@ export const generateShares = async (
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(ecCurve, serverTimeOffset, "getOrSetNonce", oAuthKey, keyType, metadataNonce);
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>[] = [];
Expand All @@ -204,8 +219,11 @@ export const generateShares = async (
const encParamsMetadata = encParamsBufToHex(encParams);
const shareData: ImportedShare = {
encrypted_seed: keyData.encryptedSeed,
pub_key_x: oAuthPubKey.getX().toString("hex", 64),
pub_key_y: oAuthPubKey.getY().toString("hex", 64),
final_user_point: keyData.finalUserPubKeyPoint,
oauth_pub_key_x: oAuthPubKey.getX().toString("hex", 64),
oauth_pub_key_y: oAuthPubKey.getY().toString("hex", 64),
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),
Expand Down
37 changes: 31 additions & 6 deletions src/helpers/metadataUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import { ec } from "elliptic";
import stringify from "json-stable-stringify";
import log from "loglevel";

import { EciesHex, GetOrSetNonceResult, KeyType, MetadataParams, NonceMetadataParams, SetNonceData } from "../interfaces";
import { encParamsHexToBuf } from "./common";
import { keccak256 } from "./keyUtils";
import { EciesHex, EncryptedSeed, GetOrSetNonceResult, KeyType, MetadataParams, NonceMetadataParams, SetNonceData } from "../interfaces";
import { encParamsHexToBuf, secp256k1Curve } from "./common";
import { getSecpKeyFromEd25519, keccak256 } from "./keyUtils";

export function convertMetadataToNonce(params: { message?: string }) {
if (!params || !params.message) {
Expand Down Expand Up @@ -95,14 +95,15 @@ export async function getNonce(
}

export function generateNonceMetadataParams(
ecCurve: ec,
serverTimeOffset: number,
operation: string,
privateKey: BN,
keyType: KeyType,
nonce?: BN
nonce?: BN,
seed?: string
): NonceMetadataParams {
const key = ecCurve.keyFromPrivate(privateKey.toString("hex", 64));
// metadata only uses secp for sig validation
const key = secp256k1Curve.keyFromPrivate(privateKey.toString("hex", 64));
const setData: Partial<SetNonceData> = {
operation,
timestamp: new BN(~~(serverTimeOffset + Date.now() / 1000)).toString(16),
Expand All @@ -111,6 +112,11 @@ export function generateNonceMetadataParams(
if (nonce) {
setData.data = nonce.toString("hex", 64);
}

if (seed) {
setData.seed = seed;
}

const sig = key.sign(keccak256(Buffer.from(stringify(setData), "utf8")).slice(2));
return {
pub_key_X: key.getPublic().getX().toString("hex", 64),
Expand All @@ -120,3 +126,22 @@ export function generateNonceMetadataParams(
signature: Buffer.from(sig.r.toString(16, 64) + sig.s.toString(16, 64) + new BN("").toString(16, 2), "hex").toString("base64"),
};
}

export const decryptSeedData = async (seedBase64: string, finalUserKey: BN) => {
const decryptionKey = getSecpKeyFromEd25519(finalUserKey);
const seedUtf8 = Buffer.from(seedBase64, "base64").toString("utf-8");
const seedJson = JSON.parse(seedUtf8) as EncryptedSeed;

const bufferMetadata = {
ephemPublicKey: Buffer.from(seedJson.metadata.ephemPublicKey, "hex"),
iv: Buffer.from(seedJson.metadata.iv, "hex"),
mac: Buffer.from(seedJson.metadata.mac, "hex"),
mode: "AES256",
};
const decText = await decrypt(decryptionKey.scalar.toArrayLike(Buffer), {
...bufferMetadata,
ciphertext: Buffer.from(seedJson.enc_text, "hex"),
});

return decText;
};
32 changes: 27 additions & 5 deletions src/helpers/nodeUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,18 @@ import {
normalizeLegacyKeysResult,
thresholdSame,
} from "./common";
import { derivePubKey, generateAddressFromPrivKey, generateAddressFromPubKey, generatePrivateKey, generateShares, keccak256 } from "./keyUtils";
import {
derivePubKey,
encodeEd25519Point,
generateAddressFromPrivKey,
generateAddressFromPubKey,
generatePrivateKey,
generateShares,
getEd25519ExtendedPublicKey,
keccak256,
} from "./keyUtils";
import { lagrangeInterpolation } from "./langrangeInterpolatePoly";
import { decryptNodeData, getMetadata, getOrSetNonce } from "./metadataUtils";
import { decryptNodeData, decryptSeedData, getMetadata, getOrSetNonce } from "./metadataUtils";

export const GetPubKeyOrKeyAssign = async (params: {
endpoints: string[];
Expand Down Expand Up @@ -348,8 +357,10 @@ export async function retrieveOrImportShare(params: {
idtoken: idToken,
nodesignatures: nodeSigs,
verifieridentifier: verifier,
pub_key_x: importedShare.pub_key_x,
pub_key_y: importedShare.pub_key_y,
pub_key_x: importedShare.oauth_pub_key_x,
pub_key_y: importedShare.oauth_pub_key_y,
signing_pub_key_x: importedShare.signing_pub_key_x,
signing_pub_key_y: importedShare.signing_pub_key_y,
encrypted_share: importedShare.encrypted_share,
encrypted_share_metadata: importedShare.encrypted_share_metadata,
node_index: importedShare.node_index,
Expand All @@ -366,7 +377,6 @@ export async function retrieveOrImportShare(params: {
encrypted: "yes",
use_temp: true,
item: items,
encrypted_seed: items[0]?.encrypted_seed || "",
key_type: keyType,
one_key_flow: true,
}),
Expand Down Expand Up @@ -738,6 +748,18 @@ export async function retrieveOrImportShare(params: {
} else if (typeOfUser === "v2") {
isUpgraded = metadataNonce.eq(new BN("0"));
}

if (keyType === "ed25519") {
if (!nonceResult.seed) {
throw new Error("Invalid data, seed data is missing for ed25519 key, Please report this bug");
}
const decryptedSeed = await decryptSeedData(nonceResult.seed, new BN(finalPrivKey, "hex"));
const extendedEd25519Key = getEd25519ExtendedPublicKey(new BN(decryptedSeed));
const encodedPubKey = encodeEd25519Point(extendedEd25519Key.point);
const totalLength = decryptedSeed.length + encodedPubKey.length;
finalPrivKey = Buffer.concat([decryptedSeed, encodedPubKey], totalLength).toString("hex");
finalPubKey = extendedEd25519Key.point;
}
// return reconstructed private key and ethereum address
return {
finalKeyData: {
Expand Down
Loading

0 comments on commit 12fb612

Please sign in to comment.