@freesignal/protocol 0.11.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/README.md ADDED
@@ -0,0 +1,103 @@
1
+ # FreeSignal Protocol
2
+
3
+ TypeScript primitives and a small reference implementation of the FreeSignal secure-messaging primitives.
4
+
5
+ This repository provides low-level building blocks: a lightweight key-exchange manager (X3DH-like), an in-memory keystore for testing, a double-ratchet style session manager, and small constructor helpers intended for building secure messaging clients or protocol tooling.
6
+
7
+ **Highlights**
8
+ - User factories and a test harness for peer handshakes and message exchange.
9
+ - Pure TypeScript implementation with a pluggable Crypto provider (see `@freesignal/crypto`).
10
+ - In-memory keystore reference implementation for testing: [src/keystore.ts](src/keystore.ts).
11
+
12
+ ## Package
13
+
14
+ This package is published as `@freesignal/protocol` (see `package.json`). The library exports the primary constructors and helpers from `src/index.ts`:
15
+
16
+ - `UserFactory` — factory for creating `User` instances
17
+ - `UserConstructor` — low-level user constructor
18
+ - `InMemoryKeystoreFactory`, `InMemoryKeystore` — test keystore helpers
19
+ - `useConstructors` — convenience constructor helpers
20
+
21
+ ## Installation
22
+
23
+ Install from npm:
24
+
25
+ ```bash
26
+ npm install @freesignal/protocol
27
+ ```
28
+
29
+ This library expects a compatible Crypto provider implementing the FreeSignal crypto interfaces — the test harness uses `@freesignal/crypto`.
30
+
31
+ ## Quick start
32
+
33
+ The repository includes a small test/example in [src/test.ts](src/test.ts) which demonstrates creating two users, performing a handshake and exchanging messages.
34
+
35
+ Example (based on `src/test.ts`):
36
+
37
+ ```ts
38
+ import crypto from "@freesignal/crypto";
39
+ import { UserFactory, InMemoryKeystoreFactory } from "@freesignal/protocol";
40
+
41
+ const userFactory = new UserFactory(new InMemoryKeystoreFactory(), crypto);
42
+ const alice = await userFactory.create();
43
+ const bob = await userFactory.create();
44
+
45
+
46
+ // Exchange pre-key bundle and complete handshake
47
+ const bundle = await alice.generatePreKeyBundle();
48
+ const message = await bob.handleIncomingPreKeyBundle(bundle);
49
+ await alice.handleIncomingPreKeyMessage(message);
50
+
51
+ // Encrypt / decrypt
52
+ const ciphertext = await alice.encrypt(bob.id, "Hello from Alice");
53
+ const plaintext = await bob.decrypt(alice.id, ciphertext);
54
+ console.log(plaintext);
55
+ ```
56
+
57
+ This demonstrates how `UserFactory` composes the keystore, key-exchange manager and session manager to create a usable `User` object with `encrypt`, `decrypt`.
58
+
59
+ ## API Overview
60
+
61
+ This package primarily exposes two constructors: `UserFactoryConstructor` and `UserConstructor` (re-exported from `src/index.ts`). Documentation below follows the runtime signatures implemented in `src/user.ts`.
62
+
63
+ - `UserFactoryConstructor` (constructor: `new UserFactoryConstructor(keyStoreFactory: KeyStoreFactory, crypto: Crypto)`)
64
+ - `create(seed?: Bytes): Promise<User>` — create a `User` with a fresh identity or a deterministic seed. Uses `useConstructors` and the provided `Crypto` implementation to derive an identity and an in-memory `KeyStore` from the provided `KeyStoreFactory`.
65
+ - `destroy(user: User): boolean` — optional cleanup; returns `true` if the factory removed internal references to the supplied `User` instance.
66
+
67
+ - `UserConstructor` (new UserConstructor(publicIdentity: PublicIdentity, keyStore: KeyStore, crypto: Crypto))
68
+ - `publicIdentity: PublicIdentity` — the public identity supplied to the constructor.
69
+ - `id: UserId` — getter for `publicIdentity.userId`.
70
+ - `encrypt<T>(to: UserId | string, plaintext: T): Promise<Ciphertext>` — encrypt a payload for a recipient.
71
+ - `decrypt<T>(ciphertext: Ciphertext | Bytes): Promise<DecryptResult<T>>` — decrypt an incoming ciphertext.
72
+ - `generatePreKeyBundle(): Promise<PreKeyBundle>` — create a pre-key bundle for publication or delivery to a peer.
73
+ - `handleIncomingPreKeyBundle(bundle: PreKeyBundle, associatedData?: Bytes): Promise<PreKeyMessage>` — process an incoming bundle and create the resulting session.
74
+ - `handleIncomingPreKeyMessage(message: PreKeyMessage): Promise<Bytes | undefined>` — process an incoming pre-key message and create the resulting session.
75
+
76
+ ## Build & test
77
+
78
+ The project compiles to `dist/` using TypeScript. The `package.json` scripts are:
79
+
80
+ ```bash
81
+ # compile (run by `prepare` and `pretest`)
82
+ npm run prepare
83
+
84
+ # run tests (compiles first via pretest)
85
+ npm test
86
+ ```
87
+
88
+ The test harness is `src/test.ts`; compiled output is `dist/test.js`.
89
+
90
+ ## Contributing
91
+
92
+ - Keep changes focused and add tests for protocol behavior and edge cases.
93
+ - If you add features, include small examples or update `src/test.ts`.
94
+
95
+ ## License
96
+
97
+ This project is licensed under `GPL-3.0-or-later`. See the `LICENSE` file in the repository root.
98
+
99
+ ## Notes
100
+
101
+ - The in-memory keystore is a test/reference implementation only — for production use implement a durable `KeyStore`.
102
+ - The library is agnostic to the Crypto provider as long as it implements the expected `Crypto` FreeSignal interface from `@freesignal/interfaces`, you can use `@freesignal/crypto` as a reference implementation.
103
+
@@ -0,0 +1,148 @@
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 type { Bytes, Ciphertext, Identity, PublicIdentity, UserId, Crypto } from "./interfaces.ts";
20
+ type Constructors = ReturnType<typeof defConstructors>;
21
+ declare function defConstructors(crypto: Crypto): {
22
+ UserIdConstructor: {
23
+ new (bytes: Bytes): {
24
+ readonly bytes: Bytes;
25
+ toString(): string;
26
+ toUrl(): string;
27
+ toJSON(): string;
28
+ };
29
+ readonly keyLength: 32;
30
+ fromKey(identityKey: string | Uint8Array | PublicIdentity): UserId;
31
+ from(userId: string | Uint8Array | UserId): UserId;
32
+ };
33
+ PublicIdentityConstructor: {
34
+ new (publicKey: Bytes): {
35
+ readonly publicKey: Bytes;
36
+ readonly userId: UserId;
37
+ toPublicECDHKey(): Bytes;
38
+ readonly bytes: Uint8Array;
39
+ toString(): string;
40
+ toJSON(): string;
41
+ };
42
+ readonly keyLength: number;
43
+ from(publicIdentity: PublicIdentity | Bytes | string): PublicIdentity;
44
+ };
45
+ IdentityConstructor: {
46
+ new (secretKey: Bytes): {
47
+ readonly secretKey: Bytes;
48
+ toSecretECDHKey(): Bytes;
49
+ readonly publicKey: Bytes;
50
+ readonly userId: UserId;
51
+ toPublicECDHKey(): Bytes;
52
+ readonly bytes: Uint8Array;
53
+ toString(): string;
54
+ toJSON(): string;
55
+ };
56
+ readonly keyLength: number;
57
+ from(identity: Identity | Uint8Array | string): Identity;
58
+ };
59
+ CiphertextHeaderConstructor: {
60
+ new (count: number, previous: number, publicKey: Uint8Array, nonce: Uint8Array): {
61
+ readonly count: number;
62
+ readonly previous: number;
63
+ readonly publicKey: Uint8Array;
64
+ readonly nonce: Uint8Array;
65
+ readonly bytes: Uint8Array;
66
+ toJSON(): {
67
+ count: number;
68
+ previous: number;
69
+ publicKey: string;
70
+ };
71
+ };
72
+ readonly keyLength: number;
73
+ readonly nonceLength: number;
74
+ readonly countLength: 2;
75
+ from(data: Uint8Array | {
76
+ readonly count: number;
77
+ readonly previous: number;
78
+ readonly publicKey: Uint8Array;
79
+ readonly nonce: Uint8Array;
80
+ readonly bytes: Uint8Array;
81
+ toJSON(): {
82
+ count: number;
83
+ previous: number;
84
+ publicKey: string;
85
+ };
86
+ }): {
87
+ readonly count: number;
88
+ readonly previous: number;
89
+ readonly publicKey: Uint8Array;
90
+ readonly nonce: Uint8Array;
91
+ readonly bytes: Uint8Array;
92
+ toJSON(): {
93
+ count: number;
94
+ previous: number;
95
+ publicKey: string;
96
+ };
97
+ };
98
+ };
99
+ CiphertextConstructor: {
100
+ new (opts: {
101
+ header: Uint8Array;
102
+ payload: Uint8Array;
103
+ version?: number;
104
+ }): {
105
+ readonly version: number;
106
+ readonly hashkey: Uint8Array;
107
+ readonly header: Uint8Array;
108
+ readonly nonce: Uint8Array;
109
+ readonly payload: Uint8Array;
110
+ readonly length: number;
111
+ readonly bytes: Uint8Array;
112
+ toJSON(): {
113
+ version: number;
114
+ header: string;
115
+ hashkey: string;
116
+ nonce: string;
117
+ payload: string;
118
+ };
119
+ };
120
+ new (opts: {
121
+ header: Uint8Array;
122
+ hashkey: Uint8Array;
123
+ nonce: Uint8Array;
124
+ payload: Uint8Array;
125
+ version?: number;
126
+ }): {
127
+ readonly version: number;
128
+ readonly hashkey: Uint8Array;
129
+ readonly header: Uint8Array;
130
+ readonly nonce: Uint8Array;
131
+ readonly payload: Uint8Array;
132
+ readonly length: number;
133
+ readonly bytes: Uint8Array;
134
+ toJSON(): {
135
+ version: number;
136
+ header: string;
137
+ hashkey: string;
138
+ nonce: string;
139
+ payload: string;
140
+ };
141
+ };
142
+ readonly version: 1;
143
+ readonly nonceLength: number;
144
+ from(data: Bytes | Ciphertext): Ciphertext;
145
+ };
146
+ };
147
+ export declare function useConstructors(crypto: Crypto): Constructors;
148
+ export {};
@@ -0,0 +1,185 @@
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
+ const cache = new WeakMap();
20
+ function defConstructors(crypto) {
21
+ class UserIdConstructor {
22
+ constructor(bytes) {
23
+ this.bytes = bytes;
24
+ }
25
+ ;
26
+ toString() {
27
+ return crypto.Utils.decodeBase64(this.bytes);
28
+ }
29
+ toUrl() {
30
+ return crypto.Utils.decodeBase64URL(this.bytes);
31
+ }
32
+ toJSON() {
33
+ return this.toString();
34
+ }
35
+ static fromKey(identityKey) {
36
+ if (typeof identityKey === 'string')
37
+ identityKey = crypto.Utils.encodeBase64(identityKey);
38
+ else if (!(identityKey instanceof Uint8Array))
39
+ identityKey = identityKey.bytes;
40
+ return new UserIdConstructor(crypto.hkdf(identityKey, new Uint8Array(32).fill(0), "/freesignal/userid", UserIdConstructor.keyLength));
41
+ }
42
+ static from(userId) {
43
+ if (typeof userId === 'string')
44
+ userId = crypto.Utils.encodeBase64(userId);
45
+ return new UserIdConstructor(userId instanceof Uint8Array ? userId : userId.bytes);
46
+ }
47
+ }
48
+ UserIdConstructor.keyLength = 32;
49
+ class PublicIdentityConstructor {
50
+ constructor(publicKey) {
51
+ this.publicKey = publicKey;
52
+ }
53
+ get userId() {
54
+ return UserIdConstructor.fromKey(this.publicKey);
55
+ }
56
+ toPublicECDHKey() {
57
+ return crypto.EdDSA.toPublicECDHKey(this.publicKey);
58
+ }
59
+ get bytes() {
60
+ return this.publicKey;
61
+ }
62
+ toString() {
63
+ return crypto.Utils.decodeBase64(this.bytes);
64
+ }
65
+ toJSON() {
66
+ return this.toString();
67
+ }
68
+ static from(publicIdentity) {
69
+ if (publicIdentity instanceof Uint8Array || typeof publicIdentity === 'string') {
70
+ if (typeof publicIdentity === 'string')
71
+ publicIdentity = crypto.Utils.encodeBase64(publicIdentity);
72
+ if (publicIdentity.length !== PublicIdentityConstructor.keyLength)
73
+ throw new Error("Invalid key length");
74
+ }
75
+ else {
76
+ publicIdentity = publicIdentity.publicKey;
77
+ }
78
+ return new PublicIdentityConstructor(publicIdentity);
79
+ }
80
+ }
81
+ PublicIdentityConstructor.keyLength = crypto.EdDSA.publicKeyLength;
82
+ class IdentityConstructor extends PublicIdentityConstructor {
83
+ constructor(secretKey) {
84
+ const keyPair = crypto.EdDSA.keyPair(secretKey);
85
+ super(keyPair.publicKey);
86
+ this.secretKey = secretKey;
87
+ this.secretKey = keyPair.secretKey;
88
+ }
89
+ toSecretECDHKey() {
90
+ return crypto.EdDSA.toSecretECDHKey(this.secretKey);
91
+ }
92
+ static from(identity) {
93
+ if (identity instanceof Uint8Array || typeof identity === 'string') {
94
+ if (typeof identity === 'string')
95
+ identity = crypto.Utils.encodeBase64(identity);
96
+ if (identity.length !== IdentityConstructor.keyLength)
97
+ throw new Error("Invalid key length");
98
+ }
99
+ else {
100
+ identity = identity.secretKey;
101
+ }
102
+ return new IdentityConstructor(identity);
103
+ }
104
+ }
105
+ IdentityConstructor.keyLength = crypto.EdDSA.secretKeyLength;
106
+ class CiphertextHeaderConstructor {
107
+ constructor(count, previous, publicKey, nonce) {
108
+ this.count = count;
109
+ this.previous = previous;
110
+ this.publicKey = publicKey;
111
+ this.nonce = nonce;
112
+ }
113
+ get bytes() {
114
+ return crypto.Utils.concatBytes(crypto.Utils.numberToBytes(this.count, CiphertextHeaderConstructor.countLength), crypto.Utils.numberToBytes(this.previous, CiphertextHeaderConstructor.countLength), this.publicKey, this.nonce);
115
+ }
116
+ toJSON() {
117
+ return {
118
+ count: this.count,
119
+ previous: this.previous,
120
+ publicKey: crypto.Utils.decodeBase64(this.publicKey)
121
+ };
122
+ }
123
+ static from(data) {
124
+ if (data instanceof CiphertextHeaderConstructor)
125
+ data = data.bytes;
126
+ let offset = 0;
127
+ return new CiphertextHeaderConstructor(crypto.Utils.bytesToNumber(data.subarray(offset, offset += CiphertextHeaderConstructor.countLength)), crypto.Utils.bytesToNumber(data.subarray(offset, offset += CiphertextHeaderConstructor.countLength)), data.subarray(offset, offset += CiphertextHeaderConstructor.keyLength), data.subarray(offset, offset += CiphertextConstructor.nonceLength));
128
+ }
129
+ }
130
+ CiphertextHeaderConstructor.keyLength = crypto.Box.keyLength;
131
+ CiphertextHeaderConstructor.nonceLength = crypto.Box.nonceLength;
132
+ CiphertextHeaderConstructor.countLength = 2;
133
+ class CiphertextConstructor {
134
+ constructor({ hashkey, header, nonce, payload, version }) {
135
+ this.version = version !== null && version !== void 0 ? version : CiphertextConstructor.version;
136
+ this.header = header;
137
+ this.hashkey = hashkey;
138
+ this.nonce = nonce;
139
+ this.payload = payload;
140
+ }
141
+ get length() {
142
+ return this.bytes.length;
143
+ }
144
+ get bytes() {
145
+ var _a, _b;
146
+ return crypto.Utils.concatBytes(crypto.Utils.numberToBytes(this.version | (this.hashkey && this.nonce ? 128 : 0), 1), crypto.Utils.numberToBytes(this.header.length, 3), this.header, (_a = this.hashkey) !== null && _a !== void 0 ? _a : new Uint8Array(), (_b = this.nonce) !== null && _b !== void 0 ? _b : new Uint8Array, this.payload);
147
+ }
148
+ toJSON() {
149
+ return {
150
+ version: this.version,
151
+ header: crypto.Utils.decodeBase64(this.header),
152
+ hashkey: crypto.Utils.decodeBase64(this.hashkey),
153
+ nonce: crypto.Utils.decodeBase64(this.nonce),
154
+ payload: crypto.Utils.decodeBase64(this.payload)
155
+ };
156
+ }
157
+ static from(data) {
158
+ if (!(data instanceof Uint8Array))
159
+ data = data.bytes;
160
+ const versionByte = crypto.Utils.bytesToNumber(data.subarray(0, 1));
161
+ const headerLength = crypto.Utils.bytesToNumber(data.subarray(1, 4));
162
+ let offset = 4;
163
+ const header = data.subarray(offset, offset += headerLength);
164
+ let hashkey, nonce;
165
+ if ((versionByte & 128) > 0) {
166
+ hashkey = data.subarray(offset, offset += 32);
167
+ nonce = data.subarray(offset, offset += this.nonceLength);
168
+ }
169
+ const payload = data.subarray(offset);
170
+ const version = versionByte & 127;
171
+ if (!hashkey || !nonce)
172
+ var obj = new CiphertextConstructor({ header, payload, version });
173
+ else
174
+ var obj = new CiphertextConstructor({ header, hashkey, nonce, payload, version });
175
+ return obj;
176
+ }
177
+ }
178
+ CiphertextConstructor.version = 1;
179
+ CiphertextConstructor.nonceLength = crypto.Box.nonceLength;
180
+ return { UserIdConstructor, PublicIdentityConstructor, IdentityConstructor, CiphertextHeaderConstructor, CiphertextConstructor };
181
+ }
182
+ export function useConstructors(crypto) {
183
+ var _a;
184
+ return (_a = cache.get(crypto)) !== null && _a !== void 0 ? _a : defConstructors(crypto);
185
+ }
@@ -0,0 +1,21 @@
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
+ export { UserFactoryConstructor as UserFactory, UserConstructor } from "./user.js";
20
+ export { InMemoryKeystoreFactory, InMemoryKeystore } from "./keystore.js";
21
+ export { useConstructors } from "./constructors.js";
package/dist/index.js ADDED
@@ -0,0 +1,21 @@
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
+ export { UserFactoryConstructor as UserFactory, UserConstructor } from "./user.js";
20
+ export { InMemoryKeystoreFactory, InMemoryKeystore } from "./keystore.js";
21
+ export { useConstructors } from "./constructors.js";