From 328dae85f29c9a4a93740b6bfcdf17a4b2d7ed0a Mon Sep 17 00:00:00 2001 From: Dzmitry Lahunouski Date: Sun, 2 Nov 2025 14:09:05 +0000 Subject: [PATCH 1/2] implementation --- README.md | 80 ++++++++++++- jest.config.ts | 3 + package-lock.json | 44 +++++++ package.json | 1 + src/index.ts | 1 + src/qcrypt.ts | 125 +++++++++++++++++++ test/qcrypt.spec.ts | 283 ++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 534 insertions(+), 3 deletions(-) create mode 100644 src/qcrypt.ts create mode 100644 test/qcrypt.spec.ts diff --git a/README.md b/README.md index 19ede0e..710e54b 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ TypeScript Development Kit for ZeroLedger Protocol - A comprehensive cryptograph - **Stealth Addresses**: Create and derive stealth addresses using secp256k1 public keys and random values - **ECDH Encryption**: Asymmetric encryption using ephemeral key pairs and AES-256-GCM +- **Quantum-Resistant Encryption**: Post-quantum encryption using ML-KEM-768 (Kyber) in a separate module - **Elliptic Curve Operations**: Secure multiplication of public and private keys on secp256k1 curve - **Type Safety**: Full TypeScript support with comprehensive type definitions @@ -19,6 +20,7 @@ TypeScript Development Kit for ZeroLedger Protocol - A comprehensive cryptograph - **Forward Secrecy**: Uses ephemeral keys for each encryption operation - **Authenticated Encryption**: AES-256-GCM provides both confidentiality and integrity +- **Quantum Resistance**: Optional ML-KEM-768 (Kyber) for post-quantum security - **Key Validation**: Comprehensive validation of private and public keys - **Random IV**: Each encryption uses a cryptographically secure random IV - **ECDH Key Agreement**: Secure key derivation using secp256k1 curve @@ -41,9 +43,11 @@ The library automatically provides the appropriate format based on your import m ```typescript // ES Modules (recommended for modern projects) import { encrypt, decrypt } from "@zeroledger/vycrypt"; +import { encryptQuantum, decryptQuantum } from "@zeroledger/vycrypt"; // CommonJS (for legacy Node.js or specific bundler requirements) const { encrypt, decrypt } = require("@zeroledger/vycrypt"); +const { encryptQuantum, decryptQuantum } = require("@zeroledger/vycrypt"); ``` ### Build Output @@ -76,6 +80,38 @@ Decrypts data using your private key. - **Returns:** Original decrypted string - **Throws:** Error if decryption fails or keys are invalid +### Quantum-Resistant Encryption & Decryption + +#### `generateQuantumKeyPair(seed?: string): QuantumKeyPair` +Generates a quantum-resistant key pair using ML-KEM-768. + +- **Parameters:** + - `seed`: Optional seed string for deterministic key generation (any string, including unicode) +- **Returns:** Object with `publicKey` and `secretKey` (both Hex strings) + - `publicKey`: 0x-prefixed hex string (1184 bytes / 2368 hex chars) + - `secretKey`: 0x-prefixed hex string (2400 bytes / 4800 hex chars) +- **Security:** + - Without seed: Uses cryptographically secure random generation + - With seed: Derives deterministic 64-byte seed using SHA-512 hashing + +#### `encryptQuantum(data: string, publicKey: Hex): Hex` +Encrypts data using quantum-resistant ML-KEM-768. + +- **Parameters:** + - `data`: String to encrypt (supports any UTF-8 data) + - `publicKey`: Recipient's ML-KEM-768 public key (0x-prefixed hex string, 1184 bytes) +- **Returns:** Encrypted data as hex string +- **Security:** Uses ML-KEM-768 key encapsulation + AES-256-GCM + +#### `decryptQuantum(secretKey: Hex, encodedData: Hex): string` +Decrypts data encrypted with quantum-resistant encryption. + +- **Parameters:** + - `secretKey`: Your ML-KEM-768 secret key (0x-prefixed hex string, 2400 bytes) + - `encodedData`: Encrypted data from `encryptQuantum()` +- **Returns:** Original decrypted string +- **Throws:** Error if decryption fails or keys are invalid + ### Stealth Addresses #### `createStealth(publicKey: Hex): { stealthAddress: string, random: bigint }` @@ -116,7 +152,7 @@ Multiplies a private key by a scalar value. ## Usage Examples -### Basic Encryption/Decryption +### Basic Encryption/Decryption (Classic) ```typescript import { encrypt, decrypt } from "@zeroledger/vycrypt"; @@ -135,6 +171,26 @@ const decryptedData = decrypt(privKey, encryptedData); console.log(decryptedData); // "Hello, World!" ``` +### Quantum-Resistant Encryption/Decryption + +```typescript +import { encryptQuantum, decryptQuantum, generateQuantumKeyPair } from "@zeroledger/vycrypt"; + +// Generate random quantum-resistant key pair +const keyPair = generateQuantumKeyPair(); + +// Or generate deterministic key pair from a seed string +const deterministicKeyPair = generateQuantumKeyPair("my-secret-passphrase"); + +// Encrypt data +const data = "Secret message protected from quantum computers"; +const encryptedData = encryptQuantum(data, keyPair.publicKey); + +// Decrypt data +const decryptedData = decryptQuantum(keyPair.secretKey, encryptedData); +console.log(decryptedData); // "Secret message protected from quantum computers" +``` + ### Stealth Address Creation ```typescript @@ -216,6 +272,13 @@ console.log(JSON.parse(decrypted)); // Original object - **Deterministic derivation**: Same inputs always produce the same stealth address - **No correlation**: Different random values produce uncorrelated stealth addresses +### Quantum-Resistant Encryption +- **ML-KEM-768 (Kyber)**: NIST-standardized post-quantum key encapsulation mechanism (FIPS 203) +- **Post-quantum security**: Protects against both classical and quantum computer attacks +- **Separate module**: Keep quantum encryption isolated in `qcrypt.ts` for clarity +- **Larger keys**: ML-KEM keys are ~1-2KB compared to ~33 bytes for secp256k1 +- **Hybrid approach**: Combines post-quantum KEM with classical AES-256-GCM + ## Error Handling The library throws descriptive errors for invalid inputs: @@ -238,7 +301,8 @@ try { The library includes comprehensive tests covering: -- **Encryption/Decryption**: Round-trip operations with various data types +- **Classic Encryption/Decryption**: Round-trip operations with various data types +- **Quantum-Resistant Encryption**: ML-KEM-768 encryption and decryption - **Error handling**: Invalid inputs and malformed data - **Edge cases**: Empty strings, large data, unicode, binary data - **Security properties**: Non-deterministic encryption, different outputs for different keys @@ -250,11 +314,21 @@ Run tests with: npm test ``` +Run specific test suites: +```bash +# Classic encryption tests +npm test -- test/crypt.spec.ts + +# Quantum-resistant encryption tests +npm test -- test/qcrypt.spec.ts +``` + ## Dependencies - **@noble/curves**: For secp256k1 elliptic curve operations - **@noble/ciphers**: For AES-256-GCM encryption -- **@noble/hashes**: For SHA-256 hashing +- **@noble/hashes**: For SHA-256 and SHA-3 hashing +- **@noble/post-quantum**: For ML-KEM-768 (Kyber) post-quantum encryption - **viem**: For Ethereum-compatible utilities and types ## Contributing diff --git a/jest.config.ts b/jest.config.ts index 525f07b..2416abb 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -7,6 +7,9 @@ const config: Config = { transform: { "^.+\\.(t|j)s$": "@swc/jest", }, + transformIgnorePatterns: [ + "node_modules/(?!(@noble/post-quantum|@noble/hashes|@noble/curves|@noble/ciphers)/)", + ], collectCoverageFrom: ["/src/**/*.ts", "!/**/*.module.ts"], coveragePathIgnorePatterns: [ "/node_modules/", diff --git a/package-lock.json b/package-lock.json index c182afa..625cce7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "SEE LICENSE IN LICENSE", "dependencies": { "@noble/ciphers": "^1.3.0", + "@noble/post-quantum": "^0.5.2", "viem": "^2.36.0" }, "devDependencies": { @@ -1916,6 +1917,49 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@noble/post-quantum": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@noble/post-quantum/-/post-quantum-0.5.2.tgz", + "integrity": "sha512-etMDBkCuB95Xj/gfsWYBD2x+84IjL4uMLd/FhGoUUG/g+eh0K2eP7pJz1EmvpN8Df3vKdoWVAc7RxIBCHQfFHQ==", + "license": "MIT", + "dependencies": { + "@noble/curves": "~2.0.0", + "@noble/hashes": "~2.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/post-quantum/node_modules/@noble/curves": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.0.1.tgz", + "integrity": "sha512-vs1Az2OOTBiP4q0pwjW5aF0xp9n4MxVrmkFBxc6EKZc6ddYx5gaZiAsZoq0uRRXWbi3AT/sBqn05eRPtn1JCPw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "2.0.1" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/post-quantum/node_modules/@noble/hashes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz", + "integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", diff --git a/package.json b/package.json index 4e3258f..fabc5a5 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ }, "dependencies": { "@noble/ciphers": "^1.3.0", + "@noble/post-quantum": "^0.5.2", "viem": "^2.36.0" } } diff --git a/src/index.ts b/src/index.ts index d5d2c9b..23e43c9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1,3 @@ export * from "./crypt"; +export * from "./qcrypt"; export * from "./stealth"; diff --git a/src/qcrypt.ts b/src/qcrypt.ts new file mode 100644 index 0000000..ba6db0f --- /dev/null +++ b/src/qcrypt.ts @@ -0,0 +1,125 @@ +import { randomBytes } from "@noble/hashes/utils"; +import { gcm } from "@noble/ciphers/aes"; +import { ml_kem768 } from "@noble/post-quantum/ml-kem.js"; +import { sha512 } from "@noble/hashes/sha2"; + +import { type Hex, isHex, sha256, toHex, toBytes } from "viem"; + +const decoder = new TextDecoder(); +const encoder = new TextEncoder(); + +/** + * Quantum-resistant key pair for ML-KEM encryption + */ +export interface QuantumKeyPair { + publicKey: Hex; + secretKey: Hex; +} + +/** + * @notice Throws if provided public key is not valid. + * @param publicKey ML-KEM-768 public key as hex string + */ +export function assertValidQuantumPublicKey(publicKey: Hex) { + if (!isHex(publicKey)) { + throw new Error("Must provide public key as hex string"); + } + const keyBytes = toBytes(publicKey); + if (keyBytes.length !== 1184) { + throw new Error("Invalid ML-KEM-768 public key length"); + } +} + +/** + * @notice Throws if provided secret key is not valid. + * @param secretKey ML-KEM-768 secret key as hex string + */ +export function assertValidQuantumSecretKey(secretKey: Hex) { + if (!isHex(secretKey)) { + throw new Error("Must provide secret key as hex string"); + } + const keyBytes = toBytes(secretKey); + if (keyBytes.length !== 2400) { + throw new Error("Invalid ML-KEM-768 secret key length"); + } +} + +/** + * @notice Generate a quantum-resistant key pair using ML-KEM-768 + * @param seed Optional seed string for deterministic key generation + * @returns QuantumKeyPair with public and secret keys as hex strings + */ +export function generateQuantumKeyPair(seed?: string): QuantumKeyPair { + let seedBytes: Uint8Array; + + if (seed !== undefined) { + // Generate 64-byte seed from string using SHA-512 (produces exactly 64 bytes) + seedBytes = sha512(encoder.encode(seed)); + } else { + seedBytes = randomBytes(64); + } + + const keyPair = ml_kem768.keygen(seedBytes); + return { + publicKey: toHex(keyPair.publicKey), + secretKey: toHex(keyPair.secretKey), + }; +} + +/** + * @notice Encrypt data using quantum-resistant ML-KEM-768 + * @param data String to encrypt (supports any UTF-8 data) + * @param publicKey Recipient's ML-KEM-768 public key (0x-prefixed hex string, 1184 bytes) + * @returns Hex string of encrypted data + */ +export const encryptQuantum = (data: string, publicKey: Hex): Hex => { + assertValidQuantumPublicKey(publicKey); + + // Convert public key from hex to bytes + const publicKeyBytes = toBytes(publicKey); + + // Encapsulate to get shared secret and KEM ciphertext + const { cipherText: kemCiphertext, sharedSecret } = + ml_kem768.encapsulate(publicKeyBytes); + + // Derive AES key from shared secret + const aesKey = toBytes(sha256(toHex(sharedSecret))); + + // Encrypt data with AES-256-GCM + const iv = randomBytes(12); + const rawData = encoder.encode(data); + const aes = gcm(aesKey, iv); + const ciphertext = aes.encrypt(rawData); + + // Format: iv(12) + kemCiphertext(1088) + ciphertext(variable) + return `${toHex(iv)}${toHex(kemCiphertext).slice(2)}${toHex(ciphertext).slice(2)}` as Hex; +}; + +/** + * @notice Decrypt data using quantum-resistant ML-KEM-768 + * @param secretKey Your ML-KEM-768 secret key (0x-prefixed hex string, 2400 bytes) + * @param encodedData Encrypted data from encryptQuantum() + * @returns Decrypted string + */ +export const decryptQuantum = (secretKey: Hex, encodedData: Hex): string => { + assertValidQuantumSecretKey(secretKey); + + // Convert secret key from hex to bytes + const secretKeyBytes = toBytes(secretKey); + + // Manually split string: bytes12 (iv) + bytes1088 (KEM ciphertext) + rest (AES ciphertext) + const iv = toBytes(encodedData.slice(0, 26)); // 0x + 12*2 = 26 + const kemCiphertext = toBytes(`0x${encodedData.slice(26, 2202)}`); // 26 + 1088*2 = 2202 + const ciphertext = toBytes(`0x${encodedData.slice(2202)}`); + + // Decapsulate to get shared secret + const sharedSecret = ml_kem768.decapsulate(kemCiphertext, secretKeyBytes); + + // Derive AES key from shared secret + const aesKey = toBytes(sha256(toHex(sharedSecret))); + + // Decrypt with AES-256-GCM + const aes = gcm(aesKey, iv); + + return decoder.decode(aes.decrypt(ciphertext)); +}; diff --git a/test/qcrypt.spec.ts b/test/qcrypt.spec.ts new file mode 100644 index 0000000..a2c7b50 --- /dev/null +++ b/test/qcrypt.spec.ts @@ -0,0 +1,283 @@ +import { isHex } from "viem"; +import { + encryptQuantum, + decryptQuantum, + generateQuantumKeyPair, + type QuantumKeyPair, +} from "../src/qcrypt"; +import * as fs from "fs"; + +describe("quantum-resistant encryption", () => { + let keyPair: QuantumKeyPair; + + beforeEach(() => { + keyPair = generateQuantumKeyPair(); + }); + + const hexData = `0xa5eaba8f6b292d059d9e8c3a2f1b16af`; + const jsonData = fs.readFileSync("./test/mocks/arbitraryData.json", "utf8"); + + describe("generateQuantumKeyPair", () => { + it("should generate a valid key pair", () => { + expect(isHex(keyPair.publicKey)).toBeTruthy(); + expect(isHex(keyPair.secretKey)).toBeTruthy(); + // Public key: 1184 bytes = 2368 hex chars + 2 for "0x" = 2370 + expect(keyPair.publicKey.length).toBe(2370); + // Secret key: 2400 bytes = 4800 hex chars + 2 for "0x" = 4802 + expect(keyPair.secretKey.length).toBe(4802); + }); + + it("should generate different key pairs each time", () => { + const keyPair1 = generateQuantumKeyPair(); + const keyPair2 = generateQuantumKeyPair(); + + expect(keyPair1.publicKey).not.toEqual(keyPair2.publicKey); + expect(keyPair1.secretKey).not.toEqual(keyPair2.secretKey); + }); + + it("should accept a seed string parameter", () => { + const seed = "my-deterministic-seed"; + const keyPair1 = generateQuantumKeyPair(seed); + const keyPair2 = generateQuantumKeyPair(seed); + + // Same seed should produce same key pair + expect(keyPair1.publicKey).toBe(keyPair2.publicKey); + expect(keyPair1.secretKey).toBe(keyPair2.secretKey); + }); + + it("should produce different keys for different seeds", () => { + const keyPair1 = generateQuantumKeyPair("seed1"); + const keyPair2 = generateQuantumKeyPair("seed2"); + + expect(keyPair1.publicKey).not.toBe(keyPair2.publicKey); + expect(keyPair1.secretKey).not.toBe(keyPair2.secretKey); + }); + + it("should handle empty seed string", () => { + const keyPair1 = generateQuantumKeyPair(""); + const keyPair2 = generateQuantumKeyPair(""); + + // Empty seed should still be deterministic + expect(keyPair1.publicKey).toBe(keyPair2.publicKey); + expect(keyPair1.secretKey).toBe(keyPair2.secretKey); + }); + + it("should handle unicode seed strings", () => { + const seed = "Hello δΈ–η•Œ 🌍"; + const keyPair1 = generateQuantumKeyPair(seed); + const keyPair2 = generateQuantumKeyPair(seed); + + expect(keyPair1.publicKey).toBe(keyPair2.publicKey); + expect(keyPair1.secretKey).toBe(keyPair2.secretKey); + }); + }); + + describe("encryptQuantum", () => { + it("should encrypt hex string", () => { + const encrypted = encryptQuantum(hexData, keyPair.publicKey); + expect(isHex(encrypted)).toBeTruthy(); + }); + + it("should encrypt json string", () => { + const encrypted = encryptQuantum(jsonData, keyPair.publicKey); + expect(isHex(encrypted)).toBeTruthy(); + }); + + it("should encrypt empty string", () => { + const encrypted = encryptQuantum("", keyPair.publicKey); + expect(isHex(encrypted)).toBeTruthy(); + expect(decryptQuantum(keyPair.secretKey, encrypted)).toBe(""); + }); + + it("should encrypt large data", () => { + const largeData = "x".repeat(10000); + const encrypted = encryptQuantum(largeData, keyPair.publicKey); + expect(isHex(encrypted)).toBeTruthy(); + expect(decryptQuantum(keyPair.secretKey, encrypted)).toBe(largeData); + }); + + it("should encrypt unicode data", () => { + const unicodeData = "Hello δΈ–η•Œ 🌍 emoji πŸš€"; + const encrypted = encryptQuantum(unicodeData, keyPair.publicKey); + expect(isHex(encrypted)).toBeTruthy(); + expect(decryptQuantum(keyPair.secretKey, encrypted)).toBe(unicodeData); + }); + + it("should encrypt binary-like data", () => { + const binaryData = "\x00\x01\x02\x03\xff\xfe\xfd"; + const encrypted = encryptQuantum(binaryData, keyPair.publicKey); + expect(isHex(encrypted)).toBeTruthy(); + expect(decryptQuantum(keyPair.secretKey, encrypted)).toBe(binaryData); + }); + + it("should produce different ciphertexts for same plaintext", () => { + const data = "test data"; + const encrypted1 = encryptQuantum(data, keyPair.publicKey); + const encrypted2 = encryptQuantum(data, keyPair.publicKey); + expect(encrypted1).not.toBe(encrypted2); + }); + + it("should throw error for non-hex public key", () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(() => encryptQuantum("test", "not-hex" as any)).toThrow( + "Must provide public key as hex string", + ); + }); + + it("should throw error for invalid public key length", () => { + const invalidKey = "0x1234" as `0x${string}`; + expect(() => encryptQuantum("test", invalidKey)).toThrow( + "Invalid ML-KEM-768 public key length", + ); + }); + }); + + describe("decryptQuantum", () => { + it("should decrypt hex string", () => { + const encrypted = encryptQuantum(hexData, keyPair.publicKey); + const decrypted = decryptQuantum(keyPair.secretKey, encrypted); + expect(decrypted).toBe(hexData); + }); + + it("should decrypt json string", () => { + const encrypted = encryptQuantum(jsonData, keyPair.publicKey); + const decrypted = decryptQuantum(keyPair.secretKey, encrypted); + expect(decrypted).toBe(jsonData); + }); + + it("should decrypt empty string", () => { + const encrypted = encryptQuantum("", keyPair.publicKey); + const decrypted = decryptQuantum(keyPair.secretKey, encrypted); + expect(decrypted).toBe(""); + }); + + it("should decrypt large data", () => { + const largeData = "x".repeat(10000); + const encrypted = encryptQuantum(largeData, keyPair.publicKey); + const decrypted = decryptQuantum(keyPair.secretKey, encrypted); + expect(decrypted).toBe(largeData); + }); + + it("should decrypt unicode data", () => { + const unicodeData = "Hello δΈ–η•Œ 🌍 emoji πŸš€"; + const encrypted = encryptQuantum(unicodeData, keyPair.publicKey); + const decrypted = decryptQuantum(keyPair.secretKey, encrypted); + expect(decrypted).toBe(unicodeData); + }); + + it("should throw error for non-hex secret key", () => { + const encrypted = encryptQuantum("test", keyPair.publicKey); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(() => decryptQuantum("not-hex" as any, encrypted)).toThrow( + "Must provide secret key as hex string", + ); + }); + + it("should throw error for invalid secret key length", () => { + const invalidKey = "0x1234" as `0x${string}`; + const encrypted = encryptQuantum("test", keyPair.publicKey); + expect(() => decryptQuantum(invalidKey, encrypted)).toThrow( + "Invalid ML-KEM-768 secret key length", + ); + }); + + it("should throw error for wrong secret key", () => { + const wrongKeyPair = generateQuantumKeyPair(); + const encrypted = encryptQuantum("test", keyPair.publicKey); + expect(() => decryptQuantum(wrongKeyPair.secretKey, encrypted)).toThrow(); + }); + + it("should throw error for malformed encrypted data", () => { + expect(() => + decryptQuantum( + keyPair.secretKey, + "0x1234567890abcdef" as `0x${string}`, + ), + ).toThrow(); + }); + }); + + describe("round-trip encryption", () => { + it("should work with different key pairs", () => { + const keyPair1 = generateQuantumKeyPair(); + const data = "secret message"; + const encrypted = encryptQuantum(data, keyPair1.publicKey); + const decrypted = decryptQuantum(keyPair1.secretKey, encrypted); + expect(decrypted).toBe(data); + }); + + it("should work with multiple encryptions", () => { + const data = "test data"; + const encrypted1 = encryptQuantum(data, keyPair.publicKey); + const encrypted2 = encryptQuantum(data, keyPair.publicKey); + const encrypted3 = encryptQuantum(data, keyPair.publicKey); + + expect(decryptQuantum(keyPair.secretKey, encrypted1)).toBe(data); + expect(decryptQuantum(keyPair.secretKey, encrypted2)).toBe(data); + expect(decryptQuantum(keyPair.secretKey, encrypted3)).toBe(data); + }); + }); + + describe("edge cases", () => { + it("should handle very long strings", () => { + const longString = "a".repeat(100000); + const encrypted = encryptQuantum(longString, keyPair.publicKey); + const decrypted = decryptQuantum(keyPair.secretKey, encrypted); + expect(decrypted).toBe(longString); + }); + + it("should handle null bytes in data", () => { + const dataWithNulls = "test\x00data\x00with\x00nulls"; + const encrypted = encryptQuantum(dataWithNulls, keyPair.publicKey); + const decrypted = decryptQuantum(keyPair.secretKey, encrypted); + expect(decrypted).toBe(dataWithNulls); + }); + + it("should handle special characters", () => { + const specialChars = "!@#$%^&*()_+-=[]{}|;':\",./<>?`~"; + const encrypted = encryptQuantum(specialChars, keyPair.publicKey); + const decrypted = decryptQuantum(keyPair.secretKey, encrypted); + expect(decrypted).toBe(specialChars); + }); + + it("should handle newlines and tabs", () => { + const dataWithNewlines = "line1\nline2\tline3\r\nline4"; + const encrypted = encryptQuantum(dataWithNewlines, keyPair.publicKey); + const decrypted = decryptQuantum(keyPair.secretKey, encrypted); + expect(decrypted).toBe(dataWithNewlines); + }); + }); + + describe("security properties", () => { + it("should not be deterministic", () => { + const data = "same data"; + const encrypted1 = encryptQuantum(data, keyPair.publicKey); + const encrypted2 = encryptQuantum(data, keyPair.publicKey); + const encrypted3 = encryptQuantum(data, keyPair.publicKey); + + expect(encrypted1).not.toBe(encrypted2); + expect(encrypted2).not.toBe(encrypted3); + expect(encrypted1).not.toBe(encrypted3); + }); + + it("should produce different ciphertexts for different public keys", () => { + const keyPair1 = generateQuantumKeyPair(); + const keyPair2 = generateQuantumKeyPair(); + + const data = "test data"; + const encrypted1 = encryptQuantum(data, keyPair1.publicKey); + const encrypted2 = encryptQuantum(data, keyPair2.publicKey); + + expect(encrypted1).not.toBe(encrypted2); + }); + + it("should produce larger ciphertext than plaintext", () => { + const data = "test"; + const encrypted = encryptQuantum(data, keyPair.publicKey); + + // Quantum encryption has overhead: 12 (IV) + 1088 (KEM) + 16 (GCM tag) = 1116 bytes + // In hex: 1116 * 2 + 2 ("0x") = 2234 chars minimum + expect(encrypted.length).toBeGreaterThan(2200); + }); + }); +}); From fd4170210bd40d11145c223f401f29bfde349821 Mon Sep 17 00:00:00 2001 From: Dzmitry Lahunouski Date: Sun, 2 Nov 2025 20:37:04 +0000 Subject: [PATCH 2/2] update readme --- README.md | 395 ++++++++----------------------- package.json | 3 +- test/build-output.spec.ts | 7 +- test/integration/esm-imports.mjs | 4 + 4 files changed, 117 insertions(+), 292 deletions(-) diff --git a/README.md b/README.md index 089546d..7e85f81 100644 --- a/README.md +++ b/README.md @@ -2,26 +2,22 @@ [![Quality gate](https://github.com/zeroledger/vycrypt/actions/workflows/quality-gate.yml/badge.svg)](https://github.com/zeroledger/vycrypt/actions/workflows/quality-gate.yml) -Crypto primitives for ZeroLedger Protocol - A comprehensive cryptographic library for stealth addresses and ECDH encryption. Pure ESM, optimized for modern JavaScript environments. +Crypto primitives for ZeroLedger Protocol - ECDH encryption, stealth addresses, and post-quantum security. -*Warning*: Software provided as is and has not passed any security checks and reviews. +> ⚠️ **Warning**: Software provided as-is. Not audited for production use. ## Features -- **Stealth Addresses**: Create and derive stealth addresses using secp256k1 public keys and random values -- **ECDH Encryption**: Asymmetric encryption using ephemeral key pairs and AES-256-GCM -- **Quantum-Resistant Encryption**: Post-quantum encryption using ML-KEM-768 (Kyber) in a separate module -- **Elliptic Curve Operations**: Secure multiplication of public and private keys on secp256k1 curve -- **Type Safety**: Full TypeScript support with comprehensive type definitions +- πŸ” **ECDH Encryption** - Ephemeral key pairs + AES-256-GCM +- πŸ›‘οΈ **Post-Quantum Encryption** - ML-KEM-768 (Kyber) resistant to quantum attacks +- πŸ‘» **Stealth Addresses** - Privacy-preserving address generation +- πŸ”’ **Elliptic Operations** - secp256k1 key multiplication +- πŸ“¦ **Pure ESM** - Modern JavaScript, TypeScript-native -## Security Features +## Requirements -- **Forward Secrecy**: Uses ephemeral keys for each encryption operation -- **Authenticated Encryption**: AES-256-GCM provides both confidentiality and integrity -- **Quantum Resistance**: Optional ML-KEM-768 (Kyber) for post-quantum security -- **Key Validation**: Comprehensive validation of private and public keys -- **Random IV**: Each encryption uses a cryptographically secure random IV -- **ECDH Key Agreement**: Secure key derivation using secp256k1 curve +- **Node.js** β‰₯ 20.19.0 +- **Pure ESM** - No CommonJS support ## Installation @@ -29,347 +25,166 @@ Crypto primitives for ZeroLedger Protocol - A comprehensive cryptographic librar npm install @zeroledger/vycrypt ``` -## Module Format +## Quick Start -This library is **pure ESM** (ES Modules) and requires **Node.js 20.19.0 or later**. - -**Import the library:** - -```typescript -import { encrypt, decrypt } from "@zeroledger/vycrypt/crypt.js"; -import { createStealth, deriveStealthAccount } from "@zeroledger/vycrypt/stealth.js"; -``` - -### Build Output - -The library builds directly to the root directory with ESM format: -- `*.js` - ES Module JavaScript files -- `*.d.ts` - TypeScript declaration files -- `stealth/` - Stealth address modules - -All files include source maps (`.js.map`, `.d.ts.map`) for debugging. - -## API Reference - -### Encryption & Decryption - -#### `encrypt(data: string, counterpartyPubKey: Hex): Hex` -Encrypts data for a specific recipient using their public key. - -- **Parameters:** - - `data`: String to encrypt (supports any UTF-8 data) - - `counterpartyPubKey`: Recipient's uncompressed public key (0x-prefixed hex) -- **Returns:** Encrypted data as hex string with ABI encoding -- **Security:** Uses ephemeral ECDH + AES-256-GCM - -#### `decrypt(privateKey: Hash, encodedData: Hex): string` -Decrypts data using your private key. - -- **Parameters:** - - `privateKey`: Your private key (0x-prefixed hex) - - `encodedData`: Encrypted data from `encrypt()` -- **Returns:** Original decrypted string -- **Throws:** Error if decryption fails or keys are invalid - -### Quantum-Resistant Encryption & Decryption - -#### `generateQuantumKeyPair(seed?: string): QuantumKeyPair` -Generates a quantum-resistant key pair using ML-KEM-768. - -- **Parameters:** - - `seed`: Optional seed string for deterministic key generation (any string, including unicode) -- **Returns:** Object with `publicKey` and `secretKey` (both Hex strings) - - `publicKey`: 0x-prefixed hex string (1184 bytes / 2368 hex chars) - - `secretKey`: 0x-prefixed hex string (2400 bytes / 4800 hex chars) -- **Security:** - - Without seed: Uses cryptographically secure random generation - - With seed: Derives deterministic 64-byte seed using SHA-512 hashing - -#### `encryptQuantum(data: string, publicKey: Hex): Hex` -Encrypts data using quantum-resistant ML-KEM-768. - -- **Parameters:** - - `data`: String to encrypt (supports any UTF-8 data) - - `publicKey`: Recipient's ML-KEM-768 public key (0x-prefixed hex string, 1184 bytes) -- **Returns:** Encrypted data as hex string -- **Security:** Uses ML-KEM-768 key encapsulation + AES-256-GCM - -#### `decryptQuantum(secretKey: Hex, encodedData: Hex): string` -Decrypts data encrypted with quantum-resistant encryption. - -- **Parameters:** - - `secretKey`: Your ML-KEM-768 secret key (0x-prefixed hex string, 2400 bytes) - - `encodedData`: Encrypted data from `encryptQuantum()` -- **Returns:** Original decrypted string -- **Throws:** Error if decryption fails or keys are invalid - -### Stealth Addresses - -#### `createStealth(publicKey: Hex): { stealthAddress: string, random: bigint }` -Creates a stealth address from a public key. - -- **Parameters:** - - `publicKey`: Uncompressed public key (0x-prefixed hex) -- **Returns:** Object containing stealth address and random value -- **Security:** Uses cryptographically secure random values - -#### `deriveStealthAccount(privateKey: Hex, random: Hex): Account` -Derives the private key for a stealth address. - -- **Parameters:** - - `privateKey`: Your private key (0x-prefixed hex) - - `random`: Random value from `createStealth()` -- **Returns:** Viem account object with address matching stealth address - -### Elliptic Curve Operations - -#### `mulPublicKey(publicKey: Hex, number: bigint, isCompressed?: boolean): Hex` -Multiplies a public key by a scalar value. - -- **Parameters:** - - `publicKey`: Uncompressed public key (0x-prefixed hex) - - `number`: Scalar to multiply by - - `isCompressed`: Whether to return compressed format (default: false) -- **Returns:** New public key (0x-prefixed hex) - -#### `mulPrivateKey(privateKey: Hex, number: bigint): Hex` -Multiplies a private key by a scalar value. - -- **Parameters:** - - `privateKey`: Private key (0x-prefixed hex) - - `number`: Scalar to multiply by -- **Returns:** New private key (0x-prefixed hex) -- **Security:** Automatically applies modulo operation to stay within curve order - -## Usage Examples - -### Basic Encryption/Decryption (Classic) +### Classic Encryption ```typescript import { encrypt, decrypt } from "@zeroledger/vycrypt/crypt.js"; import { generatePrivateKey, privateKeyToAccount } from "viem/accounts"; -// Generate key pair const privKey = generatePrivateKey(); const account = privateKeyToAccount(privKey); -// Encrypt data -const data = "Hello, World!"; -const encryptedData = encrypt(data, account.publicKey); - -// Decrypt data -const decryptedData = decrypt(privKey, encryptedData); -console.log(decryptedData); // "Hello, World!" +const encrypted = encrypt("Hello, World!", account.publicKey); +const decrypted = decrypt(privKey, encrypted); ``` -### Quantum-Resistant Encryption/Decryption +### Quantum-Resistant Encryption ```typescript -import { encryptQuantum, decryptQuantum, generateQuantumKeyPair } from "@zeroledger/vycrypt"; +import { generateQuantumKeyPair, encryptQuantum, decryptQuantum } from "@zeroledger/vycrypt/qcrypt.js"; -// Generate random quantum-resistant key pair +// Random key pair const keyPair = generateQuantumKeyPair(); -// Or generate deterministic key pair from a seed string -const deterministicKeyPair = generateQuantumKeyPair("my-secret-passphrase"); - -// Encrypt data -const data = "Secret message protected from quantum computers"; -const encryptedData = encryptQuantum(data, keyPair.publicKey); +// Or deterministic from seed +const keys = generateQuantumKeyPair("my-passphrase"); -// Decrypt data -const decryptedData = decryptQuantum(keyPair.secretKey, encryptedData); -console.log(decryptedData); // "Secret message protected from quantum computers" +const encrypted = encryptQuantum("Secret data", keyPair.publicKey); +const decrypted = decryptQuantum(keyPair.secretKey, encrypted); ``` -### Stealth Address Creation +### Stealth Addresses ```typescript -import { createStealth, deriveStealthAccount } from "@zeroledger/vycrypt/stealth.js"; +import { createStealth, deriveStealthAccount } from "@zeroledger/vycrypt/stealth/index.js"; import { generatePrivateKey, privateKeyToAccount } from "viem/accounts"; import { toHex } from "viem"; -// Generate key pair const privateKey = generatePrivateKey(); const pubKey = privateKeyToAccount(privateKey).publicKey; -// Create stealth address const { stealthAddress, random } = createStealth(pubKey); -console.log("Stealth Address:", stealthAddress); - -// Derive private key for stealth address const account = deriveStealthAccount(privateKey, toHex(random)); -console.log("Derived Address:", account.address); // Same as stealthAddress -``` - -### Elliptic Curve Operations - -```typescript -import { mulPublicKey, mulPrivateKey } from "@zeroledger/vycrypt/stealth.js"; -import { generatePrivateKey, privateKeyToAccount } from "viem/accounts"; - -const privateKey = generatePrivateKey(); -const pubKey = privateKeyToAccount(privateKey).publicKey; -const multiplier = 123n; - -// Multiply public key -const newPublicKey = mulPublicKey(pubKey, multiplier); - -// Multiply private key -const newPrivateKey = mulPrivateKey(privateKey, multiplier); - -// Verify they correspond -const newAccount = privateKeyToAccount(newPrivateKey); -console.log(newAccount.publicKey === newPublicKey); // true ``` -### Advanced: Encrypting Large Data - -```typescript -import { encrypt, decrypt } from "@zeroledger/vycrypt/crypt.js"; -import { generatePrivateKey, privateKeyToAccount } from "viem/accounts"; - -const privKey = generatePrivateKey(); -const account = privateKeyToAccount(privKey); - -// Encrypt large JSON data -const largeData = JSON.stringify({ - transaction: { - hash: "0x1234567890abcdef...", - value: "1000000000000000000", - // ... more data - } -}); +## API Reference -const encrypted = encrypt(largeData, account.publicKey); -const decrypted = decrypt(privKey, encrypted); -console.log(JSON.parse(decrypted)); // Original object -``` +### Classic Encryption (`/crypt.js`) -## Security Considerations +#### `encrypt(data: string, publicKey: Hex): Hex` +ECDH encryption with ephemeral keys and AES-256-GCM. -### Key Management -- **Never share private keys**: Keep private keys secure and never transmit them -- **Use secure random generation**: Always use cryptographically secure random number generators -- **Validate inputs**: The library validates keys, but ensure your application validates all inputs +#### `decrypt(privateKey: Hash, encodedData: Hex): string` +Decrypt data encrypted with `encrypt()`. -### Encryption Best Practices -- **Ephemeral keys**: Each encryption uses a new ephemeral key pair for forward secrecy -- **Random IVs**: Each encryption uses a cryptographically secure random IV -- **Authenticated encryption**: AES-GCM provides both confidentiality and integrity +### Quantum Encryption (`/qcrypt.js`) -### Stealth Address Security -- **Random values**: Each stealth address uses a cryptographically secure random value -- **Deterministic derivation**: Same inputs always produce the same stealth address -- **No correlation**: Different random values produce uncorrelated stealth addresses +#### `generateQuantumKeyPair(seed?: string): QuantumKeyPair` +Generate ML-KEM-768 key pair. Optional seed for deterministic generation. +- **Returns:** `{ publicKey: Hex, secretKey: Hex }` +- **Key sizes:** 1184 bytes (public), 2400 bytes (secret) -### Quantum-Resistant Encryption -- **ML-KEM-768 (Kyber)**: NIST-standardized post-quantum key encapsulation mechanism (FIPS 203) -- **Post-quantum security**: Protects against both classical and quantum computer attacks -- **Separate module**: Keep quantum encryption isolated in `qcrypt.ts` for clarity -- **Larger keys**: ML-KEM keys are ~1-2KB compared to ~33 bytes for secp256k1 -- **Hybrid approach**: Combines post-quantum KEM with classical AES-256-GCM +#### `encryptQuantum(data: string, publicKey: Hex): Hex` +Quantum-resistant encryption using ML-KEM-768 + AES-256-GCM. -## Error Handling +#### `decryptQuantum(secretKey: Hex, encodedData: Hex): string` +Decrypt quantum-encrypted data. -The library throws descriptive errors for invalid inputs: +### Stealth Addresses (`/stealth/index.js`) -```typescript -try { - const encrypted = encrypt("data", "0xinvalid"); -} catch (error) { - console.log(error.message); // "Must provide uncompressed public key as hex string" -} +#### `createStealth(publicKey: Hex): { stealthAddress: string, random: bigint }` +Generate a stealth address with cryptographically secure random. -try { - const decrypted = decrypt("0xinvalid", encryptedData); -} catch (error) { - console.log(error.message); // "Must provide private key as hash string" +#### `deriveStealthAccount(privateKey: Hex, random: Hex): Account` +Derive private key for stealth address. Returns viem Account. + +#### `mulPublicKey(publicKey: Hex, scalar: bigint, isCompressed?: boolean): Hex` +Multiply public key by scalar on secp256k1 curve. + +#### `mulPrivateKey(privateKey: Hex, scalar: bigint): Hex` +Multiply private key by scalar (modulo curve order). + +## Security + +### Classic Encryption +- βœ… Forward secrecy (ephemeral keys) +- βœ… Authenticated encryption (AES-256-GCM) +- βœ… Random IVs per operation +- βœ… ECDH on secp256k1 curve + +### Quantum Encryption +- βœ… ML-KEM-768 (NIST FIPS 203) +- βœ… Post-quantum secure +- βœ… Hybrid encryption (KEM + AES-GCM) +- βœ… Non-deterministic by default + +### Best Practices +- Never share or transmit private keys +- Use cryptographically secure random generation +- Validate all inputs in your application +- Consider quantum resistance for long-term secrets + +## Module Exports + +```json +{ + ".": "./index.js", // Main exports + "./crypt.js": "./crypt.js", // Classic encryption + "./qcrypt.js": "./qcrypt.js", // Quantum encryption + "./stealth/index.js": "./stealth/index.js" // Stealth addresses } ``` ## Testing -The library includes comprehensive tests covering: - -- **Classic Encryption/Decryption**: Round-trip operations with various data types -- **Quantum-Resistant Encryption**: ML-KEM-768 encryption and decryption -- **Error handling**: Invalid inputs and malformed data -- **Edge cases**: Empty strings, large data, unicode, binary data -- **Security properties**: Non-deterministic encryption, different outputs for different keys -- **Stealth addresses**: Address generation, derivation, and validation -- **Elliptic operations**: Key multiplication and validation - -Run tests with: ```bash +# Run all tests npm test -``` -Validate build output and ESM imports: -```bash +# Validate build and ESM imports npm run test:build -``` -This command: -1. Builds the library -2. Validates all expected files are created -3. Verifies built modules can be imported as ESM -4. Confirms the API works as documented - -## Dependencies - -- **@noble/ciphers** (v2.0.1): AES-256-GCM authenticated encryption -- **viem** (v2.38.6): Ethereum-compatible utilities, types, and hashing (SHA-256) - -**Note:** Viem internally uses and re-exports `@noble/curves` (secp256k1) and `@noble/hashes`, ensuring compatibility across the ecosystem. - -All dependencies are ESM-compatible and actively maintained. - -## Contributing +# Type checking +npm run typecheck -Contributions are always welcome! Please: +# Linting +npm run lint +``` -1. Fork the repository -2. Create a feature branch -3. Add tests for new functionality -4. Ensure all tests pass -5. Submit a pull request +**Test coverage:** 89+ tests covering encryption, stealth addresses, edge cases, and build validation. -### Development Setup +## Dependencies -```bash -git clone -cd vycrypt -npm install -npm test -``` +| Package | Version | Purpose | +|---------|---------|---------| +| `@noble/ciphers` | ^2.0.1 | AES-256-GCM encryption | +| `@noble/post-quantum` | ^0.5.2 | ML-KEM-768 (Kyber) | +| `viem` | ^2.38.6 | Ethereum utilities, secp256k1, hashing | -### Building +> **Note:** vycryp re-exports `@noble/curves` and `@noble/hashes` from Viem for compatibility. -To build the ESM output: +## Build ```bash npm run build ``` -This creates: -- `*.js` files in the root directory (ESM format) -- `stealth/` directory with stealth modules -- TypeScript declaration files (`.d.ts`) and source maps +Outputs: +- `index.js`, `crypt.js`, `qcrypt.js` - Main modules +- `stealth/` - Stealth address modules +- `*.d.ts` - TypeScript declarations +- `*.js.map` - Source maps -### Type Checking +## Contributing -```bash -npm run typecheck -``` +1. Fork the repository +2. Create a feature branch +3. Add tests for new functionality +4. Ensure `npm test` and `npm run test:build` pass +5. Submit a pull request ## License SEE LICENSE IN LICENSE - -## Support - -For issues and questions: -- Open an issue on GitHub -- Check existing issues for similar problems -- Review the test files for usage examples diff --git a/package.json b/package.json index fb12ac3..dc6573a 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,8 @@ "exports": { ".": "./index.js", "./crypt.js": "./crypt.js", - "./stealth.js": "./stealth/index.js" + "./qcrypt.js": "./qcrypt.js", + "./stealth/index.js": "./stealth/index.js" }, "engines": { "node": ">= 20.19.0" diff --git a/test/build-output.spec.ts b/test/build-output.spec.ts index 62f765e..256a4d1 100644 --- a/test/build-output.spec.ts +++ b/test/build-output.spec.ts @@ -20,6 +20,10 @@ describe("Build Output Validation", () => { "crypt.d.ts", "crypt.js.map", "crypt.d.ts.map", + "qcrypt.js", + "qcrypt.d.ts", + "qcrypt.js.map", + "qcrypt.d.ts.map", "stealth/index.js", "stealth/index.d.ts", "stealth/index.js.map", @@ -109,7 +113,8 @@ describe("Build Output Validation", () => { expect(pkg.exports).toBeDefined(); expect(pkg.exports["."]).toBe("./index.js"); expect(pkg.exports["./crypt.js"]).toBe("./crypt.js"); - expect(pkg.exports["./stealth.js"]).toBe("./stealth/index.js"); + expect(pkg.exports["./qcrypt.js"]).toBe("./qcrypt.js"); + expect(pkg.exports["./stealth/index.js"]).toBe("./stealth/index.js"); }); it("main entry point should be index.js", () => { diff --git a/test/integration/esm-imports.mjs b/test/integration/esm-imports.mjs index 4c9f0ae..a8c29a5 100644 --- a/test/integration/esm-imports.mjs +++ b/test/integration/esm-imports.mjs @@ -6,6 +6,7 @@ */ import { encrypt, decrypt } from "../../crypt.js"; +import { generateQuantumKeyPair, encryptQuantum, decryptQuantum } from "../../qcrypt.js"; import { createStealth, deriveStealthAccount } from "../../stealth/index.js"; import { mulPublicKey, mulPrivateKey } from "../../stealth/index.js"; @@ -15,6 +16,9 @@ console.log("βœ… All imports successful from built files"); const checks = [ { name: "encrypt", value: encrypt }, { name: "decrypt", value: decrypt }, + { name: "generateQuantumKeyPair", value: generateQuantumKeyPair }, + { name: "encryptQuantum", value: encryptQuantum }, + { name: "decryptQuantum", value: decryptQuantum }, { name: "createStealth", value: createStealth }, { name: "deriveStealthAccount", value: deriveStealthAccount }, { name: "mulPublicKey", value: mulPublicKey },