@freesignal/protocol 0.2.5 → 0.2.8

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/api.d.ts CHANGED
@@ -1,20 +1,37 @@
1
+ /**
2
+ * FreeSignal Protocol
3
+ *
4
+ * Copyright (C) 2025 Christian Braghette
5
+ *
6
+ * This program is free software: you can redistribute it and/or modify
7
+ * it under the terms of the GNU General Public License as published by
8
+ * the Free Software Foundation, either version 3 of the License, or
9
+ * (at your option) any later version.
10
+ *
11
+ * This program is distributed in the hope that it will be useful,
12
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ * GNU General Public License for more details.
15
+ *
16
+ * You should have received a copy of the GNU General Public License
17
+ * along with this program. If not, see <https://www.gnu.org/licenses/>
18
+ */
1
19
  import { Crypto, KeyExchangeData, KeyExchangeDataBundle, KeyExchangeSynMessage, LocalStorage } from "@freesignal/interfaces";
2
- import { KeySession } from "./double-ratchet";
20
+ import { ExportedKeySession } from "./double-ratchet";
3
21
  import { KeyExchange } from "./x3dh";
4
22
  import { Datagram, IdentityKeys, EncryptedData, UserId } from "./types";
5
- export declare const FREESIGNAL_MIME = "application/x-freesignal";
6
23
  type DatagramId = string;
7
24
  export declare class FreeSignalAPI {
8
25
  protected readonly signKey: Crypto.KeyPair;
9
26
  protected readonly boxKey: Crypto.KeyPair;
10
- protected readonly sessions: LocalStorage<UserId, KeySession>;
27
+ protected readonly sessions: LocalStorage<UserId, ExportedKeySession>;
11
28
  protected readonly keyExchange: KeyExchange;
12
29
  protected readonly users: LocalStorage<UserId, IdentityKeys>;
13
30
  readonly userId: UserId;
14
31
  constructor(opts: {
15
32
  secretSignKey: Uint8Array;
16
33
  secretBoxKey: Uint8Array;
17
- sessions: LocalStorage<UserId, KeySession>;
34
+ sessions: LocalStorage<UserId, ExportedKeySession>;
18
35
  keyExchange: LocalStorage<string, Crypto.KeyPair>;
19
36
  users: LocalStorage<UserId, IdentityKeys>;
20
37
  });
@@ -33,9 +50,5 @@ export declare class FreeSignalAPI {
33
50
  identityKeys: IdentityKeys;
34
51
  userId: UserId;
35
52
  }>;
36
- protected packIdList(datagramIds: DatagramId[]): Uint8Array;
37
- protected unpackIdList(data: Uint8Array): DatagramId[];
38
- protected packDatagrams(messages: Datagram[]): Uint8Array;
39
- protected unpackDatagrams(data: Uint8Array): Datagram[];
40
53
  }
41
54
  export {};
package/api.js CHANGED
@@ -1,4 +1,22 @@
1
1
  "use strict";
2
+ /**
3
+ * FreeSignal Protocol
4
+ *
5
+ * Copyright (C) 2025 Christian Braghette
6
+ *
7
+ * This program is free software: you can redistribute it and/or modify
8
+ * it under the terms of the GNU General Public License as published by
9
+ * the Free Software Foundation, either version 3 of the License, or
10
+ * (at your option) any later version.
11
+ *
12
+ * This program is distributed in the hope that it will be useful,
13
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ * GNU General Public License for more details.
16
+ *
17
+ * You should have received a copy of the GNU General Public License
18
+ * along with this program. If not, see <https://www.gnu.org/licenses/>
19
+ */
2
20
  var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
21
  function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
