Skip to content

Commit

Permalink
Change secp256k1 and test library (#720)
Browse files Browse the repository at this point in the history
* Change secp256k1 and test library

* Change hkdf library

* Revamp test
  • Loading branch information
kigawas authored Jul 1, 2023
1 parent 8902d54 commit f356e42
Show file tree
Hide file tree
Showing 14 changed files with 1,582 additions and 998 deletions.
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

0 comments on commit f356e42

Please sign in to comment.