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

Change secp256k1 and test library #720

Merged
merged 3 commits into from
Jul 1, 2023
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
5 changes: 4 additions & 1 deletion .cspell.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"Codacy",
"Codecov",
"consts",
"ecies",
"eciespy",
"eciesjs",
"eth",
Expand All @@ -26,7 +27,9 @@
// flagWords - list of words to be always considered incorrect
// This is useful for offensive words and common spelling errors.
// For example "hte" should be "the"
"flagWords": ["hte"],
"flagWords": [
"hte"
],
"ignorePaths": [
".git",
".github",
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
node: [14, 16, 18, 20]
node: [16, 18, 20]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,14 @@ readonly compressed: Buffer;

## Release Notes

### 0.4.0

- Change secp256k1 library to [noble-curves](https://github.com/paulmillr/noble-curves), which is [audited](https://github.com/paulmillr/noble-curves/tree/main/audit)
- Change hash library to [noble-hashes](https://github.com/paulmillr/noble-hashes)
- Change test library to [jest](https://jestjs.io/)
- Bump dependencies
- Drop Node 14 support

### 0.3.1 ~ 0.3.17

- Support Node 18, 20
Expand Down
8 changes: 8 additions & 0 deletions jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export default {
preset: "ts-jest",
testEnvironment: "node",
collectCoverage: true,
coverageDirectory: "coverage",
coverageProvider: "v8",
testTimeout: 30000,
};
22 changes: 9 additions & 13 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,27 +23,23 @@
],
"scripts": {
"build": "npx tsc",
"test": "nyc -r lcov -e .ts mocha -r ts-node/register tests/**/*.test.ts && nyc report --reporter=json"
"test": "jest"
},
"repository": {
"type": "git",
"url": "https://github.com/ecies/js.git"
},
"version": "0.3.17",
"version": "0.4.0",
"dependencies": {
"@types/secp256k1": "^4.0.3",
"futoin-hkdf": "^1.5.1",
"secp256k1": "^5.0.0"
"@noble/curves": "^1.1.0"
},
"devDependencies": {
"@types/chai": "^4.3.3",
"@types/mocha": "^10.0.0",
"@types/node": "^20.2.3",
"@types/jest": "^29.5.2",
"@types/node": "^20.3.2",
"axios": "^1.4.0",
"chai": "^4.3.6",
"mocha": "^10.2.0",
"nyc": "^15.1.0",
"ts-node": "^10.9.0",
"typescript": "^5.0.4"
"jest": "^29.5.0",
"ts-jest": "^29.1.0",
"ts-node": "^10.9.1",
"typescript": "^5.1.5"
}
}
1 change: 1 addition & 0 deletions src/consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export const AES_IV_LENGTH = 16;
export const AES_TAG_LENGTH = 16;
export const AES_IV_PLUS_TAG_LENGTH = AES_IV_LENGTH + AES_TAG_LENGTH;
export const SECRET_KEY_LENGTH = 32;
export const ONE = BigInt(1);
16 changes: 4 additions & 12 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
import { PrivateKey, PublicKey } from "./keys";
import {
aesDecrypt,
aesEncrypt,
decodeHex,
getValidSecret,
remove0x,
} from "./utils";
import { UNCOMPRESSED_PUBLIC_KEY_SIZE } from "./consts";
import { PrivateKey, PublicKey } from "./keys";
import { aesDecrypt, aesEncrypt, decodeHex, getValidSecret, remove0x } from "./utils";

export function encrypt(receiverRawPK: string | Buffer, msg: Buffer): Buffer {
const ephemeralKey = new PrivateKey();
Expand All @@ -27,10 +21,8 @@ export function decrypt(receiverRawSK: string | Buffer, msg: Buffer): Buffer {
? new PrivateKey(receiverRawSK)
: PrivateKey.fromHex(receiverRawSK);

const senderPubkey = new PublicKey(
msg.slice(0, UNCOMPRESSED_PUBLIC_KEY_SIZE)
);
const encrypted = msg.slice(UNCOMPRESSED_PUBLIC_KEY_SIZE);
const senderPubkey = new PublicKey(msg.subarray(0, UNCOMPRESSED_PUBLIC_KEY_SIZE));
const encrypted = msg.subarray(UNCOMPRESSED_PUBLIC_KEY_SIZE);
const aesKey = senderPubkey.decapsulate(receiverSK);
return aesDecrypt(aesKey, encrypted);
}
Expand Down
27 changes: 9 additions & 18 deletions src/keys/PrivateKey.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import hkdf from "futoin-hkdf";
import secp256k1 from "secp256k1";

import { secp256k1 } from "@noble/curves/secp256k1";
import { hkdf } from "@noble/hashes/hkdf";
import { sha256 } from "@noble/hashes/sha256";
import { decodeHex, getValidSecret } from "../utils";
import PublicKey from "./PublicKey";

Expand All @@ -14,32 +14,23 @@ export default class PrivateKey {

constructor(secret?: Buffer) {
this.secret = secret || getValidSecret();
if (!secp256k1.privateKeyVerify(this.secret)) {
if (!secp256k1.utils.isValidPrivateKey(this.secret)) {
throw new Error("Invalid private key");
}
this.publicKey = new PublicKey(
Buffer.from(secp256k1.publicKeyCreate(this.secret))
);
this.publicKey = new PublicKey(Buffer.from(secp256k1.getPublicKey(this.secret)));
}

public toHex(): string {
return `0x${this.secret.toString("hex")}`;
return this.secret.toString("hex");
}

public encapsulate(pub: PublicKey): Buffer {
const master = Buffer.concat([
this.publicKey.uncompressed,
this.multiply(pub),
]);
return hkdf(master, 32, {
hash: "SHA-256",
});
const master = Buffer.concat([this.publicKey.uncompressed, this.multiply(pub)]);
return Buffer.from(hkdf(sha256, master, undefined, undefined, 32));
}

public multiply(pub: PublicKey): Buffer {
return Buffer.from(
secp256k1.publicKeyTweakMul(pub.compressed, this.secret, false)
);
return Buffer.from(secp256k1.getSharedSecret(this.secret, pub.compressed, false));
}

public equals(other: PrivateKey): boolean {
Expand Down
16 changes: 7 additions & 9 deletions src/keys/PublicKey.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import hkdf from "futoin-hkdf";
import secp256k1 from "secp256k1";

import { secp256k1 } from "@noble/curves/secp256k1";
import { hkdf } from "@noble/hashes/hkdf";
import { sha256 } from "@noble/hashes/sha256";
import { ONE, UNCOMPRESSED_PUBLIC_KEY_SIZE } from "../consts";
import { decodeHex } from "../utils";
import { UNCOMPRESSED_PUBLIC_KEY_SIZE } from "../consts";
import PrivateKey from "./PrivateKey";

export default class PublicKey {
Expand All @@ -21,8 +21,8 @@ export default class PublicKey {
public readonly compressed: Buffer;

constructor(buffer: Buffer) {
this.uncompressed = Buffer.from(secp256k1.publicKeyConvert(buffer, false));
this.compressed = Buffer.from(secp256k1.publicKeyConvert(buffer, true));
this.uncompressed = Buffer.from(secp256k1.getSharedSecret(ONE, buffer, false));
this.compressed = Buffer.from(secp256k1.getSharedSecret(ONE, buffer, true));
}

public toHex(compressed: boolean = true): string {
Expand All @@ -35,9 +35,7 @@ export default class PublicKey {

public decapsulate(priv: PrivateKey): Buffer {
const master = Buffer.concat([this.uncompressed, priv.multiply(this)]);
return hkdf(master, 32, {
hash: "SHA-256",
});
return Buffer.from(hkdf(sha256, master, undefined, undefined, 32));
}

public equals(other: PublicKey): boolean {
Expand Down
16 changes: 6 additions & 10 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
import { secp256k1 } from "@noble/curves/secp256k1";
import { createCipheriv, createDecipheriv, randomBytes } from "crypto";
import secp256k1 from "secp256k1";

import {
AES_IV_LENGTH,
AES_IV_PLUS_TAG_LENGTH,
SECRET_KEY_LENGTH,
} from "./consts";
import { AES_IV_LENGTH, AES_IV_PLUS_TAG_LENGTH, SECRET_KEY_LENGTH } from "./consts";

export function remove0x(hex: string): string {
if (hex.startsWith("0x") || hex.startsWith("0X")) {
Expand All @@ -22,7 +18,7 @@ export function getValidSecret(): Buffer {
let key: Buffer;
do {
key = randomBytes(SECRET_KEY_LENGTH);
} while (!secp256k1.privateKeyVerify(key));
} while (!secp256k1.utils.isValidPrivateKey(key));
return key;
}

Expand All @@ -35,9 +31,9 @@ export function aesEncrypt(key: Buffer, plainText: Buffer): Buffer {
}

export function aesDecrypt(key: Buffer, cipherText: Buffer): Buffer {
const nonce = cipherText.slice(0, AES_IV_LENGTH);
const tag = cipherText.slice(AES_IV_LENGTH, AES_IV_PLUS_TAG_LENGTH);
const ciphered = cipherText.slice(AES_IV_PLUS_TAG_LENGTH);
const nonce = cipherText.subarray(0, AES_IV_LENGTH);
const tag = cipherText.subarray(AES_IV_LENGTH, AES_IV_PLUS_TAG_LENGTH);
const ciphered = cipherText.subarray(AES_IV_PLUS_TAG_LENGTH);
const decipher = createDecipheriv("aes-256-gcm", key, nonce);
decipher.setAuthTag(tag);
return Buffer.concat([decipher.update(ciphered), decipher.final()]);
Expand Down
64 changes: 28 additions & 36 deletions tests/crypt.test.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,19 @@
import axios from "axios";
import { expect } from "chai";
import { randomBytes } from "crypto";
import { stringify } from "querystring";

import { decrypt, encrypt } from "../src/index";
import { PrivateKey, PublicKey } from "../src/keys";
import { aesDecrypt, aesEncrypt, decodeHex } from "../src/utils";

const PYTHON_BACKEND = "https://ecies.deta.dev/";
const PYTHON_BACKEND = "https://eciespydemo-1-d5397785.deta.app/";
const TEXT = "helloworld";

describe("test encrypt and decrypt", () => {
const TEXT = "helloworld";

it("tests aes with random key", () => {
const key = randomBytes(32);
const data = Buffer.from("this is a test");
expect(data.equals(aesDecrypt(key, aesEncrypt(key, data)))).to.be.equal(true);
expect(data.equals(aesDecrypt(key, aesEncrypt(key, data)))).toBe(true);
});

it("tests aes decrypt with known key and TEXT", () => {
Expand All @@ -28,27 +26,27 @@ describe("test encrypt and decrypt", () => {

const data = Buffer.concat([nonce, tag, encrypted]);
const decrypted = aesDecrypt(key, data);
expect(decrypted.toString()).to.be.equal(TEXT);
expect(decrypted.toString()).toBe(TEXT);
});

it("tests encrypt/decrypt buffer", () => {
const prv1 = new PrivateKey();
const encrypted1 = encrypt(prv1.publicKey.uncompressed, Buffer.from(TEXT));
expect(decrypt(prv1.secret, encrypted1).toString()).to.be.equal(TEXT);
expect(decrypt(prv1.secret, encrypted1).toString()).toBe(TEXT);

const prv2 = new PrivateKey();
const encrypted2 = encrypt(prv2.publicKey.compressed, Buffer.from(TEXT));
expect(decrypt(prv2.secret, encrypted2).toString()).to.be.equal(TEXT);
expect(decrypt(prv2.secret, encrypted2).toString()).toBe(TEXT);
});

it("tests encrypt/decrypt hex", () => {
const prv1 = new PrivateKey();
const encrypted1 = encrypt(prv1.publicKey.toHex(), Buffer.from(TEXT));
expect(decrypt(prv1.toHex(), encrypted1).toString()).to.be.equal(TEXT);
expect(decrypt(prv1.toHex(), encrypted1).toString()).toBe(TEXT);

const prv2 = new PrivateKey();
const encrypted2 = encrypt(prv2.publicKey.toHex(), Buffer.from(TEXT));
expect(decrypt(prv2.toHex(), encrypted2).toString()).to.be.equal(TEXT);
expect(decrypt(prv2.toHex(), encrypted2).toString()).toBe(TEXT);
});

it("tests sk pk", () => {
Expand All @@ -59,37 +57,31 @@ describe("test encrypt and decrypt", () => {
"048e41409f2e109f2d704f0afd15d1ab53935fd443729913a7e8536b4cef8cf5773d4db7bbd99e9ed64595e24a251c9836f35d4c9842132443c17f6d501b3410d2"
);
const enc = encrypt(pk.toHex(), Buffer.from(TEXT));
expect(decrypt(sk.toHex(), enc).toString()).to.be.equal(TEXT);
expect(decrypt(sk.toHex(), enc).toString()).toBe(TEXT);
});

it("tests encrypt/decrypt against python version", () => {
it("tests encrypt/decrypt against python version", async () => {
const prv = new PrivateKey();
let res = await axios.post(
PYTHON_BACKEND,
stringify({
data: TEXT,
pub: prv.publicKey.toHex(),
})
);
const encryptedKnown = Buffer.from(decodeHex(res.data));
const decrypted = decrypt(prv.toHex(), encryptedKnown);

axios
.post(
PYTHON_BACKEND,
stringify({
data: TEXT,
pub: prv.publicKey.toHex(),
})
)
.then((res) => {
const encryptedKnown = Buffer.from(decodeHex(res.data));
const decrypted = decrypt(prv.toHex(), encryptedKnown);
expect(decrypted.toString()).to.be.equal(TEXT);
});
expect(decrypted.toString()).toEqual(TEXT);

const encrypted = encrypt(prv.publicKey.toHex(), Buffer.from(TEXT));
axios
.post(
PYTHON_BACKEND,
stringify({
data: encrypted.toString("hex"),
prv: prv.toHex(),
})
)
.then((res) => {
expect(TEXT).to.be.equal(res.data);
});
res = await axios.post(
PYTHON_BACKEND,
stringify({
data: encrypted.toString("hex"),
prv: prv.toHex(),
})
);
expect(TEXT).toEqual(res.data);
});
});
Loading