@freesignal/protocol 0.8.3 → 0.9.1
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/dist/double-ratchet.d.ts +3 -3
- package/dist/index.d.ts +2 -10
- package/dist/index.js +2 -2
- package/dist/node.d.ts +23 -32
- package/dist/node.js +261 -212
- package/dist/types.d.ts +1 -12
- package/dist/types.js +33 -46
- package/dist/x3dh.d.ts +10 -9
- package/dist/x3dh.js +82 -93
- package/package.json +45 -45
package/dist/double-ratchet.d.ts
CHANGED
|
@@ -21,8 +21,8 @@ export interface ExportedKeySession {
|
|
|
21
21
|
identityKey: string;
|
|
22
22
|
secretKey: string;
|
|
23
23
|
rootKey: string;
|
|
24
|
-
sendingChain?:
|
|
25
|
-
receivingChain?:
|
|
24
|
+
sendingChain?: KeyChainState;
|
|
25
|
+
receivingChain?: KeyChainState;
|
|
26
26
|
headerKey?: string;
|
|
27
27
|
headerKeys: [string, Uint8Array][];
|
|
28
28
|
previousKeys: [string, Uint8Array][];
|
|
@@ -87,7 +87,7 @@ export declare class KeySession {
|
|
|
87
87
|
*/
|
|
88
88
|
static from(data: ExportedKeySession): KeySession;
|
|
89
89
|
}
|
|
90
|
-
interface
|
|
90
|
+
interface KeyChainState {
|
|
91
91
|
publicKey: string;
|
|
92
92
|
remoteKey: string;
|
|
93
93
|
chainKey: string;
|
package/dist/index.d.ts
CHANGED
|
@@ -16,10 +16,8 @@
|
|
|
16
16
|
* You should have received a copy of the GNU General Public License
|
|
17
17
|
* along with this program. If not, see <https://www.gnu.org/licenses/>
|
|
18
18
|
*/
|
|
19
|
-
import { LocalStorage, Crypto, Database, KeyExchangeDataBundle } from "@freesignal/interfaces";
|
|
20
|
-
import { ExportedKeySession } from "./double-ratchet.js";
|
|
21
19
|
import { PrivateIdentityKey } from "./types.js";
|
|
22
|
-
import {
|
|
20
|
+
import { FreeSignalNode, FreeSignalNodeState } from "./node.js";
|
|
23
21
|
/**
|
|
24
22
|
* Generates identity key
|
|
25
23
|
*
|
|
@@ -28,11 +26,5 @@ import { BootstrapRequest, FreeSignalNode } from "./node.js";
|
|
|
28
26
|
*/
|
|
29
27
|
export declare function createIdentity(seed?: Uint8Array): PrivateIdentityKey;
|
|
30
28
|
/** */
|
|
31
|
-
export declare function createNode(
|
|
32
|
-
sessions: LocalStorage<string, ExportedKeySession>;
|
|
33
|
-
users: LocalStorage<string, string>;
|
|
34
|
-
keyExchange: LocalStorage<string, Crypto.KeyPair>;
|
|
35
|
-
bundles: LocalStorage<string, KeyExchangeDataBundle>;
|
|
36
|
-
bootstraps: LocalStorage<string, BootstrapRequest>;
|
|
37
|
-
}>, privateIdentityKey?: PrivateIdentityKey): FreeSignalNode;
|
|
29
|
+
export declare function createNode(args?: Partial<FreeSignalNodeState>): FreeSignalNode;
|
|
38
30
|
export * from "./types.js";
|
package/dist/index.js
CHANGED
|
@@ -35,7 +35,7 @@ export function createIdentity(seed) {
|
|
|
35
35
|
return PrivateIdentityKey.from(signatureKeyPair.secretKey, exchangeKeyPair.secretKey);
|
|
36
36
|
}
|
|
37
37
|
/** */
|
|
38
|
-
export function createNode(
|
|
39
|
-
return new FreeSignalNode(
|
|
38
|
+
export function createNode(args) {
|
|
39
|
+
return new FreeSignalNode(args);
|
|
40
40
|
}
|
|
41
41
|
export * from "./types.js";
|
package/dist/node.d.ts
CHANGED
|
@@ -16,10 +16,10 @@
|
|
|
16
16
|
* You should have received a copy of the GNU General Public License
|
|
17
17
|
* along with this program. If not, see <https://www.gnu.org/licenses/>
|
|
18
18
|
*/
|
|
19
|
-
import {
|
|
19
|
+
import { KeyExchangeDataBundle, KeyExchangeData, Crypto } from "@freesignal/interfaces";
|
|
20
20
|
import { Datagram, EncryptedDatagram, IdentityKey, PrivateIdentityKey, Protocols, UserId } from "./types.js";
|
|
21
21
|
import { KeyExchange } from "./x3dh.js";
|
|
22
|
-
import { ExportedKeySession, KeySession } from "./double-ratchet.js";
|
|
22
|
+
import { ExportedKeySession as KeySessionState, KeySession } from "./double-ratchet.js";
|
|
23
23
|
import { EventEmitter, EventCallback } from "easyemitter.ts";
|
|
24
24
|
export declare class BootstrapRequest extends EventEmitter<'change', BootstrapRequest> {
|
|
25
25
|
#private;
|
|
@@ -50,22 +50,22 @@ export type MessageEventData = {
|
|
|
50
50
|
session: KeySession;
|
|
51
51
|
payload: Uint8Array;
|
|
52
52
|
};
|
|
53
|
+
export type FreeSignalNodeState = {
|
|
54
|
+
privateIdentityKey: string;
|
|
55
|
+
sessions: Array<[string, KeySessionState]>;
|
|
56
|
+
users: Array<[string, string]>;
|
|
57
|
+
bundles: Array<[string, KeyExchangeDataBundle]>;
|
|
58
|
+
keyExchange: Array<[string, Crypto.KeyPair]>;
|
|
59
|
+
};
|
|
53
60
|
export declare class FreeSignalNode {
|
|
54
61
|
protected readonly privateIdentityKey: PrivateIdentityKey;
|
|
55
|
-
protected readonly sessions:
|
|
56
|
-
protected readonly users:
|
|
57
|
-
protected readonly bundles:
|
|
62
|
+
protected readonly sessions: Map<string, KeySession>;
|
|
63
|
+
protected readonly users: Map<string, string>;
|
|
64
|
+
protected readonly bundles: Map<string, KeyExchangeDataBundle>;
|
|
58
65
|
protected readonly keyExchange: KeyExchange;
|
|
59
|
-
protected readonly
|
|
60
|
-
protected readonly
|
|
61
|
-
|
|
62
|
-
constructor(storage: Database<{
|
|
63
|
-
sessions: LocalStorage<string, ExportedKeySession>;
|
|
64
|
-
users: LocalStorage<string, string>;
|
|
65
|
-
keyExchange: LocalStorage<string, Crypto.KeyPair>;
|
|
66
|
-
bundles: LocalStorage<string, KeyExchangeDataBundle>;
|
|
67
|
-
bootstraps: LocalStorage<string, BootstrapRequest>;
|
|
68
|
-
}>, privateIdentityKey?: PrivateIdentityKey);
|
|
66
|
+
protected readonly bootstraps: Map<string, BootstrapRequest>;
|
|
67
|
+
protected readonly emitter: EventEmitter<"error" | "debug" | "send" | "handshake" | "message" | "bootstrap", NodeEventData>;
|
|
68
|
+
constructor({ privateIdentityKey, sessions, users, bundles, keyExchange }?: Partial<FreeSignalNodeState>);
|
|
69
69
|
protected messageHandler: EventCallback<NodeEventData, typeof this.emitter>;
|
|
70
70
|
protected sendHandler: EventCallback<NodeEventData, typeof this.emitter>;
|
|
71
71
|
protected handshakeHandler: EventCallback<NodeEventData, typeof this.emitter>;
|
|
@@ -74,7 +74,11 @@ export declare class FreeSignalNode {
|
|
|
74
74
|
onSend: (data: Uint8Array) => void;
|
|
75
75
|
onHandshake: (userId: UserId) => void;
|
|
76
76
|
onRequest: (request: BootstrapRequest) => void;
|
|
77
|
-
|
|
77
|
+
onError: (error: any) => void;
|
|
78
|
+
onDebug: (...args: any[]) => void;
|
|
79
|
+
private error;
|
|
80
|
+
private debug;
|
|
81
|
+
getRequest(userId: string): BootstrapRequest | undefined;
|
|
78
82
|
waitHandshaked(userId: UserId | string, timeout?: number): Promise<void>;
|
|
79
83
|
get identityKey(): IdentityKey;
|
|
80
84
|
get userId(): UserId;
|
|
@@ -84,23 +88,10 @@ export declare class FreeSignalNode {
|
|
|
84
88
|
sendHandshake(userId: UserId | string): Promise<void>;
|
|
85
89
|
sendData<T>(receiverId: string | UserId, data: T): Promise<void>;
|
|
86
90
|
sendRelay(relayId: string | UserId, receiverId: string | UserId, data: Datagram): Promise<void>;
|
|
87
|
-
|
|
88
|
-
packBootstrap(): Promise<Datagram>;
|
|
91
|
+
packBootstrap(): Datagram;
|
|
89
92
|
sendBootstrap(receiverId: string | UserId): Promise<void>;
|
|
90
93
|
protected decrypt(datagram: EncryptedDatagram | Datagram | Uint8Array): Promise<MessageEventData>;
|
|
91
|
-
protected openHandshake(datagram: Datagram | EncryptedDatagram | Uint8Array): Promise<
|
|
94
|
+
protected openHandshake(datagram: Datagram | EncryptedDatagram | Uint8Array): Promise<"syn" | "ack">;
|
|
92
95
|
protected open(datagram: Datagram | EncryptedDatagram | Uint8Array): Promise<void>;
|
|
96
|
+
toJSON(): FreeSignalNodeState;
|
|
93
97
|
}
|
|
94
|
-
declare class SessionMap implements LocalStorage<string, KeySession> {
|
|
95
|
-
readonly storage: LocalStorage<string, ExportedKeySession>;
|
|
96
|
-
readonly maxSize: number;
|
|
97
|
-
private readonly cache;
|
|
98
|
-
constructor(storage: LocalStorage<string, ExportedKeySession>, maxSize?: number);
|
|
99
|
-
set(key: string, value: KeySession): Promise<void>;
|
|
100
|
-
get(key: string): Promise<KeySession | undefined>;
|
|
101
|
-
has(key: string): Promise<boolean>;
|
|
102
|
-
delete(key: string): Promise<boolean>;
|
|
103
|
-
clear(): Promise<void>;
|
|
104
|
-
entries(): Promise<Iterable<[string, KeySession]>>;
|
|
105
|
-
}
|
|
106
|
-
export {};
|
package/dist/node.js
CHANGED
|
@@ -37,7 +37,7 @@ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (
|
|
|
37
37
|
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
|
|
38
38
|
};
|
|
39
39
|
var _BootstrapRequest_status;
|
|
40
|
-
import { Datagram, decryptData,
|
|
40
|
+
import { Datagram, decryptData, encryptData, EncryptedDatagram, IdentityKey, PrivateIdentityKey, Protocols, UserId } from "./types.js";
|
|
41
41
|
import { KeyExchange } from "./x3dh.js";
|
|
42
42
|
import { KeySession } from "./double-ratchet.js";
|
|
43
43
|
import { createIdentity } from "./index.js";
|
|
@@ -71,8 +71,9 @@ export class BootstrapRequest extends EventEmitter {
|
|
|
71
71
|
}
|
|
72
72
|
_BootstrapRequest_status = new WeakMap();
|
|
73
73
|
export class FreeSignalNode {
|
|
74
|
-
constructor(
|
|
75
|
-
|
|
74
|
+
constructor({ privateIdentityKey, sessions, users, bundles, keyExchange } = {}) {
|
|
75
|
+
var _a;
|
|
76
|
+
this.bootstraps = new Map();
|
|
76
77
|
this.emitter = new EventEmitter();
|
|
77
78
|
this.messageHandler = (data) => this.onMessage({ session: data.session, payload: data.payload });
|
|
78
79
|
this.sendHandler = (data) => this.onSend(data.datagram.toBytes());
|
|
@@ -82,16 +83,25 @@ export class FreeSignalNode {
|
|
|
82
83
|
this.onSend = () => { };
|
|
83
84
|
this.onHandshake = () => { };
|
|
84
85
|
this.onRequest = () => { };
|
|
85
|
-
this.
|
|
86
|
-
this.
|
|
87
|
-
this.
|
|
88
|
-
this.
|
|
89
|
-
this.
|
|
90
|
-
this.
|
|
86
|
+
this.onError = () => { };
|
|
87
|
+
this.onDebug = () => { };
|
|
88
|
+
this.privateIdentityKey = privateIdentityKey ? PrivateIdentityKey.from(privateIdentityKey) : createIdentity();
|
|
89
|
+
this.sessions = new Map((_a = sessions === null || sessions === void 0 ? void 0 : sessions.map(([key, value]) => [key, KeySession.from(value)])) !== null && _a !== void 0 ? _a : []);
|
|
90
|
+
this.users = new Map(users);
|
|
91
|
+
this.bundles = new Map(bundles);
|
|
92
|
+
this.keyExchange = new KeyExchange({ privateIdentityKey: this.privateIdentityKey, storage: keyExchange });
|
|
91
93
|
this.emitter.on('message', this.messageHandler);
|
|
92
94
|
this.emitter.on('send', this.sendHandler);
|
|
93
95
|
this.emitter.on('handshake', this.handshakeHandler);
|
|
94
96
|
this.emitter.on('bootstrap', this.bootstrapHandler);
|
|
97
|
+
this.emitter.on('error', (e) => this.onError(e.error));
|
|
98
|
+
this.emitter.on('debug', (e) => this.onDebug(e.debug));
|
|
99
|
+
}
|
|
100
|
+
error(error) {
|
|
101
|
+
this.emitter.emit('error', { error });
|
|
102
|
+
}
|
|
103
|
+
debug(...args) {
|
|
104
|
+
this.emitter.emit('debug', { debug: args });
|
|
95
105
|
}
|
|
96
106
|
getRequest(userId) {
|
|
97
107
|
return this.bootstraps.get(userId);
|
|
@@ -114,37 +124,49 @@ export class FreeSignalNode {
|
|
|
114
124
|
}
|
|
115
125
|
encrypt(receiverId, protocol, data) {
|
|
116
126
|
return __awaiter(this, void 0, void 0, function* () {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
127
|
+
try {
|
|
128
|
+
const sessionTag = this.users.get(receiverId.toString());
|
|
129
|
+
if (!sessionTag)
|
|
130
|
+
throw new Error("User not found: " + receiverId);
|
|
131
|
+
const session = this.sessions.get(sessionTag);
|
|
132
|
+
if (!session)
|
|
133
|
+
throw new Error("Session not found for sessionTag: " + sessionTag);
|
|
134
|
+
const encrypted = encryptData(session, data);
|
|
135
|
+
this.sessions.set(receiverId.toString(), session);
|
|
136
|
+
return { session, userId: UserId.from(receiverId), datagram: new EncryptedDatagram(protocol, session.sessionTag, encrypted).sign(this.privateIdentityKey.signatureKey) };
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
this.error(error);
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
126
142
|
});
|
|
127
143
|
}
|
|
128
144
|
sendHandshake(data) {
|
|
129
145
|
return __awaiter(this, void 0, void 0, function* () {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
146
|
+
try {
|
|
147
|
+
if (data instanceof UserId || typeof data === 'string') {
|
|
148
|
+
const session = this.sessions.get(data.toString());
|
|
149
|
+
if (!session)
|
|
150
|
+
throw new Error("Session not found for userId: " + data.toString());
|
|
151
|
+
data = session;
|
|
152
|
+
}
|
|
153
|
+
if (data instanceof KeySession) {
|
|
154
|
+
//console.debug("Sending Handshake Ack");
|
|
155
|
+
const session = this.sessions.get(data.sessionTag);
|
|
156
|
+
if (!session)
|
|
157
|
+
throw new Error("Session not found for sessionTag: " + data.sessionTag);
|
|
158
|
+
this.emitter.emit('send', yield this.encrypt(session.userId, Protocols.HANDSHAKE, crypto.ECDH.scalarMult(this.privateIdentityKey.exchangeKey, session.identityKey.exchangeKey)));
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
135
161
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
if (!session)
|
|
140
|
-
throw new Error("Session not found for sessionTag: " + data.sessionTag);
|
|
141
|
-
this.emitter.emit('send', yield this.encrypt(session.userId, Protocols.HANDSHAKE, crypto.ECDH.scalarMult(this.privateIdentityKey.exchangeKey, session.identityKey.exchangeKey)));
|
|
142
|
-
return;
|
|
162
|
+
catch (error) {
|
|
163
|
+
this.error(error);
|
|
164
|
+
throw error;
|
|
143
165
|
}
|
|
144
166
|
//console.debug("Sending Handshake Syn");
|
|
145
|
-
const { session, message } =
|
|
146
|
-
|
|
147
|
-
|
|
167
|
+
const { session, message } = this.keyExchange.digestData(data, encodeData(this.keyExchange.generateBundle()));
|
|
168
|
+
this.sessions.set(session.sessionTag, session);
|
|
169
|
+
this.users.set(session.userId.toString(), session.sessionTag);
|
|
148
170
|
const datagram = new Datagram(Protocols.HANDSHAKE, encodeData(message), session.sessionTag).sign(this.privateIdentityKey.signatureKey);
|
|
149
171
|
this.emitter.emit('send', { session, datagram, userId: session.userId });
|
|
150
172
|
});
|
|
@@ -171,219 +193,246 @@ export class FreeSignalNode {
|
|
|
171
193
|
throw new Error("Session not found for sessionTag: " + sessionTag);
|
|
172
194
|
const datagram = new Datagram(Protocols.PING, undefined, session.sessionTag);
|
|
173
195
|
this.emitter.emit('send', { session, datagram, userId: session.userId });
|
|
174
|
-
}*/
|
|
175
|
-
sendDiscover(receiverId, discoverId) {
|
|
176
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
177
|
-
//console.debug("Sending Discover");
|
|
178
|
-
if (receiverId instanceof UserId)
|
|
179
|
-
receiverId = receiverId.toString();
|
|
180
|
-
if (discoverId instanceof UserId)
|
|
181
|
-
discoverId = discoverId.toString();
|
|
182
|
-
const message = {
|
|
183
|
-
type: DiscoverType.REQUEST,
|
|
184
|
-
discoverId
|
|
185
|
-
};
|
|
186
|
-
this.discovers.add(receiverId);
|
|
187
|
-
this.emitter.emit('send', yield this.encrypt(receiverId, Protocols.DISCOVER, encodeData(message)));
|
|
188
|
-
});
|
|
189
196
|
}
|
|
197
|
+
|
|
198
|
+
public async sendDiscover(receiverId: string | UserId, discoverId: string | UserId): Promise<void> {
|
|
199
|
+
//console.debug("Sending Discover");
|
|
200
|
+
if (receiverId instanceof UserId)
|
|
201
|
+
receiverId = receiverId.toString();
|
|
202
|
+
if (discoverId instanceof UserId)
|
|
203
|
+
discoverId = discoverId.toString();
|
|
204
|
+
const message: DiscoverMessage = {
|
|
205
|
+
type: DiscoverType.REQUEST,
|
|
206
|
+
discoverId
|
|
207
|
+
};
|
|
208
|
+
this.discovers.add(receiverId);
|
|
209
|
+
this.emitter.emit('send', await this.encrypt(receiverId, Protocols.DISCOVER, encodeData(message)));
|
|
210
|
+
}*/
|
|
190
211
|
packBootstrap() {
|
|
191
|
-
return
|
|
192
|
-
return new Datagram(Protocols.BOOTSTRAP, encodeData(yield this.keyExchange.generateData()));
|
|
193
|
-
});
|
|
212
|
+
return new Datagram(Protocols.BOOTSTRAP, encodeData(this.keyExchange.generateData()));
|
|
194
213
|
}
|
|
195
214
|
sendBootstrap(receiverId) {
|
|
196
215
|
return __awaiter(this, void 0, void 0, function* () {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
216
|
+
try {
|
|
217
|
+
//console.debug("Sending Bootstrap");
|
|
218
|
+
if (this.sessions.has(receiverId.toString()))
|
|
219
|
+
throw new Error("Session exists");
|
|
220
|
+
const datagram = this.packBootstrap();
|
|
221
|
+
this.emitter.emit('send', { datagram, userId: UserId.from(receiverId) });
|
|
222
|
+
}
|
|
223
|
+
catch (error) {
|
|
224
|
+
this.error(error);
|
|
225
|
+
throw error;
|
|
226
|
+
}
|
|
202
227
|
});
|
|
203
228
|
}
|
|
204
229
|
decrypt(datagram) {
|
|
205
230
|
return __awaiter(this, void 0, void 0, function* () {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
231
|
+
try {
|
|
232
|
+
datagram = EncryptedDatagram.from(datagram);
|
|
233
|
+
if (!datagram.sessionTag)
|
|
234
|
+
throw new Error("Datagram not encrypted");
|
|
235
|
+
const session = this.sessions.get(datagram.sessionTag);
|
|
236
|
+
if (!session)
|
|
237
|
+
throw new Error("Session not found for sessionTag: " + datagram.sessionTag);
|
|
238
|
+
if (!datagram.verify(session.identityKey.signatureKey))
|
|
239
|
+
throw new Error("Signature not verified");
|
|
240
|
+
if (!datagram.payload)
|
|
241
|
+
throw new Error("Missing payload");
|
|
242
|
+
const decrypted = decryptData(session, datagram.payload);
|
|
243
|
+
this.sessions.set(datagram.sessionTag, session);
|
|
244
|
+
return { session, payload: decrypted };
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
this.error(error);
|
|
248
|
+
throw error;
|
|
249
|
+
}
|
|
219
250
|
});
|
|
220
251
|
}
|
|
221
252
|
openHandshake(datagram) {
|
|
222
253
|
return __awaiter(this, void 0, void 0, function* () {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
254
|
+
try {
|
|
255
|
+
const encrypted = EncryptedDatagram.from(datagram);
|
|
256
|
+
if (!encrypted.payload)
|
|
257
|
+
throw new Error("Missing payload");
|
|
258
|
+
if (this.sessions.has(encrypted.sessionTag)) {
|
|
259
|
+
//console.debug("Opening Handshake Ack");
|
|
260
|
+
const session = this.sessions.get(encrypted.sessionTag);
|
|
261
|
+
const { payload } = yield this.decrypt(encrypted);
|
|
262
|
+
if (!session)
|
|
263
|
+
throw new Error("Session not found for sessionTag: " + encrypted.sessionTag);
|
|
264
|
+
if (!compareBytes(payload, crypto.ECDH.scalarMult(this.privateIdentityKey.exchangeKey, session.identityKey.exchangeKey)))
|
|
265
|
+
throw new Error("Error validating handshake data");
|
|
266
|
+
return 'ack';
|
|
267
|
+
}
|
|
268
|
+
//console.debug("Opening Handshake Syn");
|
|
269
|
+
const data = decodeData(encrypted.payload);
|
|
270
|
+
if (!encrypted.verify(IdentityKey.from(data.identityKey).signatureKey))
|
|
271
|
+
throw new Error("Signature not verified");
|
|
272
|
+
const { session, associatedData } = this.keyExchange.digestMessage(data);
|
|
273
|
+
this.sessions.set(session.sessionTag, session);
|
|
274
|
+
this.users.set(session.userId.toString(), session.sessionTag);
|
|
275
|
+
this.bundles.set(session.userId.toString(), decodeData(associatedData));
|
|
276
|
+
return 'syn';
|
|
277
|
+
}
|
|
278
|
+
catch (error) {
|
|
279
|
+
this.error(error);
|
|
280
|
+
throw error;
|
|
235
281
|
}
|
|
236
|
-
//console.debug("Opening Handshake Syn");
|
|
237
|
-
const data = decodeData(encrypted.payload);
|
|
238
|
-
if (!encrypted.verify(IdentityKey.from(data.identityKey).signatureKey))
|
|
239
|
-
throw new Error("Signature not verified");
|
|
240
|
-
const { session, associatedData } = yield this.keyExchange.digestMessage(data);
|
|
241
|
-
yield this.sessions.set(session.sessionTag, session);
|
|
242
|
-
yield this.users.set(session.userId.toString(), session.sessionTag);
|
|
243
|
-
yield this.bundles.set(session.userId.toString(), decodeData(associatedData));
|
|
244
|
-
return 'syn';
|
|
245
282
|
});
|
|
246
283
|
}
|
|
247
284
|
open(datagram) {
|
|
248
285
|
return __awaiter(this, void 0, void 0, function* () {
|
|
249
|
-
|
|
250
|
-
datagram
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
case Protocols.MESSAGE:
|
|
266
|
-
//console.debug("Opening Message");
|
|
267
|
-
this.emitter.emit('message', yield this.decrypt(datagram));
|
|
268
|
-
return;
|
|
269
|
-
case Protocols.RELAY: {
|
|
270
|
-
//console.debug("Opening Relay");
|
|
271
|
-
const opened = yield this.decrypt(datagram);
|
|
272
|
-
const userId = decodeBase64(opened.payload.subarray(0, UserId.keyLength));
|
|
273
|
-
const sessionTag = yield this.users.get(userId);
|
|
274
|
-
if (!sessionTag)
|
|
275
|
-
throw new Error("Session not found for user: " + userId);
|
|
276
|
-
const session = yield this.sessions.get(sessionTag);
|
|
277
|
-
if (!session)
|
|
278
|
-
throw new Error("Session not found for sessionTag: " + datagram.sessionTag);
|
|
279
|
-
this.emitter.emit('send', { session, datagram: Datagram.from(opened.payload.slice(UserId.keyLength)), userId: session.userId });
|
|
280
|
-
return;
|
|
281
|
-
}
|
|
282
|
-
case Protocols.DISCOVER: {
|
|
283
|
-
//console.debug("Opening Discover");
|
|
284
|
-
const { session, payload } = yield this.decrypt(datagram);
|
|
285
|
-
const message = decodeData(payload);
|
|
286
|
-
if (message.type === DiscoverType.REQUEST && message.discoverId && !(yield this.sessions.has(message.discoverId))) {
|
|
287
|
-
let data;
|
|
288
|
-
if (message.discoverId === this.userId.toString()) {
|
|
289
|
-
data = yield this.keyExchange.generateData();
|
|
290
|
-
}
|
|
291
|
-
else {
|
|
292
|
-
const bundle = yield this.bundles.get(message.discoverId);
|
|
293
|
-
if (!bundle)
|
|
294
|
-
return;
|
|
295
|
-
const { version, identityKey, signedPreKey, signature } = bundle;
|
|
296
|
-
const onetimePreKey = bundle.onetimePreKeys.shift();
|
|
297
|
-
if (!onetimePreKey) {
|
|
298
|
-
yield this.bundles.delete(message.discoverId);
|
|
299
|
-
return;
|
|
300
|
-
}
|
|
301
|
-
data = {
|
|
302
|
-
version,
|
|
303
|
-
identityKey,
|
|
304
|
-
signedPreKey,
|
|
305
|
-
signature,
|
|
306
|
-
onetimePreKey
|
|
307
|
-
};
|
|
308
|
-
}
|
|
309
|
-
const response = { type: DiscoverType.RESPONSE, discoverId: message.discoverId, data };
|
|
310
|
-
this.emitter.emit('send', yield this.encrypt(session.userId, Protocols.DISCOVER, encodeData(response)));
|
|
286
|
+
try {
|
|
287
|
+
if (datagram instanceof Uint8Array)
|
|
288
|
+
datagram = Datagram.from(datagram);
|
|
289
|
+
switch (datagram.protocol) {
|
|
290
|
+
case Protocols.HANDSHAKE: {
|
|
291
|
+
const encrypted = EncryptedDatagram.from(datagram);
|
|
292
|
+
if (!encrypted.payload)
|
|
293
|
+
throw new Error("Missing payload");
|
|
294
|
+
const handshakeState = yield this.openHandshake(datagram);
|
|
295
|
+
const session = this.sessions.get(encrypted.sessionTag);
|
|
296
|
+
if (!session)
|
|
297
|
+
throw new Error("Session not found for sessionTag: " + encrypted.sessionTag);
|
|
298
|
+
if (handshakeState === 'syn')
|
|
299
|
+
this.sendHandshake(session);
|
|
300
|
+
this.emitter.emit('handshake', { session });
|
|
301
|
+
return;
|
|
311
302
|
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
303
|
+
case Protocols.MESSAGE:
|
|
304
|
+
//console.debug("Opening Message");
|
|
305
|
+
this.emitter.emit('message', yield this.decrypt(datagram));
|
|
306
|
+
return;
|
|
307
|
+
case Protocols.RELAY: {
|
|
308
|
+
//console.debug("Opening Relay");
|
|
309
|
+
const opened = yield this.decrypt(datagram);
|
|
310
|
+
const userId = decodeBase64(opened.payload.subarray(0, UserId.keyLength));
|
|
311
|
+
const sessionTag = this.users.get(userId);
|
|
312
|
+
if (!sessionTag)
|
|
313
|
+
throw new Error("Session not found for user: " + userId);
|
|
314
|
+
const session = this.sessions.get(sessionTag);
|
|
315
|
+
if (!session)
|
|
316
|
+
throw new Error("Session not found for sessionTag: " + datagram.sessionTag);
|
|
317
|
+
this.emitter.emit('send', { session, datagram: Datagram.from(opened.payload.slice(UserId.keyLength)), userId: session.userId });
|
|
318
|
+
return;
|
|
316
319
|
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
320
|
+
case Protocols.BOOTSTRAP: {
|
|
321
|
+
//console.debug("Opening Bootstrap");
|
|
322
|
+
if (!datagram.payload)
|
|
323
|
+
throw new Error("Invalid Bootstrap");
|
|
324
|
+
const keyExchangeData = decodeData(datagram.payload);
|
|
325
|
+
const userId = UserId.fromKey(keyExchangeData.identityKey);
|
|
326
|
+
const request = new BootstrapRequest(userId, keyExchangeData);
|
|
327
|
+
let sended = false;
|
|
328
|
+
request.onChange = (request) => {
|
|
329
|
+
if (request.status === 'accepted' && !sended) {
|
|
330
|
+
sended = true;
|
|
331
|
+
this.sendHandshake(request.data);
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
this.bootstraps.set(userId.toString(), request);
|
|
335
|
+
this.emitter.emit('bootstrap', { request });
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
/*case Protocols.DISCOVER: {
|
|
339
|
+
//console.debug("Opening Discover");
|
|
340
|
+
const { session, payload } = await this.decrypt(datagram);
|
|
341
|
+
const message = decodeData<DiscoverMessage>(payload);
|
|
342
|
+
if (message.type === DiscoverType.REQUEST && message.discoverId && !(await this.sessions.has(message.discoverId))) {
|
|
343
|
+
let data: KeyExchangeData;
|
|
344
|
+
if (message.discoverId === this.userId.toString()) {
|
|
345
|
+
data = await this.keyExchange.generateData();
|
|
346
|
+
} else {
|
|
347
|
+
const bundle = await this.bundles.get(message.discoverId);
|
|
348
|
+
if (!bundle)
|
|
349
|
+
return;
|
|
350
|
+
const { version, identityKey, signedPreKey, signature } = bundle;
|
|
351
|
+
const onetimePreKey = bundle.onetimePreKeys.shift();
|
|
352
|
+
if (!onetimePreKey) {
|
|
353
|
+
await this.bundles.delete(message.discoverId);
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
data = {
|
|
357
|
+
version,
|
|
358
|
+
identityKey,
|
|
359
|
+
signedPreKey,
|
|
360
|
+
signature,
|
|
361
|
+
onetimePreKey
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
const response: DiscoverMessage = { type: DiscoverType.RESPONSE, discoverId: message.discoverId, data };
|
|
365
|
+
this.emitter.emit('send', await this.encrypt(session.userId, Protocols.DISCOVER, encodeData(response)));
|
|
366
|
+
} else if (message.type === DiscoverType.RESPONSE && this.discovers.has(message.discoverId)) {
|
|
367
|
+
this.discovers.delete(message.discoverId);
|
|
368
|
+
if (message.data)
|
|
369
|
+
await this.sendHandshake(message.data);
|
|
331
370
|
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
case Protocols.PING:
|
|
375
|
+
datagram = EncryptedDatagram.from(datagram);
|
|
376
|
+
const session = await this.sessions.get(datagram.sessionTag!);
|
|
377
|
+
if (!session)
|
|
378
|
+
throw new Error("Session not found for sessionTag: " + datagram.sessionTag);
|
|
379
|
+
this.emitter.emit('ping', { session });
|
|
380
|
+
return;*/
|
|
381
|
+
default:
|
|
382
|
+
throw new Error("Invalid protocol");
|
|
336
383
|
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
throw new Error("Session not found for sessionTag: " + datagram.sessionTag);
|
|
342
|
-
this.emitter.emit('ping', { session });
|
|
343
|
-
return;*/
|
|
344
|
-
default:
|
|
345
|
-
throw new Error("Invalid protocol");
|
|
384
|
+
}
|
|
385
|
+
catch (error) {
|
|
386
|
+
this.error(error);
|
|
387
|
+
throw error;
|
|
346
388
|
}
|
|
347
389
|
});
|
|
348
390
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
391
|
+
toJSON() {
|
|
392
|
+
return {
|
|
393
|
+
privateIdentityKey: this.privateIdentityKey.toString(),
|
|
394
|
+
sessions: Array.from(this.sessions.entries()).map(([key, session]) => [key, session.toJSON()]),
|
|
395
|
+
users: Array.from(this.users.entries()),
|
|
396
|
+
bundles: Array.from(this.bundles),
|
|
397
|
+
keyExchange: this.keyExchange.toJSON().storage,
|
|
398
|
+
};
|
|
355
399
|
}
|
|
356
|
-
|
|
400
|
+
}
|
|
401
|
+
/*class SessionMap implements LocalStorage<string, KeySession> {
|
|
402
|
+
private readonly cache = new Map<string, KeySession>()
|
|
403
|
+
|
|
404
|
+
public constructor(public readonly storage: LocalStorage<string, KeySessionState>, public readonly maxSize = 50) { }
|
|
405
|
+
|
|
406
|
+
public set(key: string, value: KeySession): Promise<void> {
|
|
357
407
|
this.cache.set(key, value);
|
|
358
408
|
return this.storage.set(key, value.toJSON());
|
|
359
409
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
});
|
|
410
|
+
|
|
411
|
+
public async get(key: string): Promise<KeySession | undefined> {
|
|
412
|
+
const session = this.cache.get(key);
|
|
413
|
+
if (!session) {
|
|
414
|
+
const sessionData = await this.storage.get(key);
|
|
415
|
+
if (!sessionData)
|
|
416
|
+
return undefined;
|
|
417
|
+
return KeySession.from(sessionData);
|
|
418
|
+
}
|
|
419
|
+
return session;
|
|
371
420
|
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
});
|
|
421
|
+
|
|
422
|
+
public async has(key: string): Promise<boolean> {
|
|
423
|
+
return this.cache.has(key) || await this.storage.has(key);
|
|
376
424
|
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
});
|
|
425
|
+
|
|
426
|
+
public async delete(key: string): Promise<boolean> {
|
|
427
|
+
return this.cache.delete(key) || await this.storage.delete(key);
|
|
381
428
|
}
|
|
382
|
-
|
|
429
|
+
|
|
430
|
+
public clear(): Promise<void> {
|
|
383
431
|
this.cache.clear();
|
|
384
432
|
return this.storage.clear();
|
|
385
433
|
}
|
|
386
|
-
|
|
434
|
+
|
|
435
|
+
public entries(): Promise<Iterable<[string, KeySession]>> {
|
|
387
436
|
throw new Error("Method not implemented.");
|
|
388
437
|
}
|
|
389
|
-
}
|
|
438
|
+
}*/
|
package/dist/types.d.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* You should have received a copy of the GNU General Public License
|
|
17
17
|
* along with this program. If not, see <https://www.gnu.org/licenses/>
|
|
18
18
|
*/
|
|
19
|
-
import type {
|
|
19
|
+
import type { Encodable, KeyExchangeData } from "@freesignal/interfaces";
|
|
20
20
|
import { EncryptionKeys, KeySession } from "./double-ratchet.js";
|
|
21
21
|
export declare function encryptData(session: KeySession, data: Uint8Array): EncryptedData;
|
|
22
22
|
export declare function decryptData(session: KeySession, encryptedData: Uint8Array): Uint8Array;
|
|
@@ -77,7 +77,6 @@ export declare enum Protocols {
|
|
|
77
77
|
MESSAGE = "/freesignal/message",
|
|
78
78
|
RELAY = "/freesignal/relay",
|
|
79
79
|
HANDSHAKE = "/freesignal/handshake",
|
|
80
|
-
DISCOVER = "/freesignal/discover",
|
|
81
80
|
BOOTSTRAP = "/freesignal/bootstrap"
|
|
82
81
|
}
|
|
83
82
|
export declare namespace Protocols {
|
|
@@ -168,14 +167,4 @@ export declare class EncryptedData implements Encodable {
|
|
|
168
167
|
};
|
|
169
168
|
static from(data: Uint8Array | EncryptedData): EncryptedData;
|
|
170
169
|
}
|
|
171
|
-
export declare class AsyncMap<K, V> implements LocalStorage<K, V> {
|
|
172
|
-
private readonly map;
|
|
173
|
-
constructor(iterable?: Iterable<readonly [K, V]>);
|
|
174
|
-
set(key: K, value: V): Promise<void>;
|
|
175
|
-
get(key: K): Promise<V | undefined>;
|
|
176
|
-
has(key: K): Promise<boolean>;
|
|
177
|
-
delete(key: K): Promise<boolean>;
|
|
178
|
-
clear(): Promise<void>;
|
|
179
|
-
entries(): Promise<Iterable<[K, V]>>;
|
|
180
|
-
}
|
|
181
170
|
export {};
|
package/dist/types.js
CHANGED
|
@@ -16,15 +16,6 @@
|
|
|
16
16
|
* You should have received a copy of the GNU General Public License
|
|
17
17
|
* along with this program. If not, see <https://www.gnu.org/licenses/>
|
|
18
18
|
*/
|
|
19
|
-
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
20
|
-
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
21
|
-
return new (P || (P = Promise))(function (resolve, reject) {
|
|
22
|
-
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
23
|
-
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
24
|
-
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
25
|
-
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
26
|
-
});
|
|
27
|
-
};
|
|
28
19
|
import { concatBytes, decodeBase64, encodeBase64, bytesToNumber, numberToBytes, compareBytes, decodeBase64URL } from "@freesignal/utils";
|
|
29
20
|
import crypto from "@freesignal/crypto";
|
|
30
21
|
export function encryptData(session, data) {
|
|
@@ -196,10 +187,10 @@ export var Protocols;
|
|
|
196
187
|
(function (Protocols) {
|
|
197
188
|
Protocols["NULL"] = "";
|
|
198
189
|
//PING = '/freesignal/ping',
|
|
190
|
+
//DISCOVER = '/freesignal/discover',
|
|
199
191
|
Protocols["MESSAGE"] = "/freesignal/message";
|
|
200
192
|
Protocols["RELAY"] = "/freesignal/relay";
|
|
201
193
|
Protocols["HANDSHAKE"] = "/freesignal/handshake";
|
|
202
|
-
Protocols["DISCOVER"] = "/freesignal/discover";
|
|
203
194
|
Protocols["BOOTSTRAP"] = "/freesignal/bootstrap";
|
|
204
195
|
})(Protocols || (Protocols = {}));
|
|
205
196
|
(function (Protocols) {
|
|
@@ -386,39 +377,35 @@ export class EncryptedData {
|
|
|
386
377
|
}
|
|
387
378
|
EncryptedData.version = 1;
|
|
388
379
|
EncryptedData.nonceLength = crypto.box.nonceLength;
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
delete(key) {
|
|
410
|
-
return
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
return this.map.entries();
|
|
422
|
-
});
|
|
423
|
-
}
|
|
424
|
-
}
|
|
380
|
+
/*class AsyncMap<K, V> implements LocalStorage<K, V> {
|
|
381
|
+
private readonly map: Map<K, V>;
|
|
382
|
+
|
|
383
|
+
constructor(iterable?: Iterable<readonly [K, V]>) {
|
|
384
|
+
this.map = new Map<K, V>(iterable);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
async set(key: K, value: V): Promise<void> {
|
|
388
|
+
this.map.set(key, value);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
async get(key: K): Promise<V | undefined> {
|
|
393
|
+
return this.map.get(key);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
async has(key: K): Promise<boolean> {
|
|
397
|
+
return this.map.has(key);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
async delete(key: K): Promise<boolean> {
|
|
401
|
+
return this.map.delete(key);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
async clear(): Promise<void> {
|
|
405
|
+
return this.map.clear();
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
async entries(): Promise<Iterable<[K, V]>> {
|
|
409
|
+
return this.map.entries();
|
|
410
|
+
}
|
|
411
|
+
}*/
|
package/dist/x3dh.d.ts
CHANGED
|
@@ -16,10 +16,10 @@
|
|
|
16
16
|
* You should have received a copy of the GNU General Public License
|
|
17
17
|
* along with this program. If not, see <https://www.gnu.org/licenses/>
|
|
18
18
|
*/
|
|
19
|
-
import { KeyExchangeData, KeyExchangeDataBundle, KeyExchangeSynMessage,
|
|
19
|
+
import { KeyExchangeData, KeyExchangeDataBundle, KeyExchangeSynMessage, Crypto } from "@freesignal/interfaces";
|
|
20
20
|
import { KeySession } from "./double-ratchet.js";
|
|
21
21
|
import { IdentityKey, PrivateIdentityKey } from "./types.js";
|
|
22
|
-
export interface
|
|
22
|
+
export interface KeyExchangeState {
|
|
23
23
|
privateIdentityKey: PrivateIdentityKey;
|
|
24
24
|
storage: Array<[string, Crypto.KeyPair]>;
|
|
25
25
|
}
|
|
@@ -29,18 +29,19 @@ export declare class KeyExchange {
|
|
|
29
29
|
private static readonly maxOPK;
|
|
30
30
|
private readonly privateIdentityKey;
|
|
31
31
|
private readonly storage;
|
|
32
|
-
constructor(storage
|
|
32
|
+
constructor({ storage, privateIdentityKey }?: Partial<KeyExchangeState>);
|
|
33
33
|
get identityKey(): IdentityKey;
|
|
34
34
|
private generateSPK;
|
|
35
35
|
private generateOPK;
|
|
36
|
-
generateBundle(length?: number):
|
|
37
|
-
generateData():
|
|
38
|
-
digestData(message: KeyExchangeData, associatedData?: Uint8Array):
|
|
36
|
+
generateBundle(length?: number): KeyExchangeDataBundle;
|
|
37
|
+
generateData(): KeyExchangeData;
|
|
38
|
+
digestData(message: KeyExchangeData, associatedData?: Uint8Array): {
|
|
39
39
|
session: KeySession;
|
|
40
40
|
message: KeyExchangeSynMessage;
|
|
41
|
-
}
|
|
42
|
-
digestMessage(message: KeyExchangeSynMessage):
|
|
41
|
+
};
|
|
42
|
+
digestMessage(message: KeyExchangeSynMessage): {
|
|
43
43
|
session: KeySession;
|
|
44
44
|
associatedData: Uint8Array;
|
|
45
|
-
}
|
|
45
|
+
};
|
|
46
|
+
toJSON(): KeyExchangeState;
|
|
46
47
|
}
|
package/dist/x3dh.js
CHANGED
|
@@ -16,23 +16,14 @@
|
|
|
16
16
|
* You should have received a copy of the GNU General Public License
|
|
17
17
|
* along with this program. If not, see <https://www.gnu.org/licenses/>
|
|
18
18
|
*/
|
|
19
|
-
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
20
|
-
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
21
|
-
return new (P || (P = Promise))(function (resolve, reject) {
|
|
22
|
-
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
23
|
-
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
24
|
-
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
25
|
-
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
26
|
-
});
|
|
27
|
-
};
|
|
28
19
|
import crypto from "@freesignal/crypto";
|
|
29
20
|
import { KeySession } from "./double-ratchet.js";
|
|
30
21
|
import { concatBytes, decodeBase64, encodeBase64, compareBytes } from "@freesignal/utils";
|
|
31
22
|
import { decryptData, encryptData, IdentityKey } from "./types.js";
|
|
32
23
|
import { createIdentity } from "./index.js";
|
|
33
24
|
export class KeyExchange {
|
|
34
|
-
constructor(storage, privateIdentityKey) {
|
|
35
|
-
this.storage = storage;
|
|
25
|
+
constructor({ storage, privateIdentityKey } = {}) {
|
|
26
|
+
this.storage = new Map(storage);
|
|
36
27
|
this.privateIdentityKey = privateIdentityKey !== null && privateIdentityKey !== void 0 ? privateIdentityKey : createIdentity();
|
|
37
28
|
}
|
|
38
29
|
get identityKey() {
|
|
@@ -51,94 +42,92 @@ export class KeyExchange {
|
|
|
51
42
|
return { onetimePreKey, onetimePreKeyHash };
|
|
52
43
|
}
|
|
53
44
|
generateBundle(length) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
};
|
|
64
|
-
});
|
|
45
|
+
const { signedPreKey, signedPreKeyHash } = this.generateSPK();
|
|
46
|
+
const onetimePreKey = new Array(length !== null && length !== void 0 ? length : KeyExchange.maxOPK).fill(0).map(() => this.generateOPK(signedPreKeyHash).onetimePreKey);
|
|
47
|
+
return {
|
|
48
|
+
version: KeyExchange.version,
|
|
49
|
+
identityKey: this.identityKey.toString(),
|
|
50
|
+
signedPreKey: decodeBase64(signedPreKey.publicKey),
|
|
51
|
+
signature: decodeBase64(crypto.EdDSA.sign(signedPreKeyHash, this.privateIdentityKey.signatureKey)),
|
|
52
|
+
onetimePreKeys: onetimePreKey.map(opk => decodeBase64(opk.publicKey))
|
|
53
|
+
};
|
|
65
54
|
}
|
|
66
55
|
generateData() {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
};
|
|
77
|
-
});
|
|
56
|
+
const { signedPreKey, signedPreKeyHash } = this.generateSPK();
|
|
57
|
+
const { onetimePreKey } = this.generateOPK(signedPreKeyHash);
|
|
58
|
+
return {
|
|
59
|
+
version: KeyExchange.version,
|
|
60
|
+
identityKey: this.identityKey.toString(),
|
|
61
|
+
signedPreKey: decodeBase64(signedPreKey.publicKey),
|
|
62
|
+
signature: decodeBase64(crypto.EdDSA.sign(signedPreKeyHash, this.privateIdentityKey.signatureKey)),
|
|
63
|
+
onetimePreKey: decodeBase64(onetimePreKey.publicKey)
|
|
64
|
+
};
|
|
78
65
|
}
|
|
79
66
|
digestData(message, associatedData) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
};
|
|
111
|
-
});
|
|
67
|
+
//console.debug("Digest Data")
|
|
68
|
+
const ephemeralKey = crypto.ECDH.keyPair();
|
|
69
|
+
const signedPreKey = encodeBase64(message.signedPreKey);
|
|
70
|
+
const identityKey = IdentityKey.from(message.identityKey);
|
|
71
|
+
if (!crypto.EdDSA.verify(encodeBase64(message.signature), crypto.hash(signedPreKey), identityKey.signatureKey))
|
|
72
|
+
throw new Error("Signature verification failed");
|
|
73
|
+
const onetimePreKey = message.onetimePreKey ? encodeBase64(message.onetimePreKey) : undefined;
|
|
74
|
+
const signedPreKeyHash = crypto.hash(signedPreKey);
|
|
75
|
+
const onetimePreKeyHash = onetimePreKey ? crypto.hash(onetimePreKey) : new Uint8Array();
|
|
76
|
+
const derivedKey = crypto.hkdf(new Uint8Array([
|
|
77
|
+
...crypto.ECDH.scalarMult(this.privateIdentityKey.exchangeKey, signedPreKey),
|
|
78
|
+
...crypto.ECDH.scalarMult(ephemeralKey.secretKey, identityKey.exchangeKey),
|
|
79
|
+
...crypto.ECDH.scalarMult(ephemeralKey.secretKey, signedPreKey),
|
|
80
|
+
...onetimePreKey ? crypto.ECDH.scalarMult(ephemeralKey.secretKey, onetimePreKey) : new Uint8Array()
|
|
81
|
+
]), new Uint8Array(KeySession.keyLength).fill(0), KeyExchange.hkdfInfo, KeySession.keyLength * 3);
|
|
82
|
+
const session = new KeySession({ identityKey, remoteKey: identityKey.exchangeKey, rootKey: derivedKey.subarray(0, KeySession.keyLength), headerKey: derivedKey.subarray(KeySession.keyLength, KeySession.keyLength * 2), nextHeaderKey: derivedKey.subarray(KeySession.keyLength * 2) });
|
|
83
|
+
const encrypted = encryptData(session, concatBytes(crypto.hash(this.identityKey.toBytes()), crypto.hash(identityKey.toBytes()), associatedData !== null && associatedData !== void 0 ? associatedData : new Uint8Array()));
|
|
84
|
+
if (!encrypted)
|
|
85
|
+
throw new Error("Decryption error");
|
|
86
|
+
return {
|
|
87
|
+
session,
|
|
88
|
+
message: {
|
|
89
|
+
version: KeyExchange.version,
|
|
90
|
+
identityKey: this.identityKey.toString(),
|
|
91
|
+
ephemeralKey: decodeBase64(ephemeralKey.publicKey),
|
|
92
|
+
signedPreKeyHash: decodeBase64(signedPreKeyHash),
|
|
93
|
+
onetimePreKeyHash: decodeBase64(onetimePreKeyHash),
|
|
94
|
+
associatedData: decodeBase64(encrypted.toBytes())
|
|
95
|
+
}
|
|
96
|
+
};
|
|
112
97
|
}
|
|
113
98
|
digestMessage(message) {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
99
|
+
//console.debug("Digest Message")
|
|
100
|
+
const signedPreKey = this.storage.get(message.signedPreKeyHash);
|
|
101
|
+
const hash = message.signedPreKeyHash.concat(message.onetimePreKeyHash);
|
|
102
|
+
const onetimePreKey = this.storage.get(hash);
|
|
103
|
+
const identityKey = IdentityKey.from(message.identityKey);
|
|
104
|
+
if (!signedPreKey || !onetimePreKey || !message.identityKey || !message.ephemeralKey)
|
|
105
|
+
throw new Error("ACK message malformed");
|
|
106
|
+
if (!this.storage.delete(hash))
|
|
107
|
+
throw new Error("Bundle store deleting error");
|
|
108
|
+
const ephemeralKey = encodeBase64(message.ephemeralKey);
|
|
109
|
+
const derivedKey = crypto.hkdf(new Uint8Array([
|
|
110
|
+
...crypto.ECDH.scalarMult(signedPreKey.secretKey, identityKey.exchangeKey),
|
|
111
|
+
...crypto.ECDH.scalarMult(this.privateIdentityKey.exchangeKey, ephemeralKey),
|
|
112
|
+
...crypto.ECDH.scalarMult(signedPreKey.secretKey, ephemeralKey),
|
|
113
|
+
...onetimePreKey ? crypto.ECDH.scalarMult(onetimePreKey.secretKey, ephemeralKey) : new Uint8Array()
|
|
114
|
+
]), new Uint8Array(KeySession.keyLength).fill(0), KeyExchange.hkdfInfo, KeySession.keyLength * 3);
|
|
115
|
+
const session = new KeySession({ identityKey, secretKey: this.privateIdentityKey.exchangeKey, rootKey: derivedKey.subarray(0, KeySession.keyLength), nextHeaderKey: derivedKey.subarray(KeySession.keyLength, KeySession.keyLength * 2), headerKey: derivedKey.subarray(KeySession.keyLength * 2) });
|
|
116
|
+
const data = decryptData(session, encodeBase64(message.associatedData));
|
|
117
|
+
if (!data)
|
|
118
|
+
throw new Error("Error decrypting ACK message");
|
|
119
|
+
if (!compareBytes(data.subarray(0, 64), concatBytes(crypto.hash(identityKey.toBytes()), crypto.hash(this.identityKey.toBytes()))))
|
|
120
|
+
throw new Error("Error verifing Associated Data");
|
|
121
|
+
return {
|
|
122
|
+
session,
|
|
123
|
+
associatedData: data.subarray(64)
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
toJSON() {
|
|
127
|
+
return {
|
|
128
|
+
privateIdentityKey: this.privateIdentityKey,
|
|
129
|
+
storage: Array.from(this.storage.entries())
|
|
130
|
+
};
|
|
142
131
|
}
|
|
143
132
|
}
|
|
144
133
|
KeyExchange.version = 1;
|
package/package.json
CHANGED
|
@@ -1,45 +1,45 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@freesignal/protocol",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Signal Protocol implementation in
|
|
5
|
-
"license": "GPL-3.0-or-later",
|
|
6
|
-
"author": "Christian Braghette",
|
|
7
|
-
"type": "module",
|
|
8
|
-
"exports": {
|
|
9
|
-
".": {
|
|
10
|
-
"import": "./dist/index.js",
|
|
11
|
-
"require": "./dist/index.js",
|
|
12
|
-
"types": "./dist/index.d.ts"
|
|
13
|
-
},
|
|
14
|
-
"./double-ratchet": {
|
|
15
|
-
"import": "./dist/double-ratchet.js",
|
|
16
|
-
"require": "./dist/double-ratchet.js",
|
|
17
|
-
"types": "./dist/double-ratchet.d.ts"
|
|
18
|
-
},
|
|
19
|
-
"./node": {
|
|
20
|
-
"import": "./dist/node.js",
|
|
21
|
-
"require": "./dist/node.js",
|
|
22
|
-
"types": "./dist/node.d.ts"
|
|
23
|
-
},
|
|
24
|
-
"./x3dh": {
|
|
25
|
-
"import": "./dist/x3dh.js",
|
|
26
|
-
"require": "./dist/x3dh.js",
|
|
27
|
-
"types": "./dist/x3dh.d.ts"
|
|
28
|
-
}
|
|
29
|
-
},
|
|
30
|
-
"main": "./dist/index.js",
|
|
31
|
-
"scripts": {
|
|
32
|
-
"pretest": "tsc",
|
|
33
|
-
"test": "node ./dist/test.js",
|
|
34
|
-
"prepare": "tsc"
|
|
35
|
-
},
|
|
36
|
-
"dependencies": {
|
|
37
|
-
"@freesignal/crypto": "^0.4.2",
|
|
38
|
-
"@freesignal/interfaces": "^0.3.0",
|
|
39
|
-
"@freesignal/utils": "^1.5.1",
|
|
40
|
-
"easyemitter.ts": "^1.1.0"
|
|
41
|
-
},
|
|
42
|
-
"devDependencies": {
|
|
43
|
-
"@types/node": "^24.2.1"
|
|
44
|
-
}
|
|
45
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@freesignal/protocol",
|
|
3
|
+
"version": "0.9.1",
|
|
4
|
+
"description": "Signal Protocol implementation in TypeScript",
|
|
5
|
+
"license": "GPL-3.0-or-later",
|
|
6
|
+
"author": "Christian Braghette",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"require": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts"
|
|
13
|
+
},
|
|
14
|
+
"./double-ratchet": {
|
|
15
|
+
"import": "./dist/double-ratchet.js",
|
|
16
|
+
"require": "./dist/double-ratchet.js",
|
|
17
|
+
"types": "./dist/double-ratchet.d.ts"
|
|
18
|
+
},
|
|
19
|
+
"./node": {
|
|
20
|
+
"import": "./dist/node.js",
|
|
21
|
+
"require": "./dist/node.js",
|
|
22
|
+
"types": "./dist/node.d.ts"
|
|
23
|
+
},
|
|
24
|
+
"./x3dh": {
|
|
25
|
+
"import": "./dist/x3dh.js",
|
|
26
|
+
"require": "./dist/x3dh.js",
|
|
27
|
+
"types": "./dist/x3dh.d.ts"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"main": "./dist/index.js",
|
|
31
|
+
"scripts": {
|
|
32
|
+
"pretest": "tsc",
|
|
33
|
+
"test": "node ./dist/test.js",
|
|
34
|
+
"prepare": "tsc"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@freesignal/crypto": "^0.4.2",
|
|
38
|
+
"@freesignal/interfaces": "^0.3.0",
|
|
39
|
+
"@freesignal/utils": "^1.5.1",
|
|
40
|
+
"easyemitter.ts": "^1.1.0"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/node": "^24.2.1"
|
|
44
|
+
}
|
|
45
|
+
}
|