10 Commits
0.7.1 ... 0.7.2

Author SHA1 Message Date
Paul Miller
0163b63532 Release 0.7.2. 2023-02-25 10:13:45 +01:00
Paul Miller
7e825520f1 README 2023-02-25 10:05:48 +01:00
Paul Miller
d739297b2c Move p192, p224 from main pkg to tests for now. Reason: not popular 2023-02-25 10:00:24 +01:00
Paul Miller
285aa6375d stark: refactor 2023-02-20 16:50:29 +01:00
Paul Miller
8c77331ef2 add hash-to-curve benchmark 2023-02-20 16:33:05 +01:00
Paul Miller
669641e0a3 README wording 2023-02-16 17:54:17 +01:00
Paul Miller
68dd57ed31 Cryptofuzz 2023-02-16 17:49:48 +01:00
Paul Miller
a9fdd6df9f readme: typo 2023-02-16 12:33:32 +01:00
Paul Miller
d485d8b0e6 Fix prettier 2023-02-16 12:32:32 +01:00
Paul Miller
0fdd763dc7 montgomery: add randomPrivateKey. Add ecdh benchmark. 2023-02-16 12:32:18 +01:00
15 changed files with 223 additions and 215 deletions

View File

@@ -9,21 +9,21 @@ Audited & minimal JS implementation of elliptic curve cryptography.
for encoding or hashing an arbitrary string to an elliptic curve point for encoding or hashing an arbitrary string to an elliptic curve point
- 🧜‍♂️ [Poseidon](https://www.poseidon-hash.info) ZK-friendly hash - 🧜‍♂️ [Poseidon](https://www.poseidon-hash.info) ZK-friendly hash
- 🏎 [Ultra-fast](#speed), hand-optimized for caveats of JS engines - 🏎 [Ultra-fast](#speed), hand-optimized for caveats of JS engines
- 🔍 Unique tests ensure correctness. Wycheproof vectors included - 🔍 Unique tests ensure correctness with Wycheproof vectors and [cryptofuzz](https://github.com/guidovranken/cryptofuzz) differential fuzzing
- 🔻 Tree-shaking-friendly: there is no entry point, which ensures small size of your app - 🔻 Tree-shaking-friendly: there is no entry point, which ensures small size of your app
Package consists of two parts: Package consists of two parts:
1. [Abstract](#abstract-api), zero-dependency EC algorithms 1. [Abstract](#abstract-api), zero-dependency EC algorithms
2. [Implementations](#implementations), utilizing one dependency `@noble/hashes`, providing ready-to-use: 2. [Implementations](#implementations), utilizing one dependency `@noble/hashes`, providing ready-to-use:
- NIST curves secp192r1/P192, secp224r1/P224, secp256r1/P256, secp384r1/P384, secp521r1/P521 - NIST curves secp256r1/P256, secp384r1/P384, secp521r1/P521
- SECG curve secp256k1 - SECG curve secp256k1
- ed25519/curve25519/x25519/ristretto255, edwards448/curve448/x448 RFC7748 / RFC8032 / ZIP215 stuff - ed25519/curve25519/x25519/ristretto255, edwards448/curve448/x448 [RFC7748](https://www.rfc-editor.org/rfc/rfc7748) / [RFC8032](https://www.rfc-editor.org/rfc/rfc8032) / [ZIP215](https://zips.z.cash/zip-0215) stuff
- pairing-friendly curves bls12-381, bn254 - pairing-friendly curves bls12-381, bn254
Check out [Upgrading](#upgrading) if you've previously used single-feature noble packages Check out [Upgrading](#upgrading) if you've previously used single-feature noble packages
([secp256k1](https://github.com/paulmillr/noble-secp256k1), [ed25519](https://github.com/paulmillr/noble-ed25519)). ([secp256k1](https://github.com/paulmillr/noble-secp256k1), [ed25519](https://github.com/paulmillr/noble-ed25519)).
See [Resources](#resouces) for articles and real-world software that uses curves. See [Resources](#resources) for articles and real-world software that uses curves.
### This library belongs to _noble_ crypto ### This library belongs to _noble_ crypto
@@ -66,10 +66,6 @@ secp256k1.verify(sig, msg, pub) === true;
const privHex = '46c930bc7bb4db7f55da20798697421b98c4175a52c630294d75a84b9c126236'; const privHex = '46c930bc7bb4db7f55da20798697421b98c4175a52c630294d75a84b9c126236';
const pub2 = secp256k1.getPublicKey(privHex); // keys & other inputs can be Uint8Array-s or hex strings const pub2 = secp256k1.getPublicKey(privHex); // keys & other inputs can be Uint8Array-s or hex strings
// Follows hash-to-curve specification to encode arbitrary hashes to EC points
import { hashToCurve, encodeToCurve } from '@noble/curves/secp256k1';
hashToCurve('0102abcd');
``` ```
All curves: All curves:
@@ -180,10 +176,9 @@ const signatures3 = privateKeys.map((p, i) => bls.sign(messages[i], p));
const aggSignature3 = bls.aggregateSignatures(signatures3); const aggSignature3 = bls.aggregateSignatures(signatures3);
const isValid3 = bls.verifyBatch(aggSignature3, messages, publicKeys); const isValid3 = bls.verifyBatch(aggSignature3, messages, publicKeys);
console.log({ publicKeys, signatures3, aggSignature3, isValid3 }); console.log({ publicKeys, signatures3, aggSignature3, isValid3 });
// bls.pairing(PointG1, PointG2) // pairings
// Pairings // hash-to-curve examples can be seen below
// bls.pairing(PointG1, PointG2)
// Also, check out hash-to-curve examples below.
``` ```
## Abstract API ## Abstract API
@@ -482,9 +477,11 @@ Every curve has exported `hashToCurve` and `encodeToCurve` methods:
```ts ```ts
import { hashToCurve, encodeToCurve } from '@noble/curves/secp256k1'; import { hashToCurve, encodeToCurve } from '@noble/curves/secp256k1';
import { randomBytes } from '@noble/hashes/utils'; import { randomBytes } from '@noble/hashes/utils';
hashToCurve('0102abcd');
console.log(hashToCurve(randomBytes())); console.log(hashToCurve(randomBytes()));
console.log(encodeToCurve(randomBytes())); console.log(encodeToCurve(randomBytes()));
import { bls12_381 } from '@noble/curves/bls12-381'; import { bls12_381 } from '@noble/curves/bls12-381';
bls12_381.G1.hashToCurve(randomBytes(), { DST: 'another' }); bls12_381.G1.hashToCurve(randomBytes(), { DST: 'another' });
bls12_381.G2.hashToCurve(randomBytes(), { DST: 'custom' }); bls12_381.G2.hashToCurve(randomBytes(), { DST: 'custom' });
@@ -610,7 +607,7 @@ utils.equalBytes(Uint8Array.from([0xde]), Uint8Array.from([0xde]));
## Security ## Security
The library had no prior security audit. The library had no prior security audit. The library has been fuzzed by [Guido Vranken's cryptofuzz](https://github.com/guidovranken/cryptofuzz): you can run the fuzzer by yourself to check it.
[Timing attack](https://en.wikipedia.org/wiki/Timing_attack) considerations: we are using non-CT bigints. However, _JIT-compiler_ and _Garbage Collector_ make "constant time" extremely hard to achieve in a scripting language. Which means _any other JS library can't have constant-timeness_. Even statically typed Rust, a language without GC, [makes it harder to achieve constant-time](https://www.chosenplaintext.ca/open-source/rust-timing-shield/security) for some cases. If your goal is absolute security, don't use any JS lib — including bindings to native ones. Use low-level libraries & languages. Nonetheless we're targetting algorithmic constant time. [Timing attack](https://en.wikipedia.org/wiki/Timing_attack) considerations: we are using non-CT bigints. However, _JIT-compiler_ and _Garbage Collector_ make "constant time" extremely hard to achieve in a scripting language. Which means _any other JS library can't have constant-timeness_. Even statically typed Rust, a language without GC, [makes it harder to achieve constant-time](https://www.chosenplaintext.ca/open-source/rust-timing-shield/security) for some cases. If your goal is absolute security, don't use any JS lib — including bindings to native ones. Use low-level libraries & languages. Nonetheless we're targetting algorithmic constant time.
@@ -674,6 +671,14 @@ pedersen x 884 ops/sec @ 1ms/op
poseidon x 8,598 ops/sec @ 116μs/op poseidon x 8,598 ops/sec @ 116μs/op
verify x 528 ops/sec @ 1ms/op verify x 528 ops/sec @ 1ms/op
ecdh
├─x25519 x 1,337 ops/sec @ 747μs/op
├─secp256k1 x 461 ops/sec @ 2ms/op
├─P256 x 441 ops/sec @ 2ms/op
├─P384 x 179 ops/sec @ 5ms/op
├─P521 x 93 ops/sec @ 10ms/op
└─x448 x 496 ops/sec @ 2ms/op
bls12-381 bls12-381
init x 32 ops/sec @ 30ms/op init x 32 ops/sec @ 30ms/op
getPublicKey 1-bit x 858 ops/sec @ 1ms/op getPublicKey 1-bit x 858 ops/sec @ 1ms/op

19
benchmark/ecdh.js Normal file
View File

@@ -0,0 +1,19 @@
import { run, mark, compare, utils } from 'micro-bmark';
import { generateData } from './_shared.js';
import { secp256k1 } from '../secp256k1.js';
import { P256 } from '../p256.js';
import { P384 } from '../p384.js';
import { P521 } from '../p521.js';
import { x25519 } from '../ed25519.js';
import { x448 } from '../ed448.js';
run(async () => {
const curves = { x25519, secp256k1, P256, P384, P521, x448 };
const fns = {};
for (let [k, c] of Object.entries(curves)) {
const pubB = c.getPublicKey(c.utils.randomPrivateKey());
const privA = c.utils.randomPrivateKey();
fns[k] = () => c.getSharedSecret(privA, pubB);
}
await compare('ecdh', 1000, fns);
});

View File

@@ -0,0 +1,28 @@
import { run, mark, utils } from 'micro-bmark';
import { hash_to_field } from '../abstract/hash-to-curve.js';
import { hashToPrivateScalar } from '../abstract/modular.js';
import { randomBytes } from '@noble/hashes/utils';
import { sha256 } from '@noble/hashes/sha256';
// import { generateData } from './_shared.js';
import { hashToCurve as secp256k1 } from '../secp256k1.js';
import { hashToCurve as P256 } from '../p256.js';
import { hashToCurve as P384 } from '../p384.js';
import { hashToCurve as P521 } from '../p521.js';
import { hashToCurve as ed25519 } from '../ed25519.js';
import { hashToCurve as ed448 } from '../ed448.js';
import { utf8ToBytes } from '../abstract/utils.js';
const N = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141n;
run(async () => {
const rand = randomBytes(40);
await mark('hashToPrivateScalar', 1000000, () => hashToPrivateScalar(rand, N));
// - p, the characteristic of F
// - m, the extension degree of F, m >= 1
// - L = ceil((ceil(log2(p)) + k) / 8), where k is the security of suite (e.g. 128)
await mark('hash_to_field', 1000000, () =>
hash_to_field(rand, 1, { DST: 'secp256k1', hash: sha256, p: N, m: 1, k: 128 })
);
const msg = utf8ToBytes('message');
for (let [title, fn] of Object.entries({ secp256k1, P256, P384, P521, ed25519, ed448 })) {
await mark(`hashToCurve ${title}`, 1000, () => fn(msg));
}
});

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "@noble/curves", "name": "@noble/curves",
"version": "0.7.0", "version": "0.7.2",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@noble/curves", "name": "@noble/curves",
"version": "0.7.0", "version": "0.7.2",
"funding": [ "funding": [
{ {
"type": "individual", "type": "individual",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@noble/curves", "name": "@noble/curves",
"version": "0.7.1", "version": "0.7.2",
"description": "Minimal, auditable JS implementation of elliptic curve cryptography", "description": "Minimal, auditable JS implementation of elliptic curve cryptography",
"files": [ "files": [
"abstract", "abstract",
@@ -12,7 +12,7 @@
"*.d.ts.map" "*.d.ts.map"
], ],
"scripts": { "scripts": {
"bench": "cd benchmark; node secp256k1.js; node curves.js; node stark.js; node bls.js", "bench": "cd benchmark; node secp256k1.js; node curves.js; node ecdh.js; node stark.js; node bls.js",
"build": "tsc && tsc -p tsconfig.esm.json", "build": "tsc && tsc -p tsconfig.esm.json",
"build:release": "rollup -c rollup.config.js", "build:release": "rollup -c rollup.config.js",
"lint": "prettier --check 'src/**/*.{js,ts}' 'test/*.js'", "lint": "prettier --check 'src/**/*.{js,ts}' 'test/*.js'",
@@ -126,16 +126,6 @@
"import": "./esm/jubjub.js", "import": "./esm/jubjub.js",
"default": "./jubjub.js" "default": "./jubjub.js"
}, },
"./p192": {
"types": "./p192.d.ts",
"import": "./esm/p192.js",
"default": "./p192.js"
},
"./p224": {
"types": "./p224.d.ts",
"import": "./esm/p224.js",
"default": "./p224.js"
},
"./p256": { "./p256": {
"types": "./p256.d.ts", "types": "./p256.d.ts",
"import": "./esm/p256.js", "import": "./esm/p256.js",

View File

@@ -16,12 +16,14 @@ export type CurveType = {
powPminus2?: (x: bigint) => bigint; powPminus2?: (x: bigint) => bigint;
xyToU?: (x: bigint, y: bigint) => bigint; xyToU?: (x: bigint, y: bigint) => bigint;
Gu: bigint; Gu: bigint;
randomBytes?: (bytesLength?: number) => Uint8Array;
}; };
export type CurveFn = { export type CurveFn = {
scalarMult: (scalar: Hex, u: Hex) => Uint8Array; scalarMult: (scalar: Hex, u: Hex) => Uint8Array;
scalarMultBase: (scalar: Hex) => Uint8Array; scalarMultBase: (scalar: Hex) => Uint8Array;
getSharedSecret: (privateKeyA: Hex, publicKeyB: Hex) => Uint8Array; getSharedSecret: (privateKeyA: Hex, publicKeyB: Hex) => Uint8Array;
getPublicKey: (privateKey: Hex) => Uint8Array; getPublicKey: (privateKey: Hex) => Uint8Array;
utils: { randomPrivateKey: () => Uint8Array };
GuBytes: Uint8Array; GuBytes: Uint8Array;
}; };
@@ -181,6 +183,7 @@ export function montgomery(curveDef: CurveType): CurveFn {
scalarMultBase, scalarMultBase,
getSharedSecret: (privateKey: Hex, publicKey: Hex) => scalarMult(privateKey, publicKey), getSharedSecret: (privateKey: Hex, publicKey: Hex) => scalarMult(privateKey, publicKey),
getPublicKey: (privateKey: Hex): Uint8Array => scalarMultBase(privateKey), getPublicKey: (privateKey: Hex): Uint8Array => scalarMultBase(privateKey),
utils: { randomPrivateKey: () => CURVE.randomBytes!(CURVE.nByteLength) },
GuBytes: GuBytes, GuBytes: GuBytes,
}; };
} }

View File

@@ -149,6 +149,7 @@ export const x25519 = montgomery({
return mod(pow2(pow_p_5_8, BigInt(3), P) * b2, P); return mod(pow2(pow_p_5_8, BigInt(3), P) * b2, P);
}, },
adjustScalarBytes, adjustScalarBytes,
randomBytes,
}); });
// Hash To Curve Elligator2 Map (NOTE: different from ristretto255 elligator) // Hash To Curve Elligator2 Map (NOTE: different from ristretto255 elligator)

View File

@@ -134,6 +134,7 @@ export const x448 = montgomery({
return mod(Pminus3 * x, P); // Pminus3 * x = Pminus2 return mod(Pminus3 * x, P); // Pminus3 * x = Pminus2
}, },
adjustScalarBytes, adjustScalarBytes,
randomBytes,
// The 4-isogeny maps between the Montgomery curve and this Edwards // The 4-isogeny maps between the Montgomery curve and this Edwards
// curve are: // curve are:
// (u, v) = (y^2/x^2, (2 - x^2 - y^2)*y/x^3) // (u, v) = (y^2/x^2, (2 - x^2 - y^2)*y/x^3)

View File

@@ -1,25 +0,0 @@
/*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
import { createCurve } from './_shortw_utils.js';
import { sha256 } from '@noble/hashes/sha256';
import { Fp } from './abstract/modular.js';
// NIST secp192r1 aka P192
// https://www.secg.org/sec2-v2.pdf, https://neuromancer.sk/std/secg/secp192r1
export const P192 = createCurve(
{
// Params: a, b
a: BigInt('0xfffffffffffffffffffffffffffffffefffffffffffffffc'),
b: BigInt('0x64210519e59c80e70fa7e9ab72243049feb8deecc146b9b1'),
// Field over which we'll do calculations; 2n ** 192n - 2n ** 64n - 1n
Fp: Fp(BigInt('0xfffffffffffffffffffffffffffffffeffffffffffffffff')),
// Curve order, total count of valid points in the field.
n: BigInt('0xffffffffffffffffffffffff99def836146bc9b1b4d22831'),
// Base point (x, y) aka generator point
Gx: BigInt('0x188da80eb03090f67cbf20eb43a18800f4ff0afd82ff1012'),
Gy: BigInt('0x07192b95ffc8da78631011ed6b24cdd573f977a11e794811'),
h: BigInt(1),
lowS: false,
} as const,
sha256
);
export const secp192r1 = P192;

View File

@@ -1,25 +0,0 @@
/*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
import { createCurve } from './_shortw_utils.js';
import { sha224 } from '@noble/hashes/sha256';
import { Fp } from './abstract/modular.js';
// NIST secp224r1 aka P224
// https://www.secg.org/sec2-v2.pdf, https://neuromancer.sk/std/nist/P-224
export const P224 = createCurve(
{
// Params: a, b
a: BigInt('0xfffffffffffffffffffffffffffffffefffffffffffffffffffffffe'),
b: BigInt('0xb4050a850c04b3abf54132565044b0b7d7bfd8ba270b39432355ffb4'),
// Field over which we'll do calculations;
Fp: Fp(BigInt('0xffffffffffffffffffffffffffffffff000000000000000000000001')),
// Curve order, total count of valid points in the field
n: BigInt('0xffffffffffffffffffffffffffff16a2e0b8f03e13dd29455c5c2a3d'),
// Base point (x, y) aka generator point
Gx: BigInt('0xb70e0cbd6bb4bf7f321390b94a03c1d356c21122343280d6115c1d21'),
Gy: BigInt('0xbd376388b5f723fb4c22dfe6cd4375a05a07476444d5819985007e34'),
h: BigInt(1),
lowS: false,
} as const,
sha224
);
export const secp224r1 = P224;

View File

@@ -1,164 +1,126 @@
/*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */ /*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
import { keccak_256 } from '@noble/hashes/sha3'; import { keccak_256 } from '@noble/hashes/sha3';
import { sha256 } from '@noble/hashes/sha256'; import { sha256 } from '@noble/hashes/sha256';
import { weierstrass, ProjPointType } from './abstract/weierstrass.js';
import * as cutils from './abstract/utils.js';
import { Fp, mod, Field, validateField } from './abstract/modular.js';
import { getHash } from './_shortw_utils.js';
import * as poseidon from './abstract/poseidon.js';
import { utf8ToBytes } from '@noble/hashes/utils'; import { utf8ToBytes } from '@noble/hashes/utils';
import { Fp, mod, Field, validateField } from './abstract/modular.js';
import { poseidon } from './abstract/poseidon.js';
import { weierstrass, ProjPointType, SignatureType } from './abstract/weierstrass.js';
import {
Hex,
bitMask,
bytesToHex,
bytesToNumberBE,
concatBytes,
ensureBytes as ensureBytesOrig,
hexToBytes,
hexToNumber,
numberToVarBytesBE,
} from './abstract/utils.js';
import { getHash } from './_shortw_utils.js';
type ProjectivePoint = ProjPointType<bigint>;
// Stark-friendly elliptic curve // Stark-friendly elliptic curve
// https://docs.starkware.co/starkex/stark-curve.html // https://docs.starkware.co/starkex/stark-curve.html
const CURVE_N = BigInt( type ProjectivePoint = ProjPointType<bigint>;
const CURVE_ORDER = BigInt(
'3618502788666131213697322783095070105526743751716087489154079457884512865583' '3618502788666131213697322783095070105526743751716087489154079457884512865583'
); );
const nBitLength = 252; const nBitLength = 252;
// Copy-pasted from weierstrass.ts
function bits2int(bytes: Uint8Array): bigint { function bits2int(bytes: Uint8Array): bigint {
while (bytes[0] === 0) bytes = bytes.subarray(1); // strip leading 0s
// Copy-pasted from weierstrass.ts
const delta = bytes.length * 8 - nBitLength; const delta = bytes.length * 8 - nBitLength;
const num = cutils.bytesToNumberBE(bytes); const num = bytesToNumberBE(bytes);
return delta > 0 ? num >> BigInt(delta) : num; return delta > 0 ? num >> BigInt(delta) : num;
} }
function bits2int_modN(bytes: Uint8Array): bigint { function hex0xToBytes(hex: string): Uint8Array {
return mod(bits2int(bytes), CURVE_N); if (typeof hex === 'string') {
hex = strip0x(hex); // allow 0x prefix
if (hex.length & 1) hex = '0' + hex; // allow unpadded hex
}
return hexToBytes(hex);
} }
export const starkCurve = weierstrass({ const curve = weierstrass({
// Params: a, b a: BigInt(1), // Params: a, b
a: BigInt(1),
b: BigInt('3141592653589793238462643383279502884197169399375105820974944592307816406665'), b: BigInt('3141592653589793238462643383279502884197169399375105820974944592307816406665'),
// Field over which we'll do calculations; 2n**251n + 17n * 2n**192n + 1n // Field over which we'll do calculations; 2n**251n + 17n * 2n**192n + 1n
// There is no efficient sqrt for field (P%4==1) // There is no efficient sqrt for field (P%4==1)
Fp: Fp(BigInt('0x800000000000011000000000000000000000000000000000000000000000001')), Fp: Fp(BigInt('0x800000000000011000000000000000000000000000000000000000000000001')),
// Curve order, total count of valid points in the field. n: CURVE_ORDER, // Curve order, total count of valid points in the field.
n: CURVE_N, nBitLength, // len(bin(N).replace('0b',''))
nBitLength: nBitLength, // len(bin(N).replace('0b',''))
// Base point (x, y) aka generator point // Base point (x, y) aka generator point
Gx: BigInt('874739451078007766457464989774322083649278607533249481151382481072868806602'), Gx: BigInt('874739451078007766457464989774322083649278607533249481151382481072868806602'),
Gy: BigInt('152666792071518830868575557812948353041420400780739481342941381225525861407'), Gy: BigInt('152666792071518830868575557812948353041420400780739481342941381225525861407'),
h: BigInt(1), h: BigInt(1), // cofactor
// Default options lowS: false, // Allow high-s signatures
lowS: false,
...getHash(sha256), ...getHash(sha256),
// Custom truncation routines for stark curve // Custom truncation routines for stark curve
bits2int: (bytes: Uint8Array): bigint => { bits2int,
while (bytes[0] === 0) bytes = bytes.subarray(1);
return bits2int(bytes);
},
bits2int_modN: (bytes: Uint8Array): bigint => { bits2int_modN: (bytes: Uint8Array): bigint => {
let hashS = cutils.bytesToNumberBE(bytes).toString(16); // 2102820b232636d200cb21f1d330f20d096cae09d1bf3edb1cc333ddee11318 =>
if (hashS.length === 63) { // 2102820b232636d200cb21f1d330f20d096cae09d1bf3edb1cc333ddee113180
hashS += '0'; const hex = bytesToNumberBE(bytes).toString(16); // toHex unpadded
bytes = hexToBytes0x(hashS); if (hex.length === 63) bytes = hex0xToBytes(hex + '0'); // append trailing 0
} return mod(bits2int(bytes), CURVE_ORDER);
// Truncate zero bytes on left (compat with elliptic)
while (bytes[0] === 0) bytes = bytes.subarray(1);
return bits2int_modN(bytes);
}, },
}); });
export const _starkCurve = curve;
// Custom Starknet type conversion functions that can handle 0x and unpadded hex function ensureBytes(hex: Hex): Uint8Array {
function hexToBytes0x(hex: string): Uint8Array { return ensureBytesOrig('', typeof hex === 'string' ? hex0xToBytes(hex) : hex);
if (typeof hex !== 'string') {
throw new Error('hexToBytes: expected string, got ' + typeof hex);
}
hex = strip0x(hex);
if (hex.length & 1) hex = '0' + hex; // padding
if (hex.length % 2) throw new Error('hexToBytes: received invalid unpadded hex ' + hex.length);
const array = new Uint8Array(hex.length / 2);
for (let i = 0; i < array.length; i++) {
const j = i * 2;
const hexByte = hex.slice(j, j + 2);
const byte = Number.parseInt(hexByte, 16);
if (Number.isNaN(byte) || byte < 0) throw new Error('Invalid byte sequence');
array[i] = byte;
}
return array;
}
function hexToNumber0x(hex: string): bigint {
if (typeof hex !== 'string') {
throw new Error('hexToNumber: expected string, got ' + typeof hex);
}
// Big Endian
// TODO: strip vs no strip?
return BigInt(`0x${strip0x(hex)}`);
}
function bytesToNumber0x(bytes: Uint8Array): bigint {
return hexToNumber0x(cutils.bytesToHex(bytes));
}
function ensureBytes0x(hex: Hex): Uint8Array {
// Uint8Array.from() instead of hash.slice() because node.js Buffer
// is instance of Uint8Array, and its slice() creates **mutable** copy
return hex instanceof Uint8Array ? Uint8Array.from(hex) : hexToBytes0x(hex);
} }
function normPrivKey(privKey: Hex) { function normPrivKey(privKey: Hex): string {
return cutils.bytesToHex(ensureBytes0x(privKey)).padStart(64, '0'); return bytesToHex(ensureBytes(privKey)).padStart(64, '0');
} }
function getPublicKey0x(privKey: Hex, isCompressed = false) { export function getPublicKey(privKey: Hex, isCompressed = false): Uint8Array {
return starkCurve.getPublicKey(normPrivKey(privKey), isCompressed); return curve.getPublicKey(normPrivKey(privKey), isCompressed);
} }
function getSharedSecret0x(privKeyA: Hex, pubKeyB: Hex) { export function getSharedSecret(privKeyA: Hex, pubKeyB: Hex): Uint8Array {
return starkCurve.getSharedSecret(normPrivKey(privKeyA), pubKeyB); return curve.getSharedSecret(normPrivKey(privKeyA), pubKeyB);
}
export function sign(msgHash: Hex, privKey: Hex, opts?: any): SignatureType {
return curve.sign(ensureBytes(msgHash), normPrivKey(privKey), opts);
}
export function verify(signature: SignatureType | Hex, msgHash: Hex, pubKey: Hex) {
const sig = signature instanceof Signature ? signature : ensureBytes(signature);
return curve.verify(sig, ensureBytes(msgHash), ensureBytes(pubKey));
} }
function sign0x(msgHash: Hex, privKey: Hex, opts?: any) { const { CURVE, ProjectivePoint, Signature, utils } = curve;
if (typeof privKey === 'string') privKey = strip0x(privKey).padStart(64, '0'); export { CURVE, ProjectivePoint, Signature, utils };
return starkCurve.sign(ensureBytes0x(msgHash), normPrivKey(privKey), opts);
function extractX(bytes: Uint8Array): string {
const hex = bytesToHex(bytes.subarray(1));
const stripped = hex.replace(/^0+/gm, ''); // strip leading 0s
return `0x${stripped}`;
} }
function verify0x(signature: Hex, msgHash: Hex, pubKey: Hex) { function strip0x(hex: string) {
const sig = signature instanceof Signature ? signature : ensureBytes0x(signature); return hex.replace(/^0x/i, '');
return starkCurve.verify(sig, ensureBytes0x(msgHash), ensureBytes0x(pubKey)); }
} function numberTo0x16(num: bigint) {
// can't use utils.numberToHexUnpadded: adds leading 0 for even byte length
const { CURVE, ProjectivePoint, Signature } = starkCurve; return `0x${num.toString(16)}`;
export const utils = starkCurve.utils;
export {
CURVE,
Signature,
ProjectivePoint,
getPublicKey0x as getPublicKey,
getSharedSecret0x as getSharedSecret,
sign0x as sign,
verify0x as verify,
};
const stripLeadingZeros = (s: string) => s.replace(/^0+/gm, '');
export const bytesToHexEth = (uint8a: Uint8Array): string =>
`0x${stripLeadingZeros(cutils.bytesToHex(uint8a))}`;
export const strip0x = (hex: string) => hex.replace(/^0x/i, '');
export const numberToHexEth = (num: bigint | number) => `0x${num.toString(16)}`;
// We accept hex strings besides Uint8Array for simplicity
type Hex = Uint8Array | string;
// 1. seed generation
function hashKeyWithIndex(key: Uint8Array, index: number) {
let indexHex = cutils.numberToHexUnpadded(index);
if (indexHex.length & 1) indexHex = '0' + indexHex;
return sha256Num(cutils.concatBytes(key, hexToBytes0x(indexHex)));
} }
// seed generation
export function grindKey(seed: Hex) { export function grindKey(seed: Hex) {
const _seed = ensureBytes0x(seed); const _seed = ensureBytes(seed);
const sha256mask = 2n ** 256n; const sha256mask = 2n ** 256n;
const limit = sha256mask - mod(sha256mask, CURVE_ORDER);
const limit = sha256mask - mod(sha256mask, CURVE_N);
for (let i = 0; ; i++) { for (let i = 0; ; i++) {
const key = hashKeyWithIndex(_seed, i); const key = sha256Num(concatBytes(_seed, numberToVarBytesBE(BigInt(i))));
// key should be in [0, limit) if (key < limit) return mod(key, CURVE_ORDER).toString(16); // key should be in [0, limit)
if (key < limit) return mod(key, CURVE_N).toString(16); if (i === 100000) throw new Error('grindKey is broken: tried 100k vals'); // prevent dos
} }
} }
export function getStarkKey(privateKey: Hex) { export function getStarkKey(privateKey: Hex): string {
return bytesToHexEth(getPublicKey0x(privateKey, true).slice(1)); return extractX(getPublicKey(privateKey, true));
} }
export function ethSigToPrivate(signature: string) { export function ethSigToPrivate(signature: string): string {
signature = strip0x(signature.replace(/^0x/, '')); signature = strip0x(signature);
if (signature.length !== 130) throw new Error('Wrong ethereum signature'); if (signature.length !== 130) throw new Error('Wrong ethereum signature');
return grindKey(signature.substring(0, 64)); return grindKey(signature.substring(0, 64));
} }
@@ -170,15 +132,15 @@ export function getAccountPath(
application: string, application: string,
ethereumAddress: string, ethereumAddress: string,
index: number index: number
) { ): string {
const layerNum = int31(sha256Num(layer)); const layerNum = int31(sha256Num(layer));
const applicationNum = int31(sha256Num(application)); const applicationNum = int31(sha256Num(application));
const eth = hexToNumber0x(ethereumAddress); const eth = hexToNumber(strip0x(ethereumAddress));
return `m/2645'/${layerNum}'/${applicationNum}'/${int31(eth)}'/${int31(eth >> 31n)}'/${index}`; return `m/2645'/${layerNum}'/${applicationNum}'/${int31(eth)}'/${int31(eth >> 31n)}'/${index}`;
} }
// https://docs.starkware.co/starkex/pedersen-hash-function.html // https://docs.starkware.co/starkex/pedersen-hash-function.html
const PEDERSEN_POINTS_AFFINE = [ const PEDERSEN_POINTS = [
new ProjectivePoint( new ProjectivePoint(
2089986280348253421170679821480865132823066470938446095505822317253594081284n, 2089986280348253421170679821480865132823066470938446095505822317253594081284n,
1713931329540660377023406109199410414810705867260802078187082345529207694986n, 1713931329540660377023406109199410414810705867260802078187082345529207694986n,
@@ -205,8 +167,6 @@ const PEDERSEN_POINTS_AFFINE = [
1n 1n
), ),
]; ];
// for (const p of PEDERSEN_POINTS) p._setWindowSize(8);
const PEDERSEN_POINTS = PEDERSEN_POINTS_AFFINE;
function pedersenPrecompute(p1: ProjectivePoint, p2: ProjectivePoint): ProjectivePoint[] { function pedersenPrecompute(p1: ProjectivePoint, p2: ProjectivePoint): ProjectivePoint[] {
const out: ProjectivePoint[] = []; const out: ProjectivePoint[] = [];
@@ -230,14 +190,16 @@ const PEDERSEN_POINTS2 = pedersenPrecompute(PEDERSEN_POINTS[3], PEDERSEN_POINTS[
type PedersenArg = Hex | bigint | number; type PedersenArg = Hex | bigint | number;
function pedersenArg(arg: PedersenArg): bigint { function pedersenArg(arg: PedersenArg): bigint {
let value: bigint; let value: bigint;
if (typeof arg === 'bigint') value = arg; if (typeof arg === 'bigint') {
else if (typeof arg === 'number') { value = arg;
} else if (typeof arg === 'number') {
if (!Number.isSafeInteger(arg)) throw new Error(`Invalid pedersenArg: ${arg}`); if (!Number.isSafeInteger(arg)) throw new Error(`Invalid pedersenArg: ${arg}`);
value = BigInt(arg); value = BigInt(arg);
} else value = bytesToNumber0x(ensureBytes0x(arg)); } else {
// [0..Fp) value = bytesToNumberBE(ensureBytes(arg));
if (!(0n <= value && value < starkCurve.CURVE.Fp.ORDER)) }
throw new Error(`PedersenArg should be 0 <= value < CURVE.P: ${value}`); if (!(0n <= value && value < curve.CURVE.Fp.ORDER))
throw new Error(`PedersenArg should be 0 <= value < CURVE.P: ${value}`); // [0..Fp)
return value; return value;
} }
@@ -253,17 +215,17 @@ function pedersenSingle(point: ProjectivePoint, value: PedersenArg, constants: P
} }
// shift_point + x_low * P_0 + x_high * P1 + y_low * P2 + y_high * P3 // shift_point + x_low * P_0 + x_high * P1 + y_low * P2 + y_high * P3
export function pedersen(x: PedersenArg, y: PedersenArg) { export function pedersen(x: PedersenArg, y: PedersenArg): string {
let point: ProjectivePoint = PEDERSEN_POINTS[0]; let point: ProjectivePoint = PEDERSEN_POINTS[0];
point = pedersenSingle(point, x, PEDERSEN_POINTS1); point = pedersenSingle(point, x, PEDERSEN_POINTS1);
point = pedersenSingle(point, y, PEDERSEN_POINTS2); point = pedersenSingle(point, y, PEDERSEN_POINTS2);
return bytesToHexEth(point.toRawBytes(true).slice(1)); return extractX(point.toRawBytes(true));
} }
export function hashChain(data: PedersenArg[], fn = pedersen) { export function hashChain(data: PedersenArg[], fn = pedersen) {
if (!Array.isArray(data) || data.length < 1) if (!Array.isArray(data) || data.length < 1)
throw new Error('data should be array of at least 1 element'); throw new Error('data should be array of at least 1 element');
if (data.length === 1) return numberToHexEth(pedersenArg(data[0])); if (data.length === 1) return numberTo0x16(pedersenArg(data[0]));
return Array.from(data) return Array.from(data)
.reverse() .reverse()
.reduce((acc, i) => fn(i, acc)); .reduce((acc, i) => fn(i, acc));
@@ -272,9 +234,9 @@ export function hashChain(data: PedersenArg[], fn = pedersen) {
export const computeHashOnElements = (data: PedersenArg[], fn = pedersen) => export const computeHashOnElements = (data: PedersenArg[], fn = pedersen) =>
[0, ...data, data.length].reduce((x, y) => fn(x, y)); [0, ...data, data.length].reduce((x, y) => fn(x, y));
const MASK_250 = cutils.bitMask(250); const MASK_250 = bitMask(250);
export const keccak = (data: Uint8Array): bigint => bytesToNumber0x(keccak_256(data)) & MASK_250; export const keccak = (data: Uint8Array): bigint => bytesToNumberBE(keccak_256(data)) & MASK_250;
const sha256Num = (data: Uint8Array | string): bigint => cutils.bytesToNumberBE(sha256(data)); const sha256Num = (data: Uint8Array | string): bigint => bytesToNumberBE(sha256(data));
// Poseidon hash // Poseidon hash
export const Fp253 = Fp( export const Fp253 = Fp(
@@ -330,7 +292,7 @@ export function poseidonBasic(opts: PoseidonOpts, mds: bigint[][]) {
for (let j = 0; j < m; j++) row.push(poseidonRoundConstant(opts.Fp, 'Hades', m * i + j)); for (let j = 0; j < m; j++) row.push(poseidonRoundConstant(opts.Fp, 'Hades', m * i + j));
roundConstants.push(row); roundConstants.push(row);
} }
return poseidon.poseidon({ return poseidon({
...opts, ...opts,
t: m, t: m,
sboxPower: 3, sboxPower: 3,

View File

@@ -0,0 +1,44 @@
/*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
import { createCurve } from '../_shortw_utils.js';
import { sha224, sha256 } from '@noble/hashes/sha256';
import { Fp } from '../abstract/modular.js';
// NIST secp192r1 aka P192
// https://www.secg.org/sec2-v2.pdf, https://neuromancer.sk/std/secg/secp192r1
export const P192 = createCurve(
{
// Params: a, b
a: BigInt('0xfffffffffffffffffffffffffffffffefffffffffffffffc'),
b: BigInt('0x64210519e59c80e70fa7e9ab72243049feb8deecc146b9b1'),
// Field over which we'll do calculations; 2n ** 192n - 2n ** 64n - 1n
Fp: Fp(BigInt('0xfffffffffffffffffffffffffffffffeffffffffffffffff')),
// Curve order, total count of valid points in the field.
n: BigInt('0xffffffffffffffffffffffff99def836146bc9b1b4d22831'),
// Base point (x, y) aka generator point
Gx: BigInt('0x188da80eb03090f67cbf20eb43a18800f4ff0afd82ff1012'),
Gy: BigInt('0x07192b95ffc8da78631011ed6b24cdd573f977a11e794811'),
h: BigInt(1),
lowS: false,
},
sha256
);
export const secp192r1 = P192;
export const P224 = createCurve(
{
// Params: a, b
a: BigInt('0xfffffffffffffffffffffffffffffffefffffffffffffffffffffffe'),
b: BigInt('0xb4050a850c04b3abf54132565044b0b7d7bfd8ba270b39432355ffb4'),
// Field over which we'll do calculations;
Fp: Fp(BigInt('0xffffffffffffffffffffffffffffffff000000000000000000000001')),
// Curve order, total count of valid points in the field
n: BigInt('0xffffffffffffffffffffffffffff16a2e0b8f03e13dd29455c5c2a3d'),
// Base point (x, y) aka generator point
Gx: BigInt('0xb70e0cbd6bb4bf7f321390b94a03c1d356c21122343280d6115c1d21'),
Gy: BigInt('0xbd376388b5f723fb4c22dfe6cd4375a05a07476444d5819985007e34'),
h: BigInt(1),
lowS: false,
},
sha224
);
export const secp224r1 = P224;

View File

@@ -4,15 +4,14 @@ import * as fc from 'fast-check';
import * as mod from '../esm/abstract/modular.js'; import * as mod from '../esm/abstract/modular.js';
import { bytesToHex as toHex } from '../esm/abstract/utils.js'; import { bytesToHex as toHex } from '../esm/abstract/utils.js';
// Generic tests for all curves in package // Generic tests for all curves in package
import { secp192r1 } from '../esm/p192.js'; import { secp192r1, secp224r1 } from './_more-curves.helpers.js';
import { secp224r1 } from '../esm/p224.js';
import { secp256r1 } from '../esm/p256.js'; import { secp256r1 } from '../esm/p256.js';
import { secp384r1 } from '../esm/p384.js'; import { secp384r1 } from '../esm/p384.js';
import { secp521r1 } from '../esm/p521.js'; import { secp521r1 } from '../esm/p521.js';
import { secp256k1 } from '../esm/secp256k1.js'; import { secp256k1 } from '../esm/secp256k1.js';
import { ed25519, ed25519ctx, ed25519ph, x25519 } from '../esm/ed25519.js'; import { ed25519, ed25519ctx, ed25519ph, x25519 } from '../esm/ed25519.js';
import { ed448, ed448ph } from '../esm/ed448.js'; import { ed448, ed448ph } from '../esm/ed448.js';
import { starkCurve } from '../esm/stark.js'; import { _starkCurve as starkCurve } from '../esm/stark.js';
import { pallas, vesta } from '../esm/pasta.js'; import { pallas, vesta } from '../esm/pasta.js';
import { bn254 } from '../esm/bn.js'; import { bn254 } from '../esm/bn.js';
import { jubjub } from '../esm/jubjub.js'; import { jubjub } from '../esm/jubjub.js';

View File

@@ -1,7 +1,6 @@
import { deepStrictEqual, throws } from 'assert'; import { deepStrictEqual } from 'assert';
import { describe, should } from 'micro-should'; import { describe, should } from 'micro-should';
import { secp192r1, P192 } from '../esm/p192.js'; import { secp192r1, secp224r1, P192, P224 } from './_more-curves.helpers.js';
import { secp224r1, P224 } from '../esm/p224.js';
import { secp256r1, P256 } from '../esm/p256.js'; import { secp256r1, P256 } from '../esm/p256.js';
import { secp384r1, P384 } from '../esm/p384.js'; import { secp384r1, P384 } from '../esm/p384.js';
import { secp521r1, P521 } from '../esm/p521.js'; import { secp521r1, P521 } from '../esm/p521.js';

View File

@@ -1,4 +1,11 @@
import { describe, should } from 'micro-should';
import './basic.test.js'; import './basic.test.js';
import './stark.test.js'; import './stark.test.js';
import './property.test.js'; import './property.test.js';
import './poseidon.test.js'; import './poseidon.test.js';
// ESM is broken.
import url from 'url';
if (import.meta.url === url.pathToFileURL(process.argv[1]).href) {
should.run();
}