bitchat-node 0.1.0
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 +223 -0
- package/dist/bin/bitchat.d.ts +7 -0
- package/dist/bin/bitchat.d.ts.map +1 -0
- package/dist/bin/bitchat.js +69 -0
- package/dist/bin/bitchat.js.map +1 -0
- package/dist/client.d.ts +77 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +411 -0
- package/dist/client.js.map +1 -0
- package/dist/crypto/index.d.ts +6 -0
- package/dist/crypto/index.d.ts.map +1 -0
- package/dist/crypto/index.js +6 -0
- package/dist/crypto/index.js.map +1 -0
- package/dist/crypto/noise.d.ts +72 -0
- package/dist/crypto/noise.d.ts.map +1 -0
- package/dist/crypto/noise.js +470 -0
- package/dist/crypto/noise.js.map +1 -0
- package/dist/crypto/signing.d.ts +34 -0
- package/dist/crypto/signing.d.ts.map +1 -0
- package/dist/crypto/signing.js +56 -0
- package/dist/crypto/signing.js.map +1 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +48 -0
- package/dist/index.js.map +1 -0
- package/dist/mesh/deduplicator.d.ts +48 -0
- package/dist/mesh/deduplicator.d.ts.map +1 -0
- package/dist/mesh/deduplicator.js +107 -0
- package/dist/mesh/deduplicator.js.map +1 -0
- package/dist/mesh/index.d.ts +6 -0
- package/dist/mesh/index.d.ts.map +1 -0
- package/dist/mesh/index.js +6 -0
- package/dist/mesh/index.js.map +1 -0
- package/dist/mesh/router.d.ts +90 -0
- package/dist/mesh/router.d.ts.map +1 -0
- package/dist/mesh/router.js +204 -0
- package/dist/mesh/router.js.map +1 -0
- package/dist/protocol/binary.d.ts +37 -0
- package/dist/protocol/binary.d.ts.map +1 -0
- package/dist/protocol/binary.js +310 -0
- package/dist/protocol/binary.js.map +1 -0
- package/dist/protocol/constants.d.ts +30 -0
- package/dist/protocol/constants.d.ts.map +1 -0
- package/dist/protocol/constants.js +37 -0
- package/dist/protocol/constants.js.map +1 -0
- package/dist/protocol/index.d.ts +8 -0
- package/dist/protocol/index.d.ts.map +1 -0
- package/dist/protocol/index.js +8 -0
- package/dist/protocol/index.js.map +1 -0
- package/dist/protocol/packets.d.ts +38 -0
- package/dist/protocol/packets.d.ts.map +1 -0
- package/dist/protocol/packets.js +177 -0
- package/dist/protocol/packets.js.map +1 -0
- package/dist/protocol/types.d.ts +134 -0
- package/dist/protocol/types.d.ts.map +1 -0
- package/dist/protocol/types.js +108 -0
- package/dist/protocol/types.js.map +1 -0
- package/dist/session/index.d.ts +5 -0
- package/dist/session/index.d.ts.map +1 -0
- package/dist/session/index.js +5 -0
- package/dist/session/index.js.map +1 -0
- package/dist/session/manager.d.ts +113 -0
- package/dist/session/manager.d.ts.map +1 -0
- package/dist/session/manager.js +371 -0
- package/dist/session/manager.js.map +1 -0
- package/dist/transport/ble.d.ts +92 -0
- package/dist/transport/ble.d.ts.map +1 -0
- package/dist/transport/ble.js +434 -0
- package/dist/transport/ble.js.map +1 -0
- package/dist/transport/index.d.ts +5 -0
- package/dist/transport/index.d.ts.map +1 -0
- package/dist/transport/index.js +5 -0
- package/dist/transport/index.js.map +1 -0
- package/dist/ui/index.d.ts +2 -0
- package/dist/ui/index.d.ts.map +1 -0
- package/dist/ui/index.js +2 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/server.d.ts +16 -0
- package/dist/ui/server.d.ts.map +1 -0
- package/dist/ui/server.js +510 -0
- package/dist/ui/server.js.map +1 -0
- package/package.json +79 -0
- package/src/bin/bitchat.ts +87 -0
- package/src/client.ts +519 -0
- package/src/crypto/index.ts +22 -0
- package/src/crypto/noise.ts +574 -0
- package/src/crypto/signing.ts +66 -0
- package/src/index.ts +95 -0
- package/src/mesh/deduplicator.ts +129 -0
- package/src/mesh/index.ts +6 -0
- package/src/mesh/router.ts +258 -0
- package/src/protocol/binary.ts +345 -0
- package/src/protocol/constants.ts +43 -0
- package/src/protocol/index.ts +15 -0
- package/src/protocol/packets.ts +223 -0
- package/src/protocol/types.ts +182 -0
- package/src/session/index.ts +9 -0
- package/src/session/manager.ts +476 -0
- package/src/transport/ble.ts +553 -0
- package/src/transport/index.ts +10 -0
- package/src/ui/index.ts +1 -0
- package/src/ui/server.ts +569 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Bitchat Node CLI
|
|
4
|
+
* Run a Bitchat mesh node with web UI
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { BitchatClient } from '../client.js';
|
|
8
|
+
import { startUIServer } from '../ui/server.js';
|
|
9
|
+
|
|
10
|
+
const args = process.argv.slice(2);
|
|
11
|
+
const nickname =
|
|
12
|
+
args.find((a) => a.startsWith('--nickname='))?.split('=')[1] ??
|
|
13
|
+
args.find((a) => a.startsWith('-n='))?.split('=')[1] ??
|
|
14
|
+
process.env.BITCHAT_NICKNAME ??
|
|
15
|
+
`node-${Math.random().toString(36).slice(2, 6)}`;
|
|
16
|
+
|
|
17
|
+
const port = parseInt(
|
|
18
|
+
args.find((a) => a.startsWith('--port='))?.split('=')[1] ??
|
|
19
|
+
args.find((a) => a.startsWith('-p='))?.split('=')[1] ??
|
|
20
|
+
process.env.BITCHAT_PORT ??
|
|
21
|
+
'3939',
|
|
22
|
+
10
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
const testnet = args.includes('--testnet');
|
|
26
|
+
|
|
27
|
+
console.log('Starting Bitchat Node...');
|
|
28
|
+
console.log(` Nickname: ${nickname}`);
|
|
29
|
+
console.log(` Port: ${port}`);
|
|
30
|
+
console.log(` Network: ${testnet ? 'testnet' : 'mainnet'}`);
|
|
31
|
+
console.log();
|
|
32
|
+
|
|
33
|
+
const client = new BitchatClient({
|
|
34
|
+
nickname,
|
|
35
|
+
testnet,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Event logging
|
|
39
|
+
client.on('ready', () => {
|
|
40
|
+
console.log(`✓ Ready`);
|
|
41
|
+
console.log(` Peer ID: ${client.peerID.toHex()}`);
|
|
42
|
+
console.log(` Fingerprint: ${client.fingerprint}`);
|
|
43
|
+
console.log();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
client.on('peer:connected', (peer) => {
|
|
47
|
+
console.log(`+ Peer: ${peer.nickname} (${peer.peerID.toHex().slice(0, 8)}...)`);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
client.on('peer:disconnected', (peerID) => {
|
|
51
|
+
console.log(`- Peer: ${peerID.toHex().slice(0, 8)}...`);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
client.on('message', (msg) => {
|
|
55
|
+
const prefix = msg.isPrivate ? '[DM]' : '[PUB]';
|
|
56
|
+
console.log(`${prefix} ${msg.senderNickname}: ${msg.content}`);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
client.on('error', (err, ctx) => {
|
|
60
|
+
console.error(`! Error (${ctx}): ${err.message}`);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Start
|
|
64
|
+
async function main() {
|
|
65
|
+
try {
|
|
66
|
+
await client.start();
|
|
67
|
+
|
|
68
|
+
const ui = startUIServer(client, { port });
|
|
69
|
+
console.log(`\n🌐 Web UI: http://localhost:${port}\n`);
|
|
70
|
+
|
|
71
|
+
// Graceful shutdown
|
|
72
|
+
const shutdown = async () => {
|
|
73
|
+
console.log('\nShutting down...');
|
|
74
|
+
ui.stop();
|
|
75
|
+
await client.stop();
|
|
76
|
+
process.exit(0);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
process.on('SIGINT', shutdown);
|
|
80
|
+
process.on('SIGTERM', shutdown);
|
|
81
|
+
} catch (error) {
|
|
82
|
+
console.error('Failed to start:', error);
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
main();
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bitchat Client
|
|
3
|
+
* High-level API for Bitchat mesh networking
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { EventEmitter } from 'node:events';
|
|
7
|
+
import type { NoiseKeyPair, NoiseSession } from './crypto/noise.js';
|
|
8
|
+
import { type SigningKeyPair, sign, verify } from './crypto/signing.js';
|
|
9
|
+
import { type Link, MeshRouter } from './mesh/router.js';
|
|
10
|
+
import { decode, encode, encodeForSigning, makeDeduplicationID } from './protocol/binary.js';
|
|
11
|
+
import { DEFAULT_TTL } from './protocol/constants.js';
|
|
12
|
+
import { decodeAnnouncement, encodeAnnouncement } from './protocol/packets.js';
|
|
13
|
+
import {
|
|
14
|
+
type BitchatPacket,
|
|
15
|
+
type ChatMessage,
|
|
16
|
+
MessageType,
|
|
17
|
+
NoisePayloadType,
|
|
18
|
+
type PeerID,
|
|
19
|
+
type PeerInfo,
|
|
20
|
+
} from './protocol/types.js';
|
|
21
|
+
import { SessionManager } from './session/manager.js';
|
|
22
|
+
import { type BLELink, BLETransport } from './transport/ble.js';
|
|
23
|
+
|
|
24
|
+
export interface BitchatClientConfig {
|
|
25
|
+
nickname: string;
|
|
26
|
+
staticKeyPair?: NoiseKeyPair;
|
|
27
|
+
signingKeyPair?: SigningKeyPair;
|
|
28
|
+
testnet?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface BitchatClientEvents {
|
|
32
|
+
ready: () => void;
|
|
33
|
+
message: (message: ChatMessage) => void;
|
|
34
|
+
'peer:connected': (peer: PeerInfo) => void;
|
|
35
|
+
'peer:disconnected': (peerID: PeerID) => void;
|
|
36
|
+
'peer:updated': (peer: PeerInfo) => void;
|
|
37
|
+
error: (error: Error, context: string) => void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Bitchat mesh network client
|
|
42
|
+
*/
|
|
43
|
+
export class BitchatClient extends EventEmitter {
|
|
44
|
+
private readonly config: BitchatClientConfig;
|
|
45
|
+
private readonly transport: BLETransport;
|
|
46
|
+
private readonly router: MeshRouter;
|
|
47
|
+
private readonly sessions: SessionManager;
|
|
48
|
+
private running = false;
|
|
49
|
+
|
|
50
|
+
// Our identity
|
|
51
|
+
readonly nickname: string;
|
|
52
|
+
readonly peerID: PeerID;
|
|
53
|
+
readonly fingerprint: string;
|
|
54
|
+
|
|
55
|
+
/** Get the client configuration */
|
|
56
|
+
getConfig(): BitchatClientConfig {
|
|
57
|
+
return this.config;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
constructor(config: BitchatClientConfig) {
|
|
61
|
+
super();
|
|
62
|
+
this.config = config;
|
|
63
|
+
this.nickname = config.nickname;
|
|
64
|
+
|
|
65
|
+
// Initialize session manager (handles keys and Noise sessions)
|
|
66
|
+
this.sessions = new SessionManager(
|
|
67
|
+
{},
|
|
68
|
+
{
|
|
69
|
+
static: config.staticKeyPair,
|
|
70
|
+
signing: config.signingKeyPair,
|
|
71
|
+
}
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
this.peerID = this.sessions.myPeerID;
|
|
75
|
+
this.fingerprint = this.sessions.fingerprint;
|
|
76
|
+
|
|
77
|
+
// Initialize router
|
|
78
|
+
this.router = new MeshRouter();
|
|
79
|
+
this.router.setMyPeerID(this.peerID);
|
|
80
|
+
|
|
81
|
+
// Initialize transport
|
|
82
|
+
this.transport = new BLETransport({
|
|
83
|
+
testnet: config.testnet ?? false,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Wire up event handlers
|
|
87
|
+
this.setupEventHandlers();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Start the client
|
|
92
|
+
*/
|
|
93
|
+
async start(): Promise<void> {
|
|
94
|
+
if (this.running) return;
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
// Start transport
|
|
98
|
+
await this.transport.start();
|
|
99
|
+
|
|
100
|
+
this.running = true;
|
|
101
|
+
this.emit('ready');
|
|
102
|
+
|
|
103
|
+
// Send initial announce
|
|
104
|
+
setTimeout(() => this.sendAnnounce(), 1000);
|
|
105
|
+
} catch (error) {
|
|
106
|
+
this.emit('error', error as Error, 'start');
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Stop the client
|
|
113
|
+
*/
|
|
114
|
+
async stop(): Promise<void> {
|
|
115
|
+
if (!this.running) return;
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
// Send leave message
|
|
119
|
+
await this.sendLeave();
|
|
120
|
+
|
|
121
|
+
// Stop transport
|
|
122
|
+
await this.transport.stop();
|
|
123
|
+
|
|
124
|
+
// Clean up
|
|
125
|
+
this.router.destroy();
|
|
126
|
+
this.sessions.destroy();
|
|
127
|
+
|
|
128
|
+
this.running = false;
|
|
129
|
+
} catch (error) {
|
|
130
|
+
this.emit('error', error as Error, 'stop');
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Send a public message to the mesh
|
|
137
|
+
*/
|
|
138
|
+
async sendPublicMessage(content: string): Promise<string> {
|
|
139
|
+
console.log('[Client] sendPublicMessage:', content);
|
|
140
|
+
const messageID = crypto.randomUUID();
|
|
141
|
+
const timestamp = BigInt(Date.now());
|
|
142
|
+
|
|
143
|
+
const packet: BitchatPacket = {
|
|
144
|
+
version: 1,
|
|
145
|
+
type: MessageType.MESSAGE,
|
|
146
|
+
ttl: DEFAULT_TTL,
|
|
147
|
+
timestamp,
|
|
148
|
+
senderID: this.peerID,
|
|
149
|
+
payload: new TextEncoder().encode(content),
|
|
150
|
+
isRSR: false,
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// Sign the packet
|
|
154
|
+
const dataToSign = encodeForSigning(packet);
|
|
155
|
+
const signature = sign(dataToSign, this.sessions.signingKeyPair.secretKey);
|
|
156
|
+
packet.signature = signature;
|
|
157
|
+
|
|
158
|
+
// Send via router (to noble links) and transport (to bleno subscribers)
|
|
159
|
+
console.log('[Client] Sending to router...');
|
|
160
|
+
await this.router.sendPacket(packet);
|
|
161
|
+
console.log('[Client] Encoding packet...');
|
|
162
|
+
const encoded = encode(packet);
|
|
163
|
+
console.log('[Client] Broadcasting via transport, size:', encoded.length);
|
|
164
|
+
await this.transport.broadcast(encoded);
|
|
165
|
+
console.log('[Client] Broadcast complete');
|
|
166
|
+
|
|
167
|
+
return messageID;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Send a private message to a specific peer
|
|
172
|
+
*/
|
|
173
|
+
async sendPrivateMessage(content: string, toPeerID: PeerID): Promise<string> {
|
|
174
|
+
const messageID = crypto.randomUUID();
|
|
175
|
+
|
|
176
|
+
// Check if we have a session
|
|
177
|
+
if (!this.sessions.hasSession(toPeerID)) {
|
|
178
|
+
// Need to establish session first
|
|
179
|
+
if (!this.sessions.hasHandshakeInProgress(toPeerID)) {
|
|
180
|
+
// Initiate handshake
|
|
181
|
+
const handshakeMsg = this.sessions.initiateHandshake(toPeerID);
|
|
182
|
+
await this.sendHandshakePacket(toPeerID, handshakeMsg);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Queue message for after handshake
|
|
186
|
+
const payload = this.buildPrivateMessagePayload(content, messageID);
|
|
187
|
+
this.sessions.queueMessage(toPeerID, payload);
|
|
188
|
+
|
|
189
|
+
return messageID;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Encrypt and send
|
|
193
|
+
await this.sendEncryptedMessage(toPeerID, content, messageID);
|
|
194
|
+
return messageID;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Get list of connected peers
|
|
199
|
+
*/
|
|
200
|
+
getConnectedPeers(): PeerInfo[] {
|
|
201
|
+
return this.sessions.getAllPeers().filter((p) => p.isConnected);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Get peer info
|
|
206
|
+
*/
|
|
207
|
+
getPeerInfo(peerID: PeerID): PeerInfo | undefined {
|
|
208
|
+
return this.sessions.getPeerInfo(peerID);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// --- Private methods ---
|
|
212
|
+
|
|
213
|
+
private setupEventHandlers(): void {
|
|
214
|
+
// Transport events
|
|
215
|
+
this.transport.on('ready', () => {
|
|
216
|
+
// Add links to router as they're discovered
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
this.transport.on('link:connected', (link: BLELink) => {
|
|
220
|
+
this.router.addLink(link);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
this.transport.on('link:disconnected', (linkId: string) => {
|
|
224
|
+
this.router.removeLink(linkId);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
this.transport.on('data', (data: Uint8Array, link: BLELink) => {
|
|
228
|
+
this.handleIncomingData(data, link);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
this.transport.on('error', (error: Error, context: string) => {
|
|
232
|
+
this.emit('error', error, `transport:${context}`);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// Router events
|
|
236
|
+
this.router.on('packet', (packet: BitchatPacket, fromLink: Link | null) => {
|
|
237
|
+
this.handlePacket(packet, fromLink as BLELink | null);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// Session events
|
|
241
|
+
this.sessions.on('session:established', (peerID: PeerID, _session: NoiseSession) => {
|
|
242
|
+
const peer = this.sessions.getPeerInfo(peerID);
|
|
243
|
+
if (peer) {
|
|
244
|
+
this.emit('peer:updated', peer);
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
this.sessions.on('handshake:message', async (peerID: PeerID, encrypted: Uint8Array) => {
|
|
249
|
+
// Send queued encrypted message
|
|
250
|
+
const packet: BitchatPacket = {
|
|
251
|
+
version: 1,
|
|
252
|
+
type: MessageType.NOISE_ENCRYPTED,
|
|
253
|
+
ttl: DEFAULT_TTL,
|
|
254
|
+
timestamp: BigInt(Date.now()),
|
|
255
|
+
senderID: this.peerID,
|
|
256
|
+
recipientID: peerID,
|
|
257
|
+
payload: encrypted,
|
|
258
|
+
isRSR: false,
|
|
259
|
+
};
|
|
260
|
+
await this.router.sendToPeer(packet, peerID);
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
private async handleIncomingData(data: Uint8Array, link: BLELink): Promise<void> {
|
|
265
|
+
const packet = decode(data);
|
|
266
|
+
if (!packet) return;
|
|
267
|
+
|
|
268
|
+
await this.router.handlePacket(packet, link);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
private handlePacket(packet: BitchatPacket, fromLink: BLELink | null): void {
|
|
272
|
+
switch (packet.type) {
|
|
273
|
+
case MessageType.ANNOUNCE:
|
|
274
|
+
this.handleAnnounce(packet, fromLink);
|
|
275
|
+
break;
|
|
276
|
+
|
|
277
|
+
case MessageType.MESSAGE:
|
|
278
|
+
this.handlePublicMessage(packet);
|
|
279
|
+
break;
|
|
280
|
+
|
|
281
|
+
case MessageType.LEAVE:
|
|
282
|
+
this.handleLeave(packet);
|
|
283
|
+
break;
|
|
284
|
+
|
|
285
|
+
case MessageType.NOISE_HANDSHAKE:
|
|
286
|
+
this.handleHandshake(packet);
|
|
287
|
+
break;
|
|
288
|
+
|
|
289
|
+
case MessageType.NOISE_ENCRYPTED:
|
|
290
|
+
this.handleEncrypted(packet);
|
|
291
|
+
break;
|
|
292
|
+
|
|
293
|
+
default:
|
|
294
|
+
// Unknown type - already relayed by router
|
|
295
|
+
break;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
private handleAnnounce(packet: BitchatPacket, fromLink: BLELink | null): void {
|
|
300
|
+
const peerID = packet.senderID;
|
|
301
|
+
|
|
302
|
+
// Decode TLV announcement payload
|
|
303
|
+
const announcement = decodeAnnouncement(packet.payload);
|
|
304
|
+
const nickname = announcement?.nickname ?? 'anon';
|
|
305
|
+
|
|
306
|
+
// Verify signature if present
|
|
307
|
+
if (packet.signature && announcement?.signingPublicKey) {
|
|
308
|
+
const dataToSign = encodeForSigning(packet);
|
|
309
|
+
const isValid = verify(dataToSign, packet.signature, announcement.signingPublicKey);
|
|
310
|
+
if (!isValid) {
|
|
311
|
+
console.warn('[Announce] Invalid signature from', nickname, peerID.toHex().slice(0, 8));
|
|
312
|
+
return; // Reject invalid announces
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Check if this is a NEW peer or an existing one
|
|
317
|
+
const existingPeer = this.sessions.getPeerInfo(peerID);
|
|
318
|
+
const isNewPeer = !existingPeer || !existingPeer.isConnected;
|
|
319
|
+
|
|
320
|
+
// Update peer info
|
|
321
|
+
this.sessions.updatePeerInfo(peerID, {
|
|
322
|
+
nickname,
|
|
323
|
+
isConnected: true,
|
|
324
|
+
lastSeen: new Date(),
|
|
325
|
+
// Store keys if available
|
|
326
|
+
noisePublicKey: announcement?.noisePublicKey,
|
|
327
|
+
signingPublicKey: announcement?.signingPublicKey,
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// Associate peer ID with link
|
|
331
|
+
if (fromLink) {
|
|
332
|
+
fromLink.setPeerID(peerID);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const peer = this.sessions.getPeerInfo(peerID);
|
|
336
|
+
if (peer) {
|
|
337
|
+
// Only emit 'peer:connected' for NEW peers, not repeat announces
|
|
338
|
+
if (isNewPeer) {
|
|
339
|
+
this.emit('peer:connected', peer);
|
|
340
|
+
} else {
|
|
341
|
+
// Existing peer sent another announce - just update internally
|
|
342
|
+
this.emit('peer:updated', peer);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
private handlePublicMessage(packet: BitchatPacket): void {
|
|
348
|
+
const content = new TextDecoder().decode(packet.payload);
|
|
349
|
+
const peer = this.sessions.getPeerInfo(packet.senderID);
|
|
350
|
+
|
|
351
|
+
const message: ChatMessage = {
|
|
352
|
+
id: makeDeduplicationID(packet),
|
|
353
|
+
sender: packet.senderID,
|
|
354
|
+
senderNickname: peer?.nickname ?? 'anon',
|
|
355
|
+
content,
|
|
356
|
+
timestamp: new Date(Number(packet.timestamp)),
|
|
357
|
+
isPrivate: false,
|
|
358
|
+
deliveryStatus: { type: 'delivered', to: this.peerID, at: new Date() },
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
this.emit('message', message);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
private handleLeave(packet: BitchatPacket): void {
|
|
365
|
+
const peerID = packet.senderID;
|
|
366
|
+
|
|
367
|
+
this.sessions.updatePeerInfo(peerID, {
|
|
368
|
+
isConnected: false,
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
this.emit('peer:disconnected', peerID);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
private async handleHandshake(packet: BitchatPacket): Promise<void> {
|
|
375
|
+
const peerID = packet.senderID;
|
|
376
|
+
const response = this.sessions.handleHandshakeMessage(peerID, packet.payload);
|
|
377
|
+
|
|
378
|
+
if (response) {
|
|
379
|
+
await this.sendHandshakePacket(peerID, response);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
private handleEncrypted(packet: BitchatPacket): void {
|
|
384
|
+
const peerID = packet.senderID;
|
|
385
|
+
|
|
386
|
+
try {
|
|
387
|
+
const decrypted = this.sessions.decrypt(peerID, packet.payload);
|
|
388
|
+
|
|
389
|
+
// First byte is payload type
|
|
390
|
+
const payloadType = decrypted[0] as NoisePayloadType;
|
|
391
|
+
const payloadData = decrypted.subarray(1);
|
|
392
|
+
|
|
393
|
+
switch (payloadType) {
|
|
394
|
+
case NoisePayloadType.PRIVATE_MESSAGE:
|
|
395
|
+
this.handlePrivateMessage(peerID, payloadData, packet.timestamp);
|
|
396
|
+
break;
|
|
397
|
+
|
|
398
|
+
case NoisePayloadType.READ_RECEIPT:
|
|
399
|
+
// TODO: Handle read receipt
|
|
400
|
+
break;
|
|
401
|
+
|
|
402
|
+
case NoisePayloadType.DELIVERED:
|
|
403
|
+
// TODO: Handle delivery confirmation
|
|
404
|
+
break;
|
|
405
|
+
|
|
406
|
+
default:
|
|
407
|
+
// Unknown payload type
|
|
408
|
+
break;
|
|
409
|
+
}
|
|
410
|
+
} catch (error) {
|
|
411
|
+
this.emit('error', error as Error, 'decrypt');
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
private handlePrivateMessage(senderID: PeerID, payload: Uint8Array, timestamp: bigint): void {
|
|
416
|
+
const content = new TextDecoder().decode(payload);
|
|
417
|
+
const peer = this.sessions.getPeerInfo(senderID);
|
|
418
|
+
|
|
419
|
+
const message: ChatMessage = {
|
|
420
|
+
id: `${senderID.toHex()}-${timestamp}-private`,
|
|
421
|
+
sender: senderID,
|
|
422
|
+
senderNickname: peer?.nickname ?? 'anon',
|
|
423
|
+
content,
|
|
424
|
+
timestamp: new Date(Number(timestamp)),
|
|
425
|
+
isPrivate: true,
|
|
426
|
+
deliveryStatus: { type: 'delivered', to: this.peerID, at: new Date() },
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
this.emit('message', message);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
private async sendAnnounce(): Promise<void> {
|
|
433
|
+
// Encode announcement with TLV format
|
|
434
|
+
const payload = encodeAnnouncement({
|
|
435
|
+
nickname: this.nickname,
|
|
436
|
+
noisePublicKey: this.sessions.staticKeyPair.publicKey,
|
|
437
|
+
signingPublicKey: this.sessions.signingKeyPair.publicKey,
|
|
438
|
+
directNeighbors: this.router.getConnectedPeerIDs().map((p) => p.toBytes()),
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
const packet: BitchatPacket = {
|
|
442
|
+
version: 1,
|
|
443
|
+
type: MessageType.ANNOUNCE,
|
|
444
|
+
ttl: DEFAULT_TTL,
|
|
445
|
+
timestamp: BigInt(Date.now()),
|
|
446
|
+
senderID: this.peerID,
|
|
447
|
+
payload,
|
|
448
|
+
isRSR: false,
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
// Sign the announce packet
|
|
452
|
+
const dataToSign = encodeForSigning(packet);
|
|
453
|
+
const signature = sign(dataToSign, this.sessions.signingKeyPair.secretKey);
|
|
454
|
+
packet.signature = signature;
|
|
455
|
+
|
|
456
|
+
await this.router.sendPacket(packet);
|
|
457
|
+
const encoded = encode(packet);
|
|
458
|
+
await this.transport.broadcast(encoded);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
private async sendLeave(): Promise<void> {
|
|
462
|
+
const packet: BitchatPacket = {
|
|
463
|
+
version: 1,
|
|
464
|
+
type: MessageType.LEAVE,
|
|
465
|
+
ttl: DEFAULT_TTL,
|
|
466
|
+
timestamp: BigInt(Date.now()),
|
|
467
|
+
senderID: this.peerID,
|
|
468
|
+
payload: new Uint8Array(0),
|
|
469
|
+
isRSR: false,
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
await this.router.sendPacket(packet);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
private async sendHandshakePacket(toPeerID: PeerID, message: Uint8Array): Promise<void> {
|
|
476
|
+
const packet: BitchatPacket = {
|
|
477
|
+
version: 1,
|
|
478
|
+
type: MessageType.NOISE_HANDSHAKE,
|
|
479
|
+
ttl: DEFAULT_TTL,
|
|
480
|
+
timestamp: BigInt(Date.now()),
|
|
481
|
+
senderID: this.peerID,
|
|
482
|
+
recipientID: toPeerID,
|
|
483
|
+
payload: message,
|
|
484
|
+
isRSR: false,
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
await this.router.sendToPeer(packet, toPeerID);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
private buildPrivateMessagePayload(content: string, _messageID: string): Uint8Array {
|
|
491
|
+
const contentBytes = new TextEncoder().encode(content);
|
|
492
|
+
const payload = new Uint8Array(1 + contentBytes.length);
|
|
493
|
+
payload[0] = NoisePayloadType.PRIVATE_MESSAGE;
|
|
494
|
+
payload.set(contentBytes, 1);
|
|
495
|
+
return payload;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
private async sendEncryptedMessage(
|
|
499
|
+
toPeerID: PeerID,
|
|
500
|
+
content: string,
|
|
501
|
+
messageID: string
|
|
502
|
+
): Promise<void> {
|
|
503
|
+
const payload = this.buildPrivateMessagePayload(content, messageID);
|
|
504
|
+
const encrypted = this.sessions.encrypt(toPeerID, payload);
|
|
505
|
+
|
|
506
|
+
const packet: BitchatPacket = {
|
|
507
|
+
version: 1,
|
|
508
|
+
type: MessageType.NOISE_ENCRYPTED,
|
|
509
|
+
ttl: DEFAULT_TTL,
|
|
510
|
+
timestamp: BigInt(Date.now()),
|
|
511
|
+
senderID: this.peerID,
|
|
512
|
+
recipientID: toPeerID,
|
|
513
|
+
payload: encrypted,
|
|
514
|
+
isRSR: false,
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
await this.router.sendToPeer(packet, toPeerID);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Crypto module exports
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export {
|
|
6
|
+
CipherState,
|
|
7
|
+
generateKeyPair,
|
|
8
|
+
HandshakeState,
|
|
9
|
+
type NoiseKeyPair,
|
|
10
|
+
type NoiseRole,
|
|
11
|
+
NoiseSession,
|
|
12
|
+
} from './noise.js';
|
|
13
|
+
|
|
14
|
+
export {
|
|
15
|
+
fingerprint,
|
|
16
|
+
formatFingerprint,
|
|
17
|
+
generateSigningKeyPair,
|
|
18
|
+
type SigningKeyPair,
|
|
19
|
+
sha256,
|
|
20
|
+
sign,
|
|
21
|
+
verify,
|
|
22
|
+
} from './signing.js';
|