22
  return new (P || (P = Promise))(function (resolve, reject) {
@@ -12,13 +30,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
12
30
  return (mod && mod.__esModule) ? mod : { "default": mod };
13
31
  };
14
32
  Object.defineProperty(exports, "__esModule", { value: true });
15
- exports.FreeSignalAPI = exports.FREESIGNAL_MIME = void 0;
33
+ exports.FreeSignalAPI = void 0;
16
34
  const crypto_1 = __importDefault(require("@freesignal/crypto"));
35
+ const double_ratchet_1 = require("./double-ratchet");
17
36
  const x3dh_1 = require("./x3dh");
18
37
  const utils_1 = require("@freesignal/utils");
19
38
  const types_1 = require("./types");
20
- const fflate_1 = __importDefault(require("fflate"));
21
- exports.FREESIGNAL_MIME = "application/x-freesignal";
22
39
  class FreeSignalAPI {
23
40
  constructor(opts) {
24
41
  const { secretSignKey, secretBoxKey, sessions, keyExchange, users } = opts;
@@ -37,23 +54,25 @@ class FreeSignalAPI {
37
54
  }
38
55
  encryptData(data, userId) {
39
56
  return __awaiter(this, void 0, void 0, function* () {
40
- const session = yield this.sessions.get(userId);
41
- if (!session)
57
+ const sessionJson = yield this.sessions.get(userId);
58
+ if (!sessionJson)
42
59
  throw new Error('Session not found for user: ' + userId);
60
+ const session = double_ratchet_1.KeySession.from(sessionJson);
43
61
  const encrypted = session.encrypt(data);
44
- this.sessions.set(userId, session); // Ensure session is updated
62
+ this.sessions.set(userId, session.toJSON()); // Ensure session is updated
45
63
  return encrypted;
46
64
  });
47
65
  }
48
66
  decryptData(data, userId) {
49
67
  return __awaiter(this, void 0, void 0, function* () {
50
- const session = yield this.sessions.get(userId);
51
- if (!session)
68
+ const sessionJson = yield this.sessions.get(userId);
69
+ if (!sessionJson)
52
70
  throw new Error('Session not found for user: ' + userId);
71
+ const session = double_ratchet_1.KeySession.from(sessionJson);
53
72
  const decrypted = session.decrypt(data);
54
73
  if (!decrypted)
55
74
  throw new Error('Decryption failed for user: ' + userId);
56
- this.sessions.set(userId, session); // Ensure session is updated
75
+ this.sessions.set(userId, session.toJSON()); // Ensure session is updated
57
76
  return decrypted;
58
77
  });
59
78
  }
@@ -62,7 +81,10 @@ class FreeSignalAPI {
62
81
  const res = yield fetch(`${url}/${userId !== null && userId !== void 0 ? userId : ''}`, {
63
82
  method: 'GET'
64
83
  });
65
- return (0, utils_1.decodeJSON)(new Uint8Array(yield res.arrayBuffer()));
84
+ const body = types_1.XFreeSignal.decodeBody(new Uint8Array(yield res.arrayBuffer()));
85
+ if (body.type === 'error')
86
+ throw new Error(body.data);
87
+ return body.data;
66
88
  });
67
89
  }
68
90
  postHandshake(url, message) {
@@ -70,9 +92,9 @@ class FreeSignalAPI {
70
92
  const res = yield fetch(url, {
71
93
  method: 'POST',
72
94
  headers: {
73
- 'Content-Type': exports.FREESIGNAL_MIME
95
+ 'Content-Type': types_1.XFreeSignal.MIME
74
96
  },
75
- body: (0, utils_1.encodeJSON)(message)
97
+ body: types_1.XFreeSignal.encodeBody('data', message)
76
98
  });
77
99
  return res.status === 200;
78
100
  });
@@ -82,10 +104,10 @@ class FreeSignalAPI {
82
104
  const res = yield fetch(url, {
83
105
  method: 'PUT',
84
106
  headers: {
85
- 'Content-Type': exports.FREESIGNAL_MIME,
107
+ 'Content-Type': types_1.XFreeSignal.MIME,
86
108
  authorization: this.createToken(publicKey instanceof Uint8Array ? publicKey : (0, utils_1.encodeBase64)(publicKey))
87
109
  },
88
- body: (0, utils_1.encodeJSON)(bundle)
110
+ body: types_1.XFreeSignal.encodeBody('data', bundle)
89
111
  });
90
112
  return res.status === 201;
91
113
  });
@@ -109,35 +131,44 @@ class FreeSignalAPI {
109
131
  authorization: this.createToken(publicKey instanceof Uint8Array ? publicKey : (0, utils_1.encodeBase64)(publicKey))
110
132
  }
111
133
  });
112
- return this.unpackDatagrams(yield this.decryptData(new Uint8Array(yield res.arrayBuffer()), types_1.UserId.getUserId(publicKey).toString()));
134
+ const body = types_1.XFreeSignal.decodeBody(new Uint8Array(yield res.arrayBuffer()));
135
+ if (body.type === 'error')
136
+ throw new Error(body.data);
137
+ return types_1.DataEncoder.from(yield this.decryptData(body.data, types_1.UserId.getUserId(publicKey).toString())).data.map(array => types_1.Datagram.from(array));
113
138
  });
114
139
  }
115
140
  postDatagrams(datagrams, publicKey, url) {
116
141
  return __awaiter(this, void 0, void 0, function* () {
117
- const data = yield this.encryptData(this.packDatagrams(datagrams), types_1.UserId.getUserId(publicKey).toString());
142
+ const data = yield this.encryptData(new types_1.DataEncoder(datagrams.map(datagram => types_1.Datagram.from(datagram).encode())).encode(), types_1.UserId.getUserId(publicKey).toString());
118
143
  const res = yield fetch(url, {
119
144
  method: 'POST',
120
145
  headers: {
121
- 'Content-Type': exports.FREESIGNAL_MIME,
146
+ 'Content-Type': types_1.XFreeSignal.MIME,
122
147
  authorization: this.createToken(publicKey instanceof Uint8Array ? publicKey : (0, utils_1.encodeBase64)(publicKey))
123
148
  },
124
- body: data.encode()
149
+ body: types_1.XFreeSignal.encodeBody('data', data.encode())
125
150
  });
126
- return (0, utils_1.numberFromUint8Array)(yield this.decryptData(new Uint8Array(yield res.arrayBuffer()), types_1.UserId.getUserId(publicKey).toString()));
151
+ const body = types_1.XFreeSignal.decodeBody(new Uint8Array(yield res.arrayBuffer()));
152
+ if (body.type === 'error')
153
+ throw new Error(body.data);
154
+ return types_1.DataEncoder.from(yield this.decryptData(body.data, types_1.UserId.getUserId(publicKey).toString())).data;
127
155
  });
128
156
  }
