@freesignal/protocol 0.1.2 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,34 @@
1
+ # freesignal-protocol
2
+
3
+ This library implements a secure messaging protocol inspired by Signal, with support for encryption, data management, and key exchange.
4
+
5
+ ## Main File Structure
6
+
7
+ - **[src/crypto.ts](src/crypto.ts)**
8
+ Contains cryptographic primitives, including box, ECDH, EdDSA, and UUID.
9
+ See [`crypto.box`](src/crypto.ts), [`crypto.ECDH`](src/crypto.ts), [`crypto.EdDSA`](src/crypto.ts), [`crypto.UUID`](src/crypto.ts).
10
+
11
+ - **[src/data.ts](src/data.ts)**
12
+ Defines data structures for messages and datagrams, including attachments and serialization.
13
+ See [`Datagram`](src/data.ts), [`Message`](src/data.ts).
14
+
15
+ - **[src/double-ratchet.ts](src/double-ratchet.ts)**
16
+ Implements the Double Ratchet algorithm for forward secrecy in conversations.
17
+
18
+ - **[src/types.ts](src/types.ts)**
19
+ Defines types and interfaces used throughout the project.
20
+
21
+ - **[src/utils.ts](src/utils.ts)**
22
+ Utility functions for manipulating arrays, strings, and binary data.
23
+
24
+ - **[src/x3dh.ts](src/x3dh.ts)**
25
+ Implements the X3DH protocol for secure key exchange between users.
26
+
27
+ ## How to Use
28
+
29
+ Import the required modules from [src/index.ts](src/index.ts) to access the main protocol features.
30
+
31
+ ## License
32
+
33
+ Distributed under the GPL v3 license.
34
+ See [LICENSE](LICENSE) for details.
package/crypto.d.ts CHANGED
@@ -18,22 +18,31 @@
18
18
  */
19
19
  export type HashAlgorithms = 'sha224' | 'sha256' | 'sha384' | 'sha512';
20
20
  export type HmacAlgorithms = 'kmac128' | 'kmac256';
21
+ interface UUIDv4 {
22
+ toString(): string;
23
+ toJSON(): string;
24
+ toBuffer(): Uint8Array;
25
+ }
21
26
  export interface Crypto {
22
27
  hash(message: Uint8Array, algorithm?: HashAlgorithms): Uint8Array;
23
28
  hmac(key: Uint8Array, message: Uint8Array, length?: number, algorithm?: HmacAlgorithms): Uint8Array;
24
29
  hkdf(key: Uint8Array, salt: Uint8Array, info?: Uint8Array | string, length?: number): Uint8Array;
30
+ readonly KeyPair: typeof Crypto.KeyPair;
25
31
  readonly box: Crypto.box;
26
32
  readonly ECDH: Crypto.ECDH;
27
33
  readonly EdDSA: Crypto.EdDSA;
34
+ readonly UUID: Crypto.UUID;
28
35
  randomBytes(n: number): Uint8Array;
29
36
  scalarMult(n: Uint8Array, p: Uint8Array): Uint8Array;
30
- generateId(): Crypto.UUID;
31
37
  }
