@xtr-dev/rondevu-client 0.21.5 → 0.21.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,5 @@
1
1
  import { RondevuAPI } from '../api/client.js';
2
+ import { WebCryptoAdapter } from '../crypto/web.js';
2
3
  import { BrowserWebRTCAdapter } from '../webrtc/browser.js';
3
4
  import { EventEmitter } from 'eventemitter3';
4
5
  import { OfferPool } from './offer-pool.js';
@@ -11,6 +12,7 @@ export { ICE_SERVER_PRESETS } from './ice-config.js';
11
12
  * Rondevu - Complete WebRTC signaling client with durable connections
12
13
  *
13
14
  * Uses a tags-based discovery system where offers have 1+ tags for matching.
15
+ * Authentication uses Ed25519 public key cryptography - your public key IS your identity.
14
16
  *
15
17
  * @example
16
18
  * ```typescript
@@ -56,13 +58,13 @@ export { ICE_SERVER_PRESETS } from './ice-config.js';
56
58
  * ```
57
59
  */
58
60
  export class Rondevu extends EventEmitter {
59
- constructor(apiUrl, credential, api, iceServers, iceTransportPolicy, webrtcAdapter, cryptoAdapter, debugEnabled = false) {
61
+ constructor(apiUrl, keyPair, api, iceServers, iceTransportPolicy, webrtcAdapter, cryptoAdapter, debugEnabled = false) {
60
62
  super();
61
63
  // Publishing state
62
64
  this.currentTags = null;
63
65
  this.offerPool = null;
64
66
  this.apiUrl = apiUrl;
65
- this.credential = credential;
67
+ this.keyPair = keyPair;
66
68
  this.api = api;
67
69
  this.iceServers = iceServers;
68
70
  this.iceTransportPolicy = iceTransportPolicy;
@@ -82,7 +84,7 @@ export class Rondevu extends EventEmitter {
82
84
  this.emit('poll:ice', data);
83
85
  });
84
86
  this.debug('Instance created:', {
85
- name: this.credential.name,
87
+ publicKey: this.keyPair.publicKey,
86
88
  hasIceServers: iceServers.length > 0,
87
89
  iceTransportPolicy: iceTransportPolicy || 'all',
88
90
  });
@@ -116,53 +118,53 @@ export class Rondevu extends EventEmitter {
116
118
  if (options.debug) {
117
119
  console.log('[Rondevu] Connecting:', {
118
120
  apiUrl,
119
- hasCredential: !!options.credential,
121
+ hasKeyPair: !!options.keyPair,
120
122
  iceServers: iceConfig.iceServers?.length ?? 0,
121
123
  iceTransportPolicy: iceConfig.iceTransportPolicy || 'all',
122
124
  });
123
125
  }
124
- // Generate credential if not provided
125
- let credential = options.credential;
126
- if (!credential) {
126
+ // Ensure crypto adapter is available (default to WebCryptoAdapter for browser)
127
+ const cryptoAdapter = options.cryptoAdapter || new WebCryptoAdapter();
128
+ // Generate keypair if not provided (purely client-side, no server registration)
129
+ let keyPair = options.keyPair;
130
+ if (!keyPair) {
127
131
  if (options.debug)
128
- console.log('[Rondevu] Generating new credentials...');
129
- credential = await RondevuAPI.generateCredentials(apiUrl, {
130
- name: options.username, // Will claim this username if provided
131
- });
132
+ console.log('[Rondevu] Generating new keypair...');
133
+ keyPair = await cryptoAdapter.generateKeyPair();
132
134
  if (options.debug)
133
- console.log('[Rondevu] Generated credentials, name:', credential.name);
135
+ console.log('[Rondevu] Generated keypair, public key:', keyPair.publicKey);
134
136
  }
135
137
  else {
136
138
  if (options.debug)
137
- console.log('[Rondevu] Using existing credential, name:', credential.name);
139
+ console.log('[Rondevu] Using existing keypair, public key:', keyPair.publicKey);
138
140
  }
139
141
  // Create API instance
140
- const api = new RondevuAPI(apiUrl, credential, options.cryptoAdapter);
142
+ const api = new RondevuAPI(apiUrl, keyPair, cryptoAdapter);
141
143
  if (options.debug)
142
144
  console.log('[Rondevu] Created API instance');
143
- return new Rondevu(apiUrl, credential, api, iceConfig.iceServers || [], iceConfig.iceTransportPolicy, webrtcAdapter, options.cryptoAdapter, options.debug || false);
145
+ return new Rondevu(apiUrl, keyPair, api, iceConfig.iceServers || [], iceConfig.iceTransportPolicy, webrtcAdapter, cryptoAdapter, options.debug || false);
144
146
  }
145
147
  // ============================================
146
- // Credential Access
148
+ // Identity Access
147
149
  // ============================================
148
150
  /**
149
- * Get the current credential name
151
+ * Get the current public key (identity)
150
152
  */
151
- getName() {
152
- return this.credential.name;
153
+ getPublicKey() {
154
+ return this.keyPair.publicKey;
153
155
  }
154
156
  /**
155
- * Get the full credential (name + secret)
156
- * Use this to persist credentials for future sessions
157
+ * Get the full keypair (publicKey + privateKey)
158
+ * Use this to persist keypair for future sessions
157
159
  *
158
160
  * ⚠️ SECURITY WARNING:
159
- * - The secret grants full access to this identity
160
- * - Store credentials securely (encrypted storage, never in logs)
161
- * - Never expose credentials in URLs, console output, or error messages
162
- * - Treat the secret like a password or API key
161
+ * - The private key grants full access to this identity
162
+ * - Store keypair securely (encrypted storage, never in logs)
163
+ * - Never expose private key in URLs, console output, or error messages
164
+ * - Treat the private key like a password or API key
163
165
  */
164
- getCredential() {
165
- return { ...this.credential };
166
+ getKeyPair() {
167
+ return { ...this.keyPair };
166
168
  }
167
169
  /**
168
170
  * Get the WebRTC adapter for creating peer connections
@@ -171,6 +173,16 @@ export class Rondevu extends EventEmitter {
171
173
  getWebRTCAdapter() {
172
174
  return this.webrtcAdapter;
173
175
  }
176
+ /**
177
+ * Get the crypto adapter for signing/verification operations
178
+ * Used for meta.json signing and other crypto operations
179
+ */
180
+ getCryptoAdapter() {
181
+ if (!this.cryptoAdapter) {
182
+ throw new Error('Crypto adapter not available');
183
+ }
184
+ return this.cryptoAdapter;
185
+ }
174
186
  // ============================================
175
187
  // Service Publishing
176
188
  // ============================================
@@ -203,7 +215,7 @@ export class Rondevu extends EventEmitter {
203
215
  * ```
204
216
  */
205
217
  async offer(options) {
206
- const { tags, maxOffers, offerFactory, ttl, connectionConfig, autoStart = true } = options;
218
+ const { tags, maxOffers, offerFactory, ttl, connectionConfig, autoStart = true, offerCreationThrottleMs, } = options;
207
219
  this.currentTags = tags;
208
220
  this.connectionConfig = connectionConfig;
209
221
  this.debug(`Creating offers with tags: ${tags.join(', ')} with maxOffers: ${maxOffers}`);
@@ -211,7 +223,7 @@ export class Rondevu extends EventEmitter {
211
223
  this.offerPool = new OfferPool({
212
224
  api: this.api,
213
225
  tags,
214
- ownerUsername: this.credential.name,
226
+ ownerPublicKey: this.keyPair.publicKey,
215
227
  maxOffers,
216
228
  offerFactory: offerFactory || this.defaultOfferFactory.bind(this),
217
229
  ttl: ttl || Rondevu.DEFAULT_TTL_MS,
@@ -220,6 +232,7 @@ export class Rondevu extends EventEmitter {
220
232
  webrtcAdapter: this.webrtcAdapter,
221
233
  connectionConfig,
222
234
  debugEnabled: this.debugEnabled,
235
+ offerCreationThrottleMs,
223
236
  });
224
237
  // Forward events from OfferPool
225
238
  this.offerPool.on('connection:opened', (offerId, connection, matchedTags) => {
@@ -345,14 +358,14 @@ export class Rondevu extends EventEmitter {
345
358
  * // Connect to any peer matching tags
346
359
  * const peer = await rondevu.peer({ tags: ['chat'] })
347
360
  *
348
- * // Connect to specific user
361
+ * // Connect to specific peer by public key
349
362
  * const peer = await rondevu.peer({
350
- * username: 'alice',
363
+ * publicKey: 'abc123...',
351
364
  * tags: ['chat']
352
365
  * })
353
366
  *
354
367
  * peer.on('open', () => {
355
- * console.log('Connected to', peer.peerUsername)
368
+ * console.log('Connected to', peer.peerPublicKey)
356
369
  * peer.send('Hello!')
357
370
  * })
358
371
  *
@@ -424,7 +437,7 @@ export class Rondevu extends EventEmitter {
424
437
  *
425
438
  * // Access offers
426
439
  * for (const offer of result.offers) {
427
- * console.log(offer.username, offer.tags)
440
+ * console.log(offer.publicKey, offer.tags)
428
441
  * }
429
442
  * ```
430
443
  */
@@ -1,9 +1,13 @@
1
1
  /**
2
2
  * Crypto adapter interface for platform-independent cryptographic operations
3
3
  */
4
- export interface Credential {
5
- name: string;
6
- secret: string;
4
+ /**
5
+ * Ed25519 key pair for identity
6
+ * The public key IS the identity (like Ethereum addresses)
7
+ */
8
+ export interface KeyPair {
9
+ publicKey: string;
10
+ privateKey: string;
7
11
  }
8
12
  /**
9
13
  * Platform-independent crypto adapter interface
@@ -11,25 +15,26 @@ export interface Credential {
11
15
  */
12
16
  export interface CryptoAdapter {
13
17
  /**
14
- * Generate HMAC-SHA256 signature for message authentication
15
- * @param secret - The credential secret (hex string)
18
+ * Generate a new Ed25519 key pair locally
19
+ * The public key serves as the identity
20
+ * @returns Key pair with public and private keys as hex strings
21
+ */
22
+ generateKeyPair(): Promise<KeyPair>;
23
+ /**
24
+ * Sign a message using Ed25519
25
+ * @param privateKey - The private key (64-char hex string)
16
26
  * @param message - The message to sign
17
27
  * @returns Base64-encoded signature
18
28
  */
19
- generateSignature(secret: string, message: string): Promise<string>;
29
+ signMessage(privateKey: string, message: string): Promise<string>;
20
30
  /**
21
- * Verify HMAC-SHA256 signature
22
- * @param secret - The credential secret (hex string)
31
+ * Verify an Ed25519 signature
32
+ * @param publicKey - The public key (64-char hex string)
23
33
  * @param message - The message that was signed
24
34
  * @param signature - The signature to verify (base64)
25
35
  * @returns True if signature is valid
26
36
  */
27
- verifySignature(secret: string, message: string, signature: string): Promise<boolean>;
28
- /**
29
- * Generate a random secret (256-bit hex string)
30
- * @returns 64-character hex string
31
- */
32
- generateSecret(): string;
37
+ verifySignature(publicKey: string, message: string, signature: string): Promise<boolean>;
33
38
  /**
34
39
  * Convert hex string to bytes
35
40
  */
@@ -1,10 +1,11 @@
1
1
  /**
2
2
  * Node.js Crypto adapter for Node.js environments
3
+ * Uses @noble/ed25519 for Ed25519 operations
3
4
  * Requires Node.js 19+ or Node.js 18 with --experimental-global-webcrypto flag
4
5
  */
5
- import { CryptoAdapter } from './adapter.js';
6
+ import { CryptoAdapter, KeyPair } from './adapter.js';
6
7
  /**
7
- * Node.js Crypto implementation using Node.js built-in APIs
8
+ * Node.js Crypto implementation using Node.js built-in APIs and @noble/ed25519
8
9
  * Uses Buffer for base64 encoding and crypto.randomBytes for random generation
9
10
  *
10
11
  * Requirements:
@@ -16,32 +17,30 @@ import { CryptoAdapter } from './adapter.js';
16
17
  * import { RondevuAPI } from '@xtr-dev/rondevu-client'
17
18
  * import { NodeCryptoAdapter } from '@xtr-dev/rondevu-client/node'
18
19
  *
20
+ * const crypto = new NodeCryptoAdapter()
21
+ * const keyPair = await crypto.generateKeyPair()
22
+ *
19
23
  * const api = new RondevuAPI(
20
24
  * 'https://signal.example.com',
21
- * 'alice',
22
- * { name: 'alice', secret: '...' },
23
- * new NodeCryptoAdapter()
25
+ * keyPair,
26
+ * crypto
24
27
  * )
25
28
  * ```
26
29
  */
27
30
  export declare class NodeCryptoAdapter implements CryptoAdapter {
28
31
  constructor();
29
32
  /**
30
- * Generate HMAC-SHA256 signature
33
+ * Generate a new Ed25519 key pair locally
31
34
  */
32
- generateSignature(secret: string, message: string): Promise<string>;
35
+ generateKeyPair(): Promise<KeyPair>;
33
36
  /**
34
- * Verify HMAC-SHA256 signature
35
- * Uses constant-time comparison via Web Crypto API to prevent timing attacks
36
- *
37
- * @returns false for invalid signatures, throws for malformed input
38
- * @throws Error if secret/signature format is invalid (not a verification failure)
37
+ * Sign a message using Ed25519
39
38
  */
40
- verifySignature(secret: string, message: string, signature: string): Promise<boolean>;
39
+ signMessage(privateKey: string, message: string): Promise<string>;
41
40
  /**
42
- * Generate a random secret (256-bit hex string)
41
+ * Verify an Ed25519 signature
43
42
  */
44
- generateSecret(): string;
43
+ verifySignature(publicKey: string, message: string, signature: string): Promise<boolean>;
45
44
  /**
46
45
  * Convert hex string to bytes
47
46
  * @throws Error if hex string is invalid
@@ -1,9 +1,16 @@
1
1
  /**
2
2
  * Node.js Crypto adapter for Node.js environments
3
+ * Uses @noble/ed25519 for Ed25519 operations
3
4
  * Requires Node.js 19+ or Node.js 18 with --experimental-global-webcrypto flag
4
5
  */
6
+ import * as ed from '@noble/ed25519';
7
+ // Configure @noble/ed25519 to use Web Crypto API's SHA-512
8
+ ed.hashes.sha512Async = async (message) => {
9
+ const hashBuffer = await crypto.subtle.digest('SHA-512', message);
10
+ return new Uint8Array(hashBuffer);
11
+ };
5
12
  /**
6
- * Node.js Crypto implementation using Node.js built-in APIs
13
+ * Node.js Crypto implementation using Node.js built-in APIs and @noble/ed25519
7
14
  * Uses Buffer for base64 encoding and crypto.randomBytes for random generation
8
15
  *
9
16
  * Requirements:
@@ -15,11 +22,13 @@
15
22
  * import { RondevuAPI } from '@xtr-dev/rondevu-client'
16
23
  * import { NodeCryptoAdapter } from '@xtr-dev/rondevu-client/node'
17
24
  *
25
+ * const crypto = new NodeCryptoAdapter()
26
+ * const keyPair = await crypto.generateKeyPair()
27
+ *
18
28
  * const api = new RondevuAPI(
19
29
  * 'https://signal.example.com',
20
- * 'alice',
21
- * { name: 'alice', secret: '...' },
22
- * new NodeCryptoAdapter()
30
+ * keyPair,
31
+ * crypto
23
32
  * )
24
33
  * ```
25
34
  */
@@ -31,75 +40,54 @@ export class NodeCryptoAdapter {
31
40
  }
32
41
  }
33
42
  /**
34
- * Generate HMAC-SHA256 signature
43
+ * Generate a new Ed25519 key pair locally
44
+ */
45
+ async generateKeyPair() {
46
+ // Generate 32 random bytes for private key
47
+ const privateKeyBytes = this.randomBytes(32);
48
+ const privateKey = this.bytesToHex(privateKeyBytes);
49
+ // Derive public key from private key
50
+ const publicKeyBytes = await ed.getPublicKeyAsync(privateKeyBytes);
51
+ const publicKey = this.bytesToHex(publicKeyBytes);
52
+ return { publicKey, privateKey };
53
+ }
54
+ /**
55
+ * Sign a message using Ed25519
35
56
  */
36
- async generateSignature(secret, message) {
37
- if (!secret || typeof secret !== 'string') {
38
- throw new Error('Invalid secret: must be a non-empty string');
57
+ async signMessage(privateKey, message) {
58
+ if (!privateKey || typeof privateKey !== 'string') {
59
+ throw new Error('Invalid private key: must be a non-empty string');
39
60
  }
40
61
  if (typeof message !== 'string') {
41
62
  throw new Error('Invalid message: must be a string');
42
63
  }
43
- const secretBytes = this.hexToBytes(secret);
44
- // Import secret as HMAC key
45
- const key = await crypto.subtle.importKey('raw', secretBytes, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
64
+ const privateKeyBytes = this.hexToBytes(privateKey);
46
65
  // Convert message to bytes
47
66
  const encoder = new TextEncoder();
48
67
  const messageBytes = encoder.encode(message);
49
- // Generate HMAC signature
50
- const signatureBytes = await crypto.subtle.sign('HMAC', key, messageBytes);
68
+ // Sign using Ed25519
69
+ const signatureBytes = await ed.signAsync(messageBytes, privateKeyBytes);
51
70
  // Convert to base64
52
- return this.bytesToBase64(new Uint8Array(signatureBytes));
71
+ return this.bytesToBase64(signatureBytes);
53
72
  }
54
73
  /**
55
- * Verify HMAC-SHA256 signature
56
- * Uses constant-time comparison via Web Crypto API to prevent timing attacks
57
- *
58
- * @returns false for invalid signatures, throws for malformed input
59
- * @throws Error if secret/signature format is invalid (not a verification failure)
74
+ * Verify an Ed25519 signature
60
75
  */
61
- async verifySignature(secret, message, signature) {
62
- // Validate inputs first
63
- // Use generic error messages to prevent timing attacks and information leakage
64
- let secretBytes;
65
- let signatureBytes;
66
- try {
67
- secretBytes = this.hexToBytes(secret);
68
- }
69
- catch (error) {
70
- // Generic error message - don't leak format details
71
- throw new Error('Invalid credential format');
72
- }
76
+ async verifySignature(publicKey, message, signature) {
73
77
  try {
74
- signatureBytes = this.base64ToBytes(signature);
75
- }
76
- catch (error) {
77
- // Generic error message - don't leak format details
78
- throw new Error('Invalid signature format');
79
- }
80
- // Perform HMAC verification
81
- try {
82
- // Import secret as HMAC key for verification
83
- const key = await crypto.subtle.importKey('raw', secretBytes, { name: 'HMAC', hash: 'SHA-256' }, false, ['verify']);
78
+ const publicKeyBytes = this.hexToBytes(publicKey);
79
+ const signatureBytes = this.base64ToBytes(signature);
84
80
  // Convert message to bytes
85
81
  const encoder = new TextEncoder();
86
82
  const messageBytes = encoder.encode(message);
87
- // Use Web Crypto API's verify() for constant-time comparison
88
- // Returns false for signature mismatch (auth failure)
89
- return await crypto.subtle.verify('HMAC', key, signatureBytes, messageBytes);
83
+ // Verify using Ed25519
84
+ return await ed.verifyAsync(signatureBytes, messageBytes, publicKeyBytes);
90
85
  }
91
86
  catch (error) {
92
- // System/crypto errors - unexpected failures
93
- throw new Error(`Signature verification error: ${error instanceof Error ? error.message : String(error)}`);
87
+ console.error('Signature verification error:', error);
88
+ return false;
94
89
  }
95
90
  }
96
- /**
97
- * Generate a random secret (256-bit hex string)
98
- */
99
- generateSecret() {
100
- const bytes = this.randomBytes(32); // 32 bytes = 256 bits
101
- return this.bytesToHex(bytes);
102
- }
103
91
  /**
104
92
  * Convert hex string to bytes
105
93
  * @throws Error if hex string is invalid
@@ -1,28 +1,25 @@
1
1
  /**
2
2
  * Web Crypto adapter for browser environments
3
+ * Uses @noble/ed25519 for Ed25519 operations
3
4
  */
4
- import { CryptoAdapter } from './adapter.js';
5
+ import { CryptoAdapter, KeyPair } from './adapter.js';
5
6
  /**
6
- * Web Crypto implementation using browser APIs
7
+ * Web Crypto implementation using browser APIs and @noble/ed25519
7
8
  * Uses btoa/atob for base64 encoding and crypto.getRandomValues for random bytes
8
9
  */
9
10
  export declare class WebCryptoAdapter implements CryptoAdapter {
10
11
  /**
11
- * Generate HMAC-SHA256 signature
12
+ * Generate a new Ed25519 key pair locally
12
13
  */
13
- generateSignature(secret: string, message: string): Promise<string>;
14
+ generateKeyPair(): Promise<KeyPair>;
14
15
  /**
15
- * Verify HMAC-SHA256 signature
16
- * Uses constant-time comparison via Web Crypto API to prevent timing attacks
17
- *
18
- * @returns false for invalid signatures, throws for malformed input
19
- * @throws Error if secret/signature format is invalid (not a verification failure)
16
+ * Sign a message using Ed25519
20
17
  */
21
- verifySignature(secret: string, message: string, signature: string): Promise<boolean>;
18
+ signMessage(privateKey: string, message: string): Promise<string>;
22
19
  /**
23
- * Generate a random secret (256-bit hex string)
20
+ * Verify an Ed25519 signature
24
21
  */
25
- generateSecret(): string;
22
+ verifySignature(publicKey: string, message: string, signature: string): Promise<boolean>;
26
23
  /**
27
24
  * Convert hex string to bytes
28
25
  * @throws Error if hex string is invalid
@@ -1,81 +1,67 @@
1
1
  /**
2
2
  * Web Crypto adapter for browser environments
3
+ * Uses @noble/ed25519 for Ed25519 operations
3
4
  */
5
+ import * as ed from '@noble/ed25519';
6
+ // Configure @noble/ed25519 to use Web Crypto API's SHA-512
7
+ ed.hashes.sha512Async = async (message) => {
8
+ const hashBuffer = await crypto.subtle.digest('SHA-512', message);
9
+ return new Uint8Array(hashBuffer);
10
+ };
4
11
  /**
5
- * Web Crypto implementation using browser APIs
12
+ * Web Crypto implementation using browser APIs and @noble/ed25519
6
13
  * Uses btoa/atob for base64 encoding and crypto.getRandomValues for random bytes
7
14
  */
8
15
  export class WebCryptoAdapter {
9
16
  /**
10
- * Generate HMAC-SHA256 signature
17
+ * Generate a new Ed25519 key pair locally
11
18
  */
12
- async generateSignature(secret, message) {
13
- if (!secret || typeof secret !== 'string') {
14
- throw new Error('Invalid secret: must be a non-empty string');
19
+ async generateKeyPair() {
20
+ // Generate 32 random bytes for private key
21
+ const privateKeyBytes = this.randomBytes(32);
22
+ const privateKey = this.bytesToHex(privateKeyBytes);
23
+ // Derive public key from private key
24
+ const publicKeyBytes = await ed.getPublicKeyAsync(privateKeyBytes);
25
+ const publicKey = this.bytesToHex(publicKeyBytes);
26
+ return { publicKey, privateKey };
27
+ }
28
+ /**
29
+ * Sign a message using Ed25519
30
+ */
31
+ async signMessage(privateKey, message) {
32
+ if (!privateKey || typeof privateKey !== 'string') {
33
+ throw new Error('Invalid private key: must be a non-empty string');
15
34
  }
16
35
  if (typeof message !== 'string') {
17
36
  throw new Error('Invalid message: must be a string');
18
37
  }
19
- const secretBytes = this.hexToBytes(secret);
20
- // Import secret as HMAC key
21
- const key = await crypto.subtle.importKey('raw', secretBytes, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
38
+ const privateKeyBytes = this.hexToBytes(privateKey);
22
39
  // Convert message to bytes
23
40
  const encoder = new TextEncoder();
24
41
  const messageBytes = encoder.encode(message);
25
- // Generate HMAC signature
26
- const signatureBytes = await crypto.subtle.sign('HMAC', key, messageBytes);
42
+ // Sign using Ed25519
43
+ const signatureBytes = await ed.signAsync(messageBytes, privateKeyBytes);
27
44
  // Convert to base64
28
- return this.bytesToBase64(new Uint8Array(signatureBytes));
45
+ return this.bytesToBase64(signatureBytes);
29
46
  }
30
47
  /**
31
- * Verify HMAC-SHA256 signature
32
- * Uses constant-time comparison via Web Crypto API to prevent timing attacks
33
- *
34
- * @returns false for invalid signatures, throws for malformed input
35
- * @throws Error if secret/signature format is invalid (not a verification failure)
48
+ * Verify an Ed25519 signature
36
49
  */
37
- async verifySignature(secret, message, signature) {
38
- // Validate inputs first
39
- // Use generic error messages to prevent timing attacks and information leakage
40
- let secretBytes;
41
- let signatureBytes;
42
- try {
43
- secretBytes = this.hexToBytes(secret);
44
- }
45
- catch (error) {
46
- // Generic error message - don't leak format details
47
- throw new Error('Invalid credential format');
48
- }
50
+ async verifySignature(publicKey, message, signature) {
49
51
  try {
50
- signatureBytes = this.base64ToBytes(signature);
51
- }
52
- catch (error) {
53
- // Generic error message - don't leak format details
54
- throw new Error('Invalid signature format');
55
- }
56
- // Perform HMAC verification
57
- try {
58
- // Import secret as HMAC key for verification
59
- const key = await crypto.subtle.importKey('raw', secretBytes, { name: 'HMAC', hash: 'SHA-256' }, false, ['verify']);
52
+ const publicKeyBytes = this.hexToBytes(publicKey);
53
+ const signatureBytes = this.base64ToBytes(signature);
60
54
  // Convert message to bytes
61
55
  const encoder = new TextEncoder();
62
56
  const messageBytes = encoder.encode(message);
63
- // Use Web Crypto API's verify() for constant-time comparison
64
- // Returns false for signature mismatch (auth failure)
65
- return await crypto.subtle.verify('HMAC', key, signatureBytes, messageBytes);
57
+ // Verify using Ed25519
58
+ return await ed.verifyAsync(signatureBytes, messageBytes, publicKeyBytes);
66
59
  }
67
60
  catch (error) {
68
- // System/crypto errors - unexpected failures
69
- throw new Error(`Signature verification error: ${error instanceof Error ? error.message : String(error)}`);
61
+ console.error('Signature verification error:', error);
62
+ return false;
70
63
  }
71
64
  }
72
- /**
73
- * Generate a random secret (256-bit hex string)
74
- */
75
- generateSecret() {
76
- const bytes = this.randomBytes(32); // 32 bytes = 256 bits
77
- return this.bytesToHex(bytes);
78
- }
79
65
  /**
80
66
  * Convert hex string to bytes
81
67
  * @throws Error if hex string is invalid