129
157
  deleteDatagrams(datagramIds, publicKey, url) {
130
158
  return __awaiter(this, void 0, void 0, function* () {
131
- const data = yield this.encryptData(this.packIdList(datagramIds), types_1.UserId.getUserId(publicKey).toString());
159
+ const data = yield this.encryptData(new types_1.DataEncoder(datagramIds.map(datagramId => crypto_1.default.UUID.parse(datagramId))).encode(), types_1.UserId.getUserId(publicKey).toString());
132
160
  const res = yield fetch(url, {
133
161
  method: 'DELETE',
134
162
  headers: {
135
- 'Content-Type': exports.FREESIGNAL_MIME,
163
+ 'Content-Type': types_1.XFreeSignal.MIME,
136
164
  authorization: this.createToken(publicKey instanceof Uint8Array ? publicKey : (0, utils_1.encodeBase64)(publicKey))
137
165
  },
138
- body: data.encode()
166
+ body: types_1.XFreeSignal.encodeBody('data', data.encode())
139
167
  });
140
- return (0, utils_1.numberFromUint8Array)(yield this.decryptData(new Uint8Array(yield res.arrayBuffer()), types_1.UserId.getUserId(publicKey).toString()));
168
+ const body = types_1.XFreeSignal.decodeBody(new Uint8Array(yield res.arrayBuffer()));
169
+ if (body.type === 'error')
170
+ throw new Error(body.data);
171
+ return types_1.DataEncoder.from(yield this.decryptData(body.data, types_1.UserId.getUserId(publicKey).toString())).data;
141
172
  });
142
173
  }
143
174
  createToken(publicKey) {
@@ -160,47 +191,5 @@ class FreeSignalAPI {
160
191
  throw new Error('Authorization header is required');
161
192
  });
162
193
  }
163
- packIdList(datagramIds) {
164
- return datagramIds.map(datagramId => crypto_1.default.UUID.parse(datagramId)).reduce((prev, curr) => new Uint8Array([...prev, ...curr]), new Uint8Array());
165
- }
166
- unpackIdList(data) {
167
- const ids = [];
168
- for (let i = 0; i < data.length; i += 16) {
169
- ids.push(crypto_1.default.UUID.stringify(data.subarray(i, i + 16)));
170
- }
171
- return ids;
172
- }
173
- packDatagrams(messages) {
174
- return fflate_1.default.deflateSync((0, utils_1.concatUint8Array)(...messages.flatMap(datagram => {
175
- const encoded = types_1.Datagram.from(datagram).encode();
176
- return [(0, utils_1.numberToUint8Array)(encoded.length, 8), encoded];
177
- })));
178
- }
179
- unpackDatagrams(data) {
180
- const messages = [];
181
- let offset = 0;
182
- data = fflate_1.default.inflateSync(data);
183
- while (offset < data.length) {
184
- const length = data.subarray(offset, offset + 8);
185
- if (length.length < 8) {
186
- throw new Error('Invalid message length');
187
- }
188
- const messageLength = (0, utils_1.numberFromUint8Array)(length);
189
- offset += 8;
190
- if (offset + messageLength > data.length) {
191
- throw new Error('Invalid message length');
192
- }
193
- const messageData = data.subarray(offset, offset + messageLength);
194
- offset += messageLength;
195
- try {
196
- const datagram = types_1.Datagram.from(messageData);
197
- messages.push(datagram);
198
- }
199
- catch (error) {
200
- throw new Error('Invalid datagram format');
201
- }
202
- }
203
- return messages;
204
- }
205
194
  }
206
195
  exports.FreeSignalAPI = FreeSignalAPI;
@@ -17,7 +17,7 @@
17
17
  * along with this program. If not, see <https://www.gnu.org/licenses/>
18
18
  */
19
19
  import { EncryptedData } from "./types";
