@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.
- package/double-ratchet.d.ts +26 -38
- package/double-ratchet.js +166 -108
- package/index.d.ts +10 -13
- package/index.js +12 -10
- package/node.d.ts +13 -2
- package/node.js +41 -7
- package/package.json +3 -2
- package/test.js +10 -7
- package/types.js +1 -1
- package/x3dh.js +5 -5
package/double-ratchet.d.ts
CHANGED
|
@@ -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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
33
|
+
static readonly keyLength = 32;
|
|
38
34
|
static readonly version = 1;
|
|
39
|
-
static readonly
|
|
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
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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.
|
|
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.
|
|
54
|
-
this.sendingChain = this.ratchetKeys();
|
|
104
|
+
this.sendingChain = this.getChain(opts.remoteKey);
|
|
55
105
|
}
|
|
56
106
|
this.storage = storage;
|
|
57
|
-
this.
|
|
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
|
-
|
|
84
|
-
|
|
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.
|
|
91
|
-
return hashkey.
|
|
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
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
this.
|
|
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
|
-
|
|
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
|
-
|
|
153
|
-
|
|
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
|
|
206
|
+
var _a, _b;
|
|
166
207
|
return {
|
|
167
|
-
secretKey: (0, utils_1.decodeBase64)(
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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 =
|
|
187
|
-
session.sendingChain =
|
|
188
|
-
session.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
|
-
|
|
197
|
-
|
|
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
|
-
|
|
200
|
-
|
|
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
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
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
|
|
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
|
|
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:
|
|
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.
|
|
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.
|
|
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
|
|
15
|
-
const
|
|
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
|
|
22
|
-
console.log((0, utils_1.decodeData)(yield alice.receive(
|
|
23
|
-
const
|
|
24
|
-
console.log(
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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 =
|
|
152
|
+
KeyExchange.hkdfInfo = "freesignal/x3dh/" + KeyExchange.version;
|
|
153
153
|
KeyExchange.maxOPK = 10;
|