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

feat: ed25519 support #49

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
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
11 changes: 6 additions & 5 deletions src/routes/metadata.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
/* eslint-disable security/detect-object-injection */
import { generatePrivate } from "@toruslabs/eccrypto";
import { celebrate, Joi, Segments } from "celebrate";
import { ec as EC } from "elliptic";
import express, { Request, Response } from "express";
Expand Down Expand Up @@ -27,8 +26,6 @@ const upload = multer({
limits: { fieldSize: 30 * 1024 * 1024 },
});

const elliptic = new EC("secp256k1");

const router = express.Router();

const NAMESPACES = {
Expand Down Expand Up @@ -329,6 +326,7 @@ router.post(
pub_key_X: Joi.string().max(64).required(),
pub_key_Y: Joi.string().max(64).required(),
namespace: Joi.string().max(128),
key_type: Joi.string().allow("").optional(),
set_data: Joi.object({
data: Joi.string(),
timestamp: Joi.string().hex(),
Expand All @@ -347,6 +345,7 @@ router.post(
set_data: { data },
namespace: oldNamespace,
tableName,
key_type: keyType,
}: SetDataInput = req.body;

const oldKey = constructKey(pubKeyX, pubKeyY, oldNamespace);
Expand Down Expand Up @@ -432,10 +431,12 @@ router.post(
if (nonce) {
pubNonce = await getPubNonce(true);
} else {
const ec = keyType === "ed25519" ? new EC("ed25519") : new EC("secp256k1");

// create new nonce
nonce = generatePrivate().toString("hex");
nonce = ec.genKeyPair().getPrivate().toString("hex", 64);

const unformattedPubNonce = elliptic.keyFromPrivate(nonce).getPublic();
const unformattedPubNonce = ec.keyFromPrivate(nonce).getPublic();
pubNonce = {
x: unformattedPubNonce.getX().toString("hex"),
y: unformattedPubNonce.getY().toString("hex"),
Expand Down
20 changes: 16 additions & 4 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,18 @@ import { ec as EC } from "elliptic";
import { keccak256 } from "js-sha3";
import stringify from "json-stable-stringify";

import { LockDataInput, SetDataInput } from "./interfaces";
import { KeyType, LockDataInput, SetDataInput } from "./interfaces";

const elliptic = new EC("secp256k1");
const ellipticSecp256k1 = new EC("secp256k1");
const ellipticEd25519 = new EC("ed25519");

export function getEllipticCurve(keyType: KeyType) {
if (keyType === "ed25519") {
return ellipticEd25519;
}
// default is secp256k1
return ellipticSecp256k1;
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
function isErrorObj(err: any): boolean {
Expand Down Expand Up @@ -32,7 +41,9 @@ export const MAX_BATCH_SIZE = 60 * 1024 * 1024; // 60MB
export const REDIS_NAME_SPACE = "EMAIL_AUTH_DATA";

export const isValidSignature = (data: SetDataInput) => {
const { pub_key_X: pubKeyX, pub_key_Y: pubKeyY, signature, set_data: setData } = data;
const { pub_key_X: pubKeyX, pub_key_Y: pubKeyY, signature, set_data: setData, key_type: keyType } = data;
const elliptic = getEllipticCurve(keyType);

const pubKey = elliptic.keyFromPublic({ x: pubKeyX, y: pubKeyY }, "hex");
const decodedSignature = Buffer.from(signature, "base64").toString("hex");
const ecSignature = {
Expand All @@ -43,6 +54,7 @@ export const isValidSignature = (data: SetDataInput) => {
};

export const isValidLockSignature = (lockData: LockDataInput) => {
const { key, signature, data } = lockData;
const { key, signature, data, key_type: keyType } = lockData;
const elliptic = getEllipticCurve(keyType);
return elliptic.verify(keccak256(stringify(data)), signature, Buffer.from(key, "hex"));
};
4 changes: 4 additions & 0 deletions src/utils/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,13 @@ export interface SetDataData {
timestamp: string;
}

export type KeyType = "secp256k1" | "ed25519";

export interface SetDataInput {
namespace?: string;
pub_key_X: string;
pub_key_Y: string;
key_type?: KeyType;
set_data: SetDataData;
tableName?: DBTableName;
signature: string;
Expand All @@ -50,6 +53,7 @@ export interface SetDataInput {
export interface LockDataInput {
key: string;
signature: string;
key_type?: KeyType;
data: Partial<SetDataData> & Pick<SetDataData, "timestamp">;
}

Expand Down
105 changes: 104 additions & 1 deletion test/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ chai.use(chaiHttp);
const { assert, request } = chai;

const randomID = () => `${Math.random().toString(36).substring(2, 9)}`;

const { generateGetOrSetNonceParams } = require("./util");
/**
* Testing API calls.
*/
Expand Down Expand Up @@ -125,6 +125,16 @@ describe("API-calls", function () {
}
});

it("#it should set new nonce for new user with ed25519 key, when validation is correct", async function () {
const msg = "getOrSetNonce";
const data = "getOrSetNonce";
const privKeyNew = new BN(generatePrivate());
const metadataParams = generateGetOrSetNonceParams(msg, data, privKeyNew, "ed25519");
const val = await post(`${server}/get_or_set_nonce`, metadataParams);

assert.isString(val.nonce);
});

it("#it should reject if signature is invalid", async function () {
const message = {
test: Math.random().toString(36).substring(7),
Expand Down Expand Up @@ -305,4 +315,97 @@ describe("API-calls", function () {
assert.strictEqual(releaseStatus, 2);
});
});

describe("/get_or_set_nonce", function () {
let privKey;
before(function () {
privKey = new BN(generatePrivate());
});

it("#it should reject if the pub_key_X/pub_key_Y is missing", async function () {
const msg = "getOrSetNonce";
const data = "";
const metadataParams = generateGetOrSetNonceParams(msg, data, privKey);
metadataParams.pub_key_X = ""; // remove pub_key_X
try {
await post(`${server}/get_or_set_nonce`, metadataParams);
} catch (err) {
const val = await err.json();
assert.deepStrictEqual(val.validation.body.message, '"pub_key_X" is not allowed to be empty');
}
});

it("#it should reject if the signature is missing", async function () {
const msg = "getOrSetNonce";
const data = "getOrSetNonce";
const metadataParams = generateGetOrSetNonceParams(msg, data, privKey);
metadataParams.signature = ""; // remove signature
try {
await post(`${server}/get_or_set_nonce`, metadataParams);
} catch (err) {
const val = await err.json();
assert.deepStrictEqual(val.validation.body.message, '"signature" is not allowed to be empty');
}
});

it("#it should reject if the timestamp is missing and signature is present", async function () {
const msg = "getOrSetNonce";
const data = "getOrSetNonce";
const metadataParams = generateGetOrSetNonceParams(msg, data, privKey);
metadataParams.set_data.timestamp = "";
try {
await post(`${server}/get_or_set_nonce`, metadataParams);
} catch (err) {
const val = await err.json();
assert.deepStrictEqual(val.statusCode, 400);
assert.deepStrictEqual(val.message, "Validation failed");
}
});

it("#it should reject if the timestamp is old", async function () {
const msg = "getOrSetNonce";
const data = "getOrSetNonce";
const metadataParams = generateGetOrSetNonceParams(msg, data, privKey);
metadataParams.set_data.timestamp = new BN(~~(Date.now() / 1000) - 95).toString(16); // set old timestimpe
try {
await post(`${server}/get_or_set_nonce`, metadataParams);
} catch (err) {
const val = await err.json();
assert.deepStrictEqual(val.error.timestamp, "Message has been signed more than 90s ago");
}
});
it("#it should set new nonce for new user with ed25519 key, when validation is correct", async function () {
const msg = "getOrSetNonce";
const data = "getOrSetNonce";
const privKeyNew = new BN(generatePrivate());
const metadataParams = generateGetOrSetNonceParams(msg, data, privKeyNew, "ed25519");
const val = await post(`${server}/get_or_set_nonce`, metadataParams);
assert.isString(val.nonce);
});

it("#it should get existing nonce for user when validation is correct", async function () {
const msg = "getOrSetNonce";
const data = "getOrSetNonce";
const metadataParams = generateGetOrSetNonceParams(msg, data, privKey);

// set nonce
const setResult = await post(`${server}/get_or_set_nonce`, metadataParams);
assert.isString(setResult.nonce);
assert.isObject(setResult.pubNonce);

// get nonce
const getResult = await post(`${server}/get_or_set_nonce`, metadataParams);
assert.equal(getResult.nonce, setResult.nonce);
assert.deepEqual(getResult.pubNonce, setResult.pubNonce);
});

it("#it should get set custom passed nonce for user when validation is correct", async function () {
const msg = "getOrSetNonce";
const data = "getOrSetNonce";
const metadataParams = generateGetOrSetNonceParams(msg, data, privKey);
const setResult = await post(`${server}/get_or_set_nonce`, metadataParams);
assert.isString(setResult.nonce);
assert.isObject(setResult.pubNonce);
});
});
});
28 changes: 28 additions & 0 deletions test/util.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/* eslint-disable n/no-extraneous-require */
/* eslint-disable import/no-extraneous-dependencies */
/* eslint-disable @typescript-eslint/no-var-requires */
const { keccak256 } = require("ethereum-cryptography/keccak");
const BN = require("bn.js");
const stringify = require("json-stable-stringify");

const { ec: EC } = require("elliptic");

function generateGetOrSetNonceParams(operation, data, privateKey, keyType) {
const curve = keyType === "ed25519" ? "ed25519" : "secp256k1";
const ec = new EC(curve);
const key = ec.keyFromPrivate(privateKey.toString("hex", 64));
const setData = {
data,
timestamp: new BN(~~(Date.now() / 1000)).toString(16),
};
const sig = key.sign(keccak256(Buffer.from(stringify(setData), "utf8")));
return {
pub_key_X: key.getPublic().getX().toString("hex"),
pub_key_Y: key.getPublic().getY().toString("hex"),
set_data: setData,
key_type: keyType,
signature: Buffer.from(sig.r.toString(16, 64) + sig.s.toString(16, 64) + new BN("").toString(16, 2), "hex").toString("base64"),
};
}

module.exports = { generateGetOrSetNonceParams };