@freesignal/protocol 0.2.11 → 0.3.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.
@@ -20,56 +20,34 @@ import { LocalStorage } from "@freesignal/interfaces";
20
20
  import { EncryptedData } from "./types";
21
21
  export interface ExportedKeySession {
22
22
  secretKey: string;
23
- remoteKey: string;
24
- rootKey: string;
25
- sendingChain: string;
26
- receivingChain: string;
27
- sendingCount: number;
28
- receivingCount: number;
29
- previousCount: number;
30
- previousKeys: [number, Uint8Array][];
23
+ rootKey?: string;
24
+ sendingChain?: ExportedKeyChain;
25
+ receivingChain?: ExportedKeyChain;
26
+ previousKeys: [string, Uint8Array][];
31
27
  }
32
28
  /**
33
29
  * Represents a secure Double Ratchet session.
34
30
  * Used for forward-secure encryption and decryption of messages.
35
31
  */
36
32
  export declare class KeySession {
37
- private static readonly skipLimit;
33
+ static readonly keyLength = 32;
38
34
  static readonly version = 1;
39
- static readonly rootKeyLength: number;
35
+ static readonly info: string;
40
36
  readonly id: string;
37
+ private readonly mutex;
38
+ private readonly storage;
41
39
  private keyPair;
42
- private _remoteKey?;
43
40
  private rootKey?;
44
41
  private sendingChain?;
45
- private sendingCount;
46
- private previousCount;
47
42
  private receivingChain?;
48
- private receivingCount;
49
43
  private previousKeys;
50
- private readonly storage;
51
44
  constructor(storage: LocalStorage<string, ExportedKeySession>, opts?: {
52
45
  id?: string;
53
46
  secretKey?: Uint8Array;
54
47
  remoteKey?: Uint8Array;
55
48
  rootKey?: Uint8Array;
56
49
  });
57
- /**
58
- * Whether both the sending and receiving chains are initialized.
59
- */
60
- get handshaked(): boolean;
61
- /**
62
- * The public key of this session.
63
- */
64
- get publicKey(): Uint8Array;
65
- /**
66
- * The last known remote public key.
67
- */
68
- get remoteKey(): Uint8Array | undefined;
69
- private setRemoteKey;
70
- private ratchetKeys;
71
- private getSendingKey;
72
- private getReceivingKey;
50
+ private getChain;
73
51
  private save;
74
52
  /**
75
53
  * Encrypts a message payload using the current sending chain.
@@ -84,7 +62,15 @@ export declare class KeySession {
84
62
  * @param payload - The received encrypted message.
85
63
  * @returns The decrypted message as a Uint8Array, or undefined if decryption fails.
86
64
  */
87
- decrypt(payload: Uint8Array | EncryptedData): Promise<Uint8Array | undefined>;
65
+ decrypt(payload: Uint8Array | EncryptedData): Promise<Uint8Array>;
66
+ /**
67
+ * Whether both the sending and receiving chains are initialized.
68
+ */
69
+ get handshaked(): boolean;
70
+ /**
71
+ * The public key of this session.
72
+ */
73
+ get publicKey(): Uint8Array;
88
74
  /**
89
75
  * Export the state of the session;
90
76
  */
@@ -96,12 +82,13 @@ export declare class KeySession {
96
82
  * @returns session with the state parsed.
97
83
  */
98
84
  static from(data: ExportedKeySession, storage: LocalStorage<string, ExportedKeySession>): KeySession;
99
- /**
100
- * The fixed key length (in bytes) used throughout the Double Ratchet session.
101
- * Typically 32 bytes (256 bits) for symmetric keys.
102
- */
103
- static readonly keyLength = 32;
104
- private static symmetricRatchet;
85
+ }
86
+ interface ExportedKeyChain {
87
+ publicKey: string;
88
+ remoteKey: string;
89
+ chainKey: string;
90
+ count: number;
91
+ previousCount: number;
105
92
  }
106
93
  export declare class EncryptedDataConstructor implements EncryptedData {
107
94
  static readonly secretKeyLength: number;
@@ -131,3 +118,4 @@ export declare class EncryptedDataConstructor implements EncryptedData {
131
118
  ciphertext: string;
132
119
  };
133
120
  }
121
+ export {};
package/double-ratchet.js CHANGED
@@ -26,6 +26,58 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
26
26
  step((generator = generator.apply(thisArg, _arguments || [])).next());
27
27
  });
28
28
  };
