@freesignal/protocol 0.1.2 → 0.1.5
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 +34 -0
- package/crypto.d.ts +15 -6
- package/crypto.js +24 -4
- package/data.d.ts +14 -62
- package/data.js +76 -132
- package/double-ratchet.d.ts +6 -6
- package/double-ratchet.js +30 -27
- package/index.d.ts +23 -2
- package/index.js +41 -2
- package/package.json +2 -1
- package/test.js +13 -7
- package/types.d.ts +61 -0
- package/types.js +29 -0
- package/utils.js +1 -0
- package/x3dh.d.ts +11 -35
- package/x3dh.js +27 -21
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
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
|
|
44
|
-
readonly
|
|
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
|
|
60
|
-
readonly
|
|
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
|
-
|
|
66
|
-
|
|
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.
|
|
24
|
+
exports.Datagram = exports.Protocols = void 0;
|
|
25
25
|
const utils_1 = require("./utils");
|
|
26
26
|
const crypto_1 = __importDefault(require("./crypto"));
|
|
27
|
-
|
|
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 =
|
|
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
|
-
|
|
88
|
-
this.
|
|
89
|
-
this.
|
|
90
|
-
this.
|
|
91
|
-
this.
|
|
92
|
-
this.
|
|
93
|
-
|
|
94
|
-
|
|
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.
|
|
101
|
-
this.
|
|
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.
|
|
117
|
+
this.id = crypto_1.default.UUID.generate().toString();
|
|
109
118
|
this.version = Datagram.version;
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
].
|
|
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.
|
|
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
|
-
|
|
138
|
-
|
|
165
|
+
senderKey: this.senderKey,
|
|
166
|
+
senderRelay: this.senderRelay,
|
|
167
|
+
receiverKey: this.receiverKey,
|
|
168
|
+
receiverRelay: this.receiverRelay,
|
|
139
169
|
protocol: this.protocol,
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
176
|
+
DatagramConstructor.headerOffset = 28 + crypto_1.default.EdDSA.publicKeyLength * 2;
|
package/double-ratchet.d.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* You should have received a copy of the GNU General Public License
|
|
17
17
|
* along with this program. If not, see <https://www.gnu.org/licenses/>
|
|
18
18
|
*/
|
|
19
|
-
import { Encodable } from "./
|
|
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):
|
|
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 |
|
|
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
|
|
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
|
|
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 |
|
|
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.
|
|
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 > (
|
|
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 > (
|
|
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 >=
|
|
103
|
+
if (this.sendingCount >= EncryptedDataConstructor.maxCount || this.previousCount >= EncryptedDataConstructor.maxCount)
|
|
104
104
|
throw new Error();
|
|
105
|
-
const nonce = crypto_1.default.randomBytes(
|
|
105
|
+
const nonce = crypto_1.default.randomBytes(EncryptedDataConstructor.nonceLength);
|
|
106
106
|
const ciphertext = crypto_1.default.box.encrypt(message, nonce, key);
|
|
107
|
-
return new
|
|
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 =
|
|
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
|
|
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
|
|
199
|
+
return new EncryptedDataConstructor(array);
|
|
200
200
|
}
|
|
201
201
|
}
|
|
202
|
-
exports.
|
|
203
|
-
class
|
|
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
|
|
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],
|
|
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],
|
|
215
|
-
if (arrays.length
|
|
216
|
-
arrays.unshift(
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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,
|
|
272
|
-
Offsets.previous = Offsets.set(Offsets.count.end,
|
|
273
|
-
Offsets.publicKey = Offsets.set(Offsets.previous.end,
|
|
274
|
-
Offsets.nonce = Offsets.set(Offsets.publicKey.end,
|
|
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 "./
|
|
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(
|
|
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(
|
|
26
|
-
return new x3dh_1.KeyExchange(
|
|
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.
|
|
3
|
+
"version": "0.1.5",
|
|
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
|
@@ -6,20 +6,26 @@ var _a, _b;
|
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
7
|
const _1 = require(".");
|
|
8
8
|
const crypto_1 = __importDefault(require("./crypto"));
|
|
9
|
+
const data_1 = require("./data");
|
|
9
10
|
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.
|
|
13
|
-
const { session: alicesession,
|
|
14
|
-
const { session: bobsession, cleartext } = (_a = bob.
|
|
11
|
+
const bob = (0, _1.createKeyExchange)(crypto_1.default.EdDSA.keyPair().secretKey, crypto_1.default.ECDH.keyPair().secretKey);
|
|
12
|
+
const alice = (0, _1.createKeyExchange)(crypto_1.default.EdDSA.keyPair().secretKey, crypto_1.default.ECDH.keyPair().secretKey);
|
|
13
|
+
const bobmessage = bob.generateData();
|
|
14
|
+
const { session: alicesession, message: aliceack } = alice.digestData(bobmessage);
|
|
15
|
+
const { session: bobsession, cleartext } = (_a = bob.digestMessage(aliceack)) !== null && _a !== void 0 ? _a : {};
|
|
15
16
|
if (bobsession && cleartext) {
|
|
16
17
|
console.log("Session established successfully between Alice and Bob.");
|
|
17
|
-
const
|
|
18
|
-
console.log(
|
|
18
|
+
const datagram = data_1.Datagram.create(bob.signatureKey, alice.signatureKey, data_1.Protocols.MESSAGE, (_b = bobsession.encrypt((0, utils_1.decodeUTF8)("Hi Alice!"))) === null || _b === void 0 ? void 0 : _b.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)));
|
|
19
22
|
if (alicesession.handshaked && bobsession.handshaked)
|
|
20
23
|
console.log("Successfully handshaked");
|
|
21
24
|
else
|
|
22
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);
|
|
23
29
|
}
|
|
24
30
|
else
|
|
25
31
|
console.log("Error");
|
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): this;
|
|
31
|
+
get(key: K): T | undefined;
|
|
32
|
+
has(key: K): boolean;
|
|
33
|
+
delete(key: K): boolean;
|
|
34
|
+
entries(): 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 "./
|
|
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
|
|
52
|
-
private readonly
|
|
26
|
+
private readonly _signatureKey;
|
|
27
|
+
private readonly _identityKey;
|
|
53
28
|
private readonly bundleStore;
|
|
54
|
-
constructor(
|
|
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):
|
|
58
|
-
|
|
59
|
-
|
|
34
|
+
generateBundle(length?: number): KeyExchangeDataBundle;
|
|
35
|
+
generateData(): KeyExchangeData;
|
|
36
|
+
digestData(message: KeyExchangeData): {
|
|
60
37
|
session: KeySession;
|
|
61
|
-
|
|
38
|
+
message: KeyExchangeSynMessage;
|
|
62
39
|
};
|
|
63
|
-
|
|
40
|
+
digestMessage(message: KeyExchangeSynMessage): {
|
|
64
41
|
session: KeySession;
|
|
65
42
|
cleartext: Uint8Array;
|
|
66
43
|
};
|
|
67
44
|
}
|
|
68
|
-
export {};
|
package/x3dh.js
CHANGED
|
@@ -26,11 +26,13 @@ const crypto_1 = __importDefault(require("./crypto"));
|
|
|
26
26
|
const double_ratchet_1 = require("./double-ratchet");
|
|
27
27
|
const utils_1 = require("./utils");
|
|
28
28
|
class KeyExchange {
|
|
29
|
-
constructor(
|
|
30
|
-
this.
|
|
31
|
-
this.
|
|
29
|
+
constructor(signSecretKey, boxSecretKey, bundleStore) {
|
|
30
|
+
this._signatureKey = crypto_1.default.EdDSA.keyPair(signSecretKey);
|
|
31
|
+
this._identityKey = crypto_1.default.ECDH.keyPair(boxSecretKey);
|
|
32
32
|
this.bundleStore = bundleStore !== null && bundleStore !== void 0 ? bundleStore : new Map();
|
|
33
33
|
}
|
|
34
|
+
get signatureKey() { return this._signatureKey.publicKey; }
|
|
35
|
+
get identityKey() { return this._identityKey.publicKey; }
|
|
34
36
|
generateSPK() {
|
|
35
37
|
const signedPreKey = crypto_1.default.ECDH.keyPair();
|
|
36
38
|
const signedPreKeyHash = crypto_1.default.hash(signedPreKey.publicKey);
|
|
@@ -48,48 +50,50 @@ class KeyExchange {
|
|
|
48
50
|
const onetimePreKey = new Array(length !== null && length !== void 0 ? length : KeyExchange.maxOPK).fill(0).map(() => this.generateOPK(signedPreKeyHash).onetimePreKey);
|
|
49
51
|
return {
|
|
50
52
|
version: KeyExchange.version,
|
|
51
|
-
publicKey: (0, utils_1.encodeBase64)(this.
|
|
52
|
-
identityKey: (0, utils_1.encodeBase64)(this.
|
|
53
|
+
publicKey: (0, utils_1.encodeBase64)(this._signatureKey.publicKey),
|
|
54
|
+
identityKey: (0, utils_1.encodeBase64)(this._identityKey.publicKey),
|
|
53
55
|
signedPreKey: (0, utils_1.encodeBase64)(signedPreKey.publicKey),
|
|
54
|
-
signature: (0, utils_1.encodeBase64)(crypto_1.default.EdDSA.sign(signedPreKeyHash, this.
|
|
55
|
-
|
|
56
|
+
signature: (0, utils_1.encodeBase64)(crypto_1.default.EdDSA.sign(signedPreKeyHash, this._signatureKey.secretKey)),
|
|
57
|
+
onetimePreKey: onetimePreKey.map(opk => (0, utils_1.encodeBase64)(opk.publicKey))
|
|
56
58
|
};
|
|
57
59
|
}
|
|
58
|
-
|
|
60
|
+
generateData() {
|
|
59
61
|
const { signedPreKey, signedPreKeyHash } = this.generateSPK();
|
|
60
62
|
const { onetimePreKey } = this.generateOPK(signedPreKeyHash);
|
|
61
63
|
return {
|
|
62
64
|
version: KeyExchange.version,
|
|
63
|
-
publicKey: (0, utils_1.encodeBase64)(this.
|
|
64
|
-
identityKey: (0, utils_1.encodeBase64)(this.
|
|
65
|
+
publicKey: (0, utils_1.encodeBase64)(this._signatureKey.publicKey),
|
|
66
|
+
identityKey: (0, utils_1.encodeBase64)(this._identityKey.publicKey),
|
|
65
67
|
signedPreKey: (0, utils_1.encodeBase64)(signedPreKey.publicKey),
|
|
66
|
-
signature: (0, utils_1.encodeBase64)(crypto_1.default.EdDSA.sign(signedPreKeyHash, this.
|
|
68
|
+
signature: (0, utils_1.encodeBase64)(crypto_1.default.EdDSA.sign(signedPreKeyHash, this._signatureKey.secretKey)),
|
|
67
69
|
onetimePreKey: (0, utils_1.encodeBase64)(onetimePreKey.publicKey)
|
|
68
70
|
};
|
|
69
71
|
}
|
|
70
|
-
|
|
72
|
+
digestData(message) {
|
|
71
73
|
const ephemeralKey = crypto_1.default.ECDH.keyPair();
|
|
72
74
|
const signedPreKey = (0, utils_1.decodeBase64)(message.signedPreKey);
|
|
75
|
+
if (!crypto_1.default.EdDSA.verify(signedPreKey, (0, utils_1.decodeBase64)(message.signature), (0, utils_1.decodeBase64)(message.publicKey)))
|
|
76
|
+
throw new Error("Signature verification failed");
|
|
73
77
|
const identityKey = (0, utils_1.decodeBase64)(message.identityKey);
|
|
74
78
|
const onetimePreKey = message.onetimePreKey ? (0, utils_1.decodeBase64)(message.onetimePreKey) : undefined;
|
|
75
79
|
const signedPreKeyHash = crypto_1.default.hash(signedPreKey);
|
|
76
80
|
const onetimePreKeyHash = onetimePreKey ? crypto_1.default.hash(onetimePreKey) : new Uint8Array();
|
|
77
81
|
const rootKey = crypto_1.default.hkdf(new Uint8Array([
|
|
78
|
-
...crypto_1.default.scalarMult(this.
|
|
82
|
+
...crypto_1.default.scalarMult(this._identityKey.secretKey, signedPreKey),
|
|
79
83
|
...crypto_1.default.scalarMult(ephemeralKey.secretKey, identityKey),
|
|
80
84
|
...crypto_1.default.scalarMult(ephemeralKey.secretKey, signedPreKey),
|
|
81
85
|
...onetimePreKey ? crypto_1.default.scalarMult(ephemeralKey.secretKey, onetimePreKey) : new Uint8Array()
|
|
82
86
|
]), new Uint8Array(double_ratchet_1.KeySession.rootKeyLength).fill(0), KeyExchange.hkdfInfo, double_ratchet_1.KeySession.rootKeyLength);
|
|
83
|
-
const session = new double_ratchet_1.KeySession({
|
|
84
|
-
const cyphertext = session.encrypt((0, utils_1.concatUint8Array)(crypto_1.default.hash(this.
|
|
87
|
+
const session = new double_ratchet_1.KeySession({ remoteKey: identityKey, rootKey });
|
|
88
|
+
const cyphertext = session.encrypt((0, utils_1.concatUint8Array)(crypto_1.default.hash(this._identityKey.publicKey), crypto_1.default.hash(identityKey)));
|
|
85
89
|
if (!cyphertext)
|
|
86
90
|
throw new Error();
|
|
87
91
|
return {
|
|
88
92
|
session,
|
|
89
|
-
|
|
93
|
+
message: {
|
|
90
94
|
version: KeyExchange.version,
|
|
91
|
-
publicKey: (0, utils_1.encodeBase64)(this.
|
|
92
|
-
identityKey: (0, utils_1.encodeBase64)(this.
|
|
95
|
+
publicKey: (0, utils_1.encodeBase64)(this._signatureKey.publicKey),
|
|
96
|
+
identityKey: (0, utils_1.encodeBase64)(this._identityKey.publicKey),
|
|
93
97
|
ephemeralKey: (0, utils_1.encodeBase64)(ephemeralKey.publicKey),
|
|
94
98
|
signedPreKeyHash: (0, utils_1.encodeBase64)(signedPreKeyHash),
|
|
95
99
|
onetimePreKeyHash: (0, utils_1.encodeBase64)(onetimePreKeyHash),
|
|
@@ -97,7 +101,7 @@ class KeyExchange {
|
|
|
97
101
|
}
|
|
98
102
|
};
|
|
99
103
|
}
|
|
100
|
-
|
|
104
|
+
digestMessage(message) {
|
|
101
105
|
const signedPreKey = this.bundleStore.get(message.signedPreKeyHash);
|
|
102
106
|
const hash = message.signedPreKeyHash.concat(message.onetimePreKeyHash);
|
|
103
107
|
const onetimePreKey = this.bundleStore.get(hash);
|
|
@@ -109,14 +113,16 @@ class KeyExchange {
|
|
|
109
113
|
const ephemeralKey = (0, utils_1.decodeBase64)(message.ephemeralKey);
|
|
110
114
|
const rootKey = crypto_1.default.hkdf(new Uint8Array([
|
|
111
115
|
...crypto_1.default.scalarMult(signedPreKey.secretKey, identityKey),
|
|
112
|
-
...crypto_1.default.scalarMult(this.
|
|
116
|
+
...crypto_1.default.scalarMult(this._identityKey.secretKey, ephemeralKey),
|
|
113
117
|
...crypto_1.default.scalarMult(signedPreKey.secretKey, ephemeralKey),
|
|
114
118
|
...onetimePreKey ? crypto_1.default.scalarMult(onetimePreKey.secretKey, ephemeralKey) : new Uint8Array()
|
|
115
119
|
]), 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.
|
|
120
|
+
const session = new double_ratchet_1.KeySession({ secretKey: this._identityKey.secretKey, rootKey });
|
|
117
121
|
const cleartext = session.decrypt((0, utils_1.decodeBase64)(message.associatedData));
|
|
118
122
|
if (!cleartext)
|
|
119
123
|
throw new Error("Error decrypting ACK message");
|
|
124
|
+
if (!(0, utils_1.verifyUint8Array)(cleartext, (0, utils_1.concatUint8Array)(crypto_1.default.hash(identityKey), crypto_1.default.hash(this._identityKey.publicKey))))
|
|
125
|
+
throw new Error("Error verifing Associated Data");
|
|
120
126
|
return { session, cleartext };
|
|
121
127
|
}
|
|
122
128
|
}
|