|
| 1 | +import crypto from 'crypto' |
| 2 | + |
| 3 | +export type P256Point = { x: bigint, y: bigint } | null |
| 4 | + |
| 5 | +type ByteSource = string | Uint8Array | ArrayBufferView |
| 6 | + |
| 7 | +const HEX_REGEX = /^[0-9a-fA-F]+$/ |
| 8 | + |
| 9 | +const P = BigInt('0xffffffff00000001000000000000000000000000ffffffffffffffffffffffff') |
| 10 | +const N = BigInt('0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551') |
| 11 | +const A = P - 3n // a = -3 mod p |
| 12 | +const B = BigInt('0x5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b') |
| 13 | +const GX = BigInt('0x6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296') |
| 14 | +const GY = BigInt('0x4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5') |
| 15 | +const G: P256Point = { x: GX, y: GY } |
| 16 | +const HALF_N = N >> 1n |
| 17 | + |
| 18 | +const COMPRESSED_EVEN = '02' |
| 19 | +const COMPRESSED_ODD = '03' |
| 20 | +const UNCOMPRESSED = '04' |
| 21 | + |
| 22 | +export default class Secp256r1 { |
| 23 | + readonly p = P |
| 24 | + readonly n = N |
| 25 | + readonly a = A |
| 26 | + readonly b = B |
| 27 | + readonly g = G |
| 28 | + |
| 29 | + private mod (x: bigint, m: bigint = this.p): bigint { |
| 30 | + const v = x % m |
| 31 | + return v >= 0n ? v : v + m |
| 32 | + } |
| 33 | + |
| 34 | + private modInv (x: bigint, m: bigint): bigint { |
| 35 | + if (x === 0n || m <= 0n) throw new Error('Invalid mod inverse input') |
| 36 | + let [a, b] = [this.mod(x, m), m] |
| 37 | + let [u, v] = [1n, 0n] |
| 38 | + while (b !== 0n) { |
| 39 | + const q = a / b |
| 40 | + ;[a, b] = [b, a - q * b] |
| 41 | + ;[u, v] = [v, u - q * v] |
| 42 | + } |
| 43 | + if (a !== 1n) throw new Error('Inverse does not exist') |
| 44 | + return this.mod(u, m) |
| 45 | + } |
| 46 | + |
| 47 | + private modPow (base: bigint, exponent: bigint, modulus: bigint): bigint { |
| 48 | + if (modulus === 1n) return 0n |
| 49 | + let result = 1n |
| 50 | + let b = this.mod(base, modulus) |
| 51 | + let e = exponent |
| 52 | + while (e > 0n) { |
| 53 | + if (e & 1n) result = this.mod(result * b, modulus) |
| 54 | + e >>= 1n |
| 55 | + b = this.mod(b * b, modulus) |
| 56 | + } |
| 57 | + return result |
| 58 | + } |
| 59 | + |
| 60 | + private isInfinity (p: P256Point): p is null { |
| 61 | + return p === null |
| 62 | + } |
| 63 | + |
| 64 | + private assertOnCurve (p: P256Point): void { |
| 65 | + if (this.isInfinity(p)) return |
| 66 | + const { x, y } = p |
| 67 | + const left = this.mod(y * y) |
| 68 | + const right = this.mod(this.mod(x * x * x + this.a * x) + this.b) |
| 69 | + if (left !== right) { |
| 70 | + throw new Error('Point is not on secp256r1') |
| 71 | + } |
| 72 | + } |
| 73 | + |
| 74 | + pointFromAffine (x: bigint, y: bigint): P256Point { |
| 75 | + const point: P256Point = { x: this.mod(x), y: this.mod(y) } |
| 76 | + this.assertOnCurve(point) |
| 77 | + return point |
| 78 | + } |
| 79 | + |
| 80 | + pointFromHex (hex: string): P256Point { |
| 81 | + if (hex.startsWith(UNCOMPRESSED)) { |
| 82 | + const x = BigInt('0x' + hex.slice(2, 66)) |
| 83 | + const y = BigInt('0x' + hex.slice(66)) |
| 84 | + return this.pointFromAffine(x, y) |
| 85 | + } |
| 86 | + if (hex.startsWith(COMPRESSED_EVEN) || hex.startsWith(COMPRESSED_ODD)) { |
| 87 | + const x = BigInt('0x' + hex.slice(2)) |
| 88 | + const ySq = this.mod(this.mod(x * x * x + this.a * x) + this.b) |
| 89 | + const y = this.modPow(ySq, (this.p + 1n) >> 2n, this.p) |
| 90 | + const isOdd = (y & 1n) === 1n |
| 91 | + const shouldBeOdd = hex.startsWith(COMPRESSED_ODD) |
| 92 | + const yFinal = (isOdd === shouldBeOdd) ? y : this.p - y |
| 93 | + return this.pointFromAffine(x, yFinal) |
| 94 | + } |
| 95 | + throw new Error('Invalid point encoding') |
| 96 | + } |
| 97 | + |
| 98 | + pointToHex (p: P256Point, compressed = false): string { |
| 99 | + if (this.isInfinity(p)) return '00' |
| 100 | + const xHex = this.to32BytesHex(p.x) |
| 101 | + const yHex = this.to32BytesHex(p.y) |
| 102 | + if (!compressed) return UNCOMPRESSED + xHex + yHex |
| 103 | + const prefix = (p.y & 1n) === 0n ? COMPRESSED_EVEN : COMPRESSED_ODD |
| 104 | + return prefix + xHex |
| 105 | + } |
| 106 | + |
| 107 | + private addPoints (p1: P256Point, p2: P256Point): P256Point { |
| 108 | + if (this.isInfinity(p1)) return p2 |
| 109 | + if (this.isInfinity(p2)) return p1 |
| 110 | + |
| 111 | + const { x: x1, y: y1 } = p1 |
| 112 | + const { x: x2, y: y2 } = p2 |
| 113 | + |
| 114 | + if (x1 === x2) { |
| 115 | + if (y1 === y2) { |
| 116 | + return this.doublePoint(p1) |
| 117 | + } |
| 118 | + return null |
| 119 | + } |
| 120 | + |
| 121 | + const m = this.mod((y2 - y1) * this.modInv(x2 - x1, this.p)) |
| 122 | + const x3 = this.mod(m * m - x1 - x2) |
| 123 | + const y3 = this.mod(m * (x1 - x3) - y1) |
| 124 | + return { x: x3, y: y3 } |
| 125 | + } |
| 126 | + |
| 127 | + private doublePoint (p: P256Point): P256Point { |
| 128 | + if (this.isInfinity(p)) return p |
| 129 | + if (p.y === 0n) return null |
| 130 | + const m = this.mod((3n * p.x * p.x + this.a) * this.modInv(2n * p.y, this.p)) |
| 131 | + const x3 = this.mod(m * m - 2n * p.x) |
| 132 | + const y3 = this.mod(m * (p.x - x3) - p.y) |
| 133 | + return { x: x3, y: y3 } |
| 134 | + } |
| 135 | + |
| 136 | + add (p1: P256Point, p2: P256Point): P256Point { |
| 137 | + return this.addPoints(p1, p2) |
| 138 | + } |
| 139 | + |
| 140 | + multiply (point: P256Point, scalar: bigint): P256Point { |
| 141 | + if (scalar === 0n || this.isInfinity(point)) return null |
| 142 | + let k = this.mod(scalar, this.n) |
| 143 | + let result: P256Point = null |
| 144 | + let addend: P256Point = point |
| 145 | + while (k > 0n) { |
| 146 | + if (k & 1n) { |
| 147 | + result = this.addPoints(result, addend) |
| 148 | + } |
| 149 | + addend = this.doublePoint(addend) |
| 150 | + k >>= 1n |
| 151 | + } |
| 152 | + return result |
| 153 | + } |
| 154 | + |
| 155 | + multiplyBase (scalar: bigint): P256Point { |
| 156 | + return this.multiply(this.g, scalar) |
| 157 | + } |
| 158 | + |
| 159 | + isOnCurve (p: P256Point): boolean { |
| 160 | + try { |
| 161 | + this.assertOnCurve(p) |
| 162 | + return true |
| 163 | + } catch (err) { |
| 164 | + return false |
| 165 | + } |
| 166 | + } |
| 167 | + |
| 168 | + generatePrivateKeyHex (): string { |
| 169 | + return this.to32BytesHex(this.randomScalar()) |
| 170 | + } |
| 171 | + |
| 172 | + private randomScalar (): bigint { |
| 173 | + while (true) { |
| 174 | + const bytes = crypto.randomBytes(32) |
| 175 | + const k = BigInt('0x' + bytes.toString('hex')) |
| 176 | + if (k > 0n && k < this.n) return k |
| 177 | + } |
| 178 | + } |
| 179 | + |
| 180 | + private normalizePrivateKey (d: bigint): bigint { |
| 181 | + const key = this.mod(d, this.n) |
| 182 | + if (key === 0n) throw new Error('Invalid private key') |
| 183 | + return key |
| 184 | + } |
| 185 | + |
| 186 | + private toScalar (input: string | bigint): bigint { |
| 187 | + if (typeof input === 'bigint') return this.normalizePrivateKey(input) |
| 188 | + const hex = input.startsWith('0x') ? input.slice(2) : input |
| 189 | + if (!HEX_REGEX.test(hex) || hex.length === 0 || hex.length > 64) { |
| 190 | + throw new Error('Private key must be a hex string <= 32 bytes') |
| 191 | + } |
| 192 | + const value = BigInt('0x' + hex.padStart(64, '0')) |
| 193 | + return this.normalizePrivateKey(value) |
| 194 | + } |
| 195 | + |
| 196 | + publicKeyFromPrivate (privateKey: string | bigint): P256Point { |
| 197 | + const d = this.toScalar(privateKey) |
| 198 | + return this.multiplyBase(d) |
| 199 | + } |
| 200 | + |
| 201 | + sign (message: ByteSource, privateKey: string | bigint, opts: { prehashed?: boolean, nonce?: bigint } = {}): { r: string, s: string } { |
| 202 | + const { prehashed = false, nonce } = opts |
| 203 | + const d = this.toScalar(privateKey) |
| 204 | + const z = this.messageToBigInt(message, prehashed) |
| 205 | + let k = nonce ?? this.randomScalar() |
| 206 | + |
| 207 | + while (true) { |
| 208 | + const p = this.multiplyBase(k) |
| 209 | + if (this.isInfinity(p)) { |
| 210 | + k = nonce ?? this.randomScalar() |
| 211 | + continue |
| 212 | + } |
| 213 | + const r = this.mod(p.x, this.n) |
| 214 | + if (r === 0n) { |
| 215 | + k = nonce ?? this.randomScalar() |
| 216 | + continue |
| 217 | + } |
| 218 | + const kinv = this.modInv(k, this.n) |
| 219 | + let s = this.mod(kinv * (z + r * d), this.n) |
| 220 | + if (s === 0n) { |
| 221 | + k = nonce ?? this.randomScalar() |
| 222 | + continue |
| 223 | + } |
| 224 | + if (s > HALF_N) s = this.n - s // enforce low-s |
| 225 | + return { r: this.to32BytesHex(r), s: this.to32BytesHex(s) } |
| 226 | + } |
| 227 | + } |
| 228 | + |
| 229 | + verify (message: ByteSource, signature: { r: string | bigint, s: string | bigint }, publicKey: P256Point | string, opts: { prehashed?: boolean } = {}): boolean { |
| 230 | + const { prehashed = false } = opts |
| 231 | + const q = typeof publicKey === 'string' ? this.pointFromHex(publicKey) : publicKey |
| 232 | + if (!q || !this.isOnCurve(q)) return false |
| 233 | + |
| 234 | + const r = typeof signature.r === 'bigint' ? signature.r : BigInt('0x' + signature.r) |
| 235 | + const s = typeof signature.s === 'bigint' ? signature.s : BigInt('0x' + signature.s) |
| 236 | + if (r <= 0n || r >= this.n || s <= 0n || s >= this.n) return false |
| 237 | + |
| 238 | + const z = this.messageToBigInt(message, prehashed) |
| 239 | + const w = this.modInv(s, this.n) |
| 240 | + const u1 = this.mod(z * w, this.n) |
| 241 | + const u2 = this.mod(r * w, this.n) |
| 242 | + const p = this.addPoints(this.multiplyBase(u1), this.multiply(q, u2)) |
| 243 | + if (this.isInfinity(p)) return false |
| 244 | + const v = this.mod(p.x, this.n) |
| 245 | + return v === r |
| 246 | + } |
| 247 | + |
| 248 | + private messageToBigInt (message: ByteSource, prehashed: boolean): bigint { |
| 249 | + const bytes = this.toBuffer(message) |
| 250 | + const digest = prehashed ? bytes : crypto.createHash('sha256').update(bytes).digest() |
| 251 | + const hex = digest.toString('hex') |
| 252 | + return BigInt('0x' + hex) % this.n |
| 253 | + } |
| 254 | + |
| 255 | + private toBuffer (data: ByteSource): Buffer { |
| 256 | + if (typeof data === 'string') { |
| 257 | + if (HEX_REGEX.test(data) && data.length % 2 === 0) { |
| 258 | + return Buffer.from(data, 'hex') |
| 259 | + } |
| 260 | + return Buffer.from(data, 'utf8') |
| 261 | + } |
| 262 | + if (data instanceof Uint8Array) return Buffer.from(data) |
| 263 | + if (ArrayBuffer.isView(data)) return Buffer.from(data.buffer, data.byteOffset, data.byteLength) |
| 264 | + throw new Error('Unsupported message format') |
| 265 | + } |
| 266 | + |
| 267 | + private to32BytesHex (num: bigint): string { |
| 268 | + return num.toString(16).padStart(64, '0') |
| 269 | + } |
| 270 | +} |
0 commit comments