29
+ var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
30
+ if (value !== null && value !== void 0) {
31
+ if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
32
+ var dispose, inner;
33
+ if (async) {
34
+ if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
35
+ dispose = value[Symbol.asyncDispose];
36
+ }
37
+ if (dispose === void 0) {
38
+ if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
39
+ dispose = value[Symbol.dispose];
40
+ if (async) inner = dispose;
41
+ }
42
+ if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
43
+ if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
44
+ env.stack.push({ value: value, dispose: dispose, async: async });
45
+ }
46
+ else if (async) {
47
+ env.stack.push({ async: true });
48
+ }
49
+ return value;
50
+ };
51
+ var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
52
+ return function (env) {
53
+ function fail(e) {
54
+ env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
55
+ env.hasError = true;
56
+ }
57
+ var r, s = 0;
58
+ function next() {
59
+ while (r = env.stack.pop()) {
60
+ try {
61
+ if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
62
+ if (r.dispose) {
63
+ var result = r.dispose.call(r.value);
64
+ if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
65
+ }
66
+ else s |= 1;
67
+ }
68
+ catch (e) {
69
+ fail(e);
70
+ }
71
+ }
72
+ if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
73
+ if (env.hasError) throw env.error;
74
+ }
75
+ return next();
76
+ };
77
+ })(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
78
+ var e = new Error(message);
79
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
80
+ });
29
81
  var __importDefault = (this && this.__importDefault) || function (mod) {
30
82
  return (mod && mod.__esModule) ? mod : { "default": mod };
31
83
  };
@@ -34,6 +86,7 @@ exports.EncryptedDataConstructor = exports.KeySession = void 0;
34
86
  const crypto_1 = __importDefault(require("@freesignal/crypto"));
35
87
  const utils_1 = require("@freesignal/utils");
36
88
  const types_1 = require("./types");
89
+ const semaphore_ts_1 = require("semaphore.ts");
37
90
  /**
38
91
  * Represents a secure Double Ratchet session.
39
92
  * Used for forward-secure encryption and decryption of messages.
@@ -41,70 +94,25 @@ const types_1 = require("./types");
41
94
  class KeySession {
42
95
  constructor(storage, opts = {}) {
43
96
  var _a;
44
- this.sendingCount = 0;
45
- this.previousCount = 0;
46
- this.receivingCount = 0;
97
+ this.mutex = new semaphore_ts_1.AsyncMutex();
47
98
  this.previousKeys = new KeyMap();
48
99
  this.id = (_a = opts.id) !== null && _a !== void 0 ? _a : crypto_1.default.UUID.generate().toString();
49
100
  this.keyPair = crypto_1.default.ECDH.keyPair(opts.secretKey);
50
101
  if (opts.rootKey)
51
102
  this.rootKey = opts.rootKey;
52
103
  if (opts.remoteKey) {
53
- this._remoteKey = opts.remoteKey;
54
- this.sendingChain = this.ratchetKeys();
104
+ this.sendingChain = this.getChain(opts.remoteKey);
55
105
  }
56
106
  this.storage = storage;
57
- this.storage.set(this.id, this.toJSON());
58
- }
59
- /**
60
- * Whether both the sending and receiving chains are initialized.
61
- */
62
- get handshaked() { return this.sendingChain && this.receivingChain ? true : false; }
63
- /**
64
- * The public key of this session.
65
- */
66
- get publicKey() { return this.keyPair.publicKey; }
67
- /**
68
- * The last known remote public key.
69
- */
70
- get remoteKey() { return this._remoteKey; }
71
- setRemoteKey(key) {
72
- this._remoteKey = key;
73
- this.receivingChain = this.ratchetKeys();
74
- if (this.receivingCount > (EncryptedDataConstructor.maxCount - KeySession.skipLimit * 2))
75
- this.receivingCount = 0;
76
- this.previousCount = this.sendingCount;
77
- this.keyPair = crypto_1.default.ECDH.keyPair();
78
- this.sendingChain = this.ratchetKeys();
79
- if (this.sendingCount > (EncryptedDataConstructor.maxCount - KeySession.skipLimit * 2))
80
- this.sendingCount = 0;
81
- return this;
107
+ this.save();
82
108
  }
