@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/LICENSE +674 -0
- package/README.md +103 -0
- package/dist/constructors.d.ts +148 -0
- package/dist/constructors.js +185 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +21 -0
- package/dist/interfaces.d.ts +274 -0
- package/dist/keyexchange.d.ts +39 -0
- package/dist/keyexchange.js +127 -0
- package/dist/keystore.d.ts +25 -0
- package/dist/keystore.js +90 -0
- package/dist/session.d.ts +49 -0
- package/dist/session.js +296 -0
- package/dist/user.d.ts +39 -0
- package/dist/user.js +85 -0
- package/package.json +40 -0
|
@@ -0,0 +1,274 @@
|
|
|
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
|
+
/** */
|
|
21
|
+
export type Bytes = Uint8Array;
|
|
22
|
+
|
|
23
|
+
export interface Encodable {
|
|
24
|
+
/**
|
|
25
|
+
* Serializes the payload into a Bytes for transport.
|
|
26
|
+
*/
|
|
27
|
+
readonly bytes: Bytes;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface UUIDv4 extends Encodable {
|
|
31
|
+
toString(): string;
|
|
32
|
+
toJSON(): string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface Crypto {
|
|
36
|
+
hash(message: Bytes, algorithm?: Crypto.HashAlgorithms): Bytes;
|
|
37
|
+
pwhash(keyLength: number, password: string | Bytes, salt: Bytes, opsLimit: number, memLimit: number): Bytes;
|
|
38
|
+
hmac(key: Bytes, message: Bytes, length?: number, algorithm?: Crypto.HmacAlgorithms): Bytes;
|
|
39
|
+
hkdf(key: Bytes, salt: Bytes, info?: Bytes | string, length?: number): Bytes;
|
|
40
|
+
|
|
41
|
+
readonly Box: {
|
|
42
|
+
readonly keyLength: number;
|
|
43
|
+
readonly nonceLength: number;
|
|
44
|
+
|
|
45
|
+
encrypt(message: Bytes, nonce: Bytes, key: Bytes): Bytes;
|
|
46
|
+
decrypt(message: Bytes, nonce: Bytes, key: Bytes): Bytes | undefined;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
readonly ECDH: {
|
|
50
|
+
readonly publicKeyLength: number;
|
|
51
|
+
readonly secretKeyLength: number;
|
|
52
|
+
|
|
53
|
+
keyPair(secretKey?: Bytes): Crypto.KeyPair;
|
|
54
|
+
scalarMult(secretKey: Bytes, publicKey: Bytes): Bytes;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
readonly EdDSA: {
|
|
58
|
+
readonly publicKeyLength: number;
|
|
59
|
+
readonly secretKeyLength: number;
|
|
60
|
+
readonly signatureLength: number;
|
|
61
|
+
|
|
62
|
+
keyPair(secretKey?: Bytes): Crypto.KeyPair;
|
|
63
|
+
keyPairFromSeed(seed: Bytes): Crypto.KeyPair;
|
|
64
|
+
sign(message: Bytes, secretKey: Bytes): Bytes;
|
|
65
|
+
verify(signature: Bytes, message: Bytes, publicKey: Bytes): boolean;
|
|
66
|
+
|
|
67
|
+
toSecretECDHKey(secretKey: Bytes): Bytes;
|
|
68
|
+
toPublicECDHKey(publicKey: Bytes): Bytes;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
readonly UUID: {
|
|
72
|
+
generate(): UUIDv4;
|
|
73
|
+
stringify(arr: Bytes, offset?: number): string;
|
|
74
|
+
parse(uuid: string): Bytes;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
readonly Utils: {
|
|
78
|
+
decodeUTF8(array: Bytes): string;
|
|
79
|
+
encodeUTF8(string: string): Bytes;
|
|
80
|
+
decodeBase64(array: Bytes): string;
|
|
81
|
+
encodeBase64(string: string): Bytes;
|
|
82
|
+
decodeBase64URL(array: Bytes): string;
|
|
83
|
+
encodeBase64URL(string: string): Bytes;
|
|
84
|
+
decodeHex(array: Bytes): string;
|
|
85
|
+
encodeHex(string: string): Bytes;
|
|
86
|
+
bytesToNumber(array: Bytes, endian?: "big" | "little"): number;
|
|
87
|
+
numberToBytes(number: number, length?: number, endian?: "big" | "little"): Bytes;
|
|
88
|
+
compareBytes(a: Bytes, b: Bytes, ...c: Bytes[]): boolean;
|
|
89
|
+
concatBytes(...arrays: Bytes[]): Bytes;
|
|
90
|
+
encodeData(obj: any): Bytes;
|
|
91
|
+
decodeData<T>(array: Bytes): T;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
randomBytes(n: number): Bytes;
|
|
95
|
+
}
|
|
96
|
+
export namespace Crypto {
|
|
97
|
+
export type HashAlgorithms = string;
|
|
98
|
+
export type HmacAlgorithms = string;
|
|
99
|
+
|
|
100
|
+
export type KeyPair = {
|
|
101
|
+
readonly publicKey: Bytes;
|
|
102
|
+
readonly secretKey: Bytes;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export type Box = Crypto['Box'];
|
|
106
|
+
export type ECDH = Crypto['ECDH'];
|
|
107
|
+
export type EdDSA = Crypto['EdDSA'];
|
|
108
|
+
export type UUID = Crypto['UUID'];
|
|
109
|
+
export type Utils = Crypto['Utils'];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface UserId extends Encodable {
|
|
113
|
+
toString(): string;
|
|
114
|
+
toUrl(): string;
|
|
115
|
+
toJSON(): string;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface PublicIdentity extends Encodable {
|
|
119
|
+
readonly userId: UserId
|
|
120
|
+
readonly publicKey: Bytes
|
|
121
|
+
|
|
122
|
+
toPublicECDHKey(): Bytes;
|
|
123
|
+
|
|
124
|
+
toString(): string;
|
|
125
|
+
toJSON(): string;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
type IdentityKeyPair = Crypto.KeyPair;
|
|
129
|
+
|
|
130
|
+
export interface Identity extends IdentityKeyPair, PublicIdentity {
|
|
131
|
+
toSecretECDHKey(): Bytes;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export interface CiphertextHeader extends Encodable {
|
|
135
|
+
readonly count: number;
|
|
136
|
+
readonly previous: number;
|
|
137
|
+
readonly publicKey: Bytes;
|
|
138
|
+
readonly nonce: Bytes;
|
|
139
|
+
|
|
140
|
+
toJSON(): {
|
|
141
|
+
count: number;
|
|
142
|
+
previous: number;
|
|
143
|
+
publicKey: string;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export interface Ciphertext extends Encodable {
|
|
148
|
+
readonly version: number;
|
|
149
|
+
readonly hashkey: Bytes;
|
|
150
|
+
readonly header: Bytes;
|
|
151
|
+
readonly nonce: Bytes;
|
|
152
|
+
readonly payload: Bytes;
|
|
153
|
+
readonly length: number;
|
|
154
|
+
|
|
155
|
+
toJSON(): {
|
|
156
|
+
version: number;
|
|
157
|
+
header: string;
|
|
158
|
+
hashkey: string;
|
|
159
|
+
nonce: string;
|
|
160
|
+
payload: string;
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export interface Session {
|
|
165
|
+
readonly userId: string;
|
|
166
|
+
readonly sessionTag: string;
|
|
167
|
+
|
|
168
|
+
encrypt(plaintext: Bytes): Ciphertext;
|
|
169
|
+
decrypt(ciphertext: Ciphertext | Bytes): Bytes;
|
|
170
|
+
|
|
171
|
+
hasSkippedKeys(): boolean;
|
|
172
|
+
save(): Promise<void>;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export type DecryptResult<T> = { userId: UserId, data: T }
|
|
176
|
+
|
|
177
|
+
export interface SessionManager {
|
|
178
|
+
createSession(initialState: InitialSessionState | Session): Promise<Session>
|
|
179
|
+
|
|
180
|
+
encrypt(userId: UserId | string, plaintext: Bytes): Promise<Ciphertext>
|
|
181
|
+
decrypt<T>(ciphertext: Ciphertext | Bytes): Promise<DecryptResult<T>>
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export interface PreKeyBundle {
|
|
185
|
+
readonly version: number;
|
|
186
|
+
readonly identityKey: string;
|
|
187
|
+
readonly signedPreKey: string;
|
|
188
|
+
readonly signature: string;
|
|
189
|
+
readonly onetimePreKeys: string[];
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export interface PreKeyMessage {
|
|
193
|
+
readonly version: number;
|
|
194
|
+
readonly identityKey: string;
|
|
195
|
+
readonly ephemeralKey: string;
|
|
196
|
+
readonly signedPreKeyHash: string;
|
|
197
|
+
readonly onetimePreKeyHash: string;
|
|
198
|
+
readonly associatedData: string;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export type PreKeyId = string;
|
|
202
|
+
export type PreKey = Crypto.KeyPair;
|
|
203
|
+
|
|
204
|
+
export interface KeyExchangeManager {
|
|
205
|
+
createPreKeyBundle(): Promise<PreKeyBundle>;
|
|
206
|
+
processPreKeyBundle(bundle: PreKeyBundle, associatedData?: Bytes): Promise<{ session: Session, message: PreKeyMessage }>;
|
|
207
|
+
processPreKeyMessage(message: PreKeyMessage): Promise<{ session: Session, associatedData?: Bytes }>;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export interface SessionState {
|
|
211
|
+
userId: string;
|
|
212
|
+
sessionTag: string;
|
|
213
|
+
secretKey: string;
|
|
214
|
+
rootKey: string;
|
|
215
|
+
sendingChain?: KeyChainState;
|
|
216
|
+
receivingChain?: KeyChainState;
|
|
217
|
+
headerKeys: [string, string][];
|
|
218
|
+
headerKey?: string;
|
|
219
|
+
nextHeaderKey?: string;
|
|
220
|
+
previousKeys: [string, string][];
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export type InitialSessionState = { userId: string, rootKey: string, remoteKey?: Bytes } & Partial<SessionState>;
|
|
224
|
+
|
|
225
|
+
export interface KeyChainState {
|
|
226
|
+
publicKey: string;
|
|
227
|
+
remoteKey: string;
|
|
228
|
+
chainKey: string;
|
|
229
|
+
headerKey?: string;
|
|
230
|
+
nextHeaderKey: string;
|
|
231
|
+
count: number;
|
|
232
|
+
previousCount: number
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export interface KeyStore {
|
|
236
|
+
getIdentity(): Promise<Identity>;
|
|
237
|
+
|
|
238
|
+
setIdentityKey(identity: PublicIdentity | string): Promise<void>
|
|
239
|
+
getIdentityKey(userId: UserId | string): Promise<string | null>
|
|
240
|
+
|
|
241
|
+
setSessionTag(hashkey: Bytes | string, sessionTag: string): Promise<void>
|
|
242
|
+
getSessionTag(hashkey: Bytes | string): Promise<string | null>
|
|
243
|
+
|
|
244
|
+
loadUserSession(userId: UserId | string): Promise<SessionState | null>
|
|
245
|
+
loadSession(sessionTag: string): Promise<SessionState | null>
|
|
246
|
+
storeSession(session: SessionState): Promise<void>
|
|
247
|
+
|
|
248
|
+
storePreKey(id: PreKeyId, value: PreKey): Promise<void>
|
|
249
|
+
loadPreKey(id: PreKeyId): Promise<PreKey | null>
|
|
250
|
+
removePreKey(id: PreKeyId): Promise<void>
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export interface KeyStoreFactory {
|
|
254
|
+
createStore(identity: Identity): Promise<KeyStore>;
|
|
255
|
+
getStore(identity: PublicIdentity | string): Promise<KeyStore | null>;
|
|
256
|
+
deleteStore(identity: PublicIdentity | string): Promise<void>;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export interface User {
|
|
260
|
+
readonly id: UserId;
|
|
261
|
+
readonly publicIdentity: PublicIdentity;
|
|
262
|
+
|
|
263
|
+
encrypt<T>(to: UserId | string, plaintext: T): Promise<Ciphertext>
|
|
264
|
+
decrypt<T>(ciphertext: Ciphertext | Bytes): Promise<DecryptResult<T>>
|
|
265
|
+
|
|
266
|
+
generatePreKeyBundle(): Promise<PreKeyBundle>
|
|
267
|
+
handleIncomingPreKeyBundle(bundle: PreKeyBundle, associatedData?: Bytes): Promise<PreKeyMessage>
|
|
268
|
+
handleIncomingPreKeyMessage(message: PreKeyMessage): Promise<Bytes | undefined>
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export interface UserFactory {
|
|
272
|
+
create(): Promise<User>;
|
|
273
|
+
destroy(user: User): boolean;
|
|
274
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
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 { KeyExchangeManager, KeyStore, PreKeyBundle, PreKeyMessage, PublicIdentity, Session, Crypto, Bytes } from "./interfaces.ts";
|
|
20
|
+
export declare class KeyExchangeManagerConstructor implements KeyExchangeManager {
|
|
21
|
+
readonly publicIdentity: PublicIdentity;
|
|
22
|
+
private readonly keyStore;
|
|
23
|
+
private readonly crypto;
|
|
24
|
+
static readonly version = 1;
|
|
25
|
+
private static readonly hkdfInfo;
|
|
26
|
+
private static readonly maxOPK;
|
|
27
|
+
constructor(publicIdentity: PublicIdentity, keyStore: KeyStore, crypto: Crypto);
|
|
28
|
+
private generateSPK;
|
|
29
|
+
private generateOPK;
|
|
30
|
+
createPreKeyBundle(): Promise<PreKeyBundle>;
|
|
31
|
+
processPreKeyBundle(bundle: PreKeyBundle, associatedData?: Bytes): Promise<{
|
|
32
|
+
session: Session;
|
|
33
|
+
message: PreKeyMessage;
|
|
34
|
+
}>;
|
|
35
|
+
processPreKeyMessage(message: PreKeyMessage): Promise<{
|
|
36
|
+
session: Session;
|
|
37
|
+
associatedData?: Bytes;
|
|
38
|
+
}>;
|
|
39
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
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 { SessionConstructor } from "./session.js";
|
|
20
|
+
import { useConstructors } from "./constructors.js";
|
|
21
|
+
export class KeyExchangeManagerConstructor {
|
|
22
|
+
constructor(publicIdentity, keyStore, crypto) {
|
|
23
|
+
this.publicIdentity = publicIdentity;
|
|
24
|
+
this.keyStore = keyStore;
|
|
25
|
+
this.crypto = crypto;
|
|
26
|
+
}
|
|
27
|
+
generateSPK() {
|
|
28
|
+
const signedPreKey = this.crypto.ECDH.keyPair();
|
|
29
|
+
const signedPreKeyHash = this.crypto.hash(signedPreKey.publicKey);
|
|
30
|
+
this.keyStore.storePreKey(this.crypto.Utils.decodeBase64(signedPreKeyHash), signedPreKey);
|
|
31
|
+
return { signedPreKey, signedPreKeyHash };
|
|
32
|
+
}
|
|
33
|
+
generateOPK(spkHash) {
|
|
34
|
+
const onetimePreKey = this.crypto.ECDH.keyPair();
|
|
35
|
+
const onetimePreKeyHash = this.crypto.hash(onetimePreKey.publicKey);
|
|
36
|
+
this.keyStore.storePreKey(this.crypto.Utils.decodeBase64(spkHash).concat(this.crypto.Utils.decodeBase64(onetimePreKeyHash)), onetimePreKey);
|
|
37
|
+
return { onetimePreKey, onetimePreKeyHash };
|
|
38
|
+
}
|
|
39
|
+
async createPreKeyBundle() {
|
|
40
|
+
const { signedPreKey, signedPreKeyHash } = this.generateSPK();
|
|
41
|
+
const onetimePreKey = new Array(KeyExchangeManagerConstructor.maxOPK).fill(0).map(() => this.generateOPK(signedPreKeyHash).onetimePreKey);
|
|
42
|
+
return {
|
|
43
|
+
version: KeyExchangeManagerConstructor.version,
|
|
44
|
+
identityKey: this.publicIdentity.toString(),
|
|
45
|
+
signedPreKey: this.crypto.Utils.decodeBase64(signedPreKey.publicKey),
|
|
46
|
+
signature: this.crypto.Utils.decodeBase64(this.crypto.EdDSA.sign(signedPreKeyHash, (await this.keyStore.getIdentity()).secretKey)),
|
|
47
|
+
onetimePreKeys: onetimePreKey.map(opk => this.crypto.Utils.decodeBase64(opk.publicKey))
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
async processPreKeyBundle(bundle, associatedData) {
|
|
51
|
+
const { PublicIdentityConstructor, UserIdConstructor } = useConstructors(this.crypto);
|
|
52
|
+
const ephemeralKey = this.crypto.ECDH.keyPair();
|
|
53
|
+
const signedPreKey = this.crypto.Utils.encodeBase64(bundle.signedPreKey);
|
|
54
|
+
const identityKey = PublicIdentityConstructor.from(bundle.identityKey);
|
|
55
|
+
if (!this.crypto.EdDSA.verify(this.crypto.Utils.encodeBase64(bundle.signature), this.crypto.hash(signedPreKey), identityKey.publicKey))
|
|
56
|
+
throw new Error("Signature verification failed");
|
|
57
|
+
const shiftKey = bundle.onetimePreKeys.shift();
|
|
58
|
+
const onetimePreKey = shiftKey ? this.crypto.Utils.encodeBase64(shiftKey) : undefined;
|
|
59
|
+
const signedPreKeyHash = this.crypto.hash(signedPreKey);
|
|
60
|
+
const onetimePreKeyHash = onetimePreKey ? this.crypto.hash(onetimePreKey) : new Uint8Array();
|
|
61
|
+
const derivedKey = this.crypto.hkdf(new Uint8Array([
|
|
62
|
+
...this.crypto.ECDH.scalarMult(this.crypto.EdDSA.toSecretECDHKey((await this.keyStore.getIdentity()).secretKey), signedPreKey),
|
|
63
|
+
...this.crypto.ECDH.scalarMult(ephemeralKey.secretKey, identityKey.toPublicECDHKey()),
|
|
64
|
+
...this.crypto.ECDH.scalarMult(ephemeralKey.secretKey, signedPreKey),
|
|
65
|
+
...onetimePreKey ? this.crypto.ECDH.scalarMult(ephemeralKey.secretKey, onetimePreKey) : new Uint8Array()
|
|
66
|
+
]), new Uint8Array(SessionConstructor.keyLength).fill(0), KeyExchangeManagerConstructor.hkdfInfo, SessionConstructor.keyLength * 3);
|
|
67
|
+
const session = new SessionConstructor({
|
|
68
|
+
userId: UserIdConstructor.fromKey(identityKey).toString(),
|
|
69
|
+
remoteKey: identityKey.toPublicECDHKey(),
|
|
70
|
+
rootKey: this.crypto.Utils.decodeBase64(derivedKey.subarray(0, SessionConstructor.keyLength)),
|
|
71
|
+
headerKey: this.crypto.Utils.decodeBase64(derivedKey.subarray(SessionConstructor.keyLength, SessionConstructor.keyLength * 2)),
|
|
72
|
+
nextHeaderKey: this.crypto.Utils.decodeBase64(derivedKey.subarray(SessionConstructor.keyLength * 2))
|
|
73
|
+
}, this.keyStore, this.crypto);
|
|
74
|
+
const encrypted = session.encrypt(this.crypto.Utils.concatBytes(this.crypto.hash(this.publicIdentity.bytes), this.crypto.hash(identityKey.bytes), associatedData !== null && associatedData !== void 0 ? associatedData : new Uint8Array()));
|
|
75
|
+
if (!encrypted)
|
|
76
|
+
throw new Error("Encryption error");
|
|
77
|
+
await this.keyStore.setIdentityKey(identityKey);
|
|
78
|
+
return {
|
|
79
|
+
session,
|
|
80
|
+
message: {
|
|
81
|
+
version: KeyExchangeManagerConstructor.version,
|
|
82
|
+
identityKey: this.publicIdentity.toString(),
|
|
83
|
+
ephemeralKey: this.crypto.Utils.decodeBase64(ephemeralKey.publicKey),
|
|
84
|
+
signedPreKeyHash: this.crypto.Utils.decodeBase64(signedPreKeyHash),
|
|
85
|
+
onetimePreKeyHash: this.crypto.Utils.decodeBase64(onetimePreKeyHash),
|
|
86
|
+
associatedData: this.crypto.Utils.decodeBase64(encrypted.bytes)
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
async processPreKeyMessage(message) {
|
|
91
|
+
const { PublicIdentityConstructor, UserIdConstructor } = useConstructors(this.crypto);
|
|
92
|
+
const signedPreKey = await this.keyStore.loadPreKey(message.signedPreKeyHash);
|
|
93
|
+
const hash = message.signedPreKeyHash.concat(message.onetimePreKeyHash);
|
|
94
|
+
const onetimePreKey = await this.keyStore.loadPreKey(hash);
|
|
95
|
+
const identityKey = PublicIdentityConstructor.from(message.identityKey);
|
|
96
|
+
if (!signedPreKey || !onetimePreKey || !message.identityKey || !message.ephemeralKey)
|
|
97
|
+
throw new Error("ACK message malformed");
|
|
98
|
+
await this.keyStore.removePreKey(hash);
|
|
99
|
+
const ephemeralKey = this.crypto.Utils.encodeBase64(message.ephemeralKey);
|
|
100
|
+
const secretKey = this.crypto.EdDSA.toSecretECDHKey((await this.keyStore.getIdentity()).secretKey);
|
|
101
|
+
const derivedKey = this.crypto.hkdf(new Uint8Array([
|
|
102
|
+
...this.crypto.ECDH.scalarMult(signedPreKey.secretKey, identityKey.toPublicECDHKey()),
|
|
103
|
+
...this.crypto.ECDH.scalarMult(secretKey, ephemeralKey),
|
|
104
|
+
...this.crypto.ECDH.scalarMult(signedPreKey.secretKey, ephemeralKey),
|
|
105
|
+
...onetimePreKey ? this.crypto.ECDH.scalarMult(onetimePreKey.secretKey, ephemeralKey) : new Uint8Array()
|
|
106
|
+
]), new Uint8Array(SessionConstructor.keyLength).fill(0), KeyExchangeManagerConstructor.hkdfInfo, SessionConstructor.keyLength * 3);
|
|
107
|
+
const session = new SessionConstructor({
|
|
108
|
+
userId: UserIdConstructor.fromKey(identityKey).toString(),
|
|
109
|
+
secretKey: this.crypto.Utils.decodeBase64(secretKey),
|
|
110
|
+
rootKey: this.crypto.Utils.decodeBase64(derivedKey.subarray(0, SessionConstructor.keyLength)),
|
|
111
|
+
nextHeaderKey: this.crypto.Utils.decodeBase64(derivedKey.subarray(SessionConstructor.keyLength, SessionConstructor.keyLength * 2)),
|
|
112
|
+
headerKey: this.crypto.Utils.decodeBase64(derivedKey.subarray(SessionConstructor.keyLength * 2))
|
|
113
|
+
}, this.keyStore, this.crypto);
|
|
114
|
+
const data = session.decrypt(this.crypto.Utils.encodeBase64(message.associatedData));
|
|
115
|
+
if (!this.crypto.Utils.compareBytes(data.subarray(0, 64), this.crypto.Utils.concatBytes(this.crypto.hash(identityKey.bytes), this.crypto.hash(this.publicIdentity.bytes))))
|
|
116
|
+
throw new Error("Error verifing Associated Data");
|
|
117
|
+
await this.keyStore.setIdentityKey(identityKey);
|
|
118
|
+
const associatedData = data.subarray(64);
|
|
119
|
+
return {
|
|
120
|
+
session,
|
|
121
|
+
associatedData: associatedData.length > 0 ? associatedData : undefined
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
KeyExchangeManagerConstructor.version = 1;
|
|
126
|
+
KeyExchangeManagerConstructor.hkdfInfo = "freesignal/x3dh/v." + KeyExchangeManagerConstructor.version;
|
|
127
|
+
KeyExchangeManagerConstructor.maxOPK = 10;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Identity, KeyStore, PreKey, PreKeyId, SessionState, KeyStoreFactory, PublicIdentity, Bytes, UserId, Crypto } from "./interfaces.ts";
|
|
2
|
+
export declare class InMemoryKeystoreFactory implements KeyStoreFactory {
|
|
3
|
+
#private;
|
|
4
|
+
private readonly crypto;
|
|
5
|
+
constructor(crypto: Crypto);
|
|
6
|
+
createStore(identity: Identity): Promise<KeyStore>;
|
|
7
|
+
getStore(identity: PublicIdentity | string): Promise<KeyStore | null>;
|
|
8
|
+
deleteStore(identity: PublicIdentity | string): Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
export declare class InMemoryKeystore implements KeyStore {
|
|
11
|
+
#private;
|
|
12
|
+
private readonly crypto;
|
|
13
|
+
constructor(identity: Identity, crypto: Crypto);
|
|
14
|
+
setIdentityKey(identity: PublicIdentity | string): Promise<void>;
|
|
15
|
+
getIdentityKey(userId: UserId | string): Promise<string | null>;
|
|
16
|
+
getIdentity(): Promise<Identity>;
|
|
17
|
+
setSessionTag(hashkey: Bytes | string, sessionTag: string): Promise<void>;
|
|
18
|
+
getSessionTag(hashkey: Bytes | string): Promise<string | null>;
|
|
19
|
+
loadUserSession(userId: UserId | string): Promise<SessionState | null>;
|
|
20
|
+
loadSession(sessionTag: string): Promise<SessionState | null>;
|
|
21
|
+
storeSession(session: SessionState): Promise<void>;
|
|
22
|
+
storePreKey(id: PreKeyId, value: PreKey): Promise<void>;
|
|
23
|
+
loadPreKey(id: PreKeyId): Promise<PreKey | null>;
|
|
24
|
+
removePreKey(id: PreKeyId): Promise<void>;
|
|
25
|
+
}
|
package/dist/keystore.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
|
2
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
|
3
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
4
|
+
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
5
|
+
};
|
|
6
|
+
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
|
|
7
|
+
if (kind === "m") throw new TypeError("Private method is not writable");
|
|
8
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
|
|
9
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
|
|
10
|
+
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
|
|
11
|
+
};
|
|
12
|
+
var _InMemoryKeystoreFactory_stores, _InMemoryKeystore_identity, _InMemoryKeystore_sessions, _InMemoryKeystore_preKeys, _InMemoryKeystore_users, _InMemoryKeystore_hashkeys, _InMemoryKeystore_identityKeys;
|
|
13
|
+
import { decodeBase64 } from "@freesignal/crypto/utils";
|
|
14
|
+
import { useConstructors } from "./constructors.js";
|
|
15
|
+
export class InMemoryKeystoreFactory {
|
|
16
|
+
constructor(crypto) {
|
|
17
|
+
this.crypto = crypto;
|
|
18
|
+
_InMemoryKeystoreFactory_stores.set(this, new Map());
|
|
19
|
+
}
|
|
20
|
+
async createStore(identity) {
|
|
21
|
+
__classPrivateFieldGet(this, _InMemoryKeystoreFactory_stores, "f").set(identity.toString(), new InMemoryKeystore(identity, this.crypto));
|
|
22
|
+
const store = await this.getStore(identity.toString());
|
|
23
|
+
if (!store)
|
|
24
|
+
throw new Error("Error creting keyStore");
|
|
25
|
+
return store;
|
|
26
|
+
}
|
|
27
|
+
async getStore(identity) {
|
|
28
|
+
var _a;
|
|
29
|
+
return (_a = __classPrivateFieldGet(this, _InMemoryKeystoreFactory_stores, "f").get(identity.toString())) !== null && _a !== void 0 ? _a : null;
|
|
30
|
+
}
|
|
31
|
+
async deleteStore(identity) {
|
|
32
|
+
__classPrivateFieldGet(this, _InMemoryKeystoreFactory_stores, "f").delete(identity.toString());
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
_InMemoryKeystoreFactory_stores = new WeakMap();
|
|
36
|
+
export class InMemoryKeystore {
|
|
37
|
+
constructor(identity, crypto) {
|
|
38
|
+
this.crypto = crypto;
|
|
39
|
+
_InMemoryKeystore_identity.set(this, void 0);
|
|
40
|
+
_InMemoryKeystore_sessions.set(this, new Map());
|
|
41
|
+
_InMemoryKeystore_preKeys.set(this, new Map());
|
|
42
|
+
_InMemoryKeystore_users.set(this, new Map());
|
|
43
|
+
_InMemoryKeystore_hashkeys.set(this, new Map());
|
|
44
|
+
_InMemoryKeystore_identityKeys.set(this, new Map());
|
|
45
|
+
__classPrivateFieldSet(this, _InMemoryKeystore_identity, identity, "f");
|
|
46
|
+
}
|
|
47
|
+
async setIdentityKey(identity) {
|
|
48
|
+
identity = useConstructors(this.crypto).PublicIdentityConstructor.from(identity);
|
|
49
|
+
__classPrivateFieldGet(this, _InMemoryKeystore_identityKeys, "f").set(identity.userId.toString(), identity.toString());
|
|
50
|
+
}
|
|
51
|
+
async getIdentityKey(userId) {
|
|
52
|
+
var _a;
|
|
53
|
+
return (_a = __classPrivateFieldGet(this, _InMemoryKeystore_identityKeys, "f").get(userId.toString())) !== null && _a !== void 0 ? _a : null;
|
|
54
|
+
}
|
|
55
|
+
async getIdentity() {
|
|
56
|
+
return __classPrivateFieldGet(this, _InMemoryKeystore_identity, "f");
|
|
57
|
+
}
|
|
58
|
+
async setSessionTag(hashkey, sessionTag) {
|
|
59
|
+
__classPrivateFieldGet(this, _InMemoryKeystore_hashkeys, "f").set(typeof hashkey === 'string' ? hashkey : decodeBase64(hashkey), sessionTag);
|
|
60
|
+
}
|
|
61
|
+
async getSessionTag(hashkey) {
|
|
62
|
+
var _a;
|
|
63
|
+
return (_a = __classPrivateFieldGet(this, _InMemoryKeystore_hashkeys, "f").get(typeof hashkey === 'string' ? hashkey : decodeBase64(hashkey))) !== null && _a !== void 0 ? _a : null;
|
|
64
|
+
}
|
|
65
|
+
async loadUserSession(userId) {
|
|
66
|
+
const sessionTag = __classPrivateFieldGet(this, _InMemoryKeystore_users, "f").get(userId.toString());
|
|
67
|
+
if (!sessionTag)
|
|
68
|
+
return null;
|
|
69
|
+
return this.loadSession(sessionTag);
|
|
70
|
+
}
|
|
71
|
+
async loadSession(sessionTag) {
|
|
72
|
+
var _a;
|
|
73
|
+
return (_a = __classPrivateFieldGet(this, _InMemoryKeystore_sessions, "f").get(sessionTag)) !== null && _a !== void 0 ? _a : null;
|
|
74
|
+
}
|
|
75
|
+
async storeSession(session) {
|
|
76
|
+
__classPrivateFieldGet(this, _InMemoryKeystore_users, "f").set(session.userId, session.sessionTag);
|
|
77
|
+
__classPrivateFieldGet(this, _InMemoryKeystore_sessions, "f").set(session.sessionTag, session);
|
|
78
|
+
}
|
|
79
|
+
async storePreKey(id, value) {
|
|
80
|
+
__classPrivateFieldGet(this, _InMemoryKeystore_preKeys, "f").set(id, value);
|
|
81
|
+
}
|
|
82
|
+
async loadPreKey(id) {
|
|
83
|
+
var _a;
|
|
84
|
+
return (_a = __classPrivateFieldGet(this, _InMemoryKeystore_preKeys, "f").get(id)) !== null && _a !== void 0 ? _a : null;
|
|
85
|
+
}
|
|
86
|
+
async removePreKey(id) {
|
|
87
|
+
__classPrivateFieldGet(this, _InMemoryKeystore_preKeys, "f").delete(id);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
_InMemoryKeystore_identity = new WeakMap(), _InMemoryKeystore_sessions = new WeakMap(), _InMemoryKeystore_preKeys = new WeakMap(), _InMemoryKeystore_users = new WeakMap(), _InMemoryKeystore_hashkeys = new WeakMap(), _InMemoryKeystore_identityKeys = new WeakMap();
|
|
@@ -0,0 +1,49 @@
|
|
|
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 { Crypto, Bytes, InitialSessionState, KeyStore, Session, SessionManager, Ciphertext, UserId, DecryptResult } from "./interfaces.ts";
|
|
20
|
+
export declare class SessionManagerConstructor implements SessionManager {
|
|
21
|
+
private readonly keyStore;
|
|
22
|
+
private readonly crypto;
|
|
23
|
+
constructor(keyStore: KeyStore, crypto: Crypto);
|
|
24
|
+
createSession(initialState: InitialSessionState | Session): Promise<Session>;
|
|
25
|
+
private getUserSession;
|
|
26
|
+
private getSession;
|
|
27
|
+
encrypt(userId: UserId | string, plaintext: Bytes): Promise<Ciphertext>;
|
|
28
|
+
decrypt<T>(ciphertext: Ciphertext | Bytes): Promise<DecryptResult<T>>;
|
|
29
|
+
}
|
|
30
|
+
export declare class SessionConstructor implements Session {
|
|
31
|
+
#private;
|
|
32
|
+
private readonly keyStore;
|
|
33
|
+
private readonly crypto;
|
|
34
|
+
static readonly keyLength = 32;
|
|
35
|
+
static readonly version = 1;
|
|
36
|
+
static readonly info: string;
|
|
37
|
+
static readonly maxCount = 65536;
|
|
38
|
+
readonly userId: string;
|
|
39
|
+
readonly sessionTag: string;
|
|
40
|
+
constructor(init: InitialSessionState | Session, keyStore: KeyStore, crypto: Crypto);
|
|
41
|
+
private getChain;
|
|
42
|
+
private getHeaderKey;
|
|
43
|
+
private getSendingKey;
|
|
44
|
+
private getReceivingKey;
|
|
45
|
+
encrypt(data: Bytes): Ciphertext;
|
|
46
|
+
decrypt(ciphertext: Ciphertext | Bytes): Bytes;
|
|
47
|
+
hasSkippedKeys(): boolean;
|
|
48
|
+
save(): Promise<void>;
|
|
49
|
+
}
|