32
38
  export declare namespace Crypto {
33
39
  type KeyPair = {
34
- publicKey: Uint8Array;
35
- secretKey: Uint8Array;
40
+ readonly publicKey: Uint8Array;
41
+ readonly secretKey: Uint8Array;
36
42
  };
43
+ namespace KeyPair {
44
+ function isKeyPair(obj: any): boolean;
45
+ }
37
46
  interface box {
38
47
  readonly keyLength: number;
39
48
  readonly nonceLength: number;
@@ -57,9 +66,9 @@ export declare namespace Crypto {
57
66
  verify(msg: Uint8Array, sig: Uint8Array, publicKey: Uint8Array): boolean;
58
67
  }
59
68
  interface UUID {
60
- toString(): string;
61
- toJSON(): string;
62
- toBuffer(): Uint8Array;
69
+ generate(): UUIDv4;
70
+ stringify(arr: Uint8Array, offset?: number): string;
71
+ parse(uuid: string): Uint8Array;
63
72
  }
64
73
  }
65
74
  declare const crypto: Crypto;
package/crypto.js CHANGED
@@ -21,15 +21,27 @@ 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.Crypto = void 0;
24
25
  const js_sha3_1 = require("js-sha3");
25
26
  const tweetnacl_1 = __importDefault(require("tweetnacl"));
26
27
  const uuid_1 = require("uuid");
27
28
  const utils_1 = require("./utils");
29
+ var Crypto;
30
+ (function (Crypto) {
31
+ })(Crypto || (exports.Crypto = Crypto = {}));
28
32
  class CryptoConstructor {
29
33
  constructor() {
34
+ this.KeyPair = {
35
+ isKeyPair(obj) {
36
+ if (typeof obj === 'object' && obj.publicKey && obj.secretKey)
37
+ return true;
38
+ return false;
39
+ }
40
+ };
30
41
  this.box = new CryptoConstructor.box();
31
42
  this.ECDH = new CryptoConstructor.ECDH();
32
43
  this.EdDSA = new CryptoConstructor.EdDSA();
44
+ this.UUID = new CryptoConstructor.UUID();
33
45
  this.randomBytes = tweetnacl_1.default.randomBytes;
34
46
  }
35
47
  hash(message, algorithm = 'sha256') {
@@ -63,9 +75,6 @@ class CryptoConstructor {
63
75
  scalarMult(n, p) {
64
76
  return tweetnacl_1.default.scalarMult(n, p);
65
77
  }
66
- generateId() {
67
- return new CryptoConstructor.UUID();
68
- }
69
78
  }
70
79
  (function (CryptoConstructor) {
71
80
  class box {
@@ -121,6 +130,18 @@ class CryptoConstructor {
121
130
  }
122
131
  CryptoConstructor.EdDSA = EdDSA;
123
132
  class UUID {
133
+ generate() {
134
+ return new UUIDv4();
135
+ }
136
+ stringify(arr, offset) {
137
+ return (0, uuid_1.stringify)(arr, offset);
138
+ }
139
+ parse(uuid) {
140
+ return (0, uuid_1.parse)(uuid);
141
+ }
142
+ }
143
+ CryptoConstructor.UUID = UUID;
144
+ class UUIDv4 {
124
145
  constructor() {
125
146
  this.value = (0, uuid_1.v4)();
126
147
  }
@@ -134,7 +155,6 @@ class CryptoConstructor {
134
155
  return (0, utils_1.decodeUTF8)(this.value);
135
156
  }
136
157
  }
137
- CryptoConstructor.UUID = UUID;
138
158
  })(CryptoConstructor || (CryptoConstructor = {}));
139
159
  const crypto = new CryptoConstructor();
140
160
  exports.default = crypto;
package/data.d.ts CHANGED
@@ -16,15 +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
- export interface Encodable {
20
- readonly length: number;
21
- encode(): Uint8Array;
22
- toString(): string;
23
- toJSON(): string;
24
- }
25
- export declare namespace Encodable {
26
- function isEncodable(obj: any): boolean;
27
- }
19
+ import { Encodable } from "./types";
28
20
  export declare enum Protocols {
29
21
  NULL = "",
30
22
  MESSAGE = "/freesignal/message/1.0.0",
@@ -40,11 +32,13 @@ export declare namespace Protocols {
40
32
  export interface Datagram {
41
33
  readonly id: string;
42
34
  readonly version: number;
43
- readonly sender: string;
44
- readonly receiver: string;
35
+ readonly senderKey: string;
36
+ readonly senderRelay?: string;
37
+ readonly receiverKey: string;
38
+ readonly receiverRelay?: string;
45
39
  readonly protocol: Protocols;
46
- payload?: Uint8Array;
47
40
  readonly createdAt: number;
41
+ payload?: Uint8Array;
48
42
  }
49
43
  export declare namespace Datagram {
50
44
  const version = 1;
@@ -53,63 +47,21 @@ export declare namespace Datagram {
53
47
  function from(data: Uint8Array): DatagramConstructor;
54
48
  }
55
49
  declare class DatagramConstructor implements Encodable, Datagram {
56
- readonly createdAt: number;
57
50
  readonly id: string;
58
51
  readonly version: number;
59
- readonly sender: string;
60
- readonly receiver: string;
52
+ readonly senderKey: string;
53
+ readonly senderRelay?: string;
54
+ readonly receiverKey: string;
55
+ readonly receiverRelay?: string;
61
56
  readonly protocol: Protocols;
57
+ readonly createdAt: number;
62
58
  payload?: Uint8Array;
59
+ private static headerOffset;
63
60
  constructor(sender: Uint8Array | string, receiver: Uint8Array | string, protocol: Protocols, payload?: Uint8Array | Encodable);
64
61
  constructor(data: Uint8Array | Datagram);
65
- get length(): number;
66
- encode(): Uint8Array;
62
+ encode(compression?: boolean): Uint8Array;
63
+ encodeSigned(secretKey: Uint8Array, compression?: boolean): Uint8Array;
67
64
  toString(): string;
68
65
  toJSON(): string;
69
66
  }
70
- type Attachment = any;
71
- export interface Message {
72
- readonly version: number;
73
- text: string;
74
- group?: string;
75
- attachments?: Attachment[];
76
- }
77
- export declare namespace Message {
78
- function isMessage(obj: any): boolean;
79
- function create(opts?: {
80
- text?: string;
81
- group?: string;
82
- attachments?: Attachment[];
83
- }): MessageConstructor;
84
- function from(data: Uint8Array | Message): MessageConstructor;
85
- }
86
- declare class MessageConstructor implements Encodable, Message {
87
- static readonly version = 1;
88
- readonly version: number;
89
- text: string;
90
- group?: string;
91
- attachments?: Attachment[];
92
- constructor(opts?: {
93
- text?: string;
94
- group?: string;
95
- attachments?: Attachment[];
96
- });
97
- constructor(data: Uint8Array | Message);
98
- get length(): number;
99
- setText(str: string): this;
100
- setGroup(str: string): this;
101
- addAttachment(obj: Attachment): this;
102
- delAttachment(obj: Attachment): this | undefined;
103
- encode(): Uint8Array;
104
- toString(): string;
105
- toJSON(): string;
106
- }
107
- type LocalStorageIterator<T> = Iterable<T>;
108
- export interface LocalStorage<K, T> {
109
- set(key: K, value: T): this;
110
- get(key: K): T | undefined;
111
- has(key: K): boolean;
112
- delete(key: K): boolean;
113
- entries(): LocalStorageIterator<[K, T]>;
114
- }
115
67
  export {};
package/data.js CHANGED
@@ -21,17 +21,10 @@ 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.Message = exports.Datagram = exports.Protocols = exports.Encodable = void 0;
24
+ exports.Datagram = exports.Protocols = void 0;
25
25
  const utils_1 = require("./utils");
26
26
  const crypto_1 = __importDefault(require("./crypto"));
27
- var Encodable;
28
- (function (Encodable) {
29
- const properties = ['length', 'encode', 'toString', 'toJSON'];
30
- function isEncodable(obj) {
31
- return !properties.some(prop => !obj[prop]);
32
- }
33
- Encodable.isEncodable = isEncodable;
34
- })(Encodable || (exports.Encodable = Encodable = {}));
27
+ const fflate_1 = __importDefault(require("fflate"));
35
28
  var Protocols;
36
29
  (function (Protocols) {
37
30
  Protocols["NULL"] = "";
@@ -52,9 +45,10 @@ var Protocols;
52
45
  }
53
46
  Protocols.toCode = toCode;
54
47
  function encode(protocol, length) {
55
- const raw = (0, utils_1.numberToUint8Array)(Protocols.toCode(protocol), length).reverse();
48
+ /*const raw = numberToUint8Array(Protocols.toCode(protocol), length).reverse();
56
49
  raw[0] |= (raw.length - 1) << 6;
57
- return raw;
50
+ return raw;*/
51
+ return (0, utils_1.numberToUint8Array)(Protocols.toCode(protocol), length !== null && length !== void 0 ? length : 4, 'big');
58
52
  }
59
53
  Protocols.encode = encode;
60
54
  function decode(array) {
@@ -84,149 +78,99 @@ class DatagramConstructor {
84
78
  constructor(data, receiver, protocol, payload) {
85
79
  if (!receiver && !protocol && !payload) {
86
80
  if (data instanceof Uint8Array) {
87
- const obj = (0, utils_1.encodeUTF8)(data).split(':');
88
- this.id = obj[0];
89
- this.version = parseInt(obj[1]);
90
- this.sender = obj[2];
91
- this.receiver = obj[3];
92
- this.protocol = Protocols.fromCode(parseInt(obj[4]));
93
- this.payload = obj[5] ? (0, utils_1.decodeUTF8)(obj[5]) : undefined;
94
- this.createdAt = parseInt(obj[6]);
81
+ this.version = data[0] & 63;
82
+ this.protocol = Protocols.decode(data.subarray(1, 4));
83
+ this.id = crypto_1.default.UUID.stringify(data.subarray(4, 20));
84
+ this.createdAt = (0, utils_1.numberFromUint8Array)(data.subarray(20, 28));
85
+ this.senderKey = (0, utils_1.encodeBase64)(data.subarray(28, 28 + crypto_1.default.EdDSA.publicKeyLength));
86
+ this.receiverKey = (0, utils_1.encodeBase64)(data.subarray(28 + crypto_1.default.EdDSA.publicKeyLength, DatagramConstructor.headerOffset));
87
+ const senderRelayOffset = data.indexOf(255, DatagramConstructor.headerOffset);
88
+ const receiverRelayOffset = data.indexOf(255, senderRelayOffset + 1);
89
+ this.senderRelay = (0, utils_1.encodeUTF8)(data.subarray(DatagramConstructor.headerOffset, senderRelayOffset)) ? "" : undefined;
90
+ this.receiverRelay = (0, utils_1.encodeUTF8)(data.subarray(senderRelayOffset + 1, receiverRelayOffset)) ? "" : undefined;
91
+ if (data[0] & 128) {
92
+ const signature = data.subarray(data.length - crypto_1.default.EdDSA.signatureLength);
93
+ if (!crypto_1.default.EdDSA.verify(data.subarray(0, data.length - crypto_1.default.EdDSA.signatureLength), signature, data.subarray(28, 28 + crypto_1.default.EdDSA.publicKeyLength)))
94
+ throw new Error('Invalid signature for Datagram');
95
+ }
96
+ if (data[0] & 64)
97
+ this.payload = fflate_1.default.inflateSync(data.subarray(receiverRelayOffset + 1, data.length));
98
+ else
99
+ this.payload = data.subarray(receiverRelayOffset + 1, data.length);
95
100
  }
96
- else {
101
+ else if (Datagram.isDatagram(data)) {
97
102
  const datagram = data;
98
103
  this.id = datagram.id;
99
104
  this.version = datagram.version;
100
- this.sender = datagram.sender;
101
- this.receiver = datagram.receiver;
105
+ this.senderKey = datagram.senderKey;
106
+ this.receiverKey = datagram.receiverKey;
102
107
  this.protocol = datagram.protocol;
103
- this.payload = datagram.payload;
104
108
  this.createdAt = datagram.createdAt;
109
+ this.senderRelay = datagram.senderRelay;
110
+ this.receiverRelay = datagram.receiverRelay;
111
+ this.payload = datagram.payload;
105
112
  }
113
+ else
114
+ throw new Error('Invalid constructor arguments for Datagram');
106
115
  }
107
116
  else if (typeof data === 'string' || data instanceof Uint8Array) {
108
- this.id = crypto_1.default.generateId().toString();
117
+ this.id = crypto_1.default.UUID.generate().toString();
109
118
  this.version = Datagram.version;
110
- this.sender = typeof data === 'string' ? data : (0, utils_1.encodeBase64)(data);
111
- this.receiver = typeof receiver === 'string' ? receiver : (0, utils_1.encodeBase64)(receiver);
119
+ if (typeof data === 'string') {
120
+ const address = data.split('@');
121
+ this.senderKey = address[0];
122
+ this.senderRelay = address[1];
123
+ }
124
+ else
125
+ this.senderKey = (0, utils_1.encodeBase64)(data);
126
+ if (typeof receiver === 'string') {
127
+ const address = receiver.split('@');
128
+ this.receiverKey = address[0];
129
+ this.receiverRelay = address[1];
130
+ }
131
+ else
132
+ this.receiverKey = (0, utils_1.encodeBase64)(receiver);
112
133
  this.protocol = protocol;
113
- this.payload = payload instanceof Uint8Array ? payload : payload === null || payload === void 0 ? void 0 : payload.encode();
114
134
  this.createdAt = Date.now();
135
+ this.payload = payload instanceof Uint8Array ? payload : payload === null || payload === void 0 ? void 0 : payload.encode();
115
136
  }
116
137
  else
117
138
  throw new Error('Invalid constructor arguments for Datagram');
118
139
  }
119
- get length() { return this.encode().length; }
120
- encode() {
121
- return (0, utils_1.decodeUTF8)([
122
- this.id,
123
- this.version,
124
- this.sender,
125
- this.receiver,
126
- Protocols.toCode(this.protocol).toString(),
127
- this.payload ? this.payload : undefined, this.createdAt
128
- ].filter(x => x !== undefined).join(':'));
140
+ encode(compression = true) {
141
+ var _a;
142
+ compression = compression && this.payload != undefined && this.payload.length > 1024;
143
+ return (0, utils_1.concatUint8Array)(new Uint8Array(1).fill(this.version | (compression ? 64 : 0)), //1
144
+ Protocols.encode(this.protocol, 3), //3
145
+ (_a = crypto_1.default.UUID.parse(this.id)) !== null && _a !== void 0 ? _a : [], //16
146
+ (0, utils_1.numberToUint8Array)(this.createdAt, 8), //8
147
+ (0, utils_1.decodeBase64)(this.senderKey), //32
148
+ (0, utils_1.decodeBase64)(this.receiverKey), //32
149
+ ...(this.senderRelay ? [(0, utils_1.decodeUTF8)(this.senderRelay)] : []), new Uint8Array(1).fill(255), ...(this.receiverRelay ? [(0, utils_1.decodeUTF8)(this.receiverRelay)] : []), new Uint8Array(1).fill(255), ...(this.payload ? [compression ? fflate_1.default.deflateSync(this.payload) : this.payload] : []));
150
+ }
151
+ encodeSigned(secretKey, compression) {
152
+ //if (!this.payload) throw new Error('Cannot sign a datagram without payload');
153
+ const header = this.encode(compression);
154
+ header[0] |= 128; // Set the sign bit
155
+ const signature = crypto_1.default.EdDSA.sign(header, secretKey);
156
+ return (0, utils_1.concatUint8Array)(header, signature);
129
157
  }
130
158
  toString() {
131
- return this.toJSON();
159
+ return (0, utils_1.encodeBase64)(this.encode());
132
160
  }
133
161
  toJSON() {
134
- return JSON.stringify({
162
+ /*return JSON.stringify({
135
163
  id: this.id,
136
164
  version: this.version,
137
- sender: this.sender,
138
- receiver: this.receiver,
165
+ senderKey: this.senderKey,
166
+ senderRelay: this.senderRelay,
167
+ receiverKey: this.receiverKey,
168
+ receiverRelay: this.receiverRelay,
139
169
  protocol: this.protocol,
140
- payload: this.payload ? (0, utils_1.encodeUTF8)(this.payload) : undefined,
141
- createdAt: this.createdAt
142
- });
143
- }
144
- }
145
- var Message;
146
- (function (Message) {
147
- function isMessage(obj) {
148
- return obj instanceof MessageConstructor || (obj && typeof obj === 'object' && 'version' in obj && 'text' in obj && 'group' in obj && 'attachments' in obj);
149
- }
150
- Message.isMessage = isMessage;
151
- function create(opts) {
152
- return new MessageConstructor(opts);
153
- }
154
- Message.create = create;
155
- ;
156
- function from(data) {
157
- return new MessageConstructor(data);
158
- }
159
- Message.from = from;
160
- })(Message || (exports.Message = Message = {}));
161
- class MessageConstructor {
162
- constructor(opts) {
163
- var _a;
164
- this.version = MessageConstructor.version;
165
- this.text = "";
166
- if (Message.isMessage(opts)) {
167
- const json = opts;
168
- this.version = json.version;
169
- this.text = json.text;
170
- this.group = json.group;
171
- this.attachments = json.attachments;
172
- }
173
- else if (!opts) {
174
- this.text = "";
175
- }
176
- else if (opts instanceof Uint8Array) {
177
- const arr = (0, utils_1.encodeUTF8)(opts).split(':');
178
- this.version = arr[0];
179
- this.text = (_a = arr[1]) !== null && _a !== void 0 ? _a : "";
180
- this.group = arr[2];
181
- this.attachments = arr[3] ? JSON.parse(arr[3]) : undefined;
182
- }
183
- else if (typeof opts === 'object') {
184
- const { text, group, attachments } = opts;
185
- this.text = text || "";
186
- this.group = group;
187
- this.attachments = attachments || [];
188
- }
189
- else {
190
- throw new Error('Invalid constructor arguments for Message');
191
- }
192
- }
193
- get length() { return this.encode().length; }
194
- setText(str) {
195
- this.text = str;
196
- return this;
197
- }
198
- setGroup(str) {
199
- this.group = str;
200
- return this;
201
- }
202
- addAttachment(obj) {
203
- if (this.attachments)
204
- this.attachments.push(obj);
205
- else
206
- this.attachments = [obj];
207
- return this;
208
- }
209
- delAttachment(obj) {
210
- var _a;
211
- const index = (_a = this.attachments) === null || _a === void 0 ? void 0 : _a.indexOf(obj);
212
- if (!index || !this.attachments)
213
- return undefined;
214
- this.attachments = this.attachments.filter((v, i) => index !== i);
215
- return this;
216
- }
217
- encode() {
218
- return (0, utils_1.decodeUTF8)([this.version, this.text, this.group, JSON.stringify(this.attachments || [])].join(':'));
219
- }
220
- toString() {
221
- return this.toJSON();
222
- }
223
- toJSON() {
224
- return JSON.stringify({
225
- version: MessageConstructor.version,
226
- text: this.text,
227
- group: this.group,
228
- attachments: JSON.stringify(this.attachments)
229
- });
170
+ createdAt: this.createdAt,
171
+ payload: this.payload ? encodeBase64(this.payload) : undefined
172
+ });*/
173
+ return this.toString();
230
174
  }
231
175
  }
232
- MessageConstructor.version = 1;
176
+ DatagramConstructor.headerOffset = 28 + crypto_1.default.EdDSA.publicKeyLength * 2;
@@ -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 { Encodable } from "./data";
19
+ import { Encodable } from "./types";
20
20
  type ExportedKeySession = {
21
21
  secretKey: string;
22
22
  remoteKey: string;
@@ -72,14 +72,14 @@ export declare class KeySession {
72
72
  * @param message - The message as a Uint8Array.
73
73
  * @returns An EncryptedPayload or undefined if encryption fails.
74
74
  */
75
- encrypt(message: Uint8Array): EncryptedPayload;
75
+ encrypt(message: Uint8Array): EncryptedData;
76
76
  /**
77
77
  * Decrypts an encrypted message.
78
78
  *
79
79
  * @param payload - The received encrypted message.
80
80
  * @returns The decrypted message as a Uint8Array, or undefined if decryption fails.
81
81
  */
82
- decrypt(payload: Uint8Array | EncryptedPayload): Uint8Array | undefined;
82
+ decrypt(payload: Uint8Array | EncryptedData): Uint8Array | undefined;
83
83
  /**
84
84
  * Export the state of the session;
85
85
  */
@@ -102,7 +102,7 @@ export declare class KeySession {
102
102
  * Interface representing an encrypted payload.
103
103
  * Provides metadata and de/serialization methods.
104
104
  */
105
- export interface EncryptedPayload extends Encodable {
105
+ export interface EncryptedData extends Encodable {
106
106
  /**
107
107
  * The length of the payload.
108
108
  */
@@ -144,13 +144,13 @@ export interface EncryptedPayload extends Encodable {
144
144
  */
145
145
  toJSON(): string;
146
146
  }
147
- export declare class EncryptedPayload {
147
+ export declare class EncryptedData {
148
148
  /**
149
149
  * Static factory method that constructs an `EncryptedPayload` from a raw Uint8Array.
150
150
  *
151
151
  * @param array - A previously serialized encrypted payload.
152
152
  * @returns An instance of `EncryptedPayload`.
153
153
  */
154
- static from(array: Uint8Array | EncryptedPayload): EncryptedPayload;
154
+ static from(array: Uint8Array | EncryptedData): EncryptedData;
155
155
  }
156
156
  export {};
package/double-ratchet.js CHANGED
@@ -21,7 +21,7 @@ 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.EncryptedPayload = exports.KeySession = void 0;
24
+ exports.EncryptedData = exports.KeySession = void 0;
25
25
  const crypto_1 = __importDefault(require("./crypto"));
26
26
  const utils_1 = require("./utils");
27
27
  /**
@@ -57,12 +57,12 @@ class KeySession {
57
57
  setRemoteKey(key) {
58
58
  this._remoteKey = key;
59
59
  this.receivingChain = this.ratchetKeys();
60
- if (this.receivingCount > (EncryptedPayloadConstructor.maxCount - KeySession.skipLimit * 2))
60
+ if (this.receivingCount > (EncryptedDataConstructor.maxCount - KeySession.skipLimit * 2))
61
61
  this.receivingCount = 0;
62
62
  this.previousCount = this.sendingCount;
63
63
  this.keyPair = crypto_1.default.ECDH.keyPair();
64
64
  this.sendingChain = this.ratchetKeys();
65
- if (this.sendingCount > (EncryptedPayloadConstructor.maxCount - KeySession.skipLimit * 2))
65
+ if (this.sendingCount > (EncryptedDataConstructor.maxCount - KeySession.skipLimit * 2))
66
66
  this.sendingCount = 0;
67
67
  return this;
68
68
  }
@@ -100,11 +100,11 @@ class KeySession {
100
100
  */
101
101
  encrypt(message) {
102
102
  const key = this.getSendingKey();
103
- if (this.sendingCount >= EncryptedPayloadConstructor.maxCount || this.previousCount >= EncryptedPayloadConstructor.maxCount)
103
+ if (this.sendingCount >= EncryptedDataConstructor.maxCount || this.previousCount >= EncryptedDataConstructor.maxCount)
104
104
  throw new Error();
105
- const nonce = crypto_1.default.randomBytes(EncryptedPayloadConstructor.nonceLength);
105
+ const nonce = crypto_1.default.randomBytes(EncryptedDataConstructor.nonceLength);
106
106
  const ciphertext = crypto_1.default.box.encrypt(message, nonce, key);
107
- return new EncryptedPayloadConstructor(this.sendingCount, this.previousCount, this.keyPair.publicKey, nonce, ciphertext);
107
+ return new EncryptedDataConstructor(this.sendingCount, this.previousCount, this.keyPair.publicKey, nonce, ciphertext);
108
108
  }
109
109
  /**
110
110
  * Decrypts an encrypted message.
@@ -114,7 +114,7 @@ class KeySession {
114
114
  */
115
115
  decrypt(payload) {
116
116
  var _a;
117
- const encrypted = EncryptedPayload.from(payload);
117
+ const encrypted = EncryptedData.from(payload);
118
118
  const publicKey = encrypted.publicKey;
119
119
  if (!(0, utils_1.verifyUint8Array)(publicKey, this._remoteKey)) {
120
120
  while (this.receivingCount < encrypted.previous)
@@ -188,7 +188,7 @@ KeySession.rootKeyLength = crypto_1.default.box.keyLength;
188
188
  * Typically 32 bytes (256 bits) for symmetric keys.
189
189
  */
190
190
  KeySession.keyLength = 32;
191
- class EncryptedPayload {
191
+ class EncryptedData {
192
192
  /**
193
193
  * Static factory method that constructs an `EncryptedPayload` from a raw Uint8Array.
194
194
  *
@@ -196,24 +196,27 @@ class EncryptedPayload {
196
196
  * @returns An instance of `EncryptedPayload`.
197
197
  */
198
198
  static from(array) {
199
- return new EncryptedPayloadConstructor(array);
199
+ return new EncryptedDataConstructor(array);
200
200
  }
201
201
  }
202
- exports.EncryptedPayload = EncryptedPayload;
203
- class EncryptedPayloadConstructor {
202
+ exports.EncryptedData = EncryptedData;
203
+ class EncryptedDataConstructor {
204
204
  constructor(...arrays) {
205
- var _a;
206
205
  arrays = arrays.filter(value => value !== undefined);
207
- if (arrays[0] instanceof EncryptedPayloadConstructor) {
206
+ if (arrays[0] instanceof EncryptedDataConstructor) {
208
207
  this.raw = arrays[0].raw;
209
208
  return this;
210
209
  }
211
210
  if (typeof arrays[0] === 'number')
212
- arrays[0] = (0, utils_1.numberToUint8Array)(arrays[0], EncryptedPayloadConstructor.countLength);
211
+ arrays[0] = (0, utils_1.numberToUint8Array)(arrays[0], EncryptedDataConstructor.countLength);
213
212
  if (typeof arrays[1] === 'number')
214
- arrays[1] = (0, utils_1.numberToUint8Array)(arrays[1], EncryptedPayloadConstructor.countLength);
215
- if (arrays.length > 1) {
216
- arrays.unshift((_a = (typeof arrays[5] === 'number' ? (0, utils_1.numberToUint8Array)(arrays[5]) : arrays[5])) !== null && _a !== void 0 ? _a : (0, utils_1.numberToUint8Array)(KeySession.version));
213
+ arrays[1] = (0, utils_1.numberToUint8Array)(arrays[1], EncryptedDataConstructor.countLength);
214
+ if (arrays.length === 6) {
215
+ arrays.unshift(typeof arrays[5] === 'number' ? (0, utils_1.numberToUint8Array)(arrays[5]) : arrays[5]);
216
+ arrays.pop();
217
+ }
218
+ else if (arrays.length > 1) {
219
+ arrays.unshift((0, utils_1.numberToUint8Array)(KeySession.version));
217
220
  }
218
221
  this.raw = (0, utils_1.concatUint8Array)(...arrays);
219
222
  }
@@ -244,12 +247,12 @@ class EncryptedPayloadConstructor {
244
247
  return JSON.stringify(this.decode());
245
248
  }
246
249
  }
247
- EncryptedPayloadConstructor.secretKeyLength = crypto_1.default.ECDH.secretKeyLength;
248
- EncryptedPayloadConstructor.publicKeyLength = crypto_1.default.ECDH.publicKeyLength;
249
- EncryptedPayloadConstructor.keyLength = crypto_1.default.box.keyLength;
250
- EncryptedPayloadConstructor.nonceLength = crypto_1.default.box.nonceLength;
251
- EncryptedPayloadConstructor.maxCount = 65536; //32768;
252
- EncryptedPayloadConstructor.countLength = 2;
250
+ EncryptedDataConstructor.secretKeyLength = crypto_1.default.ECDH.secretKeyLength;
251
+ EncryptedDataConstructor.publicKeyLength = crypto_1.default.ECDH.publicKeyLength;
252
+ EncryptedDataConstructor.keyLength = crypto_1.default.box.keyLength;
253
+ EncryptedDataConstructor.nonceLength = crypto_1.default.box.nonceLength;
254
+ EncryptedDataConstructor.maxCount = 65536; //32768;
255
+ EncryptedDataConstructor.countLength = 2;
253
256
  class Offsets {
254
257
  static set(start, length) {
255
258
  class Offset {
@@ -268,10 +271,10 @@ class Offsets {
268
271
  }
269
272
  Offsets.checksum = Offsets.set(0, 0);
270
273
  Offsets.version = Offsets.set(Offsets.checksum.end, 1);
271
- Offsets.count = Offsets.set(Offsets.version.end, EncryptedPayloadConstructor.countLength);
272
- Offsets.previous = Offsets.set(Offsets.count.end, EncryptedPayloadConstructor.countLength);
273
- Offsets.publicKey = Offsets.set(Offsets.previous.end, EncryptedPayloadConstructor.publicKeyLength);
274
- Offsets.nonce = Offsets.set(Offsets.publicKey.end, EncryptedPayloadConstructor.nonceLength);
274
+ Offsets.count = Offsets.set(Offsets.version.end, EncryptedDataConstructor.countLength);
275
+ Offsets.previous = Offsets.set(Offsets.count.end, EncryptedDataConstructor.countLength);
276
+ Offsets.publicKey = Offsets.set(Offsets.previous.end, EncryptedDataConstructor.publicKeyLength);
277
+ Offsets.nonce = Offsets.set(Offsets.publicKey.end, EncryptedDataConstructor.nonceLength);
275
278
  Offsets.ciphertext = Offsets.set(Offsets.nonce.end, undefined);
276
279
  class KeyMap extends Map {
277
280
  get(key) {
package/index.d.ts CHANGED
@@ -1,5 +1,23 @@
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 from "./crypto";
2
- import { LocalStorage } from "./data";
20
+ import { LocalStorage } from "./types";
3
21
  import { KeySession } from "./double-ratchet";
4
22
  import { KeyExchange } from "./x3dh";
5
23
  /**
@@ -22,4 +40,7 @@ export declare function createKeySession(opts?: {
22
40
  * @param bundleStore
23
41
  * @returns A new X3DH session.
24
42
  */
25
- export declare function createKeyExchange(signKeyPair: crypto.KeyPair, bundleStore?: LocalStorage<string, crypto.KeyPair>): KeyExchange;
43
+ export declare function createKeyExchange(signSecretKey: Uint8Array, boxSecretKey: Uint8Array, bundleStore?: LocalStorage<string, crypto.KeyPair>): KeyExchange;
44
+ export * from "./types";
45
+ export { Protocols, Datagram } from "./data";
46
+ export { EncryptedData } from "./double-ratchet";
package/index.js CHANGED
@@ -1,5 +1,38 @@
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
+ */
20
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
21
+ if (k2 === undefined) k2 = k;
22
+ var desc = Object.getOwnPropertyDescriptor(m, k);
23
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
24
+ desc = { enumerable: true, get: function() { return m[k]; } };
25
+ }
26
+ Object.defineProperty(o, k2, desc);
27
+ }) : (function(o, m, k, k2) {
28
+ if (k2 === undefined) k2 = k;
29
+ o[k2] = m[k];
30
+ }));
31
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
32
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
33
+ };
2
34
  Object.defineProperty(exports, "__esModule", { value: true });
35
+ exports.EncryptedData = exports.Datagram = exports.Protocols = void 0;
3
36
  exports.createKeySession = createKeySession;
4
37
  exports.createKeyExchange = createKeyExchange;
5
38
  const double_ratchet_1 = require("./double-ratchet");
@@ -22,6 +55,12 @@ function createKeySession(opts) {
22
55
  * @param bundleStore
23
56
  * @returns A new X3DH session.
24
57
  */
25
- function createKeyExchange(signKeyPair, bundleStore) {
26
- return new x3dh_1.KeyExchange(signKeyPair, bundleStore);
58
+ function createKeyExchange(signSecretKey, boxSecretKey, bundleStore) {
59
+ return new x3dh_1.KeyExchange(signSecretKey, boxSecretKey, bundleStore);
27
60
  }
61
+ __exportStar(require("./types"), exports);
62
+ var data_1 = require("./data");
63
+ Object.defineProperty(exports, "Protocols", { enumerable: true, get: function () { return data_1.Protocols; } });
64
+ Object.defineProperty(exports, "Datagram", { enumerable: true, get: function () { return data_1.Datagram; } });
65
+ var double_ratchet_2 = require("./double-ratchet");
66
+ Object.defineProperty(exports, "EncryptedData", { enumerable: true, get: function () { return double_ratchet_2.EncryptedData; } });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@freesignal/protocol",
3
- "version": "0.1.2",
3
+ "version": "0.1.6",
4
4
  "description": "Signal Protocol implementation in javascript",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "author": "Christian Braghette",
@@ -12,6 +12,7 @@
12
12
  },
13
13
  "dependencies": {
14
14
  "base64-js": "^1.5.1",
15
+ "fflate": "^0.8.2",
15
16
  "js-sha3": "^0.9.3",
16
17
  "tweetnacl": "^1.0.3",
17
18
  "uuid": "^11.1.0"
package/test.js CHANGED
@@ -2,24 +2,31 @@
2
2
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
- var _a, _b;
6
5
  Object.defineProperty(exports, "__esModule", { value: true });
7
6
  const _1 = require(".");
8
7
  const crypto_1 = __importDefault(require("./crypto"));
8
+ const data_1 = require("./data");
9
9
  const utils_1 = require("./utils");
10
- const bob = (0, _1.createKeyExchange)(crypto_1.default.EdDSA.keyPair());
11
- const alice = (0, _1.createKeyExchange)(crypto_1.default.EdDSA.keyPair());
12
- const bobmessage = bob.generateSyn();
13
- const { session: alicesession, ackMessage: aliceack } = alice.digestSyn(bobmessage);
14
- const { session: bobsession, cleartext } = (_a = bob.digestAck(aliceack)) !== null && _a !== void 0 ? _a : {};
15
- if (bobsession && cleartext) {
16
- console.log("Session established successfully between Alice and Bob.");
17
- const msg = (_b = bobsession.encrypt((0, utils_1.decodeUTF8)("Hi Alice!"))) === null || _b === void 0 ? void 0 : _b.encode();
18
- console.log((0, utils_1.encodeUTF8)(alicesession.decrypt(msg)));
19
- if (alicesession.handshaked && bobsession.handshaked)
20
- console.log("Successfully handshaked");
10
+ const bob = (0, _1.createKeyExchange)(crypto_1.default.EdDSA.keyPair().secretKey, crypto_1.default.ECDH.keyPair().secretKey);
11
+ const alice = (0, _1.createKeyExchange)(crypto_1.default.EdDSA.keyPair().secretKey, crypto_1.default.ECDH.keyPair().secretKey);
12
+ const bobmessage = bob.generateData();
13
+ const { session: alicesession, message: aliceack } = alice.digestData(bobmessage);
14
+ bob.digestMessage(aliceack).then(({ session: bobsession, cleartext }) => {
15
+ var _a;
16
+ if (bobsession && cleartext) {
17
+ console.log("Session established successfully between Alice and Bob.");
18
+ const datagram = data_1.Datagram.create(bob.signatureKey, alice.signatureKey, data_1.Protocols.MESSAGE, (_a = bobsession.encrypt((0, utils_1.decodeUTF8)("Hi Alice!"))) === null || _a === void 0 ? void 0 : _a.encode());
19
+ //console.log(datagram.payload);
20
+ const msg = datagram.encode();
21
+ console.log((0, utils_1.encodeUTF8)(alicesession.decrypt(data_1.Datagram.from(msg).payload)));
22
+ if (alicesession.handshaked && bobsession.handshaked)
23
+ console.log("Successfully handshaked");
24
+ else
25
+ console.log("Error during handshake");
26
+ const longmsg = data_1.Datagram.create(alice.signatureKey, bob.signatureKey, data_1.Protocols.MESSAGE, alicesession.encrypt(new Uint8Array(1000000).fill(33).map(val => val + Math.floor(Math.random() * 93))));
27
+ console.log(longmsg.encode().length);
28
+ console.log(longmsg.encode(false).length);
29
+ }
21
30
  else
22
- console.log("Error during handshake");
23
- }
24
- else
25
- console.log("Error");
31
+ console.log("Error");
32
+ });
package/types.d.ts ADDED
@@ -0,0 +1,61 @@
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
+ */
19
+ /** */
20
+ export interface Encodable {
21
+ encode(): Uint8Array;
22
+ toString(): string;
23
+ toJSON(): string;
24
+ }
25
+ export declare namespace Encodable {
26
+ function isEncodable(obj: any): boolean;
27
+ }
28
+ type LocalStorageIterator<T> = Iterable<T>;
29
+ export interface LocalStorage<K, T> {
30
+ set(key: K, value: T): Promise<this>;
31
+ get(key: K): Promise<T | undefined>;
32
+ has(key: K): Promise<boolean>;
33
+ delete(key: K): Promise<boolean>;
34
+ entries(): Promise<LocalStorageIterator<[K, T]>>;
35
+ }
36
+ export interface KeyExchangeData {
37
+ readonly version: number;
38
+ readonly publicKey: string;
39
+ readonly identityKey: string;
40
+ readonly signedPreKey: string;
41
+ readonly signature: string;
42
+ readonly onetimePreKey: string;
43
+ }
44
+ export interface KeyExchangeSynMessage {
45
+ readonly version: number;
46
+ readonly publicKey: string;
47
+ readonly identityKey: string;
48
+ readonly ephemeralKey: string;
49
+ readonly signedPreKeyHash: string;
50
+ readonly onetimePreKeyHash: string;
51
+ readonly associatedData: string;
52
+ }
53
+ export interface KeyExchangeDataBundle {
54
+ readonly version: number;
55
+ readonly publicKey: string;
56
+ readonly identityKey: string;
57
+ readonly signedPreKey: string;
58
+ readonly signature: string;
59
+ readonly onetimePreKey: string[];
60
+ }
61
+ export {};
package/types.js ADDED
@@ -0,0 +1,29 @@
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
+ */
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.Encodable = void 0;
22
+ var Encodable;
23
+ (function (Encodable) {
24
+ const properties = ['encode', 'toString', 'toJSON'];
25
+ function isEncodable(obj) {
26
+ return !properties.some(prop => !obj[prop]);
27
+ }
28
+ Encodable.isEncodable = isEncodable;
29
+ })(Encodable || (exports.Encodable = Encodable = {}));
package/utils.js CHANGED
@@ -135,6 +135,7 @@ function verifyUint8Array(a, ...b) {
135
135
  * @returns A Uint8Array
136
136
  */
137
137
  function concatUint8Array(...arrays) {
138
+ //arrays = arrays.filter(array => array != undefined);
138
139
  const out = new Uint8Array(arrays.map(value => value.length).reduce((prev, curr) => prev + curr));
139
140
  let offset = 0;
140
141
  arrays.forEach(array => {
package/x3dh.d.ts CHANGED
@@ -17,52 +17,28 @@
17
17
  * along with this program. If not, see <https://www.gnu.org/licenses/>
18
18
  */
19
19
  import crypto from "./crypto";
20
- import { LocalStorage } from "./data";
20
+ import { KeyExchangeData, KeyExchangeDataBundle, KeyExchangeSynMessage, LocalStorage } from "./types";
21
21
  import { KeySession } from "./double-ratchet";
22
- interface SynMessage {
23
- readonly version: number;
24
- readonly publicKey: string;
25
- readonly identityKey: string;
26
- readonly signedPreKey: string;
27
- readonly signature: string;
28
- readonly onetimePreKey: string;
29
- }
30
- interface AckMessage {
31
- readonly version: number;
32
- readonly publicKey: string;
33
- readonly identityKey: string;
34
- readonly ephemeralKey: string;
35
- readonly signedPreKeyHash: string;
36
- readonly onetimePreKeyHash: string;
37
- readonly associatedData: string;
38
- }
39
- export interface Bundle {
40
- readonly version: number;
41
- readonly publicKey: string;
42
- readonly identityKey: string;
43
- readonly signedPreKey: string;
44
- readonly signature: string;
45
- readonly onetimePreKeyHash: string[];
46
- }
47
22
  export declare class KeyExchange {
48
23
  static readonly version = 1;
49
24
  private static readonly hkdfInfo;
50
25
  private static readonly maxOPK;
51
- private readonly publicKey;
52
- private readonly identityKey;
26
+ private readonly _signatureKey;
27
+ private readonly _identityKey;
53
28
  private readonly bundleStore;
54
- constructor(signKeyPair: crypto.KeyPair, bundleStore?: LocalStorage<string, crypto.KeyPair>);
29
+ constructor(signSecretKey: Uint8Array, boxSecretKey: Uint8Array, bundleStore?: LocalStorage<string, crypto.KeyPair>);
30
+ get signatureKey(): Uint8Array;
31
+ get identityKey(): Uint8Array;
55
32
  private generateSPK;
56
33
  private generateOPK;
57
- generateBundle(length?: number): Bundle;
58
- generateSyn(): SynMessage;
59
- digestSyn(message: SynMessage): {
34
+ generateBundle(length?: number): KeyExchangeDataBundle;
35
+ generateData(): KeyExchangeData;
36
+ digestData(message: KeyExchangeData): {
60
37
  session: KeySession;
61
- ackMessage: AckMessage;
38
+ message: KeyExchangeSynMessage;
62
39
  };
63
- digestAck(message: AckMessage): {
40
+ digestMessage(message: KeyExchangeSynMessage): Promise<{
64
41
  session: KeySession;
65
42
  cleartext: Uint8Array;
66
- };
43
+ }>;
67
44
  }
68
- export {};
package/x3dh.js CHANGED
@@ -17,6 +17,15 @@
17
17
  * You should have received a copy of the GNU General Public License
18
18
  * along with this program. If not, see <https://www.gnu.org/licenses/>
19
19
  */
20
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
21
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
22
+ return new (P || (P = Promise))(function (resolve, reject) {
23
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
24
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
25
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
26
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
27
+ });
28
+ };
20
29
  var __importDefault = (this && this.__importDefault) || function (mod) {
21
30
  return (mod && mod.__esModule) ? mod : { "default": mod };
22
31
  };
@@ -26,11 +35,13 @@ const crypto_1 = __importDefault(require("./crypto"));
26
35
  const double_ratchet_1 = require("./double-ratchet");
27
36
  const utils_1 = require("./utils");
28
37
  class KeyExchange {
29
- constructor(signKeyPair, bundleStore) {
30
- this.publicKey = signKeyPair;
31
- this.identityKey = crypto_1.default.ECDH.keyPair(crypto_1.default.hash(signKeyPair.secretKey));
32
- this.bundleStore = bundleStore !== null && bundleStore !== void 0 ? bundleStore : new Map();
38
+ constructor(signSecretKey, boxSecretKey, bundleStore) {
39
+ this._signatureKey = crypto_1.default.EdDSA.keyPair(signSecretKey);
40
+ this._identityKey = crypto_1.default.ECDH.keyPair(boxSecretKey);
41
+ this.bundleStore = bundleStore !== null && bundleStore !== void 0 ? bundleStore : new AsyncMap();
33
42
  }
43
+ get signatureKey() { return this._signatureKey.publicKey; }
44
+ get identityKey() { return this._identityKey.publicKey; }
34
45
  generateSPK() {
35
46
  const signedPreKey = crypto_1.default.ECDH.keyPair();
36
47
  const signedPreKeyHash = crypto_1.default.hash(signedPreKey.publicKey);
@@ -48,48 +59,50 @@ class KeyExchange {
48
59
  const onetimePreKey = new Array(length !== null && length !== void 0 ? length : KeyExchange.maxOPK).fill(0).map(() => this.generateOPK(signedPreKeyHash).onetimePreKey);
49
60
  return {
50
61
  version: KeyExchange.version,
51
- publicKey: (0, utils_1.encodeBase64)(this.publicKey.publicKey),
52
- identityKey: (0, utils_1.encodeBase64)(this.identityKey.publicKey),
62
+ publicKey: (0, utils_1.encodeBase64)(this._signatureKey.publicKey),
63
+ identityKey: (0, utils_1.encodeBase64)(this._identityKey.publicKey),
53
64
  signedPreKey: (0, utils_1.encodeBase64)(signedPreKey.publicKey),
54
- signature: (0, utils_1.encodeBase64)(crypto_1.default.EdDSA.sign(signedPreKeyHash, this.publicKey.secretKey)),
55
- onetimePreKeyHash: onetimePreKey.map(opk => (0, utils_1.encodeBase64)(opk.publicKey))
65
+ signature: (0, utils_1.encodeBase64)(crypto_1.default.EdDSA.sign(signedPreKeyHash, this._signatureKey.secretKey)),
66
+ onetimePreKey: onetimePreKey.map(opk => (0, utils_1.encodeBase64)(opk.publicKey))
56
67
  };
57
68
  }
58
- generateSyn() {
69
+ generateData() {
59
70
  const { signedPreKey, signedPreKeyHash } = this.generateSPK();
60
71
  const { onetimePreKey } = this.generateOPK(signedPreKeyHash);
61
72
  return {
62
73
  version: KeyExchange.version,
63
- publicKey: (0, utils_1.encodeBase64)(this.publicKey.publicKey),
64
- identityKey: (0, utils_1.encodeBase64)(this.identityKey.publicKey),
74
+ publicKey: (0, utils_1.encodeBase64)(this._signatureKey.publicKey),
75
+ identityKey: (0, utils_1.encodeBase64)(this._identityKey.publicKey),
65
76
  signedPreKey: (0, utils_1.encodeBase64)(signedPreKey.publicKey),
66
- signature: (0, utils_1.encodeBase64)(crypto_1.default.EdDSA.sign(signedPreKeyHash, this.publicKey.secretKey)),
77
+ signature: (0, utils_1.encodeBase64)(crypto_1.default.EdDSA.sign(signedPreKeyHash, this._signatureKey.secretKey)),
67
78
  onetimePreKey: (0, utils_1.encodeBase64)(onetimePreKey.publicKey)
68
79
  };
69
80
  }
70
- digestSyn(message) {
81
+ digestData(message) {
71
82
  const ephemeralKey = crypto_1.default.ECDH.keyPair();
72
83
  const signedPreKey = (0, utils_1.decodeBase64)(message.signedPreKey);
84
+ if (!crypto_1.default.EdDSA.verify(crypto_1.default.hash(signedPreKey), (0, utils_1.decodeBase64)(message.signature), (0, utils_1.decodeBase64)(message.publicKey)))
85
+ throw new Error("Signature verification failed");
73
86
  const identityKey = (0, utils_1.decodeBase64)(message.identityKey);
74
87
  const onetimePreKey = message.onetimePreKey ? (0, utils_1.decodeBase64)(message.onetimePreKey) : undefined;
75
88
  const signedPreKeyHash = crypto_1.default.hash(signedPreKey);
76
89
  const onetimePreKeyHash = onetimePreKey ? crypto_1.default.hash(onetimePreKey) : new Uint8Array();
77
90
  const rootKey = crypto_1.default.hkdf(new Uint8Array([
78
- ...crypto_1.default.scalarMult(this.identityKey.secretKey, signedPreKey),
91
+ ...crypto_1.default.scalarMult(this._identityKey.secretKey, signedPreKey),
79
92
  ...crypto_1.default.scalarMult(ephemeralKey.secretKey, identityKey),
80
93
  ...crypto_1.default.scalarMult(ephemeralKey.secretKey, signedPreKey),
81
94
  ...onetimePreKey ? crypto_1.default.scalarMult(ephemeralKey.secretKey, onetimePreKey) : new Uint8Array()
82
95
  ]), new Uint8Array(double_ratchet_1.KeySession.rootKeyLength).fill(0), KeyExchange.hkdfInfo, double_ratchet_1.KeySession.rootKeyLength);
83
- const session = new double_ratchet_1.KeySession({ secretKey: this.identityKey.secretKey, remoteKey: identityKey, rootKey });
84
- const cyphertext = session.encrypt((0, utils_1.concatUint8Array)(crypto_1.default.hash(this.identityKey.publicKey), crypto_1.default.hash(identityKey)));
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)));
85
98
  if (!cyphertext)
86
99
  throw new Error();
87
100
  return {
88
101
  session,
89
- ackMessage: {
102
+ message: {
90
103
  version: KeyExchange.version,
91
- publicKey: (0, utils_1.encodeBase64)(this.publicKey.publicKey),
92
- identityKey: (0, utils_1.encodeBase64)(this.identityKey.publicKey),
104
+ publicKey: (0, utils_1.encodeBase64)(this._signatureKey.publicKey),
105
+ identityKey: (0, utils_1.encodeBase64)(this._identityKey.publicKey),
93
106
  ephemeralKey: (0, utils_1.encodeBase64)(ephemeralKey.publicKey),
94
107
  signedPreKeyHash: (0, utils_1.encodeBase64)(signedPreKeyHash),
95
108
  onetimePreKeyHash: (0, utils_1.encodeBase64)(onetimePreKeyHash),
@@ -97,30 +110,65 @@ class KeyExchange {
97
110
  }
98
111
  };
99
112
  }
100
- digestAck(message) {
101
- const signedPreKey = this.bundleStore.get(message.signedPreKeyHash);
102
- const hash = message.signedPreKeyHash.concat(message.onetimePreKeyHash);
103
- const onetimePreKey = this.bundleStore.get(hash);
104
- if (!signedPreKey || !onetimePreKey || !message.identityKey || !message.ephemeralKey)
105
- throw new Error("ACK message malformed");
106
- if (!this.bundleStore.delete(hash))
107
- throw new Error("Bundle store deleting error");
108
- const identityKey = (0, utils_1.decodeBase64)(message.identityKey);
109
- const ephemeralKey = (0, utils_1.decodeBase64)(message.ephemeralKey);
110
- const rootKey = crypto_1.default.hkdf(new Uint8Array([
111
- ...crypto_1.default.scalarMult(signedPreKey.secretKey, identityKey),
112
- ...crypto_1.default.scalarMult(this.identityKey.secretKey, ephemeralKey),
113
- ...crypto_1.default.scalarMult(signedPreKey.secretKey, ephemeralKey),
114
- ...onetimePreKey ? crypto_1.default.scalarMult(onetimePreKey.secretKey, ephemeralKey) : new Uint8Array()
115
- ]), new Uint8Array(double_ratchet_1.KeySession.rootKeyLength).fill(0), KeyExchange.hkdfInfo, double_ratchet_1.KeySession.rootKeyLength);
116
- const session = new double_ratchet_1.KeySession({ secretKey: this.identityKey.secretKey, rootKey });
117
- const cleartext = session.decrypt((0, utils_1.decodeBase64)(message.associatedData));
118
- if (!cleartext)
119
- throw new Error("Error decrypting ACK message");
120
- return { session, cleartext };
113
+ digestMessage(message) {
114
+ return __awaiter(this, void 0, void 0, function* () {
115
+ const signedPreKey = yield this.bundleStore.get(message.signedPreKeyHash);
116
+ const hash = message.signedPreKeyHash.concat(message.onetimePreKeyHash);
117
+ const onetimePreKey = yield this.bundleStore.get(hash);
118
+ if (!signedPreKey || !onetimePreKey || !message.identityKey || !message.ephemeralKey)
119
+ throw new Error("ACK message malformed");
120
+ if (!this.bundleStore.delete(hash))
121
+ throw new Error("Bundle store deleting error");
122
+ const identityKey = (0, utils_1.decodeBase64)(message.identityKey);
123
+ const ephemeralKey = (0, utils_1.decodeBase64)(message.ephemeralKey);
124
+ const rootKey = crypto_1.default.hkdf(new Uint8Array([
125
+ ...crypto_1.default.scalarMult(signedPreKey.secretKey, identityKey),
126
+ ...crypto_1.default.scalarMult(this._identityKey.secretKey, ephemeralKey),
127
+ ...crypto_1.default.scalarMult(signedPreKey.secretKey, ephemeralKey),
128
+ ...onetimePreKey ? crypto_1.default.scalarMult(onetimePreKey.secretKey, ephemeralKey) : new Uint8Array()
129
+ ]), new Uint8Array(double_ratchet_1.KeySession.rootKeyLength).fill(0), KeyExchange.hkdfInfo, double_ratchet_1.KeySession.rootKeyLength);
130
+ const session = new double_ratchet_1.KeySession({ secretKey: this._identityKey.secretKey, rootKey });
131
+ const cleartext = session.decrypt((0, utils_1.decodeBase64)(message.associatedData));
132
+ if (!cleartext)
133
+ throw new Error("Error decrypting ACK message");
134
+ if (!(0, utils_1.verifyUint8Array)(cleartext, (0, utils_1.concatUint8Array)(crypto_1.default.hash(identityKey), crypto_1.default.hash(this._identityKey.publicKey))))
135
+ throw new Error("Error verifing Associated Data");
136
+ return { session, cleartext };
137
+ });
121
138
  }
122
139
  }
123
140
  exports.KeyExchange = KeyExchange;
124
141
  KeyExchange.version = 1;
125
142
  KeyExchange.hkdfInfo = (0, utils_1.decodeUTF8)("freesignal/x3dh/" + KeyExchange.version);
126
143
  KeyExchange.maxOPK = 10;
144
+ class AsyncMap {
145
+ constructor() {
146
+ this.map = new Map();
147
+ }
148
+ set(key, value) {
149
+ return __awaiter(this, void 0, void 0, function* () {
150
+ this.map.set(key, value);
151
+ return this;
152
+ });
153
+ }
154
+ get(key) {
155
+ return __awaiter(this, void 0, void 0, function* () {
156
+ return this.map.get(key);
157
+ });
158
+ }
159
+ has(key) {
160
+ return __awaiter(this, void 0, void 0, function* () {
161
+ return this.map.has(key);
162
+ });
163
+ }
164
+ delete(key) {
165
+ return __awaiter(this, void 0, void 0, function* () {
166
+ return this.map.delete(key);
167
+ });
168
+ }
169
+ entries() {
170
+ return __awaiter(this, void 0, void 0, function* () {
171
+ return this.map.entries();
172
+ });
173
+ }
174
+ }