@xtr-dev/rondevu-client 0.21.3 → 0.21.6
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 +27 -40
- package/dist/api/client.js +39 -137
- package/dist/connections/answerer.d.ts +7 -5
- package/dist/connections/answerer.js +17 -16
- 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/offer-pool.d.ts +15 -4
- package/dist/core/offer-pool.js +30 -5
- package/dist/core/peer.d.ts +9 -9
- package/dist/core/peer.js +17 -16
- package/dist/core/polling-manager.d.ts +2 -1
- package/dist/core/polling-manager.js +2 -1
- package/dist/core/rondevu-types.d.ts +7 -9
- package/dist/core/rondevu.d.ts +26 -18
- package/dist/core/rondevu.js +44 -34
- 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/package.json +2 -1
package/dist/core/rondevu.js
CHANGED
|
@@ -11,6 +11,7 @@ export { ICE_SERVER_PRESETS } from './ice-config.js';
|
|
|
11
11
|
* Rondevu - Complete WebRTC signaling client with durable connections
|
|
12
12
|
*
|
|
13
13
|
* Uses a tags-based discovery system where offers have 1+ tags for matching.
|
|
14
|
+
* Authentication uses Ed25519 public key cryptography - your public key IS your identity.
|
|
14
15
|
*
|
|
15
16
|
* @example
|
|
16
17
|
* ```typescript
|
|
@@ -56,13 +57,13 @@ export { ICE_SERVER_PRESETS } from './ice-config.js';
|
|
|
56
57
|
* ```
|
|
57
58
|
*/
|
|
58
59
|
export class Rondevu extends EventEmitter {
|
|
59
|
-
constructor(apiUrl,
|
|
60
|
+
constructor(apiUrl, keyPair, api, iceServers, iceTransportPolicy, webrtcAdapter, cryptoAdapter, debugEnabled = false) {
|
|
60
61
|
super();
|
|
61
62
|
// Publishing state
|
|
62
63
|
this.currentTags = null;
|
|
63
64
|
this.offerPool = null;
|
|
64
65
|
this.apiUrl = apiUrl;
|
|
65
|
-
this.
|
|
66
|
+
this.keyPair = keyPair;
|
|
66
67
|
this.api = api;
|
|
67
68
|
this.iceServers = iceServers;
|
|
68
69
|
this.iceTransportPolicy = iceTransportPolicy;
|
|
@@ -82,7 +83,7 @@ export class Rondevu extends EventEmitter {
|
|
|
82
83
|
this.emit('poll:ice', data);
|
|
83
84
|
});
|
|
84
85
|
this.debug('Instance created:', {
|
|
85
|
-
|
|
86
|
+
publicKey: this.keyPair.publicKey,
|
|
86
87
|
hasIceServers: iceServers.length > 0,
|
|
87
88
|
iceTransportPolicy: iceTransportPolicy || 'all',
|
|
88
89
|
});
|
|
@@ -116,53 +117,51 @@ export class Rondevu extends EventEmitter {
|
|
|
116
117
|
if (options.debug) {
|
|
117
118
|
console.log('[Rondevu] Connecting:', {
|
|
118
119
|
apiUrl,
|
|
119
|
-
|
|
120
|
+
hasKeyPair: !!options.keyPair,
|
|
120
121
|
iceServers: iceConfig.iceServers?.length ?? 0,
|
|
121
122
|
iceTransportPolicy: iceConfig.iceTransportPolicy || 'all',
|
|
122
123
|
});
|
|
123
124
|
}
|
|
124
|
-
// Generate
|
|
125
|
-
let
|
|
126
|
-
if (!
|
|
125
|
+
// Generate keypair if not provided (purely client-side, no server registration)
|
|
126
|
+
let keyPair = options.keyPair;
|
|
127
|
+
if (!keyPair) {
|
|
127
128
|
if (options.debug)
|
|
128
|
-
console.log('[Rondevu] Generating new
|
|
129
|
-
|
|
130
|
-
name: options.username, // Will claim this username if provided
|
|
131
|
-
});
|
|
129
|
+
console.log('[Rondevu] Generating new keypair...');
|
|
130
|
+
keyPair = await RondevuAPI.generateKeyPair(options.cryptoAdapter);
|
|
132
131
|
if (options.debug)
|
|
133
|
-
console.log('[Rondevu] Generated
|
|
132
|
+
console.log('[Rondevu] Generated keypair, public key:', keyPair.publicKey);
|
|
134
133
|
}
|
|
135
134
|
else {
|
|
136
135
|
if (options.debug)
|
|
137
|
-
console.log('[Rondevu] Using existing
|
|
136
|
+
console.log('[Rondevu] Using existing keypair, public key:', keyPair.publicKey);
|
|
138
137
|
}
|
|
139
138
|
// Create API instance
|
|
140
|
-
const api = new RondevuAPI(apiUrl,
|
|
139
|
+
const api = new RondevuAPI(apiUrl, keyPair, options.cryptoAdapter);
|
|
141
140
|
if (options.debug)
|
|
142
141
|
console.log('[Rondevu] Created API instance');
|
|
143
|
-
return new Rondevu(apiUrl,
|
|
142
|
+
return new Rondevu(apiUrl, keyPair, api, iceConfig.iceServers || [], iceConfig.iceTransportPolicy, webrtcAdapter, options.cryptoAdapter, options.debug || false);
|
|
144
143
|
}
|
|
145
144
|
// ============================================
|
|
146
|
-
//
|
|
145
|
+
// Identity Access
|
|
147
146
|
// ============================================
|
|
148
147
|
/**
|
|
149
|
-
* Get the current
|
|
148
|
+
* Get the current public key (identity)
|
|
150
149
|
*/
|
|
151
|
-
|
|
152
|
-
return this.
|
|
150
|
+
getPublicKey() {
|
|
151
|
+
return this.keyPair.publicKey;
|
|
153
152
|
}
|
|
154
153
|
/**
|
|
155
|
-
* Get the full
|
|
156
|
-
* Use this to persist
|
|
154
|
+
* Get the full keypair (publicKey + privateKey)
|
|
155
|
+
* Use this to persist keypair for future sessions
|
|
157
156
|
*
|
|
158
157
|
* ⚠️ SECURITY WARNING:
|
|
159
|
-
* - The
|
|
160
|
-
* - Store
|
|
161
|
-
* - Never expose
|
|
162
|
-
* - Treat the
|
|
158
|
+
* - The private key grants full access to this identity
|
|
159
|
+
* - Store keypair securely (encrypted storage, never in logs)
|
|
160
|
+
* - Never expose private key in URLs, console output, or error messages
|
|
161
|
+
* - Treat the private key like a password or API key
|
|
163
162
|
*/
|
|
164
|
-
|
|
165
|
-
return { ...this.
|
|
163
|
+
getKeyPair() {
|
|
164
|
+
return { ...this.keyPair };
|
|
166
165
|
}
|
|
167
166
|
/**
|
|
168
167
|
* Get the WebRTC adapter for creating peer connections
|
|
@@ -211,7 +210,7 @@ export class Rondevu extends EventEmitter {
|
|
|
211
210
|
this.offerPool = new OfferPool({
|
|
212
211
|
api: this.api,
|
|
213
212
|
tags,
|
|
214
|
-
|
|
213
|
+
ownerPublicKey: this.keyPair.publicKey,
|
|
215
214
|
maxOffers,
|
|
216
215
|
offerFactory: offerFactory || this.defaultOfferFactory.bind(this),
|
|
217
216
|
ttl: ttl || Rondevu.DEFAULT_TTL_MS,
|
|
@@ -222,8 +221,8 @@ export class Rondevu extends EventEmitter {
|
|
|
222
221
|
debugEnabled: this.debugEnabled,
|
|
223
222
|
});
|
|
224
223
|
// Forward events from OfferPool
|
|
225
|
-
this.offerPool.on('connection:opened', (offerId, connection) => {
|
|
226
|
-
this.emit('connection:opened', offerId, connection);
|
|
224
|
+
this.offerPool.on('connection:opened', (offerId, connection, matchedTags) => {
|
|
225
|
+
this.emit('connection:opened', offerId, connection, matchedTags);
|
|
227
226
|
});
|
|
228
227
|
this.offerPool.on('offer:created', (offerId, tags) => {
|
|
229
228
|
this.emit('offer:created', offerId, tags);
|
|
@@ -314,6 +313,17 @@ export class Rondevu extends EventEmitter {
|
|
|
314
313
|
this.debug('Disconnecting all offers');
|
|
315
314
|
this.offerPool?.disconnectAll();
|
|
316
315
|
}
|
|
316
|
+
/**
|
|
317
|
+
* Update tags for new offers
|
|
318
|
+
* Existing offers keep their old tags until they expire/rotate
|
|
319
|
+
* New offers created during fill will use the updated tags
|
|
320
|
+
* @param newTags - The new tags to use for future offers
|
|
321
|
+
*/
|
|
322
|
+
updateOfferTags(newTags) {
|
|
323
|
+
this.debug(`Updating offer tags: ${newTags.join(', ')}`);
|
|
324
|
+
this.currentTags = newTags;
|
|
325
|
+
this.offerPool?.updateTags(newTags);
|
|
326
|
+
}
|
|
317
327
|
/**
|
|
318
328
|
* Get the current publishing status
|
|
319
329
|
* @returns Object with publishing state information
|
|
@@ -334,14 +344,14 @@ export class Rondevu extends EventEmitter {
|
|
|
334
344
|
* // Connect to any peer matching tags
|
|
335
345
|
* const peer = await rondevu.peer({ tags: ['chat'] })
|
|
336
346
|
*
|
|
337
|
-
* // Connect to specific
|
|
347
|
+
* // Connect to specific peer by public key
|
|
338
348
|
* const peer = await rondevu.peer({
|
|
339
|
-
*
|
|
349
|
+
* publicKey: 'abc123...',
|
|
340
350
|
* tags: ['chat']
|
|
341
351
|
* })
|
|
342
352
|
*
|
|
343
353
|
* peer.on('open', () => {
|
|
344
|
-
* console.log('Connected to', peer.
|
|
354
|
+
* console.log('Connected to', peer.peerPublicKey)
|
|
345
355
|
* peer.send('Hello!')
|
|
346
356
|
* })
|
|
347
357
|
*
|
|
@@ -413,7 +423,7 @@ export class Rondevu extends EventEmitter {
|
|
|
413
423
|
*
|
|
414
424
|
* // Access offers
|
|
415
425
|
* for (const offer of result.offers) {
|
|
416
|
-
* console.log(offer.
|
|
426
|
+
* console.log(offer.publicKey, offer.tags)
|
|
417
427
|
* }
|
|
418
428
|
* ```
|
|
419
429
|
*/
|
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
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xtr-dev/rondevu-client",
|
|
3
|
-
"version": "0.21.
|
|
3
|
+
"version": "0.21.6",
|
|
4
4
|
"description": "TypeScript client for Rondevu with durable WebRTC connections, automatic reconnection, and message queuing",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/core/index.js",
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
"README.md"
|
|
48
48
|
],
|
|
49
49
|
"dependencies": {
|
|
50
|
+
"@noble/ed25519": "^3.0.0",
|
|
50
51
|
"eventemitter3": "^5.0.1"
|
|
51
52
|
},
|
|
52
53
|
"lint-staged": {
|