@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.
- package/README.md +21 -25
- package/dist/api/client.d.ts +21 -39
- package/dist/api/client.js +34 -135
- package/dist/connections/answerer.d.ts +5 -5
- package/dist/connections/answerer.js +14 -14
- package/dist/connections/base.d.ts +2 -2
- package/dist/connections/offerer.d.ts +8 -8
- package/dist/connections/offerer.js +15 -15
- package/dist/core/index.d.ts +1 -0
- package/dist/core/offer-pool.d.ts +9 -2
- package/dist/core/offer-pool.js +8 -3
- package/dist/core/peer.d.ts +9 -9
- package/dist/core/peer.js +16 -16
- package/dist/core/polling-manager.d.ts +1 -1
- package/dist/core/polling-manager.js +1 -1
- package/dist/core/rondevu-types.d.ts +9 -9
- package/dist/core/rondevu.d.ts +25 -18
- package/dist/core/rondevu.js +46 -33
- package/dist/crypto/adapter.d.ts +19 -14
- package/dist/crypto/node.d.ts +14 -15
- package/dist/crypto/node.js +41 -53
- package/dist/crypto/web.d.ts +9 -12
- package/dist/crypto/web.js +36 -50
- package/dist/meta.d.ts +221 -0
- package/dist/meta.js +213 -0
- package/package.json +2 -1
package/dist/core/rondevu.js
CHANGED
|
@@ -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,
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
121
|
+
hasKeyPair: !!options.keyPair,
|
|
120
122
|
iceServers: iceConfig.iceServers?.length ?? 0,
|
|
121
123
|
iceTransportPolicy: iceConfig.iceTransportPolicy || 'all',
|
|
122
124
|
});
|
|
123
125
|
}
|
|
124
|
-
//
|
|
125
|
-
|
|
126
|
-
if (
|
|
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
|
|
129
|
-
|
|
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
|
|
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
|
|
139
|
+
console.log('[Rondevu] Using existing keypair, public key:', keyPair.publicKey);
|
|
138
140
|
}
|
|
139
141
|
// Create API instance
|
|
140
|
-
const api = new RondevuAPI(apiUrl,
|
|
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,
|
|
145
|
+
return new Rondevu(apiUrl, keyPair, api, iceConfig.iceServers || [], iceConfig.iceTransportPolicy, webrtcAdapter, cryptoAdapter, options.debug || false);
|
|
144
146
|
}
|
|
145
147
|
// ============================================
|
|
146
|
-
//
|
|
148
|
+
// Identity Access
|
|
147
149
|
// ============================================
|
|
148
150
|
/**
|
|
149
|
-
* Get the current
|
|
151
|
+
* Get the current public key (identity)
|
|
150
152
|
*/
|
|
151
|
-
|
|
152
|
-
return this.
|
|
153
|
+
getPublicKey() {
|
|
154
|
+
return this.keyPair.publicKey;
|
|
153
155
|
}
|
|
154
156
|
/**
|
|
155
|
-
* Get the full
|
|
156
|
-
* Use this to persist
|
|
157
|
+
* Get the full keypair (publicKey + privateKey)
|
|
158
|
+
* Use this to persist keypair for future sessions
|
|
157
159
|
*
|
|
158
160
|
* ⚠️ SECURITY WARNING:
|
|
159
|
-
* - The
|
|
160
|
-
* - Store
|
|
161
|
-
* - Never expose
|
|
162
|
-
* - Treat the
|
|
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
|
-
|
|
165
|
-
return { ...this.
|
|
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
|
-
|
|
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
|
|
361
|
+
* // Connect to specific peer by public key
|
|
349
362
|
* const peer = await rondevu.peer({
|
|
350
|
-
*
|
|
363
|
+
* publicKey: 'abc123...',
|
|
351
364
|
* tags: ['chat']
|
|
352
365
|
* })
|
|
353
366
|
*
|
|
354
367
|
* peer.on('open', () => {
|
|
355
|
-
* console.log('Connected to', peer.
|
|
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.
|
|
440
|
+
* console.log(offer.publicKey, offer.tags)
|
|
428
441
|
* }
|
|
429
442
|
* ```
|
|
430
443
|
*/
|
package/dist/crypto/adapter.d.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Crypto adapter interface for platform-independent cryptographic operations
|
|
3
3
|
*/
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
|
15
|
-
*
|
|
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
|
-
|
|
29
|
+
signMessage(privateKey: string, message: string): Promise<string>;
|
|
20
30
|
/**
|
|
21
|
-
* Verify
|
|
22
|
-
* @param
|
|
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(
|
|
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
|
*/
|
package/dist/crypto/node.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
22
|
-
*
|
|
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
|
|
33
|
+
* Generate a new Ed25519 key pair locally
|
|
31
34
|
*/
|
|
32
|
-
|
|
35
|
+
generateKeyPair(): Promise<KeyPair>;
|
|
33
36
|
/**
|
|
34
|
-
*
|
|
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
|
-
|
|
39
|
+
signMessage(privateKey: string, message: string): Promise<string>;
|
|
41
40
|
/**
|
|
42
|
-
*
|
|
41
|
+
* Verify an Ed25519 signature
|
|
43
42
|
*/
|
|
44
|
-
|
|
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
|
package/dist/crypto/node.js
CHANGED
|
@@ -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
|
-
*
|
|
21
|
-
*
|
|
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
|
|
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
|
|
37
|
-
if (!
|
|
38
|
-
throw new Error('Invalid
|
|
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
|
|
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
|
-
//
|
|
50
|
-
const signatureBytes = await
|
|
68
|
+
// Sign using Ed25519
|
|
69
|
+
const signatureBytes = await ed.signAsync(messageBytes, privateKeyBytes);
|
|
51
70
|
// Convert to base64
|
|
52
|
-
return this.bytesToBase64(
|
|
71
|
+
return this.bytesToBase64(signatureBytes);
|
|
53
72
|
}
|
|
54
73
|
/**
|
|
55
|
-
* Verify
|
|
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(
|
|
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
|
-
|
|
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
|
-
//
|
|
88
|
-
|
|
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
|
-
|
|
93
|
-
|
|
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
|
package/dist/crypto/web.d.ts
CHANGED
|
@@ -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
|
|
12
|
+
* Generate a new Ed25519 key pair locally
|
|
12
13
|
*/
|
|
13
|
-
|
|
14
|
+
generateKeyPair(): Promise<KeyPair>;
|
|
14
15
|
/**
|
|
15
|
-
*
|
|
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
|
-
|
|
18
|
+
signMessage(privateKey: string, message: string): Promise<string>;
|
|
22
19
|
/**
|
|
23
|
-
*
|
|
20
|
+
* Verify an Ed25519 signature
|
|
24
21
|
*/
|
|
25
|
-
|
|
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
|
package/dist/crypto/web.js
CHANGED
|
@@ -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
|
|
17
|
+
* Generate a new Ed25519 key pair locally
|
|
11
18
|
*/
|
|
12
|
-
async
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
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
|
-
//
|
|
26
|
-
const signatureBytes = await
|
|
42
|
+
// Sign using Ed25519
|
|
43
|
+
const signatureBytes = await ed.signAsync(messageBytes, privateKeyBytes);
|
|
27
44
|
// Convert to base64
|
|
28
|
-
return this.bytesToBase64(
|
|
45
|
+
return this.bytesToBase64(signatureBytes);
|
|
29
46
|
}
|
|
30
47
|
/**
|
|
31
|
-
* Verify
|
|
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(
|
|
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
|
-
|
|
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
|
-
//
|
|
64
|
-
|
|
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
|
-
|
|
69
|
-
|
|
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
|