@freesignal/protocol 0.8.2 → 0.9.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/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 +33 -42
- package/dist/node.js +170 -138
- package/dist/types.d.ts +1 -12
- package/dist/types.js +33 -46
- package/dist/x3dh.d.ts +13 -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 } 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(privateIdentityKey?: PrivateIdentityKey): 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(privateIdentityKey) {
|
|
39
|
+
return new FreeSignalNode(privateIdentityKey);
|
|
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 } from "@freesignal/interfaces";
|
|
20
20
|
import { Datagram, EncryptedDatagram, IdentityKey, PrivateIdentityKey, Protocols, UserId } from "./types.js";
|
|
21
|
-
import { KeyExchange } from "./x3dh.js";
|
|
22
|
-
import { ExportedKeySession, KeySession } from "./double-ratchet.js";
|
|
21
|
+
import { KeyExchange, KeyExchangeState } from "./x3dh.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: KeyExchangeState;
|
|
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?: PrivateIdentityKey);
|
|
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,33 +74,24 @@ 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;
|
|
81
|
-
protected encrypt(receiverId: string | UserId, protocol: Protocols, data: Uint8Array):
|
|
82
|
-
sendHandshake(data: KeyExchangeData):
|
|
83
|
-
sendHandshake(session: KeySession):
|
|
84
|
-
sendHandshake(userId: UserId | string):
|
|
85
|
-
sendData<T>(receiverId: string | UserId, data: T):
|
|
86
|
-
sendRelay(relayId: string | UserId, receiverId: string | UserId, data: Datagram):
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
protected
|
|
91
|
-
protected
|
|
92
|
-
|
|
93
|
-
}
|
|
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]>>;
|
|
85
|
+
protected encrypt(receiverId: string | UserId, protocol: Protocols, data: Uint8Array): SendEventData;
|
|
86
|
+
sendHandshake(data: KeyExchangeData): void;
|
|
87
|
+
sendHandshake(session: KeySession): void;
|
|
88
|
+
sendHandshake(userId: UserId | string): void;
|
|
89
|
+
sendData<T>(receiverId: string | UserId, data: T): void;
|
|
90
|
+
sendRelay(relayId: string | UserId, receiverId: string | UserId, data: Datagram): void;
|
|
91
|
+
packBootstrap(): Datagram;
|
|
92
|
+
sendBootstrap(receiverId: string | UserId): void;
|
|
93
|
+
protected decrypt(datagram: EncryptedDatagram | Datagram | Uint8Array): MessageEventData;
|
|
94
|
+
protected openHandshake(datagram: Datagram | EncryptedDatagram | Uint8Array): 'syn' | 'ack';
|
|
95
|
+
protected open(datagram: Datagram | EncryptedDatagram | Uint8Array): void;
|
|
96
|
+
toJSON(): FreeSignalNodeState;
|
|
105
97
|
}
|
|
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, 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,11 @@ export class BootstrapRequest extends EventEmitter {
|
|
|
71
71
|
}
|
|
72
72
|
_BootstrapRequest_status = new WeakMap();
|
|
73
73
|
export class FreeSignalNode {
|
|
74
|
-
constructor(
|
|
75
|
-
this.
|
|
74
|
+
constructor(privateIdentityKey) {
|
|
75
|
+
this.sessions = new Map();
|
|
76
|
+
this.users = new Map();
|
|
77
|
+
this.bundles = new Map();
|
|
78
|
+
this.bootstraps = new Map();
|
|
76
79
|
this.emitter = new EventEmitter();
|
|
77
80
|
this.messageHandler = (data) => this.onMessage({ session: data.session, payload: data.payload });
|
|
78
81
|
this.sendHandler = (data) => this.onSend(data.datagram.toBytes());
|
|
@@ -82,16 +85,22 @@ export class FreeSignalNode {
|
|
|
82
85
|
this.onSend = () => { };
|
|
83
86
|
this.onHandshake = () => { };
|
|
84
87
|
this.onRequest = () => { };
|
|
88
|
+
this.onError = () => { };
|
|
89
|
+
this.onDebug = () => { };
|
|
85
90
|
this.privateIdentityKey = privateIdentityKey !== null && privateIdentityKey !== void 0 ? privateIdentityKey : createIdentity();
|
|
86
|
-
this.
|
|
87
|
-
this.users = storage.users;
|
|
88
|
-
this.keyExchange = new KeyExchange(storage.keyExchange, this.privateIdentityKey);
|
|
89
|
-
this.bundles = storage.bundles;
|
|
90
|
-
this.bootstraps = storage.bootstraps;
|
|
91
|
+
this.keyExchange = new KeyExchange({ privateIdentityKey: this.privateIdentityKey });
|
|
91
92
|
this.emitter.on('message', this.messageHandler);
|
|
92
93
|
this.emitter.on('send', this.sendHandler);
|
|
93
94
|
this.emitter.on('handshake', this.handshakeHandler);
|
|
94
95
|
this.emitter.on('bootstrap', this.bootstrapHandler);
|
|
96
|
+
this.emitter.on('error', (e) => this.onError(e.error));
|
|
97
|
+
this.emitter.on('debug', (e) => this.onDebug(e.debug));
|
|
98
|
+
}
|
|
99
|
+
error(error) {
|
|
100
|
+
this.emitter.emit('error', { error });
|
|
101
|
+
}
|
|
102
|
+
debug(...args) {
|
|
103
|
+
this.emitter.emit('debug', { debug: args });
|
|
95
104
|
}
|
|
96
105
|
getRequest(userId) {
|
|
97
106
|
return this.bootstraps.get(userId);
|
|
@@ -113,53 +122,57 @@ export class FreeSignalNode {
|
|
|
113
122
|
return this.identityKey.userId;
|
|
114
123
|
}
|
|
115
124
|
encrypt(receiverId, protocol, data) {
|
|
116
|
-
|
|
117
|
-
const sessionTag =
|
|
125
|
+
try {
|
|
126
|
+
const sessionTag = this.users.get(receiverId.toString());
|
|
118
127
|
if (!sessionTag)
|
|
119
128
|
throw new Error("User not found: " + receiverId);
|
|
120
|
-
const session =
|
|
129
|
+
const session = this.sessions.get(sessionTag);
|
|
121
130
|
if (!session)
|
|
122
131
|
throw new Error("Session not found for sessionTag: " + sessionTag);
|
|
123
132
|
const encrypted = encryptData(session, data);
|
|
124
133
|
this.sessions.set(receiverId.toString(), session);
|
|
125
134
|
return { session, userId: UserId.from(receiverId), datagram: new EncryptedDatagram(protocol, session.sessionTag, encrypted).sign(this.privateIdentityKey.signatureKey) };
|
|
126
|
-
}
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
this.error(error);
|
|
138
|
+
throw error;
|
|
139
|
+
}
|
|
127
140
|
}
|
|
128
141
|
sendHandshake(data) {
|
|
129
|
-
|
|
142
|
+
try {
|
|
130
143
|
if (data instanceof UserId || typeof data === 'string') {
|
|
131
|
-
const session =
|
|
144
|
+
const session = this.sessions.get(data.toString());
|
|
132
145
|
if (!session)
|
|
133
146
|
throw new Error("Session not found for userId: " + data.toString());
|
|
134
147
|
data = session;
|
|
135
148
|
}
|
|
136
149
|
if (data instanceof KeySession) {
|
|
137
150
|
//console.debug("Sending Handshake Ack");
|
|
138
|
-
const session =
|
|
151
|
+
const session = this.sessions.get(data.sessionTag);
|
|
139
152
|
if (!session)
|
|
140
153
|
throw new Error("Session not found for sessionTag: " + data.sessionTag);
|
|
141
|
-
this.emitter.emit('send',
|
|
154
|
+
this.emitter.emit('send', this.encrypt(session.userId, Protocols.HANDSHAKE, crypto.ECDH.scalarMult(this.privateIdentityKey.exchangeKey, session.identityKey.exchangeKey)));
|
|
142
155
|
return;
|
|
143
156
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
});
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
this.error(error);
|
|
160
|
+
throw error;
|
|
161
|
+
}
|
|
162
|
+
//console.debug("Sending Handshake Syn");
|
|
163
|
+
const { session, message } = this.keyExchange.digestData(data, encodeData(this.keyExchange.generateBundle()));
|
|
164
|
+
this.sessions.set(session.sessionTag, session);
|
|
165
|
+
this.users.set(session.userId.toString(), session.sessionTag);
|
|
166
|
+
const datagram = new Datagram(Protocols.HANDSHAKE, encodeData(message), session.sessionTag).sign(this.privateIdentityKey.signatureKey);
|
|
167
|
+
this.emitter.emit('send', { session, datagram, userId: session.userId });
|
|
151
168
|
}
|
|
152
169
|
sendData(receiverId, data) {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
this.emitter.emit('send', yield this.encrypt(receiverId, Protocols.MESSAGE, encodeData(data)));
|
|
156
|
-
});
|
|
170
|
+
//console.debug("Sending Data");
|
|
171
|
+
this.emitter.emit('send', this.encrypt(receiverId, Protocols.MESSAGE, encodeData(data)));
|
|
157
172
|
}
|
|
158
173
|
sendRelay(relayId, receiverId, data) {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
this.emitter.emit('send', yield this.encrypt(relayId, Protocols.RELAY, concatBytes(UserId.from(receiverId).toBytes(), data.toBytes())));
|
|
162
|
-
});
|
|
174
|
+
//console.debug("Sending Relay");
|
|
175
|
+
this.emitter.emit('send', this.encrypt(relayId, Protocols.RELAY, concatBytes(UserId.from(receiverId).toBytes(), data.toBytes())));
|
|
163
176
|
}
|
|
164
177
|
/*public async sendPing(receiverId: string | UserId): Promise<void> {
|
|
165
178
|
//console.debug("Sending Ping");
|
|
@@ -171,42 +184,43 @@ export class FreeSignalNode {
|
|
|
171
184
|
throw new Error("Session not found for sessionTag: " + sessionTag);
|
|
172
185
|
const datagram = new Datagram(Protocols.PING, undefined, session.sessionTag);
|
|
173
186
|
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
187
|
}
|
|
188
|
+
|
|
189
|
+
public async sendDiscover(receiverId: string | UserId, discoverId: string | UserId): Promise<void> {
|
|
190
|
+
//console.debug("Sending Discover");
|
|
191
|
+
if (receiverId instanceof UserId)
|
|
192
|
+
receiverId = receiverId.toString();
|
|
193
|
+
if (discoverId instanceof UserId)
|
|
194
|
+
discoverId = discoverId.toString();
|
|
195
|
+
const message: DiscoverMessage = {
|
|
196
|
+
type: DiscoverType.REQUEST,
|
|
197
|
+
discoverId
|
|
198
|
+
};
|
|
199
|
+
this.discovers.add(receiverId);
|
|
200
|
+
this.emitter.emit('send', await this.encrypt(receiverId, Protocols.DISCOVER, encodeData(message)));
|
|
201
|
+
}*/
|
|
190
202
|
packBootstrap() {
|
|
191
|
-
return
|
|
192
|
-
return new Datagram(Protocols.BOOTSTRAP, encodeData(yield this.keyExchange.generateData()));
|
|
193
|
-
});
|
|
203
|
+
return new Datagram(Protocols.BOOTSTRAP, encodeData(this.keyExchange.generateData()));
|
|
194
204
|
}
|
|
195
205
|
sendBootstrap(receiverId) {
|
|
196
|
-
|
|
206
|
+
try {
|
|
197
207
|
//console.debug("Sending Bootstrap");
|
|
198
|
-
if (
|
|
208
|
+
if (this.sessions.has(receiverId.toString()))
|
|
199
209
|
throw new Error("Session exists");
|
|
200
|
-
const datagram =
|
|
210
|
+
const datagram = this.packBootstrap();
|
|
201
211
|
this.emitter.emit('send', { datagram, userId: UserId.from(receiverId) });
|
|
202
|
-
}
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
this.error(error);
|
|
215
|
+
throw error;
|
|
216
|
+
}
|
|
203
217
|
}
|
|
204
218
|
decrypt(datagram) {
|
|
205
|
-
|
|
219
|
+
try {
|
|
206
220
|
datagram = EncryptedDatagram.from(datagram);
|
|
207
221
|
if (!datagram.sessionTag)
|
|
208
222
|
throw new Error("Datagram not encrypted");
|
|
209
|
-
const session =
|
|
223
|
+
const session = this.sessions.get(datagram.sessionTag);
|
|
210
224
|
if (!session)
|
|
211
225
|
throw new Error("Session not found for sessionTag: " + datagram.sessionTag);
|
|
212
226
|
if (!datagram.verify(session.identityKey.signatureKey))
|
|
@@ -216,17 +230,21 @@ export class FreeSignalNode {
|
|
|
216
230
|
const decrypted = decryptData(session, datagram.payload);
|
|
217
231
|
this.sessions.set(datagram.sessionTag, session);
|
|
218
232
|
return { session, payload: decrypted };
|
|
219
|
-
}
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
this.error(error);
|
|
236
|
+
throw error;
|
|
237
|
+
}
|
|
220
238
|
}
|
|
221
239
|
openHandshake(datagram) {
|
|
222
|
-
|
|
240
|
+
try {
|
|
223
241
|
const encrypted = EncryptedDatagram.from(datagram);
|
|
224
242
|
if (!encrypted.payload)
|
|
225
243
|
throw new Error("Missing payload");
|
|
226
|
-
if (
|
|
244
|
+
if (this.sessions.has(encrypted.sessionTag)) {
|
|
227
245
|
//console.debug("Opening Handshake Ack");
|
|
228
|
-
const session =
|
|
229
|
-
const { payload } =
|
|
246
|
+
const session = this.sessions.get(encrypted.sessionTag);
|
|
247
|
+
const { payload } = this.decrypt(encrypted);
|
|
230
248
|
if (!session)
|
|
231
249
|
throw new Error("Session not found for sessionTag: " + encrypted.sessionTag);
|
|
232
250
|
if (!compareBytes(payload, crypto.ECDH.scalarMult(this.privateIdentityKey.exchangeKey, session.identityKey.exchangeKey)))
|
|
@@ -237,15 +255,19 @@ export class FreeSignalNode {
|
|
|
237
255
|
const data = decodeData(encrypted.payload);
|
|
238
256
|
if (!encrypted.verify(IdentityKey.from(data.identityKey).signatureKey))
|
|
239
257
|
throw new Error("Signature not verified");
|
|
240
|
-
const { session, associatedData } =
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
258
|
+
const { session, associatedData } = this.keyExchange.digestMessage(data);
|
|
259
|
+
this.sessions.set(session.sessionTag, session);
|
|
260
|
+
this.users.set(session.userId.toString(), session.sessionTag);
|
|
261
|
+
this.bundles.set(session.userId.toString(), decodeData(associatedData));
|
|
244
262
|
return 'syn';
|
|
245
|
-
}
|
|
263
|
+
}
|
|
264
|
+
catch (error) {
|
|
265
|
+
this.error(error);
|
|
266
|
+
throw error;
|
|
267
|
+
}
|
|
246
268
|
}
|
|
247
269
|
open(datagram) {
|
|
248
|
-
|
|
270
|
+
try {
|
|
249
271
|
if (datagram instanceof Uint8Array)
|
|
250
272
|
datagram = Datagram.from(datagram);
|
|
251
273
|
switch (datagram.protocol) {
|
|
@@ -253,49 +275,66 @@ export class FreeSignalNode {
|
|
|
253
275
|
const encrypted = EncryptedDatagram.from(datagram);
|
|
254
276
|
if (!encrypted.payload)
|
|
255
277
|
throw new Error("Missing payload");
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
const session = yield this.sessions.get(encrypted.sessionTag);
|
|
278
|
+
const handshakeState = this.openHandshake(datagram);
|
|
279
|
+
const session = this.sessions.get(encrypted.sessionTag);
|
|
259
280
|
if (!session)
|
|
260
281
|
throw new Error("Session not found for sessionTag: " + encrypted.sessionTag);
|
|
261
|
-
|
|
282
|
+
if (handshakeState === 'syn')
|
|
283
|
+
this.sendHandshake(session);
|
|
262
284
|
this.emitter.emit('handshake', { session });
|
|
263
285
|
return;
|
|
264
286
|
}
|
|
265
287
|
case Protocols.MESSAGE:
|
|
266
288
|
//console.debug("Opening Message");
|
|
267
|
-
this.emitter.emit('message',
|
|
289
|
+
this.emitter.emit('message', this.decrypt(datagram));
|
|
268
290
|
return;
|
|
269
291
|
case Protocols.RELAY: {
|
|
270
292
|
//console.debug("Opening Relay");
|
|
271
|
-
const opened =
|
|
293
|
+
const opened = this.decrypt(datagram);
|
|
272
294
|
const userId = decodeBase64(opened.payload.subarray(0, UserId.keyLength));
|
|
273
|
-
const sessionTag =
|
|
295
|
+
const sessionTag = this.users.get(userId);
|
|
274
296
|
if (!sessionTag)
|
|
275
297
|
throw new Error("Session not found for user: " + userId);
|
|
276
|
-
const session =
|
|
298
|
+
const session = this.sessions.get(sessionTag);
|
|
277
299
|
if (!session)
|
|
278
300
|
throw new Error("Session not found for sessionTag: " + datagram.sessionTag);
|
|
279
301
|
this.emitter.emit('send', { session, datagram: Datagram.from(opened.payload.slice(UserId.keyLength)), userId: session.userId });
|
|
280
302
|
return;
|
|
281
303
|
}
|
|
282
|
-
case Protocols.
|
|
304
|
+
case Protocols.BOOTSTRAP: {
|
|
305
|
+
//console.debug("Opening Bootstrap");
|
|
306
|
+
if (!datagram.payload)
|
|
307
|
+
throw new Error("Invalid Bootstrap");
|
|
308
|
+
const keyExchangeData = decodeData(datagram.payload);
|
|
309
|
+
const userId = UserId.fromKey(keyExchangeData.identityKey);
|
|
310
|
+
const request = new BootstrapRequest(userId, keyExchangeData);
|
|
311
|
+
let sended = false;
|
|
312
|
+
request.onChange = (request) => {
|
|
313
|
+
if (request.status === 'accepted' && !sended) {
|
|
314
|
+
sended = true;
|
|
315
|
+
this.sendHandshake(request.data);
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
this.bootstraps.set(userId.toString(), request);
|
|
319
|
+
this.emitter.emit('bootstrap', { request });
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
/*case Protocols.DISCOVER: {
|
|
283
323
|
//console.debug("Opening Discover");
|
|
284
|
-
const { session, payload } =
|
|
285
|
-
const message = decodeData(payload);
|
|
286
|
-
if (message.type === DiscoverType.REQUEST && message.discoverId && !(
|
|
287
|
-
let data;
|
|
324
|
+
const { session, payload } = await this.decrypt(datagram);
|
|
325
|
+
const message = decodeData<DiscoverMessage>(payload);
|
|
326
|
+
if (message.type === DiscoverType.REQUEST && message.discoverId && !(await this.sessions.has(message.discoverId))) {
|
|
327
|
+
let data: KeyExchangeData;
|
|
288
328
|
if (message.discoverId === this.userId.toString()) {
|
|
289
|
-
data =
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
const bundle = yield this.bundles.get(message.discoverId);
|
|
329
|
+
data = await this.keyExchange.generateData();
|
|
330
|
+
} else {
|
|
331
|
+
const bundle = await this.bundles.get(message.discoverId);
|
|
293
332
|
if (!bundle)
|
|
294
333
|
return;
|
|
295
334
|
const { version, identityKey, signedPreKey, signature } = bundle;
|
|
296
335
|
const onetimePreKey = bundle.onetimePreKeys.shift();
|
|
297
336
|
if (!onetimePreKey) {
|
|
298
|
-
|
|
337
|
+
await this.bundles.delete(message.discoverId);
|
|
299
338
|
return;
|
|
300
339
|
}
|
|
301
340
|
data = {
|
|
@@ -306,35 +345,17 @@ export class FreeSignalNode {
|
|
|
306
345
|
onetimePreKey
|
|
307
346
|
};
|
|
308
347
|
}
|
|
309
|
-
const response = { type: DiscoverType.RESPONSE, discoverId: message.discoverId, data };
|
|
310
|
-
this.emitter.emit('send',
|
|
311
|
-
}
|
|
312
|
-
else if (message.type === DiscoverType.RESPONSE && this.discovers.has(message.discoverId)) {
|
|
348
|
+
const response: DiscoverMessage = { type: DiscoverType.RESPONSE, discoverId: message.discoverId, data };
|
|
349
|
+
this.emitter.emit('send', await this.encrypt(session.userId, Protocols.DISCOVER, encodeData(response)));
|
|
350
|
+
} else if (message.type === DiscoverType.RESPONSE && this.discovers.has(message.discoverId)) {
|
|
313
351
|
this.discovers.delete(message.discoverId);
|
|
314
352
|
if (message.data)
|
|
315
|
-
|
|
353
|
+
await this.sendHandshake(message.data);
|
|
316
354
|
}
|
|
317
355
|
return;
|
|
318
356
|
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
if (!datagram.payload)
|
|
322
|
-
throw new Error("Invalid Bootstrap");
|
|
323
|
-
const keyExchangeData = decodeData(datagram.payload);
|
|
324
|
-
const userId = UserId.fromKey(keyExchangeData.identityKey);
|
|
325
|
-
const request = new BootstrapRequest(userId, keyExchangeData);
|
|
326
|
-
let sended = false;
|
|
327
|
-
request.onChange = (request) => {
|
|
328
|
-
if (request.status === 'accepted' && !sended) {
|
|
329
|
-
sended = true;
|
|
330
|
-
this.sendHandshake(request.data);
|
|
331
|
-
}
|
|
332
|
-
};
|
|
333
|
-
yield this.bootstraps.set(userId.toString(), request);
|
|
334
|
-
this.emitter.emit('bootstrap', { request });
|
|
335
|
-
return;
|
|
336
|
-
}
|
|
337
|
-
/*case Protocols.PING:
|
|
357
|
+
|
|
358
|
+
case Protocols.PING:
|
|
338
359
|
datagram = EncryptedDatagram.from(datagram);
|
|
339
360
|
const session = await this.sessions.get(datagram.sessionTag!);
|
|
340
361
|
if (!session)
|
|
@@ -344,46 +365,57 @@ export class FreeSignalNode {
|
|
|
344
365
|
default:
|
|
345
366
|
throw new Error("Invalid protocol");
|
|
346
367
|
}
|
|
347
|
-
}
|
|
368
|
+
}
|
|
369
|
+
catch (error) {
|
|
370
|
+
this.error(error);
|
|
371
|
+
throw error;
|
|
372
|
+
}
|
|
348
373
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
374
|
+
toJSON() {
|
|
375
|
+
return {
|
|
376
|
+
privateIdentityKey: this.privateIdentityKey.toString(),
|
|
377
|
+
sessions: Array.from(this.sessions.entries()).map(([key, session]) => [key, session.toJSON()]),
|
|
378
|
+
users: Array.from(this.users.entries()),
|
|
379
|
+
bundles: Array.from(this.bundles),
|
|
380
|
+
keyExchange: this.keyExchange.toJSON(),
|
|
381
|
+
};
|
|
355
382
|
}
|
|
356
|
-
|
|
383
|
+
}
|
|
384
|
+
/*class SessionMap implements LocalStorage<string, KeySession> {
|
|
385
|
+
private readonly cache = new Map<string, KeySession>()
|
|
386
|
+
|
|
387
|
+
public constructor(public readonly storage: LocalStorage<string, KeySessionState>, public readonly maxSize = 50) { }
|
|
388
|
+
|
|
389
|
+
public set(key: string, value: KeySession): Promise<void> {
|
|
357
390
|
this.cache.set(key, value);
|
|
358
391
|
return this.storage.set(key, value.toJSON());
|
|
359
392
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
});
|
|
393
|
+
|
|
394
|
+
public async get(key: string): Promise<KeySession | undefined> {
|
|
395
|
+
const session = this.cache.get(key);
|
|
396
|
+
if (!session) {
|
|
397
|
+
const sessionData = await this.storage.get(key);
|
|
398
|
+
if (!sessionData)
|
|
399
|
+
return undefined;
|
|
400
|
+
return KeySession.from(sessionData);
|
|
401
|
+
}
|
|
402
|
+
return session;
|
|
371
403
|
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
});
|
|
404
|
+
|
|
405
|
+
public async has(key: string): Promise<boolean> {
|
|
406
|
+
return this.cache.has(key) || await this.storage.has(key);
|
|
376
407
|
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
});
|
|
408
|
+
|
|
409
|
+
public async delete(key: string): Promise<boolean> {
|
|
410
|
+
return this.cache.delete(key) || await this.storage.delete(key);
|
|
381
411
|
}
|
|
382
|
-
|
|
412
|
+
|
|
413
|
+
public clear(): Promise<void> {
|
|
383
414
|
this.cache.clear();
|
|
384
415
|
return this.storage.clear();
|
|
385
416
|
}
|
|
386
|
-
|
|
417
|
+
|
|
418
|
+
public entries(): Promise<Iterable<[string, KeySession]>> {
|
|
387
419
|
throw new Error("Method not implemented.");
|
|
388
420
|
}
|
|
389
|
-
}
|
|
421
|
+
}*/
|
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,22 @@ 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 }: {
|
|
33
|
+
storage?: Array<[string, Crypto.KeyPair]>;
|
|
34
|
+
privateIdentityKey?: PrivateIdentityKey;
|
|
35
|
+
});
|
|
33
36
|
get identityKey(): IdentityKey;
|
|
34
37
|
private generateSPK;
|
|
35
38
|
private generateOPK;
|
|
36
|
-
generateBundle(length?: number):
|
|
37
|
-
generateData():
|
|
38
|
-
digestData(message: KeyExchangeData, associatedData?: Uint8Array):
|
|
39
|
+
generateBundle(length?: number): KeyExchangeDataBundle;
|
|
40
|
+
generateData(): KeyExchangeData;
|
|
41
|
+
digestData(message: KeyExchangeData, associatedData?: Uint8Array): {
|
|
39
42
|
session: KeySession;
|
|
40
43
|
message: KeyExchangeSynMessage;
|
|
41
|
-
}
|
|
42
|
-
digestMessage(message: KeyExchangeSynMessage):
|
|
44
|
+
};
|
|
45
|
+
digestMessage(message: KeyExchangeSynMessage): {
|
|
43
46
|
session: KeySession;
|
|
44
47
|
associatedData: Uint8Array;
|
|
45
|
-
}
|
|
48
|
+
};
|
|
49
|
+
toJSON(): KeyExchangeState;
|
|
46
50
|
}
|
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.0",
|
|
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
|
+
}
|