Skip to content

Commit

Permalink
Add details (#833)
Browse files Browse the repository at this point in the history
  • Loading branch information
kigawas authored Feb 21, 2025
1 parent 2baf436 commit a85bc84
Show file tree
Hide file tree
Showing 8 changed files with 218 additions and 91 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,13 @@ jobs:

- name: Run browser tests (chromium, firefox)
run: |
pnpm test:browser
pnpm test:browser -- --browser=firefox
pnpm test:browser --browser=chromium
pnpm test:browser --browser=firefox
- name: Run browser tests (webkit)
if: matrix.os == 'macos-latest'
run: |
pnpm test:browser -- --browser=webkit
pnpm test:browser --browser=webkit
check-runtimes:
runs-on: ubuntu-latest
Expand Down
126 changes: 126 additions & 0 deletions DETAILS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# Mechanism and Implementation in JavaScript

> [!NOTE]
>
> This document is an adapted version of the original document in [eciespy](https://github.com/ecies/py/blob/master/DETAILS.md). You may go there for detailed documentation and learn the mechanism under the hood.
This library combines `secp256k1` and `AES-256-GCM` (powered by [@noble/curves](https://github.com/paulmillr/noble-curves) and [@noble/ciphers](https://github.com/paulmillr/noble-ciphers)) to provide an API for encrypting with `secp256k1` public key and decrypting with `secp256k1`'s private key. It consists of two main parts:

1. Use [ECDH](https://en.wikipedia.org/wiki/Elliptic-curve_Diffie–Hellman) to exchange an AES session key;

> Note that the sender public key is generated every time when `encrypt` is called, thus, the AES session key varies.
>
> We use HKDF-SHA256 instead of SHA256 to derive the AES keys for better security.
2. Use this AES session key to encrypt/decrypt the data under `AES-256-GCM`.

The encrypted data structure is as follows:

```plaintext
+-------------------------------+----------+----------+-----------------+
| 65 Bytes | 16 Bytes | 16 Bytes | == data size |
+-------------------------------+----------+----------+-----------------+
| Sender Public Key (ephemeral) | Nonce/IV | Tag/MAC | Encrypted data |
+-------------------------------+----------+----------+-----------------+
| sender_pk | nonce | tag | encrypted_data |
+-------------------------------+----------+----------+-----------------+
| Secp256k1 | AES-256-GCM |
+-------------------------------+---------------------------------------+
```

## Secp256k1 in JavaScript

### ECDH Implementation

In JavaScript, we use the [@noble/curves](https://github.com/paulmillr/noble-curves) library which provides a pure JavaScript implementation of secp256k1. Here's a basic example:

```typescript
import { secp256k1 } from '@noble/curves/secp256k1';
import { equalBytes } from "@noble/ciphers/utils";

// Generate private keys (in production, use crypto.getRandomValues())
const k1 = 3n;
const k2 = 2n;

// Get public keys
const pub1 = secp256k1.getPublicKey(k1);
const pub2 = secp256k1.getPublicKey(k2);

// Calculate shared secret - both parties will get the same result
const shared1 = secp256k1.getSharedSecret(k1, pub2);
const shared2 = secp256k1.getSharedSecret(k2, pub1);

console.log(equalBytes(shared1, shared2));
// true
```

### Public Key Formats

Just like in the Python implementation, secp256k1 public keys can be represented in compressed (33 bytes) or uncompressed (65 bytes) format:

- Uncompressed format (65 bytes): `04 || x || y`
- Compressed format (33 bytes): `02/03 || x` (02 if y is even, 03 if y is odd)

The library handles both formats seamlessly:

```typescript
import { secp256k1 } from '@noble/curves/secp256k1';

const privateKey = 3n;
const publicKeyUncompressed = secp256k1.getPublicKey(privateKey, false); // 65 bytes
const publicKeyCompressed = secp256k1.getPublicKey(privateKey, true); // 33 bytes
```

## AES in JavaScript

For AES encryption, we use [@noble/ciphers](https://github.com/paulmillr/noble-ciphers) which provides a pure JavaScript implementation of AES-GCM. Here's a basic example:

```typescript
import { gcm } from '@noble/ciphers/aes';

// 32-byte key from ECDH
const key = new Uint8Array(32);
// 16-byte nonce
const nonce = new Uint8Array(16);
const data = new TextEncoder().encode('hello world');

// Encrypt
const cipher = gcm(key, nonce);
const encrypted = cipher.encrypt(data);

// Decrypt
const decipher = gcm(key, nonce);
const decrypted = decipher.decrypt(encrypted);

console.log(new TextDecoder().decode(decrypted));
// 'hello world'
```

Note that due to the format difference between @noble/ciphers with Python implementation, we need to adjust the position of nonce and tag in the encrypted data:

```js
const encrypted = cipher.encrypt(data);
const cipherTextLength = encrypted.length - tagLength;
const cipherText = encrypted.subarray(0, cipherTextLength);
const tag = encrypted.subarray(cipherTextLength);
// ecies payload format: pk || nonce || tag || cipherText
const adjustedEncrypted = concatBytes(nonce, tag, cipherText);
```

## Key Derivation

Instead of using plain SHA256 for key derivation, we use HKDF-SHA256 which is more secure:

```typescript
import { hkdf } from '@noble/hashes/hkdf';
import { sha256 } from '@noble/hashes/sha256';

// Derive AES key from ECDH shared secret
const ourPrivateKey = 3n;
// const ourPublicKey = secp256k1.getPublicKey(ourPrivateKey);
const theirPrivateKey = 2n;
const theirPublicKey = secp256k1.getPublicKey(theirPrivateKey);

const sharedSecret = secp256k1.getSharedSecret(ourPrivateKey, theirPublicKey);
const sharedKey = hkdf(sha256, sharedSecret, undefined, undefined, 32);
```
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@

Elliptic Curve Integrated Encryption Scheme for secp256k1/curve25519 in TypeScript.

This is the JavaScript/TypeScript version of [eciespy](https://github.com/ecies/py) with a built-in class-like secp256k1/curve25519 [API](#privatekey), you may go there for detailed documentation and learn the mechanism under the hood.
This is the JavaScript/TypeScript version of [eciespy](https://github.com/ecies/py) with a built-in class-like secp256k1/curve25519 [API](#privatekey).

You can learn the details in [DETAILS.md](./DETAILS.md).

## Install

Expand Down
2 changes: 1 addition & 1 deletion example/browser/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"eciesjs": "file:../.."
},
"devDependencies": {
"vite": "^6.0.7",
"vite": "^6.1.1",
"vite-bundle-visualizer": "^1.2.1"
}
}
10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"type": "git",
"url": "git+https://github.com/ecies/js.git"
},
"version": "0.4.13",
"version": "0.4.14",
"engines": {
"node": ">=16",
"bun": ">=1",
Expand Down Expand Up @@ -62,11 +62,11 @@
"@noble/hashes": "^1.5.0"
},
"devDependencies": {
"@types/node": "^22.13.0",
"@vitest/coverage-v8": "^3.0.4",
"@types/node": "^22.13.4",
"@vitest/coverage-v8": "^3.0.6",
"typescript": "^5.7.3",
"undici": "^7.3.0",
"vitest": "^3.0.4"
"vitest": "^3.0.6"
},
"packageManager": "pnpm@9.15.4+sha512.b2dc20e2fc72b3e18848459b37359a32064663e5627a51e4c74b2c29dd8e8e0491483c3abb40789cfd578bf362fb6ba8261b05f0387d76792ed6e23ea3b1b6a0"
"packageManager": "pnpm@10.4.1+sha512.c753b6c3ad7afa13af388fa6d808035a008e30ea9993f58c6663e2bc5ff21679aa834db094987129aa4d488b86df57f7b634981b2f827cdcacc698cc0cfb88af"
}
Loading

0 comments on commit a85bc84

Please sign in to comment.