fb-messenger-e2ee 0.1.1 → 0.1.2
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 +27 -2
- package/dist/index.cjs +8811 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +885 -0
- package/dist/index.d.ts +885 -0
- package/dist/index.js +8843 -0
- package/dist/index.js.map +1 -0
- package/package.json +19 -4
- package/bun.lock +0 -1014
- package/examples/basic-usage.ts +0 -52
- package/jest.config.ts +0 -18
- package/src/config/env.ts +0 -15
- package/src/controllers/client.controller.ts +0 -805
- package/src/controllers/dgw-handler.ts +0 -171
- package/src/controllers/e2ee-handler.ts +0 -832
- package/src/controllers/event-mapper.ts +0 -327
- package/src/core/client.ts +0 -151
- package/src/e2ee/application/e2ee-client.ts +0 -376
- package/src/e2ee/application/fanout-planner.ts +0 -79
- package/src/e2ee/application/outbound-message-cache.ts +0 -58
- package/src/e2ee/application/prekey-maintenance.ts +0 -55
- package/src/e2ee/application/retry-manager.ts +0 -156
- package/src/e2ee/facebook/facebook-protocol-utils.ts +0 -230
- package/src/e2ee/facebook/icdc-payload.ts +0 -31
- package/src/e2ee/index.ts +0 -76
- package/src/e2ee/internal.ts +0 -27
- package/src/e2ee/media/media-crypto.ts +0 -147
- package/src/e2ee/media/media-upload.ts +0 -90
- package/src/e2ee/message/builders/client-payload.ts +0 -54
- package/src/e2ee/message/builders/consumer-application.ts +0 -362
- package/src/e2ee/message/builders/message-application.ts +0 -64
- package/src/e2ee/message/builders/message-transport.ts +0 -84
- package/src/e2ee/message/codecs/protobuf-codecs.ts +0 -101
- package/src/e2ee/message/constants.ts +0 -4
- package/src/e2ee/message/message-builder.ts +0 -15
- package/src/e2ee/signal/prekey-manager.ts +0 -140
- package/src/e2ee/signal/signal-manager.ts +0 -365
- package/src/e2ee/store/device-json.ts +0 -47
- package/src/e2ee/store/device-repository.ts +0 -12
- package/src/e2ee/store/device-store.ts +0 -538
- package/src/e2ee/transport/binary/decoder.ts +0 -180
- package/src/e2ee/transport/binary/encoder.ts +0 -143
- package/src/e2ee/transport/binary/stanzas.ts +0 -97
- package/src/e2ee/transport/binary/tokens.ts +0 -64
- package/src/e2ee/transport/binary/wa-binary.ts +0 -8
- package/src/e2ee/transport/dgw/dgw-socket.ts +0 -345
- package/src/e2ee/transport/noise/noise-handshake.ts +0 -417
- package/src/e2ee/transport/noise/noise-socket.ts +0 -230
- package/src/index.ts +0 -34
- package/src/models/client.ts +0 -26
- package/src/models/config.ts +0 -14
- package/src/models/domain.ts +0 -299
- package/src/models/e2ee.ts +0 -295
- package/src/models/media.ts +0 -41
- package/src/models/messaging.ts +0 -88
- package/src/models/thread.ts +0 -69
- package/src/repositories/session.repository.ts +0 -20
- package/src/services/auth.service.ts +0 -55
- package/src/services/e2ee.service.ts +0 -174
- package/src/services/facebook-gateway.service.ts +0 -245
- package/src/services/icdc.service.ts +0 -177
- package/src/services/media.service.ts +0 -304
- package/src/services/messaging.service.ts +0 -28
- package/src/services/thread.service.ts +0 -199
- package/src/types/advanced-types.ts +0 -61
- package/src/types/fca-unofficial.d.ts +0 -212
- package/src/types/protocol-types.ts +0 -43
- package/src/utils/fca-utils.ts +0 -30
- package/src/utils/logger.ts +0 -24
- package/src/utils/mime.ts +0 -51
- package/tests/.env.example +0 -87
- package/tests/data/1x1.png +0 -0
- package/tests/data/example.txt +0 -1
- package/tests/data/file_example.mp3 +0 -0
- package/tests/data/file_example.mp4 +0 -0
- package/tests/integration.test.ts +0 -498
- package/tests/script/echo-e2ee.ts +0 -174
- package/tests/script/send-e2ee-media.ts +0 -227
- package/tests/script/send-e2ee-reaction.ts +0 -108
- package/tests/script/send-e2ee-unsend.ts +0 -115
- package/tests/script/send-typing.ts +0 -107
- package/tests/script/test-group-send.ts +0 -105
- package/tests/setup.ts +0 -3
- package/tests/types.test.ts +0 -57
- package/tests/unit/controllers/dgw-handler.test.ts +0 -60
- package/tests/unit/controllers/e2ee-handler.test.ts +0 -293
- package/tests/unit/controllers/event-mapper.test.ts +0 -252
- package/tests/unit/e2ee/application-helpers.test.ts +0 -418
- package/tests/unit/e2ee/device-store.test.ts +0 -126
- package/tests/unit/e2ee/facebook-protocol-utils.test.ts +0 -134
- package/tests/unit/e2ee/media-crypto.test.ts +0 -55
- package/tests/unit/e2ee/media-upload.test.ts +0 -60
- package/tests/unit/e2ee/message-builder.test.ts +0 -42
- package/tests/unit/e2ee/message-builders.test.ts +0 -230
- package/tests/unit/e2ee/noise-handshake.test.ts +0 -260
- package/tests/unit/e2ee/prekey-manager.test.ts +0 -55
- package/tests/unit/e2ee/wa-binary.test.ts +0 -127
- package/tests/unit/services/e2ee.service.test.ts +0 -101
- package/tests/unit/services/facebook-gateway.service.test.ts +0 -138
- package/tests/unit/services/media.service.test.ts +0 -169
- package/tests/unit/utils/fca-utils.test.ts +0 -48
- package/tsconfig.json +0 -19
- /package/{src/e2ee/message/proto → proto}/ArmadilloApplication.proto +0 -0
- /package/{src/e2ee/message/proto → proto}/ArmadilloICDC.proto +0 -0
- /package/{src/e2ee/message/proto → proto}/ConsumerApplication.proto +0 -0
- /package/{src/e2ee/message/proto → proto}/MessageApplication.proto +0 -0
- /package/{src/e2ee/message/proto → proto}/MessageTransport.proto +0 -0
- /package/{src/e2ee/message/proto → proto}/WACommon.proto +0 -0
- /package/{src/e2ee/message/proto → proto}/WAMediaTransport.proto +0 -0
- /package/{src/e2ee/message/proto → proto}/proto-writer.ts +0 -0
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
import * as protobuf from "protobufjs";
|
|
2
|
-
import { fileURLToPath } from "node:url";
|
|
3
|
-
import { dirname, join } from "node:path";
|
|
4
|
-
|
|
5
|
-
// Protobuf Decoders (using protobufjs for simplicity)
|
|
6
|
-
|
|
7
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
-
const __dirname = dirname(__filename);
|
|
9
|
-
|
|
10
|
-
const root = new ((protobuf as any).default?.Root || protobuf.Root)();
|
|
11
|
-
root.resolvePath = (origin: string, target: string) => {
|
|
12
|
-
return join(__dirname, "../proto", target);
|
|
13
|
-
};
|
|
14
|
-
root.loadSync([
|
|
15
|
-
"WACommon.proto",
|
|
16
|
-
"MessageTransport.proto",
|
|
17
|
-
"WAMediaTransport.proto",
|
|
18
|
-
"MessageApplication.proto",
|
|
19
|
-
"ConsumerApplication.proto",
|
|
20
|
-
"ArmadilloApplication.proto",
|
|
21
|
-
"ArmadilloICDC.proto"
|
|
22
|
-
]);
|
|
23
|
-
|
|
24
|
-
const MsgTransportType = root.lookupType("waMsgTransport.MessageTransport");
|
|
25
|
-
const MsgApplicationType = root.lookupType("WAMsgApplication.MessageApplication");
|
|
26
|
-
const ConsumerAppType = root.lookupType("waConsumerApplication.ConsumerApplication");
|
|
27
|
-
const ArmadilloAppType = root.lookupType("waArmadilloApplication.Armadillo");
|
|
28
|
-
const ICDCIdentityListType = root.lookupType("waArmadilloICDC.ICDCIdentityList");
|
|
29
|
-
const SignedICDCIdentityListType = root.lookupType("waArmadilloICDC.SignedICDCIdentityList");
|
|
30
|
-
const ImageTransportType = root.lookupType("WAMediaTransport.ImageTransport");
|
|
31
|
-
const VideoTransportType = root.lookupType("WAMediaTransport.VideoTransport");
|
|
32
|
-
const AudioTransportType = root.lookupType("WAMediaTransport.AudioTransport");
|
|
33
|
-
const DocumentTransportType = root.lookupType("WAMediaTransport.DocumentTransport");
|
|
34
|
-
const StickerTransportType = root.lookupType("WAMediaTransport.StickerTransport");
|
|
35
|
-
|
|
36
|
-
export function decodeMessageTransport(buffer: Buffer): any {
|
|
37
|
-
const msg = MsgTransportType.decode(buffer);
|
|
38
|
-
return MsgTransportType.toObject(msg, { longs: Number, enums: String, bytes: Buffer });
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function decodeMessageApplication(buffer: Buffer): any {
|
|
42
|
-
const msg = MsgApplicationType.decode(buffer);
|
|
43
|
-
return MsgApplicationType.toObject(msg, { longs: Number, enums: String, bytes: Buffer });
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function decodeConsumerApplication(buffer: Buffer): any {
|
|
47
|
-
const msg = ConsumerAppType.decode(buffer);
|
|
48
|
-
return ConsumerAppType.toObject(msg, { longs: String, enums: String, bytes: Buffer });
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export function decodeArmadillo(buffer: Buffer): any {
|
|
52
|
-
const msg = ArmadilloAppType.decode(buffer);
|
|
53
|
-
return ArmadilloAppType.toObject(msg, { longs: String, enums: String, bytes: Buffer });
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function decodeImageTransport(buffer: Buffer): any {
|
|
57
|
-
const msg = ImageTransportType.decode(buffer);
|
|
58
|
-
return ImageTransportType.toObject(msg, { longs: Number, enums: String, bytes: Buffer });
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export function decodeVideoTransport(buffer: Buffer): any {
|
|
62
|
-
const msg = VideoTransportType.decode(buffer);
|
|
63
|
-
return VideoTransportType.toObject(msg, { longs: Number, enums: String, bytes: Buffer });
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export function decodeAudioTransport(buffer: Buffer): any {
|
|
67
|
-
const msg = AudioTransportType.decode(buffer);
|
|
68
|
-
return AudioTransportType.toObject(msg, { longs: Number, enums: String, bytes: Buffer });
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export function decodeDocumentTransport(buffer: Buffer): any {
|
|
72
|
-
const msg = DocumentTransportType.decode(buffer);
|
|
73
|
-
return DocumentTransportType.toObject(msg, { longs: Number, enums: String, bytes: Buffer });
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export function decodeStickerTransport(buffer: Buffer): any {
|
|
77
|
-
const msg = StickerTransportType.decode(buffer);
|
|
78
|
-
return StickerTransportType.toObject(msg, { longs: Number, enums: String, bytes: Buffer });
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export function encodeICDCIdentityList(data: {
|
|
82
|
-
seq: number;
|
|
83
|
-
timestamp: number;
|
|
84
|
-
devices: Buffer[];
|
|
85
|
-
signingDeviceIndex: number;
|
|
86
|
-
}): Buffer {
|
|
87
|
-
const msg = ICDCIdentityListType.create(data);
|
|
88
|
-
return Buffer.from(ICDCIdentityListType.encode(msg).finish());
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export function encodeSignedICDCIdentityList(data: {
|
|
92
|
-
details: Buffer;
|
|
93
|
-
signature: Buffer;
|
|
94
|
-
}): Buffer {
|
|
95
|
-
const msg = SignedICDCIdentityListType.create(data);
|
|
96
|
-
return Buffer.from(SignedICDCIdentityListType.encode(msg).finish());
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export function decodeICDCFetchResponse(buffer: Buffer): any {
|
|
100
|
-
// Not a proto, but we might need it
|
|
101
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Message layer public barrel.
|
|
3
|
-
*
|
|
4
|
-
* Keep imports here stable while implementations live under proto/, builders/,
|
|
5
|
-
* and codecs/. The top-level compatibility shim re-exports this barrel.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
export type { MediaFields, MessageTransportOptions } from "../../models/e2ee.ts";
|
|
9
|
-
export * from "./constants.ts";
|
|
10
|
-
export { ProtoWriter } from "./proto/proto-writer.ts";
|
|
11
|
-
export * from "./builders/client-payload.ts";
|
|
12
|
-
export * from "./builders/consumer-application.ts";
|
|
13
|
-
export * from "./builders/message-application.ts";
|
|
14
|
-
export * from "./builders/message-transport.ts";
|
|
15
|
-
export * from "./codecs/protobuf-codecs.ts";
|
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* E2EE PreKey Manager - Layer 2 (Signal Protocol)
|
|
3
|
-
*
|
|
4
|
-
* Manages prekey generation and the IQ stanzas needed to:
|
|
5
|
-
* - Upload prekeys to the WhatsApp/Messenger server on registration
|
|
6
|
-
* - Fetch prekey bundles for recipients before establishing sessions
|
|
7
|
-
*
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import {
|
|
11
|
-
PrivateKey,
|
|
12
|
-
PreKeyRecord,
|
|
13
|
-
SignedPreKeyRecord,
|
|
14
|
-
PreKeyBundle,
|
|
15
|
-
KEMKeyPair,
|
|
16
|
-
KEMPublicKey,
|
|
17
|
-
PublicKey,
|
|
18
|
-
} from "@signalapp/libsignal-client";
|
|
19
|
-
import type { DeviceStore } from "../store/device-store.ts";
|
|
20
|
-
import { type GeneratedPreKey, type PreKeyUploadPayload, type RawPreKeyBundle } from "../../models/e2ee.ts";
|
|
21
|
-
export type { GeneratedPreKey, PreKeyUploadPayload, RawPreKeyBundle };
|
|
22
|
-
|
|
23
|
-
export const INITIAL_PREKEY_COUNT = 812; // First registration batch
|
|
24
|
-
export const WANTED_PREKEY_COUNT = 50; // Normal replenishment batch
|
|
25
|
-
export const MIN_PREKEY_COUNT = 5; // Trigger replenishment below this
|
|
26
|
-
|
|
27
|
-
// Cast Uint8Array -> Buffer for strict libsignal params
|
|
28
|
-
const u8 = (b: Uint8Array | Buffer): Buffer => Buffer.isBuffer(b) ? b : Buffer.from(b.buffer, b.byteOffset, b.byteLength);
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
/** Generate `count` fresh one-time prekeys and store them in the DeviceStore. */
|
|
32
|
-
export async function generatePreKeys(
|
|
33
|
-
store: DeviceStore,
|
|
34
|
-
count: number = WANTED_PREKEY_COUNT,
|
|
35
|
-
): Promise<GeneratedPreKey[]> {
|
|
36
|
-
const result: GeneratedPreKey[] = [];
|
|
37
|
-
for (let i = 0; i < count; i++) {
|
|
38
|
-
const id = store.allocPreKeyId();
|
|
39
|
-
const priv = PrivateKey.generate();
|
|
40
|
-
const record = PreKeyRecord.new(id, priv.getPublicKey(), priv);
|
|
41
|
-
await store.savePreKey(id, record);
|
|
42
|
-
result.push({ id, record });
|
|
43
|
-
}
|
|
44
|
-
return result;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** Generate or refresh the signed prekey and store it. */
|
|
48
|
-
export async function generateSignedPreKey(store: DeviceStore): Promise<SignedPreKeyRecord> {
|
|
49
|
-
const identityPriv = await store.getIdentityKey();
|
|
50
|
-
const priv = PrivateKey.generate();
|
|
51
|
-
const pub = priv.getPublicKey();
|
|
52
|
-
const sig = identityPriv.sign(pub.serialize());
|
|
53
|
-
const id = store.signedPreKeyId + 1;
|
|
54
|
-
const record = SignedPreKeyRecord.new(id, Date.now(), pub, priv, u8(sig));
|
|
55
|
-
await store.saveSignedPreKey(id, record);
|
|
56
|
-
return record;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
export async function buildPreKeyUploadPayload(
|
|
61
|
-
store: DeviceStore,
|
|
62
|
-
preKeys: GeneratedPreKey[],
|
|
63
|
-
): Promise<PreKeyUploadPayload> {
|
|
64
|
-
const idPair = await store.getIdentityKeyPair();
|
|
65
|
-
const sPreKey = await store.getSignedPreKey(store.signedPreKeyId);
|
|
66
|
-
|
|
67
|
-
return {
|
|
68
|
-
registrationId: store.registrationId,
|
|
69
|
-
// Use serialize() (33-byte DER) for consistency - server accepts both formats
|
|
70
|
-
// getPublicKeyBytes() returns 32-byte raw; serialize() returns 33-byte compressed DER
|
|
71
|
-
identityKey: idPair.publicKey.serialize(),
|
|
72
|
-
signedPreKey: {
|
|
73
|
-
keyId: sPreKey.id(),
|
|
74
|
-
publicKey: sPreKey.publicKey().serialize(),
|
|
75
|
-
signature: sPreKey.signature(),
|
|
76
|
-
},
|
|
77
|
-
preKeys: preKeys.map(pk => ({
|
|
78
|
-
keyId: pk.id,
|
|
79
|
-
publicKey: pk.record.publicKey().serialize(),
|
|
80
|
-
})),
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
export function buildPreKeyBundle(raw: RawPreKeyBundle): PreKeyBundle {
|
|
86
|
-
const identityKey = PublicKey_deserialize(raw.identityKey);
|
|
87
|
-
const spkPub = PublicKey_deserialize(raw.signedPreKey.publicKey);
|
|
88
|
-
|
|
89
|
-
// Messenger doesn't use Kyber PQ prekeys, but PreKeyBundle.new() requires
|
|
90
|
-
// real Kyber objects with a valid signature.
|
|
91
|
-
// If the raw bundle includes a pre-computed Kyber field, use it directly.
|
|
92
|
-
// Otherwise generate a dummy Kyber key and sign it with a new ephemeral identity
|
|
93
|
-
// key just to satisfy the API. This is safe for Messenger since the server
|
|
94
|
-
// never sends Kyber material and the recipient won't verify it.
|
|
95
|
-
let kyberPub: import("@signalapp/libsignal-client").KEMPublicKey | null = null;
|
|
96
|
-
let kyberSig: Uint8Array | null = null;
|
|
97
|
-
let kyberKeyId: number | null = null;
|
|
98
|
-
|
|
99
|
-
if (raw.kyberPreKey) {
|
|
100
|
-
kyberPub = KEMPublicKey.deserialize(u8(raw.kyberPreKey.publicKey));
|
|
101
|
-
kyberSig = u8(raw.kyberPreKey.signature);
|
|
102
|
-
kyberKeyId = raw.kyberPreKey.keyId;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
if (raw.preKey) {
|
|
106
|
-
const pkPub = PublicKey_deserialize(raw.preKey.publicKey);
|
|
107
|
-
return PreKeyBundle.new(
|
|
108
|
-
raw.registrationId,
|
|
109
|
-
raw.deviceId,
|
|
110
|
-
raw.preKey.keyId,
|
|
111
|
-
pkPub,
|
|
112
|
-
raw.signedPreKey.keyId,
|
|
113
|
-
spkPub,
|
|
114
|
-
u8(raw.signedPreKey.signature),
|
|
115
|
-
identityKey,
|
|
116
|
-
kyberKeyId as any,
|
|
117
|
-
kyberPub as any,
|
|
118
|
-
kyberSig as any,
|
|
119
|
-
);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
return PreKeyBundle.new(
|
|
123
|
-
raw.registrationId,
|
|
124
|
-
raw.deviceId,
|
|
125
|
-
null,
|
|
126
|
-
null,
|
|
127
|
-
raw.signedPreKey.keyId,
|
|
128
|
-
spkPub,
|
|
129
|
-
u8(raw.signedPreKey.signature),
|
|
130
|
-
identityKey,
|
|
131
|
-
kyberKeyId as any,
|
|
132
|
-
kyberPub as any,
|
|
133
|
-
kyberSig as any,
|
|
134
|
-
);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// Sync PublicKey deserialize helper (libsignal 0.92 is sync)
|
|
138
|
-
function PublicKey_deserialize(bytes: Uint8Array): PublicKey {
|
|
139
|
-
return PublicKey.deserialize(u8(bytes));
|
|
140
|
-
}
|
|
@@ -1,365 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* E2EE Signal Manager - Layer 2 (Signal Protocol)
|
|
3
|
-
*
|
|
4
|
-
* Handles all Signal Protocol encrypt/decrypt operations for:
|
|
5
|
-
* - DM (1-to-1): X3DH session establishment + Double Ratchet
|
|
6
|
-
* - Group: Sender Key distribution + group cipher
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import {
|
|
10
|
-
ProtocolAddress,
|
|
11
|
-
SenderKeyDistributionMessage,
|
|
12
|
-
CiphertextMessageType,
|
|
13
|
-
SignalMessage,
|
|
14
|
-
PreKeySignalMessage,
|
|
15
|
-
signalEncrypt,
|
|
16
|
-
signalDecrypt,
|
|
17
|
-
signalDecryptPreKey,
|
|
18
|
-
processPreKeyBundle,
|
|
19
|
-
groupEncrypt,
|
|
20
|
-
groupDecrypt,
|
|
21
|
-
processSenderKeyDistributionMessage,
|
|
22
|
-
SenderKeyMessage,
|
|
23
|
-
} from "@signalapp/libsignal-client";
|
|
24
|
-
import { randomUUID } from "node:crypto";
|
|
25
|
-
|
|
26
|
-
import type { DeviceStore } from "../store/device-store.ts";
|
|
27
|
-
import type { RawPreKeyBundle } from "./prekey-manager.ts";
|
|
28
|
-
import { buildPreKeyBundle } from "./prekey-manager.ts";
|
|
29
|
-
import {
|
|
30
|
-
wrapAsSignalSKMSG,
|
|
31
|
-
parseFBProtobufSKMSG,
|
|
32
|
-
stableDistributionId,
|
|
33
|
-
uuidStringify,
|
|
34
|
-
} from "../facebook/facebook-protocol-utils.ts";
|
|
35
|
-
import { logger } from "../../utils/logger.ts";
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Cast for strict libsignal params.
|
|
39
|
-
* Converts Uint8Array to Buffer if needed.
|
|
40
|
-
*/
|
|
41
|
-
const u8 = (b: Uint8Array | undefined | null): Buffer => {
|
|
42
|
-
if (!b) return Buffer.alloc(0);
|
|
43
|
-
return Buffer.isBuffer(b) ? b : Buffer.from(b.buffer, b.byteOffset, b.byteLength);
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
// Address helpers
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Build a ProtocolAddress from a Messenger/WhatsApp JID string.
|
|
50
|
-
* Format: "user.agent:device@server" or "user.agent@server".
|
|
51
|
-
* The agent suffix is part of the Signal user ID; the device suffix is the Signal device ID.
|
|
52
|
-
*/
|
|
53
|
-
export function jidToAddress(jid: string): ProtocolAddress {
|
|
54
|
-
const [userPartRaw = jid, server = ""] = jid.split("@");
|
|
55
|
-
|
|
56
|
-
// Messenger FBJID stores the device in the FBJID device field. Our decoder
|
|
57
|
-
// represents that as either user.device@msgr or user:device@msgr, while
|
|
58
|
-
// participant hashes may use ADString form user.0:device@msgr. In all cases
|
|
59
|
-
// the Signal username is the bare FBID and the Signal device is the device id.
|
|
60
|
-
if (server === "msgr") {
|
|
61
|
-
const colonIdx = userPartRaw.indexOf(":");
|
|
62
|
-
const dotIdx = userPartRaw.indexOf(".");
|
|
63
|
-
const userEnd = dotIdx !== -1 ? dotIdx : (colonIdx !== -1 ? colonIdx : userPartRaw.length);
|
|
64
|
-
const user = userPartRaw.slice(0, userEnd) || userPartRaw;
|
|
65
|
-
const rawDevice = colonIdx !== -1
|
|
66
|
-
? userPartRaw.slice(colonIdx + 1)
|
|
67
|
-
: (dotIdx !== -1 ? userPartRaw.slice(dotIdx + 1) : "0");
|
|
68
|
-
const device = Number(rawDevice) || 0;
|
|
69
|
-
return ProtocolAddress.new(user, device);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const [userAndAgent = jid, rawDevicePart = ""] = userPartRaw.split(":");
|
|
73
|
-
const [user = jid, rawAgentPart = ""] = userAndAgent.split(".");
|
|
74
|
-
const signalUser = rawAgentPart ? `${user}_${rawAgentPart}` : user;
|
|
75
|
-
const device = rawDevicePart ? Number(rawDevicePart) : 0;
|
|
76
|
-
return ProtocolAddress.new(signalUser, device);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export function addressToJidKey(addr: ProtocolAddress): string {
|
|
80
|
-
return `${addr.name()}:${addr.deviceId()}`;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function legacyJidToAddress(jid: string): ProtocolAddress {
|
|
84
|
-
const [userPart] = jid.split("@");
|
|
85
|
-
const [userAndAgent = jid, rawDevicePart = ""] = (userPart ?? jid).split(":");
|
|
86
|
-
const [user = jid, rawAgentPart = ""] = userAndAgent.split(".");
|
|
87
|
-
const signalUser = rawAgentPart ? `${user}_${rawAgentPart}` : user;
|
|
88
|
-
const device = rawDevicePart ? Number(rawDevicePart) : 0;
|
|
89
|
-
return ProtocolAddress.new(signalUser, device);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function addressCandidatesForJid(jid: string): ProtocolAddress[] {
|
|
93
|
-
const candidates = [jidToAddress(jid), legacyJidToAddress(jid)];
|
|
94
|
-
const seen = new Set<string>();
|
|
95
|
-
return candidates.filter((addr) => {
|
|
96
|
-
const key = addr.toString();
|
|
97
|
-
if (seen.has(key)) return false;
|
|
98
|
-
seen.add(key);
|
|
99
|
-
return true;
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function listSenderKeyDistributionIds(store: DeviceStore, senderAddr: ProtocolAddress): string[] {
|
|
104
|
-
const list = (store as any).listSenderKeyDistributionIds;
|
|
105
|
-
return typeof list === "function" ? list.call(store, senderAddr) : [];
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// DM - X3DH session establish + Double Ratchet encrypt/decrypt
|
|
109
|
-
|
|
110
|
-
/** Establish an outgoing session with a new contact using their prekey bundle (X3DH). */
|
|
111
|
-
export async function establishSession(
|
|
112
|
-
store: DeviceStore,
|
|
113
|
-
recipient: ProtocolAddress,
|
|
114
|
-
rawBundle: RawPreKeyBundle,
|
|
115
|
-
): Promise<void> {
|
|
116
|
-
const bundle = buildPreKeyBundle(rawBundle);
|
|
117
|
-
await processPreKeyBundle(bundle, recipient, store as any, store as any);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/** Encrypt plaintext for a DM recipient. */
|
|
121
|
-
export async function encryptDM(
|
|
122
|
-
store: DeviceStore,
|
|
123
|
-
recipient: ProtocolAddress,
|
|
124
|
-
selfAddress: ProtocolAddress,
|
|
125
|
-
plaintext: Uint8Array,
|
|
126
|
-
): Promise<{ type: "msg" | "pkmsg"; ciphertext: Uint8Array }> {
|
|
127
|
-
const cipherMsg = await signalEncrypt(u8(plaintext), recipient, store as any, store as any);
|
|
128
|
-
const type = cipherMsg.type() === CiphertextMessageType.Whisper ? "msg" : "pkmsg";
|
|
129
|
-
return { type, ciphertext: cipherMsg.serialize() };
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/** Decrypt a normal Signal message (not first-message). */
|
|
133
|
-
export async function decryptDM(
|
|
134
|
-
store: DeviceStore,
|
|
135
|
-
sender: ProtocolAddress,
|
|
136
|
-
ciphertext: Uint8Array,
|
|
137
|
-
): Promise<Buffer> {
|
|
138
|
-
const msg = SignalMessage.deserialize(u8(ciphertext));
|
|
139
|
-
return Buffer.from(await signalDecrypt(msg, sender, store as any, store as any));
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/** Decrypt a PreKeySignalMessage (first message from sender). */
|
|
143
|
-
export async function decryptDMPreKey(
|
|
144
|
-
store: DeviceStore,
|
|
145
|
-
sender: ProtocolAddress,
|
|
146
|
-
selfAddress: ProtocolAddress,
|
|
147
|
-
ciphertext: Uint8Array,
|
|
148
|
-
): Promise<Buffer> {
|
|
149
|
-
const msg = PreKeySignalMessage.deserialize(u8(ciphertext));
|
|
150
|
-
return Buffer.from(
|
|
151
|
-
await signalDecryptPreKey(msg, sender, store as any, store as any, store as any, store as any, store as any)
|
|
152
|
-
);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// Group - Sender Key
|
|
156
|
-
|
|
157
|
-
/** Create or retrieve a SenderKeyDistributionMessage for the given group/sender. */
|
|
158
|
-
export async function createSenderKeyDistributionMessage(
|
|
159
|
-
store: DeviceStore,
|
|
160
|
-
groupJid: string,
|
|
161
|
-
senderJid: string,
|
|
162
|
-
): Promise<{ skdm: SenderKeyDistributionMessage; distributionId: string }> {
|
|
163
|
-
const distributionId = randomUUID();
|
|
164
|
-
const senderAddr = jidToAddress(senderJid);
|
|
165
|
-
const skdm = await SenderKeyDistributionMessage.create(senderAddr, distributionId, store as any);
|
|
166
|
-
return { skdm, distributionId };
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Process a received SenderKeyDistributionMessage from a group member.
|
|
171
|
-
* Handles both standard Signal and Facebook's signature-less variants.
|
|
172
|
-
*/
|
|
173
|
-
export async function processSKDM(
|
|
174
|
-
store: DeviceStore,
|
|
175
|
-
senderJid: string,
|
|
176
|
-
skdmBytes: Uint8Array,
|
|
177
|
-
groupJid?: string,
|
|
178
|
-
): Promise<void> {
|
|
179
|
-
const senderAddr = jidToAddress(senderJid);
|
|
180
|
-
const buf = u8(skdmBytes as Buffer);
|
|
181
|
-
|
|
182
|
-
const processAndAlias = async (skdm: SenderKeyDistributionMessage): Promise<void> => {
|
|
183
|
-
const distributionId = String(skdm.distributionId());
|
|
184
|
-
await processSenderKeyDistributionMessage(senderAddr, skdm, store as any);
|
|
185
|
-
|
|
186
|
-
// Some Facebook group SKMSG packets arrive without a usable distribution ID.
|
|
187
|
-
// decryptGroup() rewraps those packets with stableDistributionId(group, sender).
|
|
188
|
-
// Alias the freshly-processed SKDM record to that deterministic ID so the
|
|
189
|
-
// immediately-following group decrypt can find the sender key state.
|
|
190
|
-
if (groupJid) {
|
|
191
|
-
const stableId = stableDistributionId(groupJid, senderJid);
|
|
192
|
-
if (stableId !== distributionId) {
|
|
193
|
-
const record = await store.getSenderKey(senderAddr, distributionId);
|
|
194
|
-
if (record) {
|
|
195
|
-
await store.saveSenderKey(senderAddr, stableId, record);
|
|
196
|
-
logger.debug("signal-manager", `Aliased sender key ${distributionId} -> ${stableId} for ${senderJid} in ${groupJid}`);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
};
|
|
201
|
-
|
|
202
|
-
// Try the standard libsignal format first. Facebook captures we have so far
|
|
203
|
-
// use the same 0x33-prefixed serialized message, so we should not invent our
|
|
204
|
-
// own distribution ID or signing key when the library can parse it directly.
|
|
205
|
-
try {
|
|
206
|
-
await processAndAlias(SenderKeyDistributionMessage.deserialize(buf));
|
|
207
|
-
return;
|
|
208
|
-
} catch (primaryErr) {
|
|
209
|
-
if (buf[0] === 0x33 && buf.length > 1) {
|
|
210
|
-
await processAndAlias(SenderKeyDistributionMessage.deserialize(buf.slice(1)));
|
|
211
|
-
return;
|
|
212
|
-
}
|
|
213
|
-
throw primaryErr;
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/** Encrypt plaintext for a group using sender key. */
|
|
218
|
-
export async function encryptGroup(
|
|
219
|
-
store: DeviceStore,
|
|
220
|
-
groupJid: string,
|
|
221
|
-
senderJid: string,
|
|
222
|
-
plaintext: Uint8Array,
|
|
223
|
-
distributionId?: string,
|
|
224
|
-
): Promise<Uint8Array> {
|
|
225
|
-
const activeDistributionId = distributionId ?? randomUUID();
|
|
226
|
-
const senderAddr = jidToAddress(senderJid);
|
|
227
|
-
const cipherMsg = await groupEncrypt(senderAddr, activeDistributionId, store as any, u8(plaintext));
|
|
228
|
-
const buf = u8(cipherMsg.serialize());
|
|
229
|
-
// If this is a Facebook-style SKMSG (0x33 prefix, signature-less), re-wrap into
|
|
230
|
-
// a signed Signal SKMSG compatible with libsignal group decrypt expectations.
|
|
231
|
-
if (buf && buf.length > 0 && buf[0] === 0x33) {
|
|
232
|
-
try {
|
|
233
|
-
// Try protobuf-style FB SKMSG first
|
|
234
|
-
const parsed = parseFBProtobufSKMSG(buf.slice(1));
|
|
235
|
-
if (parsed) {
|
|
236
|
-
const wrapped = wrapAsSignalSKMSG({ distributionId: activeDistributionId, id: parsed.id, iteration: parsed.iteration, ciphertext: parsed.ciphertext, senderJid });
|
|
237
|
-
return wrapped;
|
|
238
|
-
}
|
|
239
|
-
// Fallback: legacy binary FB SKMSG (distributionId(16) | id(4) | ct...)
|
|
240
|
-
if (buf.length >= 21 && buf[1] !== 0x08) {
|
|
241
|
-
const distId = uuidStringify(buf.slice(1, 17));
|
|
242
|
-
const id = buf.readUInt32BE(17);
|
|
243
|
-
const iteration = 0;
|
|
244
|
-
const ct = buf.slice(21);
|
|
245
|
-
const wrapped = wrapAsSignalSKMSG({ distributionId: distId, id, iteration, ciphertext: ct, senderJid });
|
|
246
|
-
return wrapped;
|
|
247
|
-
}
|
|
248
|
-
} catch (e) {
|
|
249
|
-
// fallback to raw buf
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
return buf;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/** Decrypt a group SenderKeyMessage. */
|
|
256
|
-
export async function decryptGroup(
|
|
257
|
-
store: DeviceStore,
|
|
258
|
-
senderJid: string,
|
|
259
|
-
ciphertext: Uint8Array,
|
|
260
|
-
groupJid?: string,
|
|
261
|
-
): Promise<Buffer> {
|
|
262
|
-
const senderAddrs = addressCandidatesForJid(senderJid);
|
|
263
|
-
const senderAddr = senderAddrs[0]!;
|
|
264
|
-
let buf = u8(ciphertext);
|
|
265
|
-
let activeDistributionId: string | undefined;
|
|
266
|
-
let rewrapInfo: { id: number; iteration: number; ciphertext: Buffer } | null = null;
|
|
267
|
-
|
|
268
|
-
// If it's a Facebook-style message (version 0x33, usually lacking 64-byte signature)
|
|
269
|
-
if (buf[0] === 0x33) {
|
|
270
|
-
try {
|
|
271
|
-
const msg = SenderKeyMessage.deserialize(buf);
|
|
272
|
-
activeDistributionId = String(msg.distributionId());
|
|
273
|
-
// Already a valid Signal message
|
|
274
|
-
} catch (e) {
|
|
275
|
-
// Likely a Facebook signature-less message, needs re-encoding
|
|
276
|
-
logger.debug("signal-manager", `Re-encoding Facebook-style SKMSG from ${senderJid}`);
|
|
277
|
-
|
|
278
|
-
let id: number, iteration: number, ct: Buffer, distId: string;
|
|
279
|
-
|
|
280
|
-
if (buf.length >= 21 && buf[1] !== 0x08) {
|
|
281
|
-
// Legacy Binary
|
|
282
|
-
distId = uuidStringify(buf.slice(1, 17));
|
|
283
|
-
id = buf.readUInt32BE(17);
|
|
284
|
-
iteration = 0;
|
|
285
|
-
ct = buf.slice(21);
|
|
286
|
-
} else {
|
|
287
|
-
// Protobuf Style
|
|
288
|
-
const parsed = parseFBProtobufSKMSG(buf.slice(1));
|
|
289
|
-
if (!parsed) throw new Error("Failed to parse Facebook Protobuf SKMSG");
|
|
290
|
-
id = parsed.id;
|
|
291
|
-
iteration = parsed.iteration;
|
|
292
|
-
ct = parsed.ciphertext;
|
|
293
|
-
distId = stableDistributionId(groupJid || "unknown", senderJid);
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
activeDistributionId = distId;
|
|
297
|
-
rewrapInfo = { id, iteration, ciphertext: ct };
|
|
298
|
-
buf = wrapAsSignalSKMSG({ distributionId: distId, id, iteration, ciphertext: ct, senderJid });
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
try {
|
|
303
|
-
const result = await groupDecrypt(senderAddr, store as any, buf);
|
|
304
|
-
return Buffer.from(result);
|
|
305
|
-
} catch (primaryErr: any) {
|
|
306
|
-
const tried = new Set<string>([`${senderAddr.toString()}::${activeDistributionId ?? "original"}`]);
|
|
307
|
-
|
|
308
|
-
// Migration fallback: older builds used a different Messenger JID ->
|
|
309
|
-
// ProtocolAddress mapping (for example user.device@msgr became
|
|
310
|
-
// user_device.0). Try the same serialized SKMSG against those legacy
|
|
311
|
-
// sender-key namespaces before giving up.
|
|
312
|
-
for (const candidateAddr of senderAddrs.slice(1)) {
|
|
313
|
-
const attemptKey = `${candidateAddr.toString()}::${activeDistributionId ?? "original"}`;
|
|
314
|
-
if (tried.has(attemptKey)) continue;
|
|
315
|
-
tried.add(attemptKey);
|
|
316
|
-
try {
|
|
317
|
-
const result = await groupDecrypt(candidateAddr, store as any, buf);
|
|
318
|
-
logger.debug("signal-manager", `groupDecrypt succeeded with legacy sender address ${candidateAddr.toString()}`);
|
|
319
|
-
return Buffer.from(result);
|
|
320
|
-
} catch (candidateErr: any) {
|
|
321
|
-
logger.debug("signal-manager", `groupDecrypt fallback failed for ${attemptKey}: ${candidateErr.message}`);
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
// Facebook protobuf SKMSGs do not carry a distribution ID. The primary
|
|
326
|
-
// path uses a deterministic placeholder and processSKDM aliases fresh SKDMs
|
|
327
|
-
// to it, but existing stores may already have the right sender-key record
|
|
328
|
-
// under the real distribution ID. Rewrap the same ciphertext with each known
|
|
329
|
-
// distribution ID for this sender and let libsignal try the matching state.
|
|
330
|
-
if (rewrapInfo) {
|
|
331
|
-
for (const candidateAddr of senderAddrs) {
|
|
332
|
-
for (const distributionId of listSenderKeyDistributionIds(store, candidateAddr)) {
|
|
333
|
-
const attemptKey = `${candidateAddr.toString()}::${distributionId}`;
|
|
334
|
-
if (tried.has(attemptKey)) continue;
|
|
335
|
-
tried.add(attemptKey);
|
|
336
|
-
|
|
337
|
-
const candidateBuf = wrapAsSignalSKMSG({
|
|
338
|
-
distributionId,
|
|
339
|
-
id: rewrapInfo.id,
|
|
340
|
-
iteration: rewrapInfo.iteration,
|
|
341
|
-
ciphertext: rewrapInfo.ciphertext,
|
|
342
|
-
senderJid,
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
try {
|
|
346
|
-
const result = await groupDecrypt(candidateAddr, store as any, candidateBuf);
|
|
347
|
-
logger.debug("signal-manager", `groupDecrypt succeeded with sender key ${attemptKey}`);
|
|
348
|
-
return Buffer.from(result);
|
|
349
|
-
} catch (candidateErr: any) {
|
|
350
|
-
logger.debug("signal-manager", `groupDecrypt fallback failed for ${attemptKey}: ${candidateErr.message}`);
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
logger.error("signal-manager", `groupDecrypt failed: ${primaryErr.message} (op: ${primaryErr.operation})`);
|
|
357
|
-
throw primaryErr;
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
/** Check if a session exists for a given address. */
|
|
362
|
-
export async function hasSession(store: DeviceStore, address: ProtocolAddress): Promise<boolean> {
|
|
363
|
-
const record = await store.getSession(address);
|
|
364
|
-
return record != null;
|
|
365
|
-
}
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import type { DeviceJSON } from "../../models/e2ee.ts";
|
|
2
|
-
|
|
3
|
-
export const DEVICE_STORE_SCHEMA_VERSION = 1;
|
|
4
|
-
|
|
5
|
-
export function parseDeviceJSON(json: string): DeviceJSON {
|
|
6
|
-
return migrateDeviceJSON(JSON.parse(json) as DeviceJSON);
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function migrateDeviceJSON(data: DeviceJSON): DeviceJSON {
|
|
10
|
-
return {
|
|
11
|
-
...data,
|
|
12
|
-
schema_version: data.schema_version ?? DEVICE_STORE_SCHEMA_VERSION,
|
|
13
|
-
next_pre_key_id: data.next_pre_key_id ?? 1,
|
|
14
|
-
identities: data.identities ?? {},
|
|
15
|
-
sessions: data.sessions ?? {},
|
|
16
|
-
pre_keys: data.pre_keys ?? {},
|
|
17
|
-
sender_keys: data.sender_keys ?? {},
|
|
18
|
-
signed_pre_keys: data.signed_pre_keys ?? {},
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function decodeBase64(value: string): Buffer {
|
|
23
|
-
return Buffer.from(value, "base64");
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function encodeBase64(value: Buffer | Uint8Array): string {
|
|
27
|
-
return Buffer.from(value).toString("base64");
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function mapFromBase64Record<K extends string | number>(
|
|
31
|
-
record: Record<string, string> | undefined,
|
|
32
|
-
keyFromString: (key: string) => K,
|
|
33
|
-
): Map<K, Uint8Array> {
|
|
34
|
-
const out = new Map<K, Uint8Array>();
|
|
35
|
-
for (const [key, value] of Object.entries(record ?? {})) {
|
|
36
|
-
out.set(keyFromString(key), Buffer.from(value, "base64"));
|
|
37
|
-
}
|
|
38
|
-
return out;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function base64RecordFromMap<K extends string | number>(map: Map<K, Uint8Array>): Record<string, string> {
|
|
42
|
-
const out: Record<string, string> = {};
|
|
43
|
-
for (const [key, value] of map) {
|
|
44
|
-
out[String(key)] = encodeBase64(value);
|
|
45
|
-
}
|
|
46
|
-
return out;
|
|
47
|
-
}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
-
import type { DeviceJSON } from "../../models/e2ee.ts";
|
|
3
|
-
import { parseDeviceJSON } from "./device-json.ts";
|
|
4
|
-
|
|
5
|
-
export function readDeviceJSONFile(path: string): DeviceJSON | null {
|
|
6
|
-
if (!existsSync(path)) return null;
|
|
7
|
-
return parseDeviceJSON(readFileSync(path, "utf8"));
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function writeDeviceJSONFile(path: string, data: string): void {
|
|
11
|
-
writeFileSync(path, data, { mode: 0o600 });
|
|
12
|
-
}
|