OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-web-ui commits / 05dea0a

Remove identity/auth from hyper-web-ui — belongs in hyper-identity
Removed: createChallenge, verifyChallenge (identity ops, not web-ui) Removed: web/js/identity.js, anproto-client.js, vendor/ (identity files) Kept: authMiddleware (Bearer token extraction is web-layer concern) Kept: Router, json, html, render helpers, static serving, CSS Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
05dea0a4227508a1ded6902cfd5a0c73ea157453
by plankatron <fleeky@gmail.com> · 2026-03-30T22:15:32.000Z

Changed files (5) +4 -671

index.js +1 -1
……@@ -1,5 +1,5 @@
11// Server-side exports
22export { Router, json, html, readBody, readJson, parsePagination, cacheControl, withEtag, jsonCached } from './lib/router.js'
3export { authMiddleware, createChallenge, verifyChallenge } from './lib/auth.js'
3export { authMiddleware } from './lib/auth.js'
44export { serveStatic } from './lib/static.js'
55export { renderMarkdown, renderDiff, renderCode, renderPagination, renderBranchSelector, detectLanguage } from './lib/render.js'
lib/auth.js +3 -59
……@@ -1,12 +1,10 @@
11/**
2 * Auth Middleware + Challenge-Response
2 * Auth Middleware — Bearer token extraction from HTTP requests
33 *
4 * Shared authentication for all P2P apps.
5 * Bearer token extraction + challenge-response flow.
4 * Web-layer concern only. Does NOT handle challenge-response or
5 * signature verification — that belongs in hyper-identity.
66 */
77
8import crypto from 'hypercore-crypto'
9
108/**
119 * Create auth middleware for routes
1210 *
……@@ -49,60 +47,6 @@ export function authMiddleware (opts = {}) {
4947 }
5048}
5149
52/**
53 * Generate a challenge for signature-based auth
54 */
55export async function createChallenge (registry) {
56 const challengeId = crypto.randomBytes(16).toString('hex')
57 const challenge = crypto.randomBytes(32).toString('base64')
58
59 const entry = {
60 challenge,
61 expiresAt: Date.now() + 5 * 60 * 1000,
62 consumed: false
63 }
64
65 await registry.put(`challenge:${challengeId}`, entry)
66 return { challengeId, challenge }
67}
68
69/**
70 * Verify a signed challenge and issue a token
71 */
72export async function verifyChallenge (registry, { challengeId, publicKey, signature }) {
73 const entry = await registry.get(`challenge:${challengeId}`)
74 if (!entry) return { error: 'Invalid challenge' }
75
76 const ch = entry.value
77 if (ch.consumed) return { error: 'Challenge already used' }
78 if (ch.expiresAt < Date.now()) return { error: 'Challenge expired' }
79
80 try {
81 const valid = crypto.verify(
82 Buffer.from(ch.challenge),
83 Buffer.from(signature, 'base64'),
84 Buffer.from(publicKey, 'hex')
85 )
86 if (!valid) return { error: 'Invalid signature' }
87 } catch {
88 return { error: 'Signature verification failed' }
89 }
90
91 // Consume challenge
92 ch.consumed = true
93 await registry.put(`challenge:${challengeId}`, ch)
94
95 // Generate token
96 const token = crypto.randomBytes(32).toString('hex')
97 await registry.put(`token:${token}`, {
98 publicKey,
99 createdAt: Date.now(),
100 expiresAt: Date.now() + 30 * 24 * 60 * 60 * 1000
101 })
102
103 return { token, publicKey }
104}
105
10650function extractToken (req) {
10751 const auth = req.headers.authorization
10852 if (!auth) return null
web/js/anproto-client.js +0 -108
……@@ -1,108 +0,0 @@
1/**
2 * ANProto Browser Client — Ed25519 identity for browser contexts
3 *
4 * Uses vendored @noble/ed25519 (pure JS, no native deps, works on HTTP).
5 * Same protocol as server-side ANProto — keypairs, challenge-response signing.
6 *
7 * Keypair format: 132-char base64 string (44 pubkey + 88 secretkey)
8 * Signature format: 88-char base64 (64 bytes)
9 */
10
11import * as ed from './vendor/noble-ed25519.js'
12
13export const ANProtoClient = {
14 /**
15 * Generate a new Ed25519 keypair
16 * @returns {Promise<string>} 132-char base64 keypair (44 pub + 88 secret)
17 */
18 async gen () {
19 const privateKey = ed.utils.randomPrivateKey()
20 const publicKey = await ed.getPublicKeyAsync(privateKey)
21
22 // Build 64-byte secret key (32 seed + 32 pubkey) to match sodium format
23 const secretKey = new Uint8Array(64)
24 secretKey.set(privateKey, 0)
25 secretKey.set(publicKey, 32)
26
27 return bytesToBase64(publicKey) + bytesToBase64(secretKey)
28 },
29
30 /**
31 * Get public key from keypair string
32 * @param {string} keypair — 132-char base64
33 * @returns {string} 44-char base64 public key
34 */
35 getPublicKey (keypair) {
36 return keypair.substring(0, 44)
37 },
38
39 /**
40 * Get public key as hex string
41 * @param {string} keypair — 132-char base64
42 * @returns {string} 64-char hex public key
43 */
44 getPublicKeyHex (keypair) {
45 const bytes = base64ToBytes(keypair.substring(0, 44))
46 return bytesToHex(bytes)
47 },
48
49 /**
50 * Sign a challenge (detached Ed25519 signature)
51 * Compatible with server-side ANProto.verify()
52 *
53 * @param {string} challenge — challenge string (from server)
54 * @param {string} keypair — 132-char base64 keypair
55 * @returns {Promise<string>} 88-char base64 signature
56 */
57 async sign (challenge, keypair) {
58 const secretKeyB64 = keypair.substring(44, 132)
59 const secretKeyBytes = base64ToBytes(secretKeyB64)
60
61 // noble/ed25519 sign() takes the 32-byte seed (first half of 64-byte secret)
62 const seed = secretKeyBytes.slice(0, 32)
63 const challengeBytes = new TextEncoder().encode(challenge)
64
65 const signature = await ed.signAsync(challengeBytes, seed)
66 return bytesToBase64(signature)
67 },
68
69 /**
70 * Verify a detached signature
71 * @param {string} challenge
72 * @param {string} signatureB64 — 88-char base64
73 * @param {string} publicKeyB64 — 44-char base64
74 * @returns {Promise<boolean>}
75 */
76 async verify (challenge, signatureB64, publicKeyB64) {
77 try {
78 const challengeBytes = new TextEncoder().encode(challenge)
79 const signature = base64ToBytes(signatureB64)
80 const publicKey = base64ToBytes(publicKeyB64)
81 return await ed.verifyAsync(signature, challengeBytes, publicKey)
82 } catch {
83 return false
84 }
85 }
86}
87
88// ── Encoding helpers ─────────────────────────────────────────
89
90function base64ToBytes (base64) {
91 const binary = atob(base64)
92 const bytes = new Uint8Array(binary.length)
93 for (let i = 0; i < binary.length; i++) {
94 bytes[i] = binary.charCodeAt(i)
95 }
96 return bytes
97}
98
99function bytesToBase64 (bytes) {
100 const binary = String.fromCharCode(...bytes)
101 return btoa(binary)
102}
103
104function bytesToHex (bytes) {
105 return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('')
106}
107
108export default ANProtoClient
web/js/identity.js +0 -129
……@@ -1,129 +0,0 @@
1/**
2 * Browser Identity Client — Ed25519 keypair + challenge-response auth
3 *
4 * Uses ANProtoClient (vendored noble/ed25519, works on HTTP).
5 * Shared across all P2P apps (pear-git, hypernote, hyper-booking, etc.)
6 * Same-origin on 7rnx.net means identity persists across apps.
7 */
8
9import { ANProtoClient } from './anproto-client.js'
10import { get, post } from './api.js'
11
12const STORAGE_KEY = 'hyper-identity-keypair'
13const TOKEN_KEY = 'hyper-identity-token'
14
15let _keypair = null
16
17/**
18 * Get or create keypair (stored in localStorage as 132-char base64)
19 */
20export async function getKeypair () {
21 if (_keypair) return _keypair
22
23 const stored = localStorage.getItem(STORAGE_KEY)
24 if (stored && stored.length === 132) {
25 _keypair = stored
26 return _keypair
27 }
28
29 _keypair = await ANProtoClient.gen()
30 localStorage.setItem(STORAGE_KEY, _keypair)
31 return _keypair
32}
33
34/**
35 * Get public key as hex string (for API calls)
36 */
37export async function getPublicKey () {
38 const kp = await getKeypair()
39 return ANProtoClient.getPublicKeyHex(kp)
40}
41
42/**
43 * Get public key as base64 (for display, 44 chars)
44 */
45export async function getPublicKeyB64 () {
46 const kp = await getKeypair()
47 return ANProtoClient.getPublicKey(kp)
48}
49
50/**
51 * Sign a challenge string
52 */
53export async function sign (challenge) {
54 const kp = await getKeypair()
55 return ANProtoClient.sign(challenge, kp)
56}
57
58/**
59 * Authenticate with server — challenge-response flow
60 */
61export async function authenticate () {
62 const publicKey = await getPublicKey()
63
64 // 1. Get challenge from server
65 const { challengeId, challenge } = await get('/api/auth/challenge')
66
67 // 2. Sign challenge with our key
68 const signature = await sign(challenge)
69
70 // 3. Send signed challenge to server
71 const result = await post('/api/auth/verify', {
72 publicKey,
73 signature,
74 challengeId
75 })
76
77 // 4. Store token
78 localStorage.setItem(TOKEN_KEY, result.token)
79
80 return result
81}
82
83/**
84 * Get stored auth token
85 */
86export function getToken () {
87 return localStorage.getItem(TOKEN_KEY)
88}
89
90/**
91 * Check if authenticated (has token)
92 */
93export function isAuthenticated () {
94 return !!getToken()
95}
96
97/**
98 * Get current user info from server
99 */
100export async function getMe () {
101 const token = getToken()
102 if (!token) return null
103
104 try {
105 const res = await fetch(window.location.origin + '/api/users/me', {
106 headers: { Authorization: `Bearer ${token}` }
107 })
108 if (!res.ok) return null
109 return res.json()
110 } catch {
111 return null
112 }
113}
114
115/**
116 * Logout — clear token (keeps keypair for re-auth)
117 */
118export function logout () {
119 localStorage.removeItem(TOKEN_KEY)
120}
121
122/**
123 * Clear everything (keypair + token)
124 */
125export function clearIdentity () {
126 localStorage.removeItem(TOKEN_KEY)
127 localStorage.removeItem(STORAGE_KEY)
128 _keypair = null
129}
web/js/vendor/noble-ed25519.js +0 -374
……@@ -1,374 +0,0 @@
1/*! noble-ed25519 - MIT License (c) 2019 Paul Miller (paulmillr.com) */
2const P = 2n ** 255n - 19n; // ed25519 is twisted edwards curve
3const N = 2n ** 252n + 27742317777372353535851937790883648493n; // curve's (group) order
4const Gx = 0x216936d3cd6e53fec0a4e231fdd6dc5c692cc7609525a7b2c9562d608f25d51an; // base point x
5const Gy = 0x6666666666666666666666666666666666666666666666666666666666666658n; // base point y
6const CURVE = {
7 a: -1n,
8 d: 37095705934669439343138083508754565189542113879843219016388785533085940283555n,
9 p: P, n: N, h: 8, Gx, Gy // field prime, curve (group) order, cofactor
10};
11const err = (m = '') => { throw new Error(m); }; // error helper, messes-up stack trace
12const str = (s) => typeof s === 'string'; // is string
13const au8 = (a, l) => // is Uint8Array (of specific length)
14 !(a instanceof Uint8Array) || (typeof l === 'number' && l > 0 && a.length !== l) ?
15 err('Uint8Array expected') : a;
16const u8n = (data) => new Uint8Array(data); // creates Uint8Array
17const toU8 = (a, len) => au8(str(a) ? h2b(a) : u8n(a), len); // norm(hex/u8a) to u8a
18const mod = (a, b = P) => { let r = a % b; return r >= 0n ? r : b + r; }; // mod division
19const isPoint = (p) => (p instanceof Point ? p : err('Point expected')); // is xyzt point
20let Gpows = undefined; // precomputes for base point G
21class Point {
22 constructor(ex, ey, ez, et) {
23 this.ex = ex;
24 this.ey = ey;
25 this.ez = ez;
26 this.et = et;
27 }
28 static fromAffine(p) { return new Point(p.x, p.y, 1n, mod(p.x * p.y)); }
29 static fromHex(hex, strict = true) {
30 const { d } = CURVE;
31 hex = toU8(hex, 32);
32 const normed = hex.slice(); // copy the array to not mess it up
33 normed[31] = hex[31] & ~0x80; // adjust first LE byte = last BE byte
34 const y = b2n_LE(normed); // decode as little-endian, convert to num
35 if (y === 0n) { // y=0 is valid, proceed
36 }
37 else {
38 if (strict && !(0n < y && y < P))
39 err('bad y coord 1'); // strict=true [1..P-1]
40 if (!strict && !(0n < y && y < 2n ** 256n))
41 err('bad y coord 2'); // strict=false [1..2^256-1]
42 }
43 const y2 = mod(y * y); // y²
44 const u = mod(y2 - 1n); // u=y²-1
45 const v = mod(d * y2 + 1n); // v=dy²+1
46 let { isValid, value: x } = uvRatio(u, v); // (uv³)(uv⁷)^(p-5)/8; square root
47 if (!isValid)
48 err('bad y coordinate 3'); // not square root: bad point
49 const isXOdd = (x & 1n) === 1n; // adjust sign of x coordinate
50 const isHeadOdd = (hex[31] & 0x80) !== 0;
51 if (isHeadOdd !== isXOdd)
52 x = mod(-x);
53 return new Point(x, y, 1n, mod(x * y)); // Z=1, T=xy
54 }
55 get x() { return this.toAffine().x; } // .x, .y will call expensive toAffine.
56 get y() { return this.toAffine().y; } // Should be used with care.
57 equals(other) {
58 const { ex: X1, ey: Y1, ez: Z1 } = this;
59 const { ex: X2, ey: Y2, ez: Z2 } = isPoint(other); // isPoint() checks class equality
60 const X1Z2 = mod(X1 * Z2), X2Z1 = mod(X2 * Z1);
61 const Y1Z2 = mod(Y1 * Z2), Y2Z1 = mod(Y2 * Z1);
62 return X1Z2 === X2Z1 && Y1Z2 === Y2Z1;
63 }
64 is0() { return this.equals(I); }
65 negate() {
66 return new Point(mod(-this.ex), this.ey, this.ez, mod(-this.et));
67 }
68 double() {
69 const { ex: X1, ey: Y1, ez: Z1 } = this; // Cost: 4M + 4S + 1*a + 6add + 1*2
70 const { a } = CURVE; // https://hyperelliptic.org/EFD/g1p/auto-twisted-extended.html#doubling-dbl-2008-hwcd
71 const A = mod(X1 * X1);
72 const B = mod(Y1 * Y1);
73 const C = mod(2n * mod(Z1 * Z1));
74 const D = mod(a * A);
75 const x1y1 = X1 + Y1;
76 const E = mod(mod(x1y1 * x1y1) - A - B);
77 const G = D + B;
78 const F = G - C;
79 const H = D - B;
80 const X3 = mod(E * F);
81 const Y3 = mod(G * H);
82 const T3 = mod(E * H);
83 const Z3 = mod(F * G);
84 return new Point(X3, Y3, Z3, T3);
85 }
86 add(other) {
87 const { ex: X1, ey: Y1, ez: Z1, et: T1 } = this; // Cost: 8M + 1*k + 8add + 1*2.
88 const { ex: X2, ey: Y2, ez: Z2, et: T2 } = isPoint(other); // doesn't check if other on-curve
89 const { a, d } = CURVE; // http://hyperelliptic.org/EFD/g1p/auto-twisted-extended-1.html#addition-add-2008-hwcd-3
90 const A = mod(X1 * X2);
91 const B = mod(Y1 * Y2);
92 const C = mod(T1 * d * T2);
93 const D = mod(Z1 * Z2);
94 const E = mod((X1 + Y1) * (X2 + Y2) - A - B);
95 const F = mod(D - C);
96 const G = mod(D + C);
97 const H = mod(B - a * A);
98 const X3 = mod(E * F);
99 const Y3 = mod(G * H);
100 const T3 = mod(E * H);
101 const Z3 = mod(F * G);
102 return new Point(X3, Y3, Z3, T3);
103 }
104 mul(n, safe = true) {
105 if (n === 0n)
106 return safe === true ? err('cannot multiply by 0') : I;
107 if (!(typeof n === 'bigint' && 0n < n && n < N))
108 err('invalid scalar, must be < L');
109 if (!safe && this.is0() || n === 1n)
110 return this; // safe=true bans 0. safe=false allows 0.
111 if (this.equals(G))
112 return wNAF(n).p; // use wNAF precomputes for base points
113 let p = I, f = G; // init result point & fake point
114 for (let d = this; n > 0n; d = d.double(), n >>= 1n) { // double-and-add ladder
115 if (n & 1n)
116 p = p.add(d); // if bit is present, add to point
117 else if (safe)
118 f = f.add(d); // if not, add to fake for timing safety
119 }
120 return p;
121 }
122 multiply(scalar) { return this.mul(scalar); } // Aliases for compatibilty
123 clearCofactor() { return this.mul(BigInt(CURVE.h), false); } // multiply by cofactor
124 isSmallOrder() { return this.clearCofactor().is0(); } // check if P is small order
125 isTorsionFree() {
126 let p = this.mul(N / 2n, false).double(); // ensures the point is not "bad".
127 if (N % 2n)
128 p = p.add(this); // P^(N+1) // P*N == (P*(N/2))*2+P
129 return p.is0();
130 }
131 toAffine() {
132 const { ex: x, ey: y, ez: z } = this; // (x, y, z, t) ∋ (x=x/z, y=y/z, t=xy)
133 if (this.is0())
134 return { x: 0n, y: 0n }; // fast-path for zero point
135 const iz = invert(z); // z^-1: invert z
136 if (mod(z * iz) !== 1n)
137 err('invalid inverse'); // (z * z^-1) must be 1, otherwise bad math
138 return { x: mod(x * iz), y: mod(y * iz) }; // x = x*z^-1; y = y*z^-1
139 }
140 toRawBytes() {
141 const { x, y } = this.toAffine(); // convert to affine 2d point
142 const b = n2b_32LE(y); // encode number to 32 bytes
143 b[31] |= x & 1n ? 0x80 : 0; // store sign in first LE byte
144 return b;
145 }
146 toHex() { return b2h(this.toRawBytes()); } // encode to hex string
147}
148Point.BASE = new Point(Gx, Gy, 1n, mod(Gx * Gy)); // Generator / Base point
149Point.ZERO = new Point(0n, 1n, 1n, 0n); // Identity / Zero point
150const { BASE: G, ZERO: I } = Point; // Generator, identity points
151const padh = (num, pad) => num.toString(16).padStart(pad, '0');
152const b2h = (b) => Array.from(b).map(e => padh(e, 2)).join(''); // bytes to hex
153const h2b = (hex) => {
154 const l = hex.length; // error if not string,
155 if (!str(hex) || l % 2)
156 err('hex invalid 1'); // or has odd length like 3, 5.
157 const arr = u8n(l / 2); // create result array
158 for (let i = 0; i < arr.length; i++) {
159 const j = i * 2;
160 const h = hex.slice(j, j + 2); // hexByte. slice is faster than substr
161 const b = Number.parseInt(h, 16); // byte, created from string part
162 if (Number.isNaN(b) || b < 0)
163 err('hex invalid 2'); // byte must be valid 0 <= byte < 256
164 arr[i] = b;
165 }
166 return arr;
167};
168const n2b_32LE = (num) => h2b(padh(num, 32 * 2)).reverse(); // number to bytes LE
169const b2n_LE = (b) => BigInt('0x' + b2h(u8n(au8(b)).reverse())); // bytes LE to num
170const concatB = (...arrs) => {
171 const r = u8n(arrs.reduce((sum, a) => sum + au8(a).length, 0)); // create u8a of summed length
172 let pad = 0; // walk through each array,
173 arrs.forEach(a => { r.set(a, pad); pad += a.length; }); // ensure they have proper type
174 return r;
175};
176const invert = (num, md = P) => {
177 if (num === 0n || md <= 0n)
178 err('no inverse n=' + num + ' mod=' + md); // no neg exponent for now
179 let a = mod(num, md), b = md, x = 0n, y = 1n, u = 1n, v = 0n;
180 while (a !== 0n) { // uses euclidean gcd algorithm
181 const q = b / a, r = b % a; // not constant-time
182 const m = x - u * q, n = y - v * q;
183 b = a, a = r, x = u, y = v, u = m, v = n;
184 }
185 return b === 1n ? mod(x, md) : err('no inverse'); // b is gcd at this point
186};
187const pow2 = (x, power) => {
188 let r = x;
189 while (power-- > 0n) {
190 r *= r;
191 r %= P;
192 }
193 return r;
194};
195const pow_2_252_3 = (x) => {
196 const x2 = (x * x) % P; // x^2, bits 1
197 const b2 = (x2 * x) % P; // x^3, bits 11
198 const b4 = (pow2(b2, 2n) * b2) % P; // x^(2^4-1), bits 1111
199 const b5 = (pow2(b4, 1n) * x) % P; // x^(2^5-1), bits 11111
200 const b10 = (pow2(b5, 5n) * b5) % P; // x^(2^10)
201 const b20 = (pow2(b10, 10n) * b10) % P; // x^(2^20)
202 const b40 = (pow2(b20, 20n) * b20) % P; // x^(2^40)
203 const b80 = (pow2(b40, 40n) * b40) % P; // x^(2^80)
204 const b160 = (pow2(b80, 80n) * b80) % P; // x^(2^160)
205 const b240 = (pow2(b160, 80n) * b80) % P; // x^(2^240)
206 const b250 = (pow2(b240, 10n) * b10) % P; // x^(2^250)
207 const pow_p_5_8 = (pow2(b250, 2n) * x) % P; // < To pow to (p+3)/8, multiply it by x.
208 return { pow_p_5_8, b2 };
209};
210const RM1 = 19681161376707505956807079304988542015446066515923890162744021073123829784752n; // √-1
211const uvRatio = (u, v) => {
212 const v3 = mod(v * v * v); // v³
213 const v7 = mod(v3 * v3 * v); // v⁷
214 const pow = pow_2_252_3(u * v7).pow_p_5_8; // (uv⁷)^(p-5)/8
215 let x = mod(u * v3 * pow); // (uv³)(uv⁷)^(p-5)/8
216 const vx2 = mod(v * x * x); // vx²
217 const root1 = x; // First root candidate
218 const root2 = mod(x * RM1); // Second root candidate; RM1 is √-1
219 const useRoot1 = vx2 === u; // If vx² = u (mod p), x is a square root
220 const useRoot2 = vx2 === mod(-u); // If vx² = -u, set x <-- x * 2^((p-1)/4)
221 const noRoot = vx2 === mod(-u * RM1); // There is no valid root, vx² = -u√-1
222 if (useRoot1)
223 x = root1;
224 if (useRoot2 || noRoot)
225 x = root2; // We return root2 anyway, for const-time
226 if ((mod(x) & 1n) === 1n)
227 x = mod(-x); // edIsNegative
228 return { isValid: useRoot1 || useRoot2, value: x };
229};
230const modL_LE = (hash) => mod(b2n_LE(hash), N); // modulo L; but little-endian
231let _shaS;
232const sha512a = (...m) => etc.sha512Async(...m); // Async SHA512
233const sha512s = (...m) => // Sync SHA512, not set by default
234 typeof _shaS === 'function' ? _shaS(...m) : err('etc.sha512Sync not set');
235const hash2extK = (hashed) => {
236 const head = hashed.slice(0, 32); // slice creates a copy, unlike subarray
237 head[0] &= 248; // Clamp bits: 0b1111_1000,
238 head[31] &= 127; // 0b0111_1111,
239 head[31] |= 64; // 0b0100_0000
240 const prefix = hashed.slice(32, 64); // private key "prefix"
241 const scalar = modL_LE(head); // modular division over curve order
242 const point = G.mul(scalar); // public key point
243 const pointBytes = point.toRawBytes(); // point serialized to Uint8Array
244 return { head, prefix, scalar, point, pointBytes };
245};
246// RFC8032 5.1.5; getPublicKey async, sync. Hash priv key and extract point.
247const getExtendedPublicKeyAsync = (priv) => sha512a(toU8(priv, 32)).then(hash2extK);
248const getExtendedPublicKey = (priv) => hash2extK(sha512s(toU8(priv, 32)));
249const getPublicKeyAsync = (priv) => getExtendedPublicKeyAsync(priv).then(p => p.pointBytes);
250const getPublicKey = (priv) => getExtendedPublicKey(priv).pointBytes;
251function hashFinish(asynchronous, res) {
252 if (asynchronous)
253 return sha512a(res.hashable).then(res.finish);
254 return res.finish(sha512s(res.hashable));
255}
256const _sign = (e, rBytes, msg) => {
257 const { pointBytes: P, scalar: s } = e;
258 const r = modL_LE(rBytes); // r was created outside, reduce it modulo L
259 const R = G.mul(r).toRawBytes(); // R = [r]B
260 const hashable = concatB(R, P, msg); // dom2(F, C) || R || A || PH(M)
261 const finish = (hashed) => {
262 const S = mod(r + modL_LE(hashed) * s, N); // S = (r + k * s) mod L; 0 <= s < l
263 return au8(concatB(R, n2b_32LE(S)), 64); // 64-byte sig: 32b R.x + 32b LE(S)
264 };
265 return { hashable, finish };
266};
267const signAsync = async (msg, privKey) => {
268 const m = toU8(msg); // RFC8032 5.1.6: sign msg with key async
269 const e = await getExtendedPublicKeyAsync(privKey); // pub,prfx
270 const rBytes = await sha512a(e.prefix, m); // r = SHA512(dom2(F, C) || prefix || PH(M))
271 return hashFinish(true, _sign(e, rBytes, m)); // gen R, k, S, then 64-byte signature
272};
273const sign = (msg, privKey) => {
274 const m = toU8(msg); // RFC8032 5.1.6: sign msg with key sync
275 const e = getExtendedPublicKey(privKey); // pub,prfx
276 const rBytes = sha512s(e.prefix, m); // r = SHA512(dom2(F, C) || prefix || PH(M))
277 return hashFinish(false, _sign(e, rBytes, m)); // gen R, k, S, then 64-byte signature
278};
279const _verify = (sig, msg, pub) => {
280 msg = toU8(msg); // Message hex str/Bytes
281 sig = toU8(sig, 64); // Signature hex str/Bytes, must be 64 bytes
282 const A = Point.fromHex(pub, false); // public key A decoded
283 const R = Point.fromHex(sig.slice(0, 32), false); // 0 <= R < 2^256: ZIP215 R can be >= P
284 const s = b2n_LE(sig.slice(32, 64)); // Decode second half as an integer S
285 const SB = G.mul(s, false); // in the range 0 <= s < L
286 const hashable = concatB(R.toRawBytes(), A.toRawBytes(), msg); // dom2(F, C) || R || A || PH(M)
287 const finish = (hashed) => {
288 const k = modL_LE(hashed); // decode in little-endian, modulo L
289 const RkA = R.add(A.mul(k, false)); // [8]R + [8][k]A'
290 return RkA.add(SB.negate()).clearCofactor().is0(); // [8][S]B = [8]R + [8][k]A'
291 };
292 return { hashable, finish };
293};
294// RFC8032 5.1.7: verification async, sync
295const verifyAsync = async (s, m, p) => hashFinish(true, _verify(s, m, p));
296const verify = (s, m, p) => hashFinish(false, _verify(s, m, p));
297const cr = () => // We support: 1) browsers 2) node.js 19+
298 typeof globalThis === 'object' && 'crypto' in globalThis ? globalThis.crypto : undefined;
299const etc = {
300 bytesToHex: b2h, hexToBytes: h2b, concatBytes: concatB,
301 mod, invert,
302 randomBytes: (len) => {
303 const crypto = cr(); // Can be shimmed in node.js <= 18 to prevent error:
304 // import { webcrypto } from 'node:crypto';
305 // if (!globalThis.crypto) globalThis.crypto = webcrypto;
306 if (!crypto)
307 err('crypto.getRandomValues must be defined');
308 return crypto.getRandomValues(u8n(len));
309 },
310 sha512Async: async (...messages) => {
311 const crypto = cr();
312 if (!crypto)
313 err('crypto.subtle or etc.sha512Async must be defined');
314 const m = concatB(...messages);
315 return u8n(await crypto.subtle.digest('SHA-512', m.buffer));
316 },
317 sha512Sync: undefined, // Actual logic below
318};
319Object.defineProperties(etc, { sha512Sync: {
320 configurable: false, get() { return _shaS; }, set(f) { if (!_shaS)
321 _shaS = f; },
322 } });
323const utils = {
324 getExtendedPublicKeyAsync, getExtendedPublicKey,
325 randomPrivateKey: () => etc.randomBytes(32),
326 precompute(w = 8, p = G) { p.multiply(3n); return p; }, // no-op
327};
328const W = 8; // Precomputes-related code. W = window size
329const precompute = () => {
330 const points = []; // 10x sign(), 2x verify(). To achieve this,
331 const windows = 256 / W + 1; // app needs to spend 40ms+ to calculate
332 let p = G, b = p; // a lot of points related to base point G.
333 for (let w = 0; w < windows; w++) { // Points are stored in array and used
334 b = p; // any time Gx multiplication is done.
335 points.push(b); // They consume 16-32 MiB of RAM.
336 for (let i = 1; i < 2 ** (W - 1); i++) {
337 b = b.add(p);
338 points.push(b);
339 }
340 p = b.double(); // Precomputes don't speed-up getSharedKey,
341 } // which multiplies user point by scalar,
342 return points; // when precomputes are using base point
343};
344const wNAF = (n) => {
345 // Compared to other point mult methods,
346 const comp = Gpows || (Gpows = precompute()); // stores 2x less points using subtraction
347 const neg = (cnd, p) => { let n = p.negate(); return cnd ? n : p; }; // negate
348 let p = I, f = G; // f must be G, or could become I in the end
349 const windows = 1 + 256 / W; // W=8 17 windows
350 const wsize = 2 ** (W - 1); // W=8 128 window size
351 const mask = BigInt(2 ** W - 1); // W=8 will create mask 0b11111111
352 const maxNum = 2 ** W; // W=8 256
353 const shiftBy = BigInt(W); // W=8 8
354 for (let w = 0; w < windows; w++) {
355 const off = w * wsize;
356 let wbits = Number(n & mask); // extract W bits.
357 n >>= shiftBy; // shift number by W bits.
358 if (wbits > wsize) {
359 wbits -= maxNum;
360 n += 1n;
361 } // split if bits > max: +224 => 256-32
362 const off1 = off, off2 = off + Math.abs(wbits) - 1; // offsets, evaluate both
363 const cnd1 = w % 2 !== 0, cnd2 = wbits < 0; // conditions, evaluate both
364 if (wbits === 0) {
365 f = f.add(neg(cnd1, comp[off1])); // bits are 0: add garbage to fake point
366 }
367 else { // ^ can't add off2, off2 = I
368 p = p.add(neg(cnd2, comp[off2])); // bits are 1: add to result point
369 }
370 }
371 return { p, f }; // return both real and fake points for JIT
372}; // !! you can disable precomputes by commenting-out call of the wNAF() inside Point#mul()
373export { getPublicKey, getPublicKeyAsync, sign, verify, // Remove the export to easily use in REPL
374signAsync, verifyAsync, CURVE, etc, utils, Point as ExtendedPoint }; // envs like browser console