20
- type ExportedKeySession = {
20
+ export interface ExportedKeySession {
21
21
  secretKey: string;
22
22
  remoteKey: string;
23
23
  rootKey: string;
@@ -27,7 +27,7 @@ type ExportedKeySession = {
27
27
  receivingCount: number;
28
28
  previousCount: number;
29
29
  previousKeys: [number, Uint8Array][];
30
- };
30
+ }
31
31
  /**
32
32
  * Represents a secure Double Ratchet session.
33
33
  * Used for forward-secure encryption and decryption of messages.
@@ -90,7 +90,7 @@ export declare class KeySession {
90
90
  * @param json string returned by `export()` method.
91
91
  * @returns session with the state parsed.
92
92
  */
93
- static parse(json: string): KeySession;
93
+ static from(data: ExportedKeySession): KeySession;
94
94
  /**
95
95
  * The fixed key length (in bytes) used throughout the Double Ratchet session.
96
96
  * Typically 32 bytes (256 bits) for symmetric keys.
@@ -98,4 +98,3 @@ export declare class KeySession {
98
98
  static readonly keyLength = 32;
99
99
  private static symmetricRatchet;
100
100
  }
101
- export {};
package/double-ratchet.js CHANGED
@@ -117,7 +117,9 @@ class KeySession {
117
117
  var _a;
118
118
  const encrypted = types_1.EncryptedData.from(payload);
119
119
  const publicKey = encrypted.publicKey;
120
- if (!(0, utils_1.verifyUint8Array)(publicKey, this._remoteKey)) {
120
+ if (!this._remoteKey)
121
+ throw new Error("Missing remoteKey");
122
+ if (!(0, utils_1.verifyArrays)(publicKey, this._remoteKey)) {
121
123
  while (this.receivingCount < encrypted.previous)
122
124
  this.previousKeys.set(this.receivingCount, this.getReceivingKey());
123
125
  this.setRemoteKey(publicKey);
@@ -143,7 +145,7 @@ class KeySession {
143
145
  */
144
146
  toJSON() {
145
147
  return {
146
- secretKey: (0, utils_1.decodeBase64)((0, utils_1.concatUint8Array)(this.keyPair.secretKey)),
148
+ secretKey: (0, utils_1.decodeBase64)((0, utils_1.concatArrays)(this.keyPair.secretKey)),
147
149
  remoteKey: (0, utils_1.decodeBase64)(this._remoteKey),
148
150
  rootKey: (0, utils_1.decodeBase64)(this.rootKey),
149
151
  sendingChain: (0, utils_1.decodeBase64)(this.sendingChain),
@@ -160,8 +162,7 @@ class KeySession {
160
162
  * @param json string returned by `export()` method.
161
163
  * @returns session with the state parsed.
162
164
  */
163
- static parse(json) {
164
- const data = JSON.parse(json);
165
+ static from(data) {
165
166
  const session = new KeySession({ secretKey: (0, utils_1.encodeBase64)(data.secretKey), rootKey: (0, utils_1.encodeBase64)(data.rootKey) });
166
167
  session._remoteKey = (0, utils_1.encodeBase64)(data.remoteKey);
167
168
  session.sendingChain = (0, utils_1.encodeBase64)(data.sendingChain);
package/index.js CHANGED
@@ -61,15 +61,6 @@ function createKeyExchange(signSecretKey, boxSecretKey, bundleStore) {
61
61
  function createIdentityKeys(signSecretKey, boxSecretKey) {
62
62
  return { sign: crypto_1.default.EdDSA.keyPair(signSecretKey), box: crypto_1.default.ECDH.keyPair(boxSecretKey) };
63
63
  }
64
- /*export function createAPI(opts: {
65
- secretSignKey: Uint8Array;
66
- secretBoxKey: Uint8Array;
67
- sessions: LocalStorage<UserId, KeySession>;
68
- keyExchange: LocalStorage<string, Crypto.KeyPair>;
69
- users: LocalStorage<UserId, IdentityKeys>;
70
- }): FreeSignalAPI {
71
- return new FreeSignalAPI(opts);
72
- }*/
73
64
  var types_1 = require("./types");
74
65
  Object.defineProperty(exports, "UserId", { enumerable: true, get: function () { return types_1.UserId; } });
75
66
  Object.defineProperty(exports, "IdentityKeys", { enumerable: true, get: function () { return types_1.IdentityKeys; } });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@freesignal/protocol",
3
- "version": "0.2.5",
3
+ "version": "0.2.8",
4
4
  "description": "Signal Protocol implementation in javascript",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "author": "Christian Braghette",
@@ -13,7 +13,7 @@
13
13
  "dependencies": {
14
14
  "@freesignal/crypto": "^0.3.0",
15
15
  "@freesignal/interfaces": "^0.1.1",
16
- "@freesignal/utils": "^1.1.0",
16
+ "@freesignal/utils": "^1.2.0",
17
17
  "base64-js": "^1.5.1",
18
18
  "fflate": "^0.8.2",
19
19
  "js-sha3": "^0.9.3",
package/types.d.ts CHANGED
@@ -144,7 +144,14 @@ export interface EncryptedData extends Encodable {
144
144
  /**
145
145
  * Returns the decoded object as a JSON string.
146
146
  */
147
- toJSON(): string;
147
+ toJSON(): {
148
+ version: number;
149
+ count: number;
150
+ previous: number;
151
+ publicKey: string;
152
+ nonce: string;
153
+ ciphertext: string;
154
+ };
148
155
  }
149
156
  export declare class EncryptedData {
150
157
  /**
@@ -174,5 +181,55 @@ export declare class EncryptedDataConstructor implements EncryptedData {
174
181
  get ciphertext(): Uint8Array;
175
182
  encode(): Uint8Array;
176
183
  toString(): string;
177
- toJSON(): string;
184
+ toJSON(): {
185
+ version: number;
186
+ count: number;
187
+ previous: number;
188
+ publicKey: string;
189
+ nonce: string;
190
+ ciphertext: string;
191
+ };
178
192
  }
193
+ declare enum DataType {
194
+ UKNOWN = -1,
195
+ RAW = 0,
196
+ NUMBER = 1,
197
+ STRING = 2,
198
+ ARRAY = 3,
199
+ OBJECT = 4
200
+ }
201
+ declare namespace DataType {
202
+ function getType(type: string): DataType;
203
+ function getName(type: DataType): string;
204
+ function from(data: any): DataType;
205
+ }
206
+ export declare class DataEncoder<T> implements Encodable {
207
+ readonly data: T;
208
+ readonly type: string;
209
+ constructor(data: T);
210
+ protected get _type(): DataType;
211
+ encode(): Uint8Array;
212
+ toString(): string;
213
+ toJSON(): T;
214
+ static from<T = any>(array: Uint8Array): DataEncoder<T>;
215
+ }
216
+ export declare namespace XFreeSignal {
217
+ export const MIME = "application/x-freesignal";
218
+ export const version = 1;
219
+ export function encodeBody(type: 'data' | 'error', data: any, compressed?: boolean): BodyInit;
220
+ export function decodeBody<T = any>(body: Uint8Array): Body<T>;
221
+ class Body<T> implements Encodable {
222
+ readonly type: 'data' | 'error';
223
+ readonly data: T;
224
+ constructor(type: 'data' | 'error', data: T);
225
+ encode(compressed?: boolean): Uint8Array;
226
+ toString(): string;
227
+ toJSON(): {
228
+ type: "data" | "error";
229
+ data: T;
230
+ };
231
+ static from<T = any>(array: Uint8Array): Body<T>;
232
+ }
233
+ export {};
234
+ }
235
+ export {};
package/types.js CHANGED
@@ -21,10 +21,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
21
21
  return (mod && mod.__esModule) ? mod : { "default": mod };
22
22
  };
23
23
  Object.defineProperty(exports, "__esModule", { value: true });
24
- exports.EncryptedDataConstructor = exports.EncryptedData = exports.Datagram = exports.Protocols = exports.IdentityKeys = exports.UserId = void 0;
24
+ exports.XFreeSignal = exports.DataEncoder = exports.EncryptedDataConstructor = exports.EncryptedData = exports.Datagram = exports.Protocols = exports.IdentityKeys = exports.UserId = void 0;
25
25
  const utils_1 = require("@freesignal/utils");
26
26
  const crypto_1 = __importDefault(require("@freesignal/crypto"));
27
27
  const double_ratchet_1 = require("./double-ratchet");
28
+ const fflate_1 = __importDefault(require("fflate"));
28
29
  var UserId;
29
30
  (function (UserId) {
30
31
  class UserIdConstructor {
@@ -36,7 +37,7 @@ var UserId;
36
37
  return (0, utils_1.decodeBase64)(this.array);
37
38
  }
38
39
  toJSON() {
39
- return JSON.stringify(this.toString());
40
+ return this.toString();
40
41
  }
41
42
  toUint8Array() {
42
43
  return this.array;
@@ -68,13 +69,13 @@ var IdentityKeys;
68
69
  }
69
70
  }
70
71
  encode() {
71
- return (0, utils_1.concatUint8Array)((0, utils_1.encodeBase64)(this.publicKey), (0, utils_1.encodeBase64)(this.identityKey));
72
+ return (0, utils_1.concatArrays)((0, utils_1.encodeBase64)(this.publicKey), (0, utils_1.encodeBase64)(this.identityKey));
72
73
  }
73
74
  toString() {
74
75
  throw (0, utils_1.decodeBase64)(this.encode());
75
76
  }
76
77
  toJSON() {
77
- throw JSON.stringify(this.toString());
78
+ throw this.toString();
78
79
  }
79
80
  }
80
81
  function isIdentityKeys(obj) {
@@ -107,11 +108,11 @@ var Protocols;
107
108
  }
108
109
  Protocols.toCode = toCode;
109
110
  function encode(protocol, length) {
110
- return (0, utils_1.numberToUint8Array)(Protocols.toCode(protocol), length);
111
+ return (0, utils_1.numberToArray)(Protocols.toCode(protocol), length);
111
112
  }
112
113
  Protocols.encode = encode;
113
114
  function decode(array) {
114
- return Protocols.fromCode((0, utils_1.numberFromUint8Array)(array));
115
+ return Protocols.fromCode((0, utils_1.numberFromArray)(array));
115
116
  }
116
117
  Protocols.decode = decode;
117
118
  })(Protocols || (exports.Protocols = Protocols = {}));
@@ -125,7 +126,7 @@ var Datagram;
125
126
  this.version = data[0] & 127;
126
127
  this.protocol = Protocols.decode(data.subarray(1, 2));
127
128
  this.id = crypto_1.default.UUID.stringify(data.subarray(2, 18));
128
- this.createdAt = (0, utils_1.numberFromUint8Array)(data.subarray(18, 26));
129
+ this.createdAt = (0, utils_1.numberFromArray)(data.subarray(18, 26));
129
130
  this.sender = (0, utils_1.decodeBase64)(data.subarray(26, 26 + crypto_1.default.EdDSA.publicKeyLength));
130
131
  this.receiver = (0, utils_1.decodeBase64)(data.subarray(26 + crypto_1.default.EdDSA.publicKeyLength, DatagramConstructor.headerOffset));
131
132
  if (data[0] & 128)
@@ -177,16 +178,16 @@ var Datagram;
177
178
  }
178
179
  encode() {
179
180
  var _a, _b, _c;
180
- const data = (0, utils_1.concatUint8Array)(new Uint8Array(1).fill(this.version | (this.secretKey ? 128 : 0)), //1
181
+ const data = (0, utils_1.concatArrays)(new Uint8Array(1).fill(this.version | (this.secretKey ? 128 : 0)), //1
181
182
  Protocols.encode(this.protocol), //1
182
183
  (_a = crypto_1.default.UUID.parse(this.id)) !== null && _a !== void 0 ? _a : [], //16
183
- (0, utils_1.numberToUint8Array)(this.createdAt, 8), //8
184
+ (0, utils_1.numberToArray)(this.createdAt, 8), //8
184
185
  (0, utils_1.encodeBase64)(this.sender), //32
185
186
  (0, utils_1.encodeBase64)(this.receiver), //32
186
187
  (_b = this._payload) !== null && _b !== void 0 ? _b : new Uint8Array());
187
188
  if (this.secretKey)
188
189
  this._signature = crypto_1.default.EdDSA.sign(data, this.secretKey);
189
- return (0, utils_1.concatUint8Array)(data, (_c = this._signature) !== null && _c !== void 0 ? _c : new Uint8Array());
190
+ return (0, utils_1.concatArrays)(data, (_c = this._signature) !== null && _c !== void 0 ? _c : new Uint8Array());
190
191
  }
191
192
  sign(secretKey) {
192
193
  this.secretKey = secretKey;
@@ -196,7 +197,7 @@ var Datagram;
196
197
  return (0, utils_1.decodeBase64)(this.encode());
197
198
  }
198
199
  toJSON() {
199
- return JSON.stringify(this.toString());
200
+ return this.toString();
200
201
  }
201
202
  }
202
203
  DatagramConstructor.headerOffset = 26 + crypto_1.default.EdDSA.publicKeyLength * 2;
@@ -238,22 +239,22 @@ class EncryptedDataConstructor {
238
239
  return this;
239
240
  }
240
241
  if (typeof arrays[0] === 'number')
241
- arrays[0] = (0, utils_1.numberToUint8Array)(arrays[0], EncryptedDataConstructor.countLength);
242
+ arrays[0] = (0, utils_1.numberToArray)(arrays[0], EncryptedDataConstructor.countLength);
242
243
  if (typeof arrays[1] === 'number')
243
- arrays[1] = (0, utils_1.numberToUint8Array)(arrays[1], EncryptedDataConstructor.countLength);
244
+ arrays[1] = (0, utils_1.numberToArray)(arrays[1], EncryptedDataConstructor.countLength);
244
245
  if (arrays.length === 6) {
245
- arrays.unshift(typeof arrays[5] === 'number' ? (0, utils_1.numberToUint8Array)(arrays[5]) : arrays[5]);
246
+ arrays.unshift(typeof arrays[5] === 'number' ? (0, utils_1.numberToArray)(arrays[5]) : arrays[5]);
246
247
  arrays.pop();
247
248
  }
248
249
  else if (arrays.length > 1) {
249
- arrays.unshift((0, utils_1.numberToUint8Array)(double_ratchet_1.KeySession.version));
250
+ arrays.unshift((0, utils_1.numberToArray)(double_ratchet_1.KeySession.version));
250
251
  }
251
- this.raw = (0, utils_1.concatUint8Array)(...arrays);
252
+ this.raw = (0, utils_1.concatArrays)(...arrays);
252
253
  }
253
254
  get length() { return this.raw.length; }
254
- get version() { return (0, utils_1.numberFromUint8Array)(new Uint8Array(this.raw.buffer, ...Offsets.version.get)); }
255
- get count() { return (0, utils_1.numberFromUint8Array)(new Uint8Array(this.raw.buffer, ...Offsets.count.get)); }
256
- get previous() { return (0, utils_1.numberFromUint8Array)(new Uint8Array(this.raw.buffer, ...Offsets.previous.get)); }
255
+ get version() { return (0, utils_1.numberFromArray)(new Uint8Array(this.raw.buffer, ...Offsets.version.get)); }
256
+ get count() { return (0, utils_1.numberFromArray)(new Uint8Array(this.raw.buffer, ...Offsets.count.get)); }
257
+ get previous() { return (0, utils_1.numberFromArray)(new Uint8Array(this.raw.buffer, ...Offsets.previous.get)); }
257
258
  get publicKey() { return new Uint8Array(this.raw.buffer, ...Offsets.publicKey.get); }
258
259
  get nonce() { return new Uint8Array(this.raw.buffer, ...Offsets.nonce.get); }
259
260
  get ciphertext() { return new Uint8Array(this.raw.buffer, Offsets.ciphertext.start); }
@@ -264,14 +265,14 @@ class EncryptedDataConstructor {
264
265
  return (0, utils_1.decodeBase64)(this.raw);
265
266
  }
266
267
  toJSON() {
267
- return JSON.stringify({
268
+ return {
268
269
  version: this.version,
269
270
  count: this.count,
270
271
  previous: this.previous,
271
272
  publicKey: (0, utils_1.decodeBase64)(this.publicKey),
272
273
  nonce: (0, utils_1.decodeBase64)(this.nonce),
273
274
  ciphertext: (0, utils_1.decodeBase64)(this.ciphertext)
274
- });
275
+ };
275
276
  }
276
277
  }
277
278
  exports.EncryptedDataConstructor = EncryptedDataConstructor;
@@ -304,3 +305,156 @@ Offsets.previous = Offsets.set(Offsets.count.end, EncryptedDataConstructor.count
304
305
  Offsets.publicKey = Offsets.set(Offsets.previous.end, EncryptedDataConstructor.publicKeyLength);
305
306
  Offsets.nonce = Offsets.set(Offsets.publicKey.end, EncryptedDataConstructor.nonceLength);
306
307
  Offsets.ciphertext = Offsets.set(Offsets.nonce.end, undefined);
308
+ var DataType;
309
+ (function (DataType) {
310
+ DataType[DataType["UKNOWN"] = -1] = "UKNOWN";
311
+ DataType[DataType["RAW"] = 0] = "RAW";
312
+ DataType[DataType["NUMBER"] = 1] = "NUMBER";
313
+ DataType[DataType["STRING"] = 2] = "STRING";
314
+ DataType[DataType["ARRAY"] = 3] = "ARRAY";
315
+ DataType[DataType["OBJECT"] = 4] = "OBJECT";
316
+ })(DataType || (DataType = {}));
317
+ (function (DataType) {
318
+ function getType(type) {
319
+ return Object.values(DataType).indexOf(type.toLocaleUpperCase());
320
+ }
321
+ DataType.getType = getType;
322
+ function getName(type) {
323
+ return DataType[type].toLowerCase();
324
+ }
325
+ DataType.getName = getName;
326
+ function from(data) {
327
+ if (data instanceof Uint8Array)
328
+ return DataType.RAW;
329
+ return getType(typeof data);
330
+ }
331
+ DataType.from = from;
332
+ })(DataType || (DataType = {}));
333
+ class DataEncoder {
334
+ constructor(data) {
335
+ this.data = data;
336
+ this.type = DataType.getName(DataType.from(this.data));
337
+ }
338
+ get _type() {
339
+ return DataType.getType(this.type);
340
+ }
341
+ encode() {
342
+ let data;
343
+ switch (this._type) {
344
+ case DataType.RAW:
345
+ data = this.data;
346
+ break;
347
+ case DataType.NUMBER:
348
+ data = (0, utils_1.numberToArray)(this._type);
349
+ break;
350
+ case DataType.STRING:
351
+ data = (0, utils_1.encodeUTF8)(this.data);
352
+ break;
353
+ case DataType.ARRAY:
354
+ data = (0, utils_1.concatArrays)(...Array.from(this.data).flatMap(value => {
355
+ const data = new DataEncoder(value).encode();
356
+ return [(0, utils_1.numberToArray)(data.length, 8), data];
357
+ }));
358
+ break;
359
+ case DataType.OBJECT:
360
+ data = (0, utils_1.encodeJSON)(this.data);
361
+ break;
362
+ default:
363
+ throw new Error("Uknown type");
364
+ }
365
+ return (0, utils_1.concatArrays)((0, utils_1.numberToArray)(this._type), data);
366
+ }
367
+ toString() {
368
+ return "[Object XFreeSignalData]";
369
+ }
370
+ toJSON() {
371
+ return this.data;
372
+ }
373
+ static from(array) {
374
+ const type = array[0];
375
+ let rawData = array.subarray(1), data;
376
+ switch (type) {
377
+ case DataType.RAW:
378
+ data = rawData;
379
+ break;
380
+ case DataType.NUMBER:
381
+ data = (0, utils_1.numberFromArray)(rawData);
382
+ break;
383
+ case DataType.STRING:
384
+ data = (0, utils_1.decodeUTF8)(rawData);
385
+ break;
386
+ case DataType.ARRAY:
387
+ const arrayData = [];
388
+ let offset = 0;
389
+ while (offset < rawData.length) {
390
+ const length = rawData.subarray(offset, offset + 8);
391
+ if (length.length < 8)
392
+ throw new Error('Invalid data length');
393
+ const messageLength = (0, utils_1.numberFromArray)(length);
394
+ offset += 8;
395
+ if (offset + messageLength > rawData.length) {
396
+ throw new Error('Invalid data length');
397
+ }
398
+ arrayData.push(rawData.subarray(offset, offset + messageLength));
399
+ offset += messageLength;
400
+ }
401
+ data = arrayData;
402
+ break;
403
+ case DataType.OBJECT:
404
+ data = (0, utils_1.decodeJSON)(rawData);
405
+ break;
406
+ default:
407
+ throw new Error('Invalid data format');
408
+ }
409
+ return new DataEncoder(data);
410
+ }
411
+ }
412
+ exports.DataEncoder = DataEncoder;
413
+ var XFreeSignal;
414
+ (function (XFreeSignal) {
415
+ XFreeSignal.MIME = "application/x-freesignal";
416
+ XFreeSignal.version = 1;
417
+ function encodeBody(type, data, compressed = false) {
418
+ return new Body(type, data).encode(compressed);
419
+ }
420
+ XFreeSignal.encodeBody = encodeBody;
421
+ function decodeBody(body) {
422
+ return Body.from(body);
423
+ }
424
+ XFreeSignal.decodeBody = decodeBody;
425
+ let BodyType;
426
+ (function (BodyType) {
427
+ BodyType[BodyType["DATA"] = 0] = "DATA";
428
+ BodyType[BodyType["ERROR"] = 1] = "ERROR";
429
+ })(BodyType || (BodyType = {}));
430
+ (function (BodyType) {
431
+ function getName(type) {
432
+ return BodyType[type].toLowerCase();
433
+ }
434
+ BodyType.getName = getName;
435
+ })(BodyType || (BodyType = {}));
436
+ class Body {
437
+ constructor(type, data) {
438
+ this.type = type;
439
+ this.data = data;
440
+ }
441
+ encode(compressed = false) {
442
+ const data = new DataEncoder(this.data).encode();
443
+ return (0, utils_1.concatArrays)((0, utils_1.numberToArray)(((this.type === 'data' ? BodyType.DATA : BodyType.ERROR) << 6)
444
+ + (compressed ? 32 : 0)
445
+ + XFreeSignal.version), compressed ? fflate_1.default.deflateSync(data) : data);
446
+ }
447
+ toString() {
448
+ return "[Object XFreeSignalBody]";
449
+ }
450
+ toJSON() {
451
+ return {
452
+ type: this.type,
453
+ data: this.data
454
+ };
455
+ }
456
+ static from(array) {
457
+ return new Body(BodyType.getName((array[0] & 64) >> 6), DataEncoder.from((array[0] & 32) >> 5 === 1 ? fflate_1.default.inflateSync(array.subarray(1)) : array.subarray(1)).data);
458
+ }
459
+ }
460
+ })(XFreeSignal || (exports.XFreeSignal = XFreeSignal = {}));
package/x3dh.d.ts CHANGED
@@ -19,6 +19,11 @@
19
19
  import { KeyExchangeData, KeyExchangeDataBundle, KeyExchangeSynMessage, LocalStorage, Crypto } from "@freesignal/interfaces";
20
20
  import { KeySession } from "./double-ratchet";
21
21
  import { IdentityKeys } from "./types";
22
+ export interface ExportedKeyExchange {
23
+ signatureKey: Crypto.KeyPair;
24
+ identityKey: Crypto.KeyPair;
25
+ bundleStore: Array<[string, Crypto.KeyPair]>;
26
+ }
22
27
  export declare class KeyExchange {
23
28
  static readonly version = 1;
24
29
  private static readonly hkdfInfo;
@@ -42,4 +47,6 @@ export declare class KeyExchange {
42
47
  session: KeySession;
43
48
  identityKeys: IdentityKeys;
44
49
  }>;
50
+ toJSON(): ExportedKeyExchange;
51
+ static from(data: ExportedKeyExchange): KeyExchange;
45
52
  }
package/x3dh.js CHANGED
@@ -94,7 +94,7 @@ class KeyExchange {
94
94
  ...onetimePreKey ? crypto_1.default.ECDH.scalarMult(ephemeralKey.secretKey, onetimePreKey) : new Uint8Array()
95
95
  ]), new Uint8Array(double_ratchet_1.KeySession.rootKeyLength).fill(0), KeyExchange.hkdfInfo, double_ratchet_1.KeySession.rootKeyLength);
96
96
  const session = new double_ratchet_1.KeySession({ remoteKey: identityKey, rootKey });
97
- const cyphertext = session.encrypt((0, utils_1.concatUint8Array)(crypto_1.default.hash(this._identityKey.publicKey), crypto_1.default.hash(identityKey)));
97
+ const cyphertext = session.encrypt((0, utils_1.concatArrays)(crypto_1.default.hash(this._identityKey.publicKey), crypto_1.default.hash(identityKey)));
98
98
  if (!cyphertext)
99
99
  throw new Error("Decryption error");
100
100
  return {
@@ -135,7 +135,7 @@ class KeyExchange {
135
135
  const cleartext = session.decrypt((0, utils_1.encodeBase64)(message.associatedData));
136
136
  if (!cleartext)
137
137
  throw new Error("Error decrypting ACK message");
138
- if (!(0, utils_1.verifyUint8Array)(cleartext, (0, utils_1.concatUint8Array)(crypto_1.default.hash(identityKey), crypto_1.default.hash(this._identityKey.publicKey))))
138
+ if (!(0, utils_1.verifyArrays)(cleartext, (0, utils_1.concatArrays)(crypto_1.default.hash(identityKey), crypto_1.default.hash(this._identityKey.publicKey))))
139
139
  throw new Error("Error verifing Associated Data");
140
140
  return {
141
141
  session,
@@ -146,19 +146,29 @@ class KeyExchange {
146
146
  };
147
147
  });
148
148
  }
149
+ toJSON() {
150
+ return {
151
+ identityKey: this._identityKey,
152
+ signatureKey: this._signatureKey,
153
+ bundleStore: Array.from(this.bundleStore.entries())
154
+ };
155
+ }
156
+ static from(data) {
157
+ return new KeyExchange(data.signatureKey.secretKey, data.identityKey.secretKey, new AsyncMap(data.bundleStore));
158
+ }
149
159
  }
150
160
  exports.KeyExchange = KeyExchange;
151
161
  KeyExchange.version = 1;
152
162
  KeyExchange.hkdfInfo = (0, utils_1.encodeUTF8)("freesignal/x3dh/" + KeyExchange.version);
153
163
  KeyExchange.maxOPK = 10;
154
164
  class AsyncMap {
155
- constructor() {
156
- this.map = new Map();
165
+ constructor(iterable) {
166
+ this.map = new Map(iterable);
157
167
  }
158
168
  set(key, value) {
159
169
  return __awaiter(this, void 0, void 0, function* () {
160
170
  this.map.set(key, value);
161
- return this;
171
+ return;
162
172
  });
163
173
  }
164
174
  get(key) {
@@ -177,8 +187,6 @@ class AsyncMap {
177
187
  });
178
188
  }
179
189
  entries() {
180
- return __awaiter(this, void 0, void 0, function* () {
181
- return this.map.entries();
182
- });
190
+ return this.map.entries();
183
191
  }
184
192
  }