@freesignal/protocol 0.1.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/LICENSE +674 -0
- package/crypto.d.ts +69 -0
- package/crypto.js +140 -0
- package/data.d.ts +107 -0
- package/data.js +232 -0
- package/double-ratchet.d.ts +172 -0
- package/double-ratchet.js +338 -0
- package/package.json +22 -0
- package/test.js +17 -0
- package/utils.d.ts +78 -0
- package/utils.js +145 -0
- package/x3dh.d.ts +74 -0
- package/x3dh.js +136 -0
package/utils.js
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
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.encodeUTF8 = encodeUTF8;
|
|
22
|
+
exports.decodeUTF8 = decodeUTF8;
|
|
23
|
+
exports.encodeBase64 = encodeBase64;
|
|
24
|
+
exports.decodeBase64 = decodeBase64;
|
|
25
|
+
exports.encodeHex = encodeHex;
|
|
26
|
+
exports.decodeHex = decodeHex;
|
|
27
|
+
exports.numberFromUint8Array = numberFromUint8Array;
|
|
28
|
+
exports.numberToUint8Array = numberToUint8Array;
|
|
29
|
+
exports.verifyUint8Array = verifyUint8Array;
|
|
30
|
+
exports.concatUint8Array = concatUint8Array;
|
|
31
|
+
const base64_js_1 = require("base64-js");
|
|
32
|
+
const tweetnacl_1 = require("tweetnacl");
|
|
33
|
+
/**
|
|
34
|
+
* Decodes a Uint8Array into a UTF-8 string.
|
|
35
|
+
*
|
|
36
|
+
* @param array - The input byte array.
|
|
37
|
+
* @returns The UTF-8 encoded string.
|
|
38
|
+
*/
|
|
39
|
+
function encodeUTF8(array) {
|
|
40
|
+
return new TextDecoder().decode(array);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Encodes a UTF-8 string into a Uint8Array.
|
|
44
|
+
*
|
|
45
|
+
* @param string - The input string.
|
|
46
|
+
* @returns The resulting Uint8Array.
|
|
47
|
+
*/
|
|
48
|
+
function decodeUTF8(string) {
|
|
49
|
+
return new TextEncoder().encode(string);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Encodes a Uint8Array into a Base64 string.
|
|
53
|
+
*
|
|
54
|
+
* @param array - The input byte array.
|
|
55
|
+
* @returns The Base64 encoded string.
|
|
56
|
+
*/
|
|
57
|
+
function encodeBase64(array) {
|
|
58
|
+
return (0, base64_js_1.fromByteArray)(array !== null && array !== void 0 ? array : new Uint8Array());
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Decodes a Base64 string into a Uint8Array.
|
|
62
|
+
*
|
|
63
|
+
* @param string - The Base64 string.
|
|
64
|
+
* @returns The decoded Uint8Array.
|
|
65
|
+
*/
|
|
66
|
+
function decodeBase64(string) {
|
|
67
|
+
return (0, base64_js_1.toByteArray)(string !== null && string !== void 0 ? string : "");
|
|
68
|
+
}
|
|
69
|
+
function encodeHex(array) {
|
|
70
|
+
var _a;
|
|
71
|
+
return Array.from((_a = array === null || array === void 0 ? void 0 : array.values()) !== null && _a !== void 0 ? _a : []).map(value => value.toString(16).padStart(2, '0')).join('');
|
|
72
|
+
}
|
|
73
|
+
function decodeHex(string) {
|
|
74
|
+
return new Uint8Array(!string ? [] :
|
|
75
|
+
Array.from(string).reduce((prev, curr, index) => {
|
|
76
|
+
if (index % 2 === 0)
|
|
77
|
+
prev.push(curr);
|
|
78
|
+
else
|
|
79
|
+
prev[prev.length - 1] += curr;
|
|
80
|
+
return prev;
|
|
81
|
+
}, []).map(value => Number.parseInt(value, 16)));
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Converts a Uint8Array into a number.
|
|
85
|
+
*
|
|
86
|
+
* @param array - The input byte array.
|
|
87
|
+
* @returns The resulting number.
|
|
88
|
+
*/
|
|
89
|
+
function numberFromUint8Array(array, endian = 'little') {
|
|
90
|
+
let total = 0;
|
|
91
|
+
if (array) {
|
|
92
|
+
if (endian === 'big')
|
|
93
|
+
array = array.reverse();
|
|
94
|
+
for (let c = 0; c < array.length; c++)
|
|
95
|
+
total += array[c] << (c * 8);
|
|
96
|
+
}
|
|
97
|
+
return total;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Converts a number into a Uint8Array of specified length.
|
|
101
|
+
*
|
|
102
|
+
* @param number - The number to convert.
|
|
103
|
+
* @param length - The desired output length.
|
|
104
|
+
* @returns A Uint8Array representing the number.
|
|
105
|
+
*/
|
|
106
|
+
function numberToUint8Array(number, length, endian = 'little') {
|
|
107
|
+
if (!number)
|
|
108
|
+
return new Uint8Array(length !== null && length !== void 0 ? length : 0).fill(0);
|
|
109
|
+
const arr = [];
|
|
110
|
+
while (number > 0) {
|
|
111
|
+
arr.push(number & 255);
|
|
112
|
+
number = number >>> 8;
|
|
113
|
+
}
|
|
114
|
+
const out = new Uint8Array(length !== null && length !== void 0 ? length : arr.length);
|
|
115
|
+
out.set(arr);
|
|
116
|
+
return endian === 'little' ? out : out.reverse();
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Compare Uint8Arrays.
|
|
120
|
+
*
|
|
121
|
+
* @param a - First Uint8Array to compare to.
|
|
122
|
+
* @param b - Arrays to compare to the first one.
|
|
123
|
+
* @returns A boolean value.
|
|
124
|
+
*/
|
|
125
|
+
function verifyUint8Array(a, ...b) {
|
|
126
|
+
b = b.filter(value => value !== undefined);
|
|
127
|
+
if (!a || b.length === 0)
|
|
128
|
+
return false;
|
|
129
|
+
return b.every(b => (0, tweetnacl_1.verify)(a, b));
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Concat Uint8Arrays.
|
|
133
|
+
*
|
|
134
|
+
* @param arrays - Uint8Array to concat.
|
|
135
|
+
* @returns A Uint8Array
|
|
136
|
+
*/
|
|
137
|
+
function concatUint8Array(...arrays) {
|
|
138
|
+
const out = new Uint8Array(arrays.map(value => value.length).reduce((prev, curr) => prev + curr));
|
|
139
|
+
let offset = 0;
|
|
140
|
+
arrays.forEach(array => {
|
|
141
|
+
out.set(array, offset);
|
|
142
|
+
offset += array.length;
|
|
143
|
+
});
|
|
144
|
+
return out;
|
|
145
|
+
}
|
package/x3dh.d.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
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
|
+
import crypto from "./crypto";
|
|
20
|
+
type BoxKeyPair = crypto.KeyPair;
|
|
21
|
+
type SignKeyPair = crypto.KeyPair;
|
|
22
|
+
type ExportedX3DH = [
|
|
23
|
+
SignKeyPair,
|
|
24
|
+
[
|
|
25
|
+
Array<[string, BoxKeyPair]>,
|
|
26
|
+
Array<[string, BoxKeyPair]>
|
|
27
|
+
]
|
|
28
|
+
];
|
|
29
|
+
interface SynMessage {
|
|
30
|
+
readonly version: number;
|
|
31
|
+
readonly PK: string;
|
|
32
|
+
readonly IK: string;
|
|
33
|
+
readonly SPK: string;
|
|
34
|
+
readonly SPKsign: string;
|
|
35
|
+
readonly OPK: string;
|
|
36
|
+
}
|
|
37
|
+
interface AckMessage {
|
|
38
|
+
readonly version: number;
|
|
39
|
+
readonly PK: string;
|
|
40
|
+
readonly IK: string;
|
|
41
|
+
readonly EK: string;
|
|
42
|
+
readonly SPKhash: string;
|
|
43
|
+
readonly OPKhash: string;
|
|
44
|
+
readonly AD: string;
|
|
45
|
+
}
|
|
46
|
+
export interface Bundle {
|
|
47
|
+
readonly version: number;
|
|
48
|
+
readonly PK: string;
|
|
49
|
+
readonly IK: string;
|
|
50
|
+
readonly SPK: string;
|
|
51
|
+
readonly SPKsign: string;
|
|
52
|
+
readonly OPK: string[];
|
|
53
|
+
}
|
|
54
|
+
export declare class X3DH {
|
|
55
|
+
static readonly version = 1;
|
|
56
|
+
private static readonly hkdfInfo;
|
|
57
|
+
private static readonly maxOPK;
|
|
58
|
+
private readonly PK;
|
|
59
|
+
private readonly IK;
|
|
60
|
+
private readonly bundleStore;
|
|
61
|
+
constructor(signKeyPair: SignKeyPair, instance?: [Iterable<[string, BoxKeyPair]>, Iterable<[string, BoxKeyPair]>]);
|
|
62
|
+
private generateSPK;
|
|
63
|
+
private generateOPK;
|
|
64
|
+
generateBundle(length?: number): Bundle;
|
|
65
|
+
generateSyn(): SynMessage;
|
|
66
|
+
digestSyn(message: SynMessage, encrypter?: (msg: Uint8Array, key: Uint8Array) => Uint8Array): {
|
|
67
|
+
rootKey: Uint8Array;
|
|
68
|
+
ackMessage: AckMessage;
|
|
69
|
+
};
|
|
70
|
+
digestAck(message: AckMessage, verifier?: (ciphertext: Uint8Array, key: Uint8Array) => boolean): Uint8Array | undefined;
|
|
71
|
+
export(): ExportedX3DH;
|
|
72
|
+
static import(input: ExportedX3DH): X3DH;
|
|
73
|
+
}
|
|
74
|
+
export {};
|
package/x3dh.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
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 __importDefault = (this && this.__importDefault) || function (mod) {
|
|
21
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
22
|
+
};
|
|
23
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
+
exports.X3DH = void 0;
|
|
25
|
+
const crypto_1 = __importDefault(require("./crypto"));
|
|
26
|
+
const double_ratchet_1 = require("./double-ratchet");
|
|
27
|
+
const utils_1 = require("./utils");
|
|
28
|
+
class X3DH {
|
|
29
|
+
constructor(signKeyPair, instance) {
|
|
30
|
+
this.PK = signKeyPair;
|
|
31
|
+
this.IK = crypto_1.default.ECDH.keyPair(crypto_1.default.hash(signKeyPair.secretKey));
|
|
32
|
+
this.bundleStore = {
|
|
33
|
+
SPK: new Map(instance ? instance[0] : []),
|
|
34
|
+
OPK: new Map(instance ? instance[1] : [])
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
generateSPK() {
|
|
38
|
+
const SPK = crypto_1.default.ECDH.keyPair();
|
|
39
|
+
const SPKhash = crypto_1.default.hash(SPK.publicKey);
|
|
40
|
+
this.bundleStore.SPK.set((0, utils_1.encodeBase64)(SPKhash), SPK);
|
|
41
|
+
return { SPK, SPKhash };
|
|
42
|
+
}
|
|
43
|
+
generateOPK(spkHash) {
|
|
44
|
+
const OPK = crypto_1.default.ECDH.keyPair();
|
|
45
|
+
const OPKhash = crypto_1.default.hash(OPK.publicKey);
|
|
46
|
+
this.bundleStore.OPK.set((0, utils_1.encodeBase64)(spkHash).concat((0, utils_1.encodeBase64)(OPKhash)), OPK);
|
|
47
|
+
return { OPK, OPKhash };
|
|
48
|
+
}
|
|
49
|
+
generateBundle(length) {
|
|
50
|
+
const { SPK, SPKhash } = this.generateSPK();
|
|
51
|
+
const OPK = new Array(length !== null && length !== void 0 ? length : X3DH.maxOPK).fill(0).map(() => this.generateOPK(SPKhash).OPK);
|
|
52
|
+
return {
|
|
53
|
+
version: X3DH.version,
|
|
54
|
+
PK: (0, utils_1.encodeBase64)(this.PK.publicKey),
|
|
55
|
+
IK: (0, utils_1.encodeBase64)(this.IK.publicKey),
|
|
56
|
+
SPK: (0, utils_1.encodeBase64)(SPK.publicKey),
|
|
57
|
+
SPKsign: (0, utils_1.encodeBase64)(crypto_1.default.EdDSA.sign((0, utils_1.concatUint8Array)(crypto_1.default.hash(this.IK.publicKey), SPKhash), this.PK.secretKey)),
|
|
58
|
+
OPK: OPK.map(opk => (0, utils_1.encodeBase64)(opk.publicKey))
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
generateSyn() {
|
|
62
|
+
const { SPK, SPKhash } = this.generateSPK();
|
|
63
|
+
const { OPK } = this.generateOPK(SPKhash);
|
|
64
|
+
return {
|
|
65
|
+
version: X3DH.version,
|
|
66
|
+
PK: (0, utils_1.encodeBase64)(this.PK.publicKey),
|
|
67
|
+
IK: (0, utils_1.encodeBase64)(this.IK.publicKey),
|
|
68
|
+
SPK: (0, utils_1.encodeBase64)(SPK.publicKey),
|
|
69
|
+
SPKsign: (0, utils_1.encodeBase64)(crypto_1.default.EdDSA.sign((0, utils_1.concatUint8Array)(crypto_1.default.hash(this.IK.publicKey), SPKhash), this.PK.secretKey)),
|
|
70
|
+
OPK: (0, utils_1.encodeBase64)(OPK.publicKey)
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
digestSyn(message, encrypter) {
|
|
74
|
+
const EK = crypto_1.default.ECDH.keyPair();
|
|
75
|
+
const SPK = (0, utils_1.decodeBase64)(message.SPK);
|
|
76
|
+
const IK = (0, utils_1.decodeBase64)(message.IK);
|
|
77
|
+
const OPK = message.OPK ? (0, utils_1.decodeBase64)(message.OPK) : undefined;
|
|
78
|
+
const spkHash = crypto_1.default.hash(SPK);
|
|
79
|
+
const opkHash = OPK ? crypto_1.default.hash(OPK) : new Uint8Array();
|
|
80
|
+
const rootKey = crypto_1.default.hkdf(new Uint8Array([
|
|
81
|
+
...crypto_1.default.scalarMult(this.IK.secretKey, SPK),
|
|
82
|
+
...crypto_1.default.scalarMult(EK.secretKey, IK),
|
|
83
|
+
...crypto_1.default.scalarMult(EK.secretKey, SPK),
|
|
84
|
+
...OPK ? crypto_1.default.scalarMult(EK.secretKey, OPK) : new Uint8Array()
|
|
85
|
+
]), new Uint8Array(double_ratchet_1.Session.rootKeyLength).fill(0), X3DH.hkdfInfo, double_ratchet_1.Session.rootKeyLength);
|
|
86
|
+
if (!encrypter)
|
|
87
|
+
encrypter = (msg, key) => crypto_1.default.box.encrypt(msg, new Uint8Array(crypto_1.default.box.nonceLength).fill(0), key);
|
|
88
|
+
return {
|
|
89
|
+
rootKey,
|
|
90
|
+
ackMessage: {
|
|
91
|
+
version: X3DH.version,
|
|
92
|
+
PK: (0, utils_1.encodeBase64)(this.PK.publicKey),
|
|
93
|
+
IK: (0, utils_1.encodeBase64)(this.IK.publicKey),
|
|
94
|
+
EK: (0, utils_1.encodeBase64)(EK.publicKey),
|
|
95
|
+
SPKhash: (0, utils_1.encodeBase64)(spkHash),
|
|
96
|
+
OPKhash: (0, utils_1.encodeBase64)(opkHash),
|
|
97
|
+
AD: (0, utils_1.encodeBase64)(encrypter((0, utils_1.concatUint8Array)(crypto_1.default.hash(this.IK.publicKey), crypto_1.default.hash(IK)), rootKey))
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
digestAck(message, verifier) {
|
|
102
|
+
const SPK = this.bundleStore.SPK.get(message.SPKhash);
|
|
103
|
+
const OPK = this.bundleStore.OPK.get(message.SPKhash.concat(message.OPKhash));
|
|
104
|
+
if (!SPK || !OPK || !message.IK || !message.EK)
|
|
105
|
+
return;
|
|
106
|
+
const IK = (0, utils_1.decodeBase64)(message.IK);
|
|
107
|
+
const EK = (0, utils_1.decodeBase64)(message.EK);
|
|
108
|
+
const rootKey = crypto_1.default.hkdf(new Uint8Array([
|
|
109
|
+
...crypto_1.default.scalarMult(SPK.secretKey, IK),
|
|
110
|
+
...crypto_1.default.scalarMult(this.IK.secretKey, EK),
|
|
111
|
+
...crypto_1.default.scalarMult(SPK.secretKey, EK),
|
|
112
|
+
...OPK ? crypto_1.default.scalarMult(OPK.secretKey, EK) : new Uint8Array()
|
|
113
|
+
]), new Uint8Array(double_ratchet_1.Session.rootKeyLength).fill(0), X3DH.hkdfInfo, double_ratchet_1.Session.rootKeyLength);
|
|
114
|
+
if (!verifier)
|
|
115
|
+
verifier = (ciphertext, key) => (0, utils_1.verifyUint8Array)(crypto_1.default.box.decrypt(ciphertext, new Uint8Array(crypto_1.default.box.nonceLength).fill(0), key), (0, utils_1.concatUint8Array)(crypto_1.default.hash(IK), crypto_1.default.hash(this.IK.publicKey)));
|
|
116
|
+
if (!verifier((0, utils_1.decodeBase64)(message.AD), rootKey))
|
|
117
|
+
return;
|
|
118
|
+
return rootKey;
|
|
119
|
+
}
|
|
120
|
+
export() {
|
|
121
|
+
return [
|
|
122
|
+
this.IK,
|
|
123
|
+
[
|
|
124
|
+
Array.from(this.bundleStore.SPK.entries()),
|
|
125
|
+
Array.from(this.bundleStore.OPK.entries())
|
|
126
|
+
]
|
|
127
|
+
];
|
|
128
|
+
}
|
|
129
|
+
static import(input) {
|
|
130
|
+
return new X3DH(...input);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
exports.X3DH = X3DH;
|
|
134
|
+
X3DH.version = 1;
|
|
135
|
+
X3DH.hkdfInfo = (0, utils_1.decodeUTF8)("freesignal/x3dh/" + X3DH.version);
|
|
136
|
+
X3DH.maxOPK = 10;
|