83
- ratchetKeys(info) {
84
- if (!this._remoteKey)
85
- throw new Error();
86
- const sharedKey = crypto_1.default.ECDH.scalarMult(this.keyPair.secretKey, this._remoteKey);
109
+ getChain(remoteKey, previousCount) {
110
+ const sharedKey = crypto_1.default.ECDH.scalarMult(this.keyPair.secretKey, remoteKey);
87
111
  if (!this.rootKey)
88
112
  this.rootKey = crypto_1.default.hash(sharedKey);
89
- const hashkey = crypto_1.default.hkdf(sharedKey, this.rootKey, info, KeySession.keyLength * 2);
90
- this.rootKey = hashkey.slice(0, KeySession.keyLength);
91
- return hashkey.slice(KeySession.keyLength);
92
- }
93
- getSendingKey() {
94
- if (!this.sendingChain)
95
- throw new Error;
96
- const { chainKey, sharedKey } = KeySession.symmetricRatchet(this.sendingChain);
97
- this.sendingChain = chainKey;
98
- this.sendingCount++;
99
- return sharedKey;
100
- }
101
- getReceivingKey() {
102
- if (!this.receivingChain)
103
- throw new Error();
104
- const { chainKey, sharedKey } = KeySession.symmetricRatchet(this.receivingChain);
105
- this.receivingChain = chainKey;
106
- this.receivingCount++;
107
- return sharedKey;
113
+ const hashkey = crypto_1.default.hkdf(sharedKey, this.rootKey, KeySession.info, KeySession.keyLength * 2);
114
+ this.rootKey = hashkey.subarray(0, KeySession.keyLength);
115
+ return new KeyChain(this.publicKey, remoteKey, hashkey.subarray(KeySession.keyLength), previousCount);
108
116
  }
109
117
  save() {
110
118
  return this.storage.set(this.id, this.toJSON());
@@ -117,13 +125,24 @@ class KeySession {
117
125
  */
118
126
  encrypt(message) {
119
127
  return __awaiter(this, void 0, void 0, function* () {
120
- const key = this.getSendingKey();
121
- if (this.sendingCount >= EncryptedDataConstructor.maxCount || this.previousCount >= EncryptedDataConstructor.maxCount)
122
- throw new Error();
123
- const nonce = crypto_1.default.randomBytes(EncryptedDataConstructor.nonceLength);
124
- const ciphertext = crypto_1.default.box.encrypt(message, nonce, key);
125
- yield this.save();
126
- return new EncryptedDataConstructor(this.sendingCount, this.previousCount, this.keyPair.publicKey, nonce, ciphertext);
128
+ const env_1 = { stack: [], error: void 0, hasError: false };
129
+ try {
130
+ const lock = __addDisposableResource(env_1, yield this.mutex.acquire(), false);
131
+ if (!this.sendingChain)
132
+ throw new Error("SendingChain not initialized");
133
+ const key = this.sendingChain.getKey();
134
+ const nonce = crypto_1.default.randomBytes(EncryptedDataConstructor.nonceLength);
135
+ const ciphertext = crypto_1.default.box.encrypt(message, nonce, key);
136
+ yield this.save();
137
+ return new EncryptedDataConstructor(this.sendingChain.count, this.sendingChain.previousCount, this.keyPair.publicKey, nonce, ciphertext);
138
+ }
139
+ catch (e_1) {
140
+ env_1.error = e_1;
141
+ env_1.hasError = true;
142
+ }
143
+ finally {
144
+ __disposeResources(env_1);
145
+ }
127
146
  });
128
147
  }
129
148
  /**
@@ -134,44 +153,62 @@ class KeySession {
134
153
  */
135
154
  decrypt(payload) {
136
155
  return __awaiter(this, void 0, void 0, function* () {
137
- const encrypted = types_1.EncryptedData.from(payload);
138
- const publicKey = encrypted.publicKey;
139
- if (this._remoteKey && !(0, utils_1.verifyArrays)(publicKey, this._remoteKey))
140
- while (this.receivingCount < encrypted.previous)
141
- this.previousKeys.set(this.receivingCount, this.getReceivingKey());
142
- this.setRemoteKey(publicKey);
143
- let key;
144
- const count = encrypted.count;
145
- if (this.receivingCount < count) {
146
- let i = 0;
147
- while (this.receivingCount < count - 1 && i < KeySession.skipLimit) {
148
- this.previousKeys.set(this.receivingCount, this.getReceivingKey());
156
+ var _a, _b, _c;
157
+ const env_2 = { stack: [], error: void 0, hasError: false };
158
+ try {
159
+ const lock = __addDisposableResource(env_2, yield this.mutex.acquire(), false);
160
+ const encrypted = types_1.EncryptedData.from(payload);
161
+ if (!(0, utils_1.verifyArrays)(encrypted.publicKey, (_b = (_a = this.receivingChain) === null || _a === void 0 ? void 0 : _a.remoteKey) !== null && _b !== void 0 ? _b : new Uint8Array())) {
162
+ while (this.receivingChain && this.receivingChain.count < encrypted.previous) {
163
+ const key = this.receivingChain.getKey();
164
+ this.previousKeys.set((0, utils_1.decodeBase64)(this.receivingChain.remoteKey) + this.receivingChain.count.toString(), key);
165
+ }
166
+ this.receivingChain = this.getChain(encrypted.publicKey);
167
+ this.keyPair = crypto_1.default.ECDH.keyPair();
168
+ this.sendingChain = this.getChain(encrypted.publicKey, (_c = this.sendingChain) === null || _c === void 0 ? void 0 : _c.count);
149
169
  }
150
- key = this.getReceivingKey();
170
+ if (!this.receivingChain)
171
+ throw new Error("Error initializing receivingChain");
172
+ while (this.receivingChain.count < encrypted.count) {
173
+ const key = this.receivingChain.getKey();
174
+ this.previousKeys.set((0, utils_1.decodeBase64)(this.receivingChain.remoteKey) + this.receivingChain.count.toString(), key);
175
+ }
176
+ const key = this.previousKeys.get((0, utils_1.decodeBase64)(encrypted.publicKey) + encrypted.count.toString());
177
+ if (!key)
178
+ throw new Error("Error calculating key");
179
+ yield this.save();
180
+ const cleartext = crypto_1.default.box.decrypt(encrypted.ciphertext, encrypted.nonce, key);
181
+ if (!cleartext)
182
+ throw new Error("Error decrypting ciphertext");
183
+ return cleartext;
184
+ }
185
+ catch (e_2) {
186
+ env_2.error = e_2;
187
+ env_2.hasError = true;
151
188
  }
152
- else {
153
- key = this.previousKeys.get(count);
189
+ finally {
190
+ __disposeResources(env_2);
154
191
  }
155
- if (!key)
156
- return undefined;
157
- yield this.save();
158
- return crypto_1.default.box.decrypt(encrypted.ciphertext, encrypted.nonce, key);
159
192
  });
160
193
  }
194
+ /**
195
+ * Whether both the sending and receiving chains are initialized.
196
+ */
197
+ get handshaked() { return this.sendingChain && this.receivingChain ? true : false; }
198
+ /**
199
+ * The public key of this session.
200
+ */
201
+ get publicKey() { return this.keyPair.publicKey; }
161
202
  /**
162
203
  * Export the state of the session;
163
204
  */
164
205
  toJSON() {
165
- var _a, _b, _c, _d;
206
+ var _a, _b;
166
207
  return {
167
- secretKey: (0, utils_1.decodeBase64)((0, utils_1.concatArrays)(this.keyPair.secretKey)),
168
- remoteKey: (0, utils_1.decodeBase64)((_a = this._remoteKey) !== null && _a !== void 0 ? _a : new Uint8Array()),
169
- rootKey: (0, utils_1.decodeBase64)((_b = this.rootKey) !== null && _b !== void 0 ? _b : new Uint8Array()),
170
- sendingChain: (0, utils_1.decodeBase64)((_c = this.sendingChain) !== null && _c !== void 0 ? _c : new Uint8Array()),
171
- receivingChain: (0, utils_1.decodeBase64)((_d = this.receivingChain) !== null && _d !== void 0 ? _d : new Uint8Array()),
172
- sendingCount: this.sendingCount,
173
- receivingCount: this.receivingCount,
174
- previousCount: this.previousCount,
208
+ secretKey: (0, utils_1.decodeBase64)(this.keyPair.secretKey),
209
+ rootKey: this.rootKey ? (0, utils_1.decodeBase64)(this.rootKey) : undefined,
210
+ sendingChain: (_a = this.sendingChain) === null || _a === void 0 ? void 0 : _a.toJSON(),
211
+ receivingChain: (_b = this.receivingChain) === null || _b === void 0 ? void 0 : _b.toJSON(),
175
212
  previousKeys: Array.from(this.previousKeys.entries())
176
213
  };
177
214
  }
@@ -182,34 +219,55 @@ class KeySession {
182
219
  * @returns session with the state parsed.
183
220
  */
184
221
  static from(data, storage) {
185
- const session = new KeySession(storage, { secretKey: (0, utils_1.encodeBase64)(data.secretKey), rootKey: (0, utils_1.encodeBase64)(data.rootKey) });
186
- session._remoteKey = (0, utils_1.encodeBase64)(data.remoteKey);
187
- session.sendingChain = (0, utils_1.encodeBase64)(data.sendingChain);
188
- session.receivingChain = (0, utils_1.encodeBase64)(data.receivingChain);
189
- session.sendingCount = data.sendingCount;
190
- session.receivingCount = data.receivingCount;
191
- session.previousCount = data.previousCount;
222
+ const session = new KeySession(storage, { secretKey: (0, utils_1.encodeBase64)(data.secretKey), rootKey: data.rootKey ? (0, utils_1.encodeBase64)(data.rootKey) : undefined });
223
+ //session._remoteKey = data.remoteKey ? encodeBase64(data.remoteKey) : undefined;
224
+ session.sendingChain = data.sendingChain ? KeyChain.from(data.sendingChain) : undefined;
225
+ session.receivingChain = data.receivingChain ? KeyChain.from(data.receivingChain) : undefined;
192
226
  session.previousKeys = new KeyMap(data.previousKeys);
193
227
  session.save();
194
228
  return session;
195
229
  }
196
- static symmetricRatchet(chain, salt, info) {
197
- const hash = crypto_1.default.hkdf(chain, salt !== null && salt !== void 0 ? salt : new Uint8Array(), info, KeySession.keyLength * 2);
230
+ }
231
+ exports.KeySession = KeySession;
232
+ KeySession.keyLength = 32;
233
+ KeySession.version = 1;
234
+ KeySession.info = "/freesignal/double-ratchet/v0." + KeySession.version;
235
+ class KeyChain {
236
+ constructor(publicKey, remoteKey, chainKey, previousCount = 0) {
237
+ this.publicKey = publicKey;
238
+ this.remoteKey = remoteKey;
239
+ this.chainKey = chainKey;
240
+ this.previousCount = previousCount;
241
+ this._count = 0;
242
+ }
243
+ getKey() {
244
+ if (++this._count >= EncryptedDataConstructor.maxCount)
245
+ throw new Error("SendingChain count too big");
246
+ const hash = crypto_1.default.hkdf(this.chainKey, new Uint8Array(KeySession.keyLength).fill(0), KeySession.info, KeySession.keyLength * 2);
247
+ this.chainKey = hash.subarray(0, KeySession.keyLength);
248
+ return hash.subarray(KeySession.keyLength);
249
+ }
250
+ toString() {
251
+ return "[object KeyChain]";
252
+ }
253
+ get count() {
254
+ return this._count;
255
+ }
256
+ toJSON() {
198
257
  return {
199
- chainKey: new Uint8Array(hash.buffer, 0, KeySession.keyLength),
200
- sharedKey: new Uint8Array(hash.buffer, KeySession.keyLength)
258
+ publicKey: (0, utils_1.decodeBase64)(this.publicKey),
259
+ remoteKey: (0, utils_1.decodeBase64)(this.remoteKey),
260
+ chainKey: (0, utils_1.decodeBase64)(this.chainKey),
261
+ count: this.count,
262
+ previousCount: this.previousCount
201
263
  };
202
264
  }
265
+ static from(obj) {
266
+ const chain = new KeyChain((0, utils_1.encodeBase64)(obj.publicKey), (0, utils_1.encodeBase64)(obj.remoteKey), (0, utils_1.encodeBase64)(obj.chainKey), obj.previousCount);
267
+ chain._count = obj.count;
268
+ return chain;
269
+ }
203
270
  }
204
- exports.KeySession = KeySession;
205
- KeySession.skipLimit = 1000;
206
- KeySession.version = 1;
207
- KeySession.rootKeyLength = crypto_1.default.box.keyLength;
208
- /**
209
- * The fixed key length (in bytes) used throughout the Double Ratchet session.
210
- * Typically 32 bytes (256 bits) for symmetric keys.
211
- */
212
- KeySession.keyLength = 32;
213
271
  class EncryptedDataConstructor {
214
272
  constructor(...arrays) {
215
273
  arrays = arrays.filter(value => value !== undefined);
package/index.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 { LocalStorage, Crypto } from "@freesignal/interfaces";
20
- import { ExportedKeySession, KeySession } from "./double-ratchet";
21
- import { KeyExchange } from "./x3dh";
22
- import { PrivateIdentityKey } from "./types";
19
+ import { LocalStorage, Crypto, Database } from "@freesignal/interfaces";
20
+ import { ExportedKeySession } from "./double-ratchet";
21
+ import { IdentityKey, PrivateIdentityKey } from "./types";
22
+ import { FreeSignalNode } from "./node";
23
23
  /**
24
24
  * Creates a new Double Ratchet session for secure message exchange.
25
25
  *
@@ -29,21 +29,12 @@ import { PrivateIdentityKey } from "./types";
29
29
  * @param opts.rootKey - An optional root key to initialize the session.
30
30
  * @returns A new instance of {@link KeySession}.
31
31
  */
32
- export declare function createKeySession(storage: LocalStorage<string, ExportedKeySession>, opts?: {
33
- secretKey?: Uint8Array;
34
- remoteKey?: Uint8Array;
35
- rootKey?: Uint8Array;
36
- }): KeySession;
37
32
  /**
38
33
  * Creates a new X3DH (Extended Triple Diffie-Hellman) key exchange session.
39
34
  *
40
35
  * @param storage - Local storage for keys.
41
36
  * @returns A new instance of {@link KeyExchange}.
42
37
  */
43
- export declare function createKeyExchange(storage: {
44
- keys: LocalStorage<string, Crypto.KeyPair>;
45
- sessions: LocalStorage<string, ExportedKeySession>;
46
- }, privateIdentityKey?: PrivateIdentityKey): KeyExchange;
47
38
  /**
48
39
  * Generates identity key
49
40
  *
@@ -51,4 +42,10 @@ export declare function createKeyExchange(storage: {
51
42
  * @returns An object containing readonly signing and box key pairs.
52
43
  */
53
44
  export declare function createIdentity(seed?: Uint8Array): PrivateIdentityKey;
45
+ /** */
46
+ export declare function createNode(storage: Database<{
47
+ sessions: LocalStorage<string, ExportedKeySession>;
48
+ keyExchange: LocalStorage<string, Crypto.KeyPair>;
49
+ users: LocalStorage<string, IdentityKey>;
50
+ }>, privateIdentityKey?: PrivateIdentityKey): FreeSignalNode;
54
51
  export * from "./types";
package/index.js CHANGED
@@ -35,13 +35,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
35
35
  return (mod && mod.__esModule) ? mod : { "default": mod };
36
36
  };
37
37
  Object.defineProperty(exports, "__esModule", { value: true });
38
- exports.createKeySession = createKeySession;
39
- exports.createKeyExchange = createKeyExchange;
40
38
  exports.createIdentity = createIdentity;
39
+ exports.createNode = createNode;
41
40
  const crypto_1 = __importDefault(require("@freesignal/crypto"));
42
- const double_ratchet_1 = require("./double-ratchet");
43
- const x3dh_1 = require("./x3dh");
44
41
  const types_1 = require("./types");
42
+ const node_1 = require("./node");
45
43
  /**
46
44
  * Creates a new Double Ratchet session for secure message exchange.
47
45
  *
@@ -51,18 +49,18 @@ const types_1 = require("./types");
51
49
  * @param opts.rootKey - An optional root key to initialize the session.
52
50
  * @returns A new instance of {@link KeySession}.
53
51
  */
54
- function createKeySession(storage, opts) {
55
- return new double_ratchet_1.KeySession(storage, opts);
56
- }
52
+ /*export function createKeySession(storage: LocalStorage<string, ExportedKeySession>, opts?: { secretKey?: Uint8Array, remoteKey?: Uint8Array, rootKey?: Uint8Array }): KeySession {
53
+ return new KeySession(storage, opts);
54
+ }*/
57
55
  /**
58
56
  * Creates a new X3DH (Extended Triple Diffie-Hellman) key exchange session.
59
57
  *
60
58
  * @param storage - Local storage for keys.
61
59
  * @returns A new instance of {@link KeyExchange}.
62
60
  */
63
- function createKeyExchange(storage, privateIdentityKey) {
64
- return new x3dh_1.KeyExchange(storage, privateIdentityKey);
65
- }
61
+ /*export function createKeyExchange(storage: { keys: LocalStorage<string, Crypto.KeyPair>, sessions: LocalStorage<string, ExportedKeySession> }, privateIdentityKey?: PrivateIdentityKey): KeyExchange {
62
+ return new KeyExchange(storage, privateIdentityKey);
63
+ }*/
66
64
  /**
67
65
  * Generates identity key
68
66
  *
@@ -77,4 +75,8 @@ function createIdentity(seed) {
77
75
  const exchangeKeyPair = crypto_1.default.ECDH.keyPair(exchangeSeed);
78
76
  return types_1.PrivateIdentityKey.from(signatureKeyPair.secretKey, exchangeKeyPair.secretKey);
79
77
  }
78
+ /** */
79
+ function createNode(storage, privateIdentityKey) {
80
+ return new node_1.FreeSignalNode(storage, privateIdentityKey);
81
+ }
80
82
  __exportStar(require("./types"), exports);
package/node.d.ts CHANGED
@@ -4,8 +4,7 @@ import { KeyExchange } from "./x3dh";
4
4
  import { ExportedKeySession, KeySession } from "./double-ratchet";
5
5
  export declare class FreeSignalNode {
6
6
  protected readonly privateIdentityKey: PrivateIdentityKey;
7
- protected readonly sessions: Map<string, KeySession>;
8
- protected readonly sessionsData: LocalStorage<string, ExportedKeySession>;
7
+ protected readonly sessions: SessionMap;
9
8
  protected readonly users: LocalStorage<string, IdentityKey>;
10
9
  protected readonly keyExchange: KeyExchange;
11
10
  constructor(storage: Database<{
@@ -25,3 +24,15 @@ export declare class FreeSignalNode {
25
24
  decrypt(datagram: Datagram): Promise<Uint8Array>;
26
25
  receive<T extends Uint8Array | UserId | Datagram | UserId | void>(datagram: Datagram | Uint8Array): Promise<T>;
27
26
  }
27
+ declare class SessionMap implements LocalStorage<string, KeySession> {
28
+ readonly storage: LocalStorage<string, ExportedKeySession>;
29
+ readonly maxSize: number;
30
+ private readonly cache;
31
+ constructor(storage: LocalStorage<string, ExportedKeySession>, maxSize?: number);
32
+ set(key: string, value: KeySession): Promise<void>;
33
+ get(key: string): Promise<KeySession | undefined>;
34
+ has(key: string): Promise<boolean>;
35
+ delete(key: string): Promise<boolean>;
36
+ clear(): Promise<void>;
37
+ }
38
+ export {};
package/node.js CHANGED
@@ -12,18 +12,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.FreeSignalNode = void 0;
13
13
  const types_1 = require("./types");
14
14
  const x3dh_1 = require("./x3dh");
15
+ const double_ratchet_1 = require("./double-ratchet");
15
16
  const _1 = require(".");
16
17
  const utils_1 = require("@freesignal/utils");
17
18
  class FreeSignalNode {
18
19
  constructor(storage, privateIdentityKey) {
19
- this.sessions = new Map();
20
20
  this.privateIdentityKey = privateIdentityKey !== null && privateIdentityKey !== void 0 ? privateIdentityKey : (0, _1.createIdentity)();
21
- this.sessionsData = storage.sessions;
21
+ this.sessions = new SessionMap(storage.sessions);
22
22
  this.keyExchange = new x3dh_1.KeyExchange({ keys: storage.keyExchange, sessions: storage.sessions }, this.privateIdentityKey);
23
23
  this.users = storage.users;
24
- /*Array.from(this.sessionsData.entries()).forEach(([userId, sessionData]) => {
25
- this.sessions.set(userId, KeySession.from(sessionData, this.sessionsData));
26
- });*/
27
24
  }
28
25
  get userId() {
29
26
  return types_1.UserId.fromKey(this.privateIdentityKey.identityKey);
@@ -41,7 +38,7 @@ class FreeSignalNode {
41
38
  ;
42
39
  encrypt(receiverId, protocol, data) {
43
40
  return __awaiter(this, void 0, void 0, function* () {
44
- const session = this.sessions.get(receiverId);
41
+ const session = yield this.sessions.get(receiverId);
45
42
  if (!session)
46
43
  throw new Error("Session not found for user: " + receiverId);
47
44
  return new types_1.Datagram(this.userId.toString(), receiverId, protocol, yield session.encrypt(data));
@@ -67,7 +64,7 @@ class FreeSignalNode {
67
64
  decrypt(datagram) {
68
65
  return __awaiter(this, void 0, void 0, function* () {
69
66
  const userId = datagram.sender;
70
- const session = this.sessions.get(userId);
67
+ const session = yield this.sessions.get(userId);
71
68
  if (!session)
72
69
  throw new Error("Session not found for user: " + userId);
73
70
  if (!datagram.payload)
@@ -103,3 +100,40 @@ class FreeSignalNode {
103
100
  }
104
101
  }
105
102
  exports.FreeSignalNode = FreeSignalNode;
103
+ class SessionMap {
104
+ constructor(storage, maxSize = 50) {
105
+ this.storage = storage;
106
+ this.maxSize = maxSize;
107
+ this.cache = new Map();
108
+ }
109
+ set(key, value) {
110
+ this.cache.set(key, value);
111
+ return this.storage.set(key, value.toJSON());
112
+ }
113
+ get(key) {
114
+ return __awaiter(this, void 0, void 0, function* () {
115
+ const session = this.cache.get(key);
116
+ if (!session) {
117
+ const sessionData = yield this.storage.get(key);
118
+ if (!sessionData)
119
+ return undefined;
120
+ return double_ratchet_1.KeySession.from(sessionData, this.storage);
121
+ }
122
+ return session;
123
+ });
124
+ }
125
+ has(key) {
126
+ return __awaiter(this, void 0, void 0, function* () {
127
+ return this.cache.has(key) || (yield this.storage.has(key));
128
+ });
129
+ }
130
+ delete(key) {
131
+ return __awaiter(this, void 0, void 0, function* () {
132
+ return this.cache.delete(key) || (yield this.storage.delete(key));
133
+ });
134
+ }
135
+ clear() {
136
+ this.cache.clear();
137
+ return this.storage.clear();
138
+ }
139
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@freesignal/protocol",
3
- "version": "0.2.11",
3
+ "version": "0.3.0",
4
4
  "description": "Signal Protocol implementation in javascript",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "author": "Christian Braghette",
@@ -13,7 +13,8 @@
13
13
  "dependencies": {
14
14
  "@freesignal/crypto": "^0.3.0",
15
15
  "@freesignal/interfaces": "^0.2.0",
16
- "@freesignal/utils": "^1.3.0"
16
+ "@freesignal/utils": "^1.3.0",
17
+ "semaphore.ts": "^0.2.0"
17
18
  },
18
19
  "devDependencies": {
19
20
  "@types/node": "^24.2.1"
package/test.js CHANGED
@@ -11,15 +11,18 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  const utils_1 = require("@freesignal/utils");
13
13
  const _1 = require(".");
14
- const node_1 = require("./node");
15
- const bob = new node_1.FreeSignalNode({ keyExchange: new _1.AsyncMap(), sessions: new _1.AsyncMap(), users: new _1.AsyncMap() });
16
- const alice = new node_1.FreeSignalNode({ keyExchange: new _1.AsyncMap(), sessions: new _1.AsyncMap(), users: new _1.AsyncMap() });
14
+ const bob = (0, _1.createNode)({ keyExchange: new _1.AsyncMap(), sessions: new _1.AsyncMap(), users: new _1.AsyncMap() });
15
+ const alice = (0, _1.createNode)({ keyExchange: new _1.AsyncMap(), sessions: new _1.AsyncMap(), users: new _1.AsyncMap() });
17
16
  setImmediate(() => __awaiter(void 0, void 0, void 0, function* () {
18
17
  const aliceHandshake = yield alice.sendHandshake(yield bob.generateKeyData());
19
18
  yield bob.receive(aliceHandshake);
20
19
  console.log("Session established successfully between Alice and Bob.");
21
- const data = (yield bob.encrypt(alice.userId.toString(), _1.Protocols.MESSAGE, (0, utils_1.encodeData)("Hi Alice!"))).toBytes();
22
- console.log((0, utils_1.decodeData)(yield alice.receive(data)));
23
- const longmsg = yield alice.sendData(bob.userId.toString(), new Uint8Array(1000000).fill(33).map(val => val + Math.floor(Math.random() * 93)));
24
- console.log(longmsg.toBytes().length);
20
+ const first = (yield bob.sendData(alice.userId.toString(), (0, utils_1.encodeData)("Hi Alice!"))).toBytes();
21
+ console.log("Bob: ", (0, utils_1.decodeData)(yield alice.receive(first)));
22
+ const second = yield alice.sendData(bob.userId.toString(), (0, utils_1.encodeData)("Hi Bob!"));
23
+ console.log("Alice: ", (0, utils_1.decodeData)(yield bob.receive(second)));
24
+ const third = yield Promise.all(["How are you?", "How are this days?", "For me it's a good time"].map(msg => bob.sendData(alice.userId.toString(), (0, utils_1.encodeData)(msg))));
25
+ third.forEach((value) => __awaiter(void 0, void 0, void 0, function* () {
26
+ console.log("Bob: ", (0, utils_1.decodeData)(yield alice.receive(value)));
27
+ }));
25
28
  }));
package/types.js CHANGED
@@ -53,7 +53,7 @@ class UserId {
53
53
  identityKey = (0, utils_1.encodeBase64)(identityKey);
54
54
  else if (IdentityKey.isIdentityKeys(identityKey))
55
55
  identityKey = identityKey.toBytes();
56
- return new UserId(crypto_1.default.hash(identityKey));
56
+ return new UserId(crypto_1.default.hkdf(identityKey, new Uint8Array(32).fill(0), "/freesignal/userid"));
57
57
  }
58
58
  static from(userId) {
59
59
  if (typeof userId === 'string')
package/x3dh.js CHANGED
@@ -98,9 +98,9 @@ class KeyExchange {
98
98
  ...crypto_1.default.ECDH.scalarMult(ephemeralKey.secretKey, identityKey.exchangeKey),
99
99
  ...crypto_1.default.ECDH.scalarMult(ephemeralKey.secretKey, signedPreKey),
100
100
  ...onetimePreKey ? crypto_1.default.ECDH.scalarMult(ephemeralKey.secretKey, onetimePreKey) : new Uint8Array()
101
- ]), new Uint8Array(double_ratchet_1.KeySession.rootKeyLength).fill(0), KeyExchange.hkdfInfo, double_ratchet_1.KeySession.rootKeyLength);
101
+ ]), new Uint8Array(double_ratchet_1.KeySession.keyLength).fill(0), KeyExchange.hkdfInfo, double_ratchet_1.KeySession.keyLength);
102
102
  const session = new double_ratchet_1.KeySession(this.sessions, { remoteKey: identityKey.exchangeKey, rootKey });
103
- const cyphertext = yield session.encrypt((0, utils_1.concatArrays)(crypto_1.default.hash(this.identityKey.exchangeKey), crypto_1.default.hash(identityKey.exchangeKey)));
103
+ const cyphertext = yield session.encrypt((0, utils_1.concatArrays)(crypto_1.default.hash(this.identityKey.toBytes()), crypto_1.default.hash(identityKey.toBytes())));
104
104
  if (!cyphertext)
105
105
  throw new Error("Decryption error");
106
106
  return {
@@ -133,12 +133,12 @@ class KeyExchange {
133
133
  ...crypto_1.default.ECDH.scalarMult(this.privateIdentityKey.exchangeKey, ephemeralKey),
134
134
  ...crypto_1.default.ECDH.scalarMult(signedPreKey.secretKey, ephemeralKey),
135
135
  ...onetimePreKey ? crypto_1.default.ECDH.scalarMult(onetimePreKey.secretKey, ephemeralKey) : new Uint8Array()
136
- ]), new Uint8Array(double_ratchet_1.KeySession.rootKeyLength).fill(0), KeyExchange.hkdfInfo, double_ratchet_1.KeySession.rootKeyLength);
136
+ ]), new Uint8Array(double_ratchet_1.KeySession.keyLength).fill(0), KeyExchange.hkdfInfo, double_ratchet_1.KeySession.keyLength);
137
137
  const session = new double_ratchet_1.KeySession(this.sessions, { secretKey: this.privateIdentityKey.exchangeKey, rootKey });
138
138
  const cleartext = yield session.decrypt((0, utils_1.encodeBase64)(message.associatedData));
139
139
  if (!cleartext)
140
140
  throw new Error("Error decrypting ACK message");
141
- if (!(0, utils_1.verifyArrays)(cleartext, (0, utils_1.concatArrays)(crypto_1.default.hash(identityKey.exchangeKey), crypto_1.default.hash(this.identityKey.exchangeKey))))
141
+ if (!(0, utils_1.verifyArrays)(cleartext, (0, utils_1.concatArrays)(crypto_1.default.hash(identityKey.toBytes()), crypto_1.default.hash(this.identityKey.toBytes()))))
142
142
  throw new Error("Error verifing Associated Data");
143
143
  return {
144
144
  session,
@@ -149,5 +149,5 @@ class KeyExchange {
149
149
  }
150
150
  exports.KeyExchange = KeyExchange;
151
151
  KeyExchange.version = 1;
152
- KeyExchange.hkdfInfo = (0, utils_1.encodeUTF8)("freesignal/x3dh/" + KeyExchange.version);
152
+ KeyExchange.hkdfInfo = "freesignal/x3dh/" + KeyExchange.version;
153
153
  KeyExchange.maxOPK = 10;