shadowx-fca 8.0.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 +1066 -0
- package/build/messagix.dll +0 -0
- package/build/messagix.so +0 -0
- package/checkUpdate.js +393 -0
- package/config.json +17 -0
- package/e2ee.js +563 -0
- package/e2eetest.js +356 -0
- package/index.js +611 -0
- package/lib/index.mjs +1412 -0
- package/logger.js +500 -0
- package/package.json +65 -0
- package/src/GetBotInfo.js +66 -0
- package/src/OldMessage.js +182 -0
- package/src/Screenshot.js +83 -0
- package/src/addExternalModule.js +13 -0
- package/src/addUserToGroup.js +33 -0
- package/src/approveGroupJoinRequests.js +18 -0
- package/src/changeAdminStatus.js +16 -0
- package/src/changeArchivedStatus.js +17 -0
- package/src/changeAvatar.js +136 -0
- package/src/changeAvatarV2.js +86 -0
- package/src/changeAvt.js +85 -0
- package/src/changeBio.js +76 -0
- package/src/changeBlockedStatus.js +20 -0
- package/src/changeBlockedStatusMqtt.js +80 -0
- package/src/changeCover.js +72 -0
- package/src/changeGroupImage.js +16 -0
- package/src/changeName.js +79 -0
- package/src/changeNickname.js +16 -0
- package/src/changeThreadColor.js +15 -0
- package/src/changeThreadEmoji.js +15 -0
- package/src/changeThreadMemberNickname.js +6 -0
- package/src/changeUsername.js +59 -0
- package/src/createCommentPost.js +230 -0
- package/src/createNewGroup.js +38 -0
- package/src/createNote.js +35 -0
- package/src/createPoll.js +27 -0
- package/src/createPost.js +276 -0
- package/src/createThemeAI.js +129 -0
- package/src/data/cache/system/data.json +4 -0
- package/src/data/cache/system/datahandle.js +21 -0
- package/src/data/getThreadInfo.json +1 -0
- package/src/deleteComment.js +23 -0
- package/src/deleteMessage.js +15 -0
- package/src/deleteThread.js +15 -0
- package/src/denyGroupJoinRequests.js +18 -0
- package/src/e2ee/crypto.js +173 -0
- package/src/e2ee/index.js +144 -0
- package/src/e2ee/proto/ArmadilloApplication.proto +281 -0
- package/src/e2ee/proto/ArmadilloICDC.proto +14 -0
- package/src/e2ee/proto/ConsumerApplication.proto +232 -0
- package/src/e2ee/proto/MessageApplication.proto +82 -0
- package/src/e2ee/proto/MessageTransport.proto +77 -0
- package/src/e2ee/proto/WACommon.proto +66 -0
- package/src/e2ee/proto/WAMediaTransport.proto +176 -0
- package/src/e2ee/proto/proto-writer.ts +76 -0
- package/src/e2ee/protocol.js +196 -0
- package/src/e2ee/ratchet.js +219 -0
- package/src/e2ee/store.js +182 -0
- package/src/e2ee.js +8 -0
- package/src/editMessage.js +56 -0
- package/src/editMessageOld.js +67 -0
- package/src/enableReactions.js +24 -0
- package/src/follow.js +74 -0
- package/src/followUser.js +23 -0
- package/src/forwardAttachment.js +16 -0
- package/src/friendList.js +98 -0
- package/src/getAccess.js +112 -0
- package/src/getAppState.js +13 -0
- package/src/getAvatarUser.js +11 -0
- package/src/getBio.js +24 -0
- package/src/getBotInitialData.js +42 -0
- package/src/getCtx.js +5 -0
- package/src/getCurrentUserID.js +6 -0
- package/src/getEmojiUrl.js +29 -0
- package/src/getFriendsList.js +36 -0
- package/src/getMessage.js +37 -0
- package/src/getNotes.js +17 -0
- package/src/getOptions.js +5 -0
- package/src/getPinnedMessages.js +33 -0
- package/src/getPostInfo.js +17 -0
- package/src/getProfileInfo.js +17 -0
- package/src/getPublicData.js +25 -0
- package/src/getRegion.js +7 -0
- package/src/getRepInfo.js +17 -0
- package/src/getStickerPacks.js +25 -0
- package/src/getStickers.js +39 -0
- package/src/getStoryReactions.js +18 -0
- package/src/getThreadHistory.js +45 -0
- package/src/getThreadHistoryDeprecated.js +71 -0
- package/src/getThreadInfo.js +73 -0
- package/src/getThreadInfoDeprecated.js +56 -0
- package/src/getThreadList.js +76 -0
- package/src/getThreadListDeprecated.js +46 -0
- package/src/getThreadPictures.js +59 -0
- package/src/getThreadTheme.js +77 -0
- package/src/getUID.js +17 -0
- package/src/getUserID.js +17 -0
- package/src/getUserInfo.js +28 -0
- package/src/handleFriendRequest.js +21 -0
- package/src/handleMessageRequest.js +15 -0
- package/src/httpGet.js +13 -0
- package/src/httpPost.js +12 -0
- package/src/httpPostFormData.js +12 -0
- package/src/listenE2EE.js +75 -0
- package/src/listenMqtt.js +802 -0
- package/src/listenNotification.js +85 -0
- package/src/logout.js +22 -0
- package/src/markAsDelivered.js +17 -0
- package/src/markAsRead.js +14 -0
- package/src/markAsReadAll.js +15 -0
- package/src/markAsSeen.js +15 -0
- package/src/metaTheme.js +185 -0
- package/src/muteThread.js +52 -0
- package/src/note.js +228 -0
- package/src/pin.js +53 -0
- package/src/pinMessage.js +6 -0
- package/src/postComment.js +29 -0
- package/src/postFormData.js +46 -0
- package/src/reactToComment.js +31 -0
- package/src/reactToPost.js +32 -0
- package/src/refreshFb_dtsg.js +31 -0
- package/src/removeSuspiciousAccount.js +74 -0
- package/src/removeUserFromGroup.js +15 -0
- package/src/reply.js +442 -0
- package/src/resolvePhotoUrl.js +15 -0
- package/src/searchForThread.js +20 -0
- package/src/searchFriends.js +28 -0
- package/src/searchStickers.js +53 -0
- package/src/send.js +46 -0
- package/src/sendAudio.js +8 -0
- package/src/sendBroadcast.js +93 -0
- package/src/sendButtons.js +161 -0
- package/src/sendComment.js +159 -0
- package/src/sendEmoji.js +10 -0
- package/src/sendFile.js +9 -0
- package/src/sendFriendRequest.js +33 -0
- package/src/sendGif.js +24 -0
- package/src/sendImage.js +9 -0
- package/src/sendLocation.js +9 -0
- package/src/sendMessage.js +487 -0
- package/src/sendMessage1.js +309 -0
- package/src/sendMessageMqtt.js +68 -0
- package/src/sendSticker.js +8 -0
- package/src/sendTypingIndicator.js +36 -0
- package/src/sendTypingIndicatorV2.js +28 -0
- package/src/sendVideo.js +9 -0
- package/src/sessionGuard.js +130 -0
- package/src/setActiveStatus.js +16 -0
- package/src/setMessageReaction.js +61 -0
- package/src/setMessageReactionMqtt.js +62 -0
- package/src/setOptions.js +22 -0
- package/src/setPollVote.js +17 -0
- package/src/setPostReaction.js +112 -0
- package/src/setProfileGuard.js +44 -0
- package/src/setProfileLock.js +93 -0
- package/src/setStoryReaction.js +129 -0
- package/src/setStorySeen.js +99 -0
- package/src/setThreadTheme.js +17 -0
- package/src/setTitle.js +15 -0
- package/src/shareContact.js +33 -0
- package/src/shareLink.js +8 -0
- package/src/sharePost.js +31 -0
- package/src/stopListenMqtt.js +23 -0
- package/src/storyManager.js +353 -0
- package/src/suggestFriend.js +128 -0
- package/src/threadColors.js +131 -0
- package/src/unfollowUser.js +23 -0
- package/src/unfriend.js +15 -0
- package/src/unpinMessage.js +6 -0
- package/src/unsendMessage.js +14 -0
- package/src/uploadAttachment.js +58 -0
- package/src/uploadImageToImgbb.js +29 -0
- package/utils.js +2945 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Facebook-specific E2EE wire protocol handling.
|
|
5
|
+
*
|
|
6
|
+
* Facebook Secret Conversations use Signal Protocol (X3DH + Double Ratchet)
|
|
7
|
+
* with their own framing on top. This module handles:
|
|
8
|
+
* - Key bundle upload/download to Facebook's graph endpoint
|
|
9
|
+
* - Message framing (encode/decode SHADOWX_E2EE frame format)
|
|
10
|
+
* - Signed pre-key signatures using HMAC-SHA256 (FB uses Ed25519 in practice;
|
|
11
|
+
* we sign with HMAC for our own inter-device sessions)
|
|
12
|
+
* - E2EE-capable thread detection from message metadata
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const crypto = require("crypto");
|
|
16
|
+
const { hmacSha256, generateKeyPair, randomBytes } = require("./crypto");
|
|
17
|
+
|
|
18
|
+
// ─── Frame format ──────────────────────────────────────────────────────────────
|
|
19
|
+
// 2 bytes: magic (0x4E58 = "NX")
|
|
20
|
+
// 1 byte: version (0x01)
|
|
21
|
+
// 1 byte: type (0=init, 1=message, 2=reaction, 3=receipt, 4=typing)
|
|
22
|
+
// 4 bytes: sender ID length
|
|
23
|
+
// N bytes: sender ID (utf8)
|
|
24
|
+
// 4 bytes: thread ID length
|
|
25
|
+
// N bytes: thread ID (utf8)
|
|
26
|
+
// 4 bytes: header length [only for type 0,1,2]
|
|
27
|
+
// N bytes: header
|
|
28
|
+
// 4 bytes: payload length
|
|
29
|
+
// N bytes: payload (encrypted or raw for receipt/typing)
|
|
30
|
+
|
|
31
|
+
const MAGIC = 0x4E58;
|
|
32
|
+
const VERSION = 0x01;
|
|
33
|
+
const TYPE = { INIT: 0, MESSAGE: 1, REACTION: 2, RECEIPT: 3, TYPING: 4 };
|
|
34
|
+
|
|
35
|
+
function encodeFrame(type, senderID, threadID, header, payload) {
|
|
36
|
+
const sidBuf = Buffer.from(String(senderID), "utf8");
|
|
37
|
+
const tidBuf = Buffer.from(String(threadID), "utf8");
|
|
38
|
+
const hdrBuf = header || Buffer.alloc(0);
|
|
39
|
+
const payBuf = Buffer.isBuffer(payload) ? payload : Buffer.from(payload || "");
|
|
40
|
+
|
|
41
|
+
const totalSize = 2 + 1 + 1 + 4 + sidBuf.length + 4 + tidBuf.length + 4 + hdrBuf.length + 4 + payBuf.length;
|
|
42
|
+
const buf = Buffer.allocUnsafe(totalSize);
|
|
43
|
+
let offset = 0;
|
|
44
|
+
|
|
45
|
+
buf.writeUInt16BE(MAGIC, offset); offset += 2;
|
|
46
|
+
buf.writeUInt8(VERSION, offset); offset += 1;
|
|
47
|
+
buf.writeUInt8(type, offset); offset += 1;
|
|
48
|
+
|
|
49
|
+
buf.writeUInt32BE(sidBuf.length, offset); offset += 4;
|
|
50
|
+
sidBuf.copy(buf, offset); offset += sidBuf.length;
|
|
51
|
+
|
|
52
|
+
buf.writeUInt32BE(tidBuf.length, offset); offset += 4;
|
|
53
|
+
tidBuf.copy(buf, offset); offset += tidBuf.length;
|
|
54
|
+
|
|
55
|
+
buf.writeUInt32BE(hdrBuf.length, offset); offset += 4;
|
|
56
|
+
hdrBuf.copy(buf, offset); offset += hdrBuf.length;
|
|
57
|
+
|
|
58
|
+
buf.writeUInt32BE(payBuf.length, offset); offset += 4;
|
|
59
|
+
payBuf.copy(buf, offset);
|
|
60
|
+
|
|
61
|
+
return buf;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function decodeFrame(buf) {
|
|
65
|
+
if (!Buffer.isBuffer(buf)) buf = Buffer.from(buf);
|
|
66
|
+
if (buf.length < 8) throw new Error("Frame too short");
|
|
67
|
+
let offset = 0;
|
|
68
|
+
|
|
69
|
+
const magic = buf.readUInt16BE(offset); offset += 2;
|
|
70
|
+
if (magic !== MAGIC) throw new Error("Invalid frame magic: " + magic.toString(16));
|
|
71
|
+
const version = buf.readUInt8(offset); offset += 1;
|
|
72
|
+
const type = buf.readUInt8(offset); offset += 1;
|
|
73
|
+
|
|
74
|
+
const sidLen = buf.readUInt32BE(offset); offset += 4;
|
|
75
|
+
const senderID = buf.slice(offset, offset + sidLen).toString("utf8"); offset += sidLen;
|
|
76
|
+
|
|
77
|
+
const tidLen = buf.readUInt32BE(offset); offset += 4;
|
|
78
|
+
const threadID = buf.slice(offset, offset + tidLen).toString("utf8"); offset += tidLen;
|
|
79
|
+
|
|
80
|
+
const hdrLen = buf.readUInt32BE(offset); offset += 4;
|
|
81
|
+
const header = buf.slice(offset, offset + hdrLen); offset += hdrLen;
|
|
82
|
+
|
|
83
|
+
const payLen = buf.readUInt32BE(offset); offset += 4;
|
|
84
|
+
const payload = buf.slice(offset, offset + payLen);
|
|
85
|
+
|
|
86
|
+
return { version, type, senderID, threadID, header, payload };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ─── Key Bundle ────────────────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
function buildKeyBundle(identityKey, signedPreKey, oneTimePreKeys) {
|
|
92
|
+
return {
|
|
93
|
+
identityKey: identityKey.publicKeyRaw.toString("base64"),
|
|
94
|
+
signedPreKey: {
|
|
95
|
+
keyId: signedPreKey.keyId || 1,
|
|
96
|
+
publicKey: signedPreKey.publicKeyRaw.toString("base64"),
|
|
97
|
+
signature: signedPreKey.signature ? signedPreKey.signature.toString("base64") : ""
|
|
98
|
+
},
|
|
99
|
+
oneTimePreKeys: (oneTimePreKeys || []).map(k => ({
|
|
100
|
+
keyId: k.keyId,
|
|
101
|
+
publicKey: k.publicKeyRaw.toString("base64")
|
|
102
|
+
}))
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function signKey(identityPrivateKey, keyToSign) {
|
|
107
|
+
return hmacSha256(identityPrivateKey, keyToSign.publicKeyRaw);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ─── Facebook Graph API: key registration/fetching ───────────────────────────
|
|
111
|
+
|
|
112
|
+
async function uploadKeyBundle(defaultFuncs, ctx, keyBundle) {
|
|
113
|
+
try {
|
|
114
|
+
const res = await defaultFuncs.post(
|
|
115
|
+
"https://www.facebook.com/api/graphql/",
|
|
116
|
+
ctx.jar,
|
|
117
|
+
{
|
|
118
|
+
fb_api_req_friendly_name: "MessengerE2EERegistrationMutation",
|
|
119
|
+
fb_api_caller_class: "RelayModern",
|
|
120
|
+
doc_id: "6325661730877783",
|
|
121
|
+
variables: JSON.stringify({
|
|
122
|
+
input: {
|
|
123
|
+
actor_id: ctx.userID,
|
|
124
|
+
client_mutation_id: String(ctx.clientMutationId++),
|
|
125
|
+
identity_key: keyBundle.identityKey,
|
|
126
|
+
signed_pre_key: keyBundle.signedPreKey,
|
|
127
|
+
one_time_pre_keys: keyBundle.oneTimePreKeys
|
|
128
|
+
}
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
);
|
|
132
|
+
return res;
|
|
133
|
+
} catch (e) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function fetchRecipientKeyBundle(defaultFuncs, ctx, recipientUserID) {
|
|
139
|
+
try {
|
|
140
|
+
const res = await defaultFuncs.post(
|
|
141
|
+
"https://www.facebook.com/api/graphql/",
|
|
142
|
+
ctx.jar,
|
|
143
|
+
{
|
|
144
|
+
fb_api_req_friendly_name: "MessengerE2EEKeyBundleQuery",
|
|
145
|
+
fb_api_caller_class: "RelayModern",
|
|
146
|
+
doc_id: "6325661730877784",
|
|
147
|
+
variables: JSON.stringify({ user_id: String(recipientUserID) })
|
|
148
|
+
}
|
|
149
|
+
);
|
|
150
|
+
return res;
|
|
151
|
+
} catch (e) {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ─── Message payload builders ─────────────────────────────────────────────────
|
|
157
|
+
|
|
158
|
+
function buildInitPayload(senderEphemeralPub, senderOneTimeKeyId, recipientOneTimeKeyId) {
|
|
159
|
+
return JSON.stringify({
|
|
160
|
+
type: "x3dh_init",
|
|
161
|
+
ek: senderEphemeralPub.toString("base64"),
|
|
162
|
+
skid: senderOneTimeKeyId || null,
|
|
163
|
+
opk_id: recipientOneTimeKeyId || null,
|
|
164
|
+
ts: Date.now()
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function parseInitPayload(buf) {
|
|
169
|
+
const str = Buffer.isBuffer(buf) ? buf.toString("utf8") : buf;
|
|
170
|
+
return JSON.parse(str);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function buildMessagePayload(text, attachments, replyToMessageId) {
|
|
174
|
+
return JSON.stringify({
|
|
175
|
+
v: 1,
|
|
176
|
+
text: text || null,
|
|
177
|
+
attachments: attachments || [],
|
|
178
|
+
reply_to: replyToMessageId || null,
|
|
179
|
+
ts: Date.now(),
|
|
180
|
+
nonce: randomBytes(8).toString("hex")
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function parseMessagePayload(buf) {
|
|
185
|
+
const str = Buffer.isBuffer(buf) ? buf.toString("utf8") : buf;
|
|
186
|
+
return JSON.parse(str);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
module.exports = {
|
|
190
|
+
MAGIC, VERSION, TYPE,
|
|
191
|
+
encodeFrame, decodeFrame,
|
|
192
|
+
buildKeyBundle, signKey,
|
|
193
|
+
uploadKeyBundle, fetchRecipientKeyBundle,
|
|
194
|
+
buildInitPayload, parseInitPayload,
|
|
195
|
+
buildMessagePayload, parseMessagePayload
|
|
196
|
+
};
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Double Ratchet Algorithm (Signal Protocol)
|
|
5
|
+
* Based on the Signal Protocol specification:
|
|
6
|
+
* https://signal.org/docs/specifications/doubleratchet/
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { generateKeyPair, dh, hkdf, hkdfExtract, hkdfExpand, encrypt, decrypt, hmacSha256 } = require("./crypto");
|
|
10
|
+
|
|
11
|
+
const MAX_SKIP = 1000;
|
|
12
|
+
const INFO_RATCHET = Buffer.from("WhisperRatchet", "utf8");
|
|
13
|
+
const INFO_MSG = Buffer.from("WhisperMessageKeys", "utf8");
|
|
14
|
+
|
|
15
|
+
function kdfCK(ck) {
|
|
16
|
+
const ck2 = hmacSha256(ck, Buffer.from([0x02]));
|
|
17
|
+
const mk = hmacSha256(ck, Buffer.from([0x01]));
|
|
18
|
+
return { ck: ck2, mk };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function kdfRK(rk, dh_out) {
|
|
22
|
+
const salt = rk;
|
|
23
|
+
const out = hkdf(dh_out, 64, salt, INFO_RATCHET);
|
|
24
|
+
return { rk: out.slice(0, 32), ck: out.slice(32, 64) };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function deriveMessageKeys(mk) {
|
|
28
|
+
const out = hkdf(mk, 80, Buffer.alloc(32, 0), INFO_MSG);
|
|
29
|
+
return {
|
|
30
|
+
encKey: out.slice(0, 32),
|
|
31
|
+
authKey: out.slice(32, 64),
|
|
32
|
+
iv: out.slice(64, 80)
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
class DoubleRatchetSession {
|
|
37
|
+
constructor() {
|
|
38
|
+
this.DHs = null; // Our DH ratchet key pair
|
|
39
|
+
this.DHr = null; // Their DH ratchet public key (raw 32 bytes)
|
|
40
|
+
this.RK = null; // Root key (32 bytes)
|
|
41
|
+
this.CKs = null; // Sending chain key
|
|
42
|
+
this.CKr = null; // Receiving chain key
|
|
43
|
+
this.Ns = 0; // Send message number
|
|
44
|
+
this.Nr = 0; // Receive message number
|
|
45
|
+
this.PN = 0; // Previous sending chain length
|
|
46
|
+
this.MKSKIPPED = new Map(); // Skipped message keys
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Initialize as sender (Alice) after X3DH
|
|
51
|
+
*/
|
|
52
|
+
initAsSender(sk, recipientRatchetPublicKey) {
|
|
53
|
+
this.DHs = generateKeyPair();
|
|
54
|
+
this.DHr = Buffer.from(recipientRatchetPublicKey);
|
|
55
|
+
const dhOut = dh(this.DHs.privateKey, this.DHr);
|
|
56
|
+
const { rk, ck } = kdfRK(sk, dhOut);
|
|
57
|
+
this.RK = rk;
|
|
58
|
+
this.CKs = ck;
|
|
59
|
+
this.CKr = null;
|
|
60
|
+
this.Ns = 0; this.Nr = 0; this.PN = 0;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Initialize as receiver (Bob) after X3DH
|
|
65
|
+
*/
|
|
66
|
+
initAsReceiver(sk, ourRatchetKeyPair) {
|
|
67
|
+
this.DHs = ourRatchetKeyPair;
|
|
68
|
+
this.DHr = null;
|
|
69
|
+
this.RK = sk;
|
|
70
|
+
this.CKs = null;
|
|
71
|
+
this.CKr = null;
|
|
72
|
+
this.Ns = 0; this.Nr = 0; this.PN = 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Encrypt a message
|
|
77
|
+
* Returns { header, ciphertext }
|
|
78
|
+
*/
|
|
79
|
+
encrypt(plaintext, aad) {
|
|
80
|
+
const { ck, mk } = kdfCK(this.CKs);
|
|
81
|
+
this.CKs = ck;
|
|
82
|
+
const header = this._buildHeader(this.DHs.publicKeyRaw, this.PN, this.Ns);
|
|
83
|
+
this.Ns++;
|
|
84
|
+
const keys = deriveMessageKeys(mk);
|
|
85
|
+
const aadFull = aad ? Buffer.concat([aad, header]) : header;
|
|
86
|
+
const ciphertext = encrypt(keys.encKey, Buffer.isBuffer(plaintext) ? plaintext : Buffer.from(plaintext, "utf8"), aadFull);
|
|
87
|
+
return { header, ciphertext };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Decrypt a message
|
|
92
|
+
* @param {Buffer} header - message header
|
|
93
|
+
* @param {Buffer} ciphertext - encrypted payload
|
|
94
|
+
* @param {Buffer} [aad] - additional auth data
|
|
95
|
+
* @returns {Buffer} decrypted plaintext
|
|
96
|
+
*/
|
|
97
|
+
decrypt(header, ciphertext, aad) {
|
|
98
|
+
const { dh_pub, pn, n } = this._parseHeader(header);
|
|
99
|
+
const aadFull = aad ? Buffer.concat([aad, header]) : header;
|
|
100
|
+
|
|
101
|
+
// Check skipped keys first
|
|
102
|
+
const skipKey = `${dh_pub.toString("hex")}:${n}`;
|
|
103
|
+
if (this.MKSKIPPED.has(skipKey)) {
|
|
104
|
+
const mk = this.MKSKIPPED.get(skipKey);
|
|
105
|
+
this.MKSKIPPED.delete(skipKey);
|
|
106
|
+
const keys = deriveMessageKeys(mk);
|
|
107
|
+
return decrypt(keys.encKey, ciphertext, aadFull);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Check if we need to ratchet
|
|
111
|
+
const needsRatchet = !this.DHr || !dh_pub.equals(this.DHr);
|
|
112
|
+
if (needsRatchet) {
|
|
113
|
+
this._skipMessageKeys(pn);
|
|
114
|
+
this._dhRatchetStep(dh_pub);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
this._skipMessageKeys(n);
|
|
118
|
+
const { ck, mk } = kdfCK(this.CKr);
|
|
119
|
+
this.CKr = ck;
|
|
120
|
+
this.Nr++;
|
|
121
|
+
|
|
122
|
+
const keys = deriveMessageKeys(mk);
|
|
123
|
+
return decrypt(keys.encKey, ciphertext, aadFull);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
_skipMessageKeys(until) {
|
|
127
|
+
if (this.Nr + MAX_SKIP < until) throw new Error("Too many skipped messages: " + until);
|
|
128
|
+
if (this.CKr) {
|
|
129
|
+
while (this.Nr < until) {
|
|
130
|
+
const { ck, mk } = kdfCK(this.CKr);
|
|
131
|
+
this.CKr = ck;
|
|
132
|
+
const key = `${this.DHr.toString("hex")}:${this.Nr}`;
|
|
133
|
+
this.MKSKIPPED.set(key, mk);
|
|
134
|
+
this.Nr++;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
_dhRatchetStep(remoteDhPub) {
|
|
140
|
+
this.PN = this.Ns;
|
|
141
|
+
this.Ns = 0;
|
|
142
|
+
this.Nr = 0;
|
|
143
|
+
this.DHr = remoteDhPub;
|
|
144
|
+
|
|
145
|
+
// Receive ratchet
|
|
146
|
+
const dhOut1 = dh(this.DHs.privateKey, this.DHr);
|
|
147
|
+
const { rk: rk1, ck: ckr } = kdfRK(this.RK, dhOut1);
|
|
148
|
+
this.RK = rk1;
|
|
149
|
+
this.CKr = ckr;
|
|
150
|
+
|
|
151
|
+
// Send ratchet
|
|
152
|
+
this.DHs = generateKeyPair();
|
|
153
|
+
const dhOut2 = dh(this.DHs.privateKey, this.DHr);
|
|
154
|
+
const { rk: rk2, ck: cks } = kdfRK(this.RK, dhOut2);
|
|
155
|
+
this.RK = rk2;
|
|
156
|
+
this.CKs = cks;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
_buildHeader(dhPub, pn, n) {
|
|
160
|
+
const buf = Buffer.allocUnsafe(32 + 4 + 4);
|
|
161
|
+
Buffer.from(dhPub).copy(buf, 0);
|
|
162
|
+
buf.writeUInt32BE(pn, 32);
|
|
163
|
+
buf.writeUInt32BE(n, 36);
|
|
164
|
+
return buf;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
_parseHeader(header) {
|
|
168
|
+
if (header.length < 40) throw new Error("Invalid ratchet header length");
|
|
169
|
+
return {
|
|
170
|
+
dh_pub: header.slice(0, 32),
|
|
171
|
+
pn: header.readUInt32BE(32),
|
|
172
|
+
n: header.readUInt32BE(36)
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Serialize session state for persistence
|
|
178
|
+
*/
|
|
179
|
+
serialize() {
|
|
180
|
+
return JSON.stringify({
|
|
181
|
+
DHs_priv: this.DHs ? this.DHs.privateKey.toString("base64") : null,
|
|
182
|
+
DHs_pub: this.DHs ? this.DHs.publicKeyRaw.toString("base64") : null,
|
|
183
|
+
DHr: this.DHr ? this.DHr.toString("base64") : null,
|
|
184
|
+
RK: this.RK ? this.RK.toString("base64") : null,
|
|
185
|
+
CKs: this.CKs ? this.CKs.toString("base64") : null,
|
|
186
|
+
CKr: this.CKr ? this.CKr.toString("base64") : null,
|
|
187
|
+
Ns: this.Ns,
|
|
188
|
+
Nr: this.Nr,
|
|
189
|
+
PN: this.PN,
|
|
190
|
+
MKSKIPPED: Array.from(this.MKSKIPPED.entries()).map(([k, v]) => [k, v.toString("base64")])
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Restore session state from serialized string
|
|
196
|
+
*/
|
|
197
|
+
static deserialize(json) {
|
|
198
|
+
const d = JSON.parse(json);
|
|
199
|
+
const session = new DoubleRatchetSession();
|
|
200
|
+
if (d.DHs_priv) {
|
|
201
|
+
session.DHs = {
|
|
202
|
+
privateKey: Buffer.from(d.DHs_priv, "base64"),
|
|
203
|
+
publicKey: null,
|
|
204
|
+
publicKeyRaw: Buffer.from(d.DHs_pub, "base64")
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
session.DHr = d.DHr ? Buffer.from(d.DHr, "base64") : null;
|
|
208
|
+
session.RK = d.RK ? Buffer.from(d.RK, "base64") : null;
|
|
209
|
+
session.CKs = d.CKs ? Buffer.from(d.CKs, "base64") : null;
|
|
210
|
+
session.CKr = d.CKr ? Buffer.from(d.CKr, "base64") : null;
|
|
211
|
+
session.Ns = d.Ns || 0;
|
|
212
|
+
session.Nr = d.Nr || 0;
|
|
213
|
+
session.PN = d.PN || 0;
|
|
214
|
+
session.MKSKIPPED = new Map((d.MKSKIPPED || []).map(([k, v]) => [k, Buffer.from(v, "base64")]));
|
|
215
|
+
return session;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
module.exports = { DoubleRatchetSession, kdfCK, kdfRK, deriveMessageKeys };
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* E2EE Device/Session Store
|
|
5
|
+
* Persists key material to a JSON file (encrypted with a device secret).
|
|
6
|
+
* Each E2EE session (thread) is stored separately.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require("fs");
|
|
10
|
+
const path = require("path");
|
|
11
|
+
const crypto = require("crypto");
|
|
12
|
+
|
|
13
|
+
class E2EEStore {
|
|
14
|
+
constructor(storePath, userID) {
|
|
15
|
+
this.storePath = storePath;
|
|
16
|
+
this.userID = userID;
|
|
17
|
+
this.data = {
|
|
18
|
+
identityKey: null,
|
|
19
|
+
signedPreKey: null,
|
|
20
|
+
oneTimePreKeys: [],
|
|
21
|
+
sessions: {}, // threadID -> serialized DoubleRatchetSession
|
|
22
|
+
deviceSecret: null // 32-byte random secret for local encryption
|
|
23
|
+
};
|
|
24
|
+
this._loaded = false;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
_storeFile() {
|
|
28
|
+
return path.join(this.storePath, `shadowx_e2ee_${this.userID}.json`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
_salt() {
|
|
32
|
+
return Buffer.from(this.userID.padEnd(16, "0").slice(0, 16), "utf8");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
_localEncrypt(data) {
|
|
36
|
+
if (!this.data.deviceSecret) return data;
|
|
37
|
+
const key = Buffer.from(this.data.deviceSecret, "base64");
|
|
38
|
+
if (key.length !== 32) return data;
|
|
39
|
+
const iv = crypto.randomBytes(12);
|
|
40
|
+
const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
|
|
41
|
+
const enc = Buffer.concat([cipher.update(Buffer.from(data, "utf8")), cipher.final()]);
|
|
42
|
+
const tag = cipher.getAuthTag();
|
|
43
|
+
return Buffer.concat([iv, tag, enc]).toString("base64");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
_localDecrypt(data) {
|
|
47
|
+
if (!this.data.deviceSecret) return data;
|
|
48
|
+
try {
|
|
49
|
+
const key = Buffer.from(this.data.deviceSecret, "base64");
|
|
50
|
+
if (key.length !== 32) return data;
|
|
51
|
+
const buf = Buffer.from(data, "base64");
|
|
52
|
+
const iv = buf.slice(0, 12);
|
|
53
|
+
const tag = buf.slice(12, 28);
|
|
54
|
+
const enc = buf.slice(28);
|
|
55
|
+
const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
|
|
56
|
+
decipher.setAuthTag(tag);
|
|
57
|
+
return Buffer.concat([decipher.update(enc), decipher.final()]).toString("utf8");
|
|
58
|
+
} catch (_) {
|
|
59
|
+
return data;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
load() {
|
|
64
|
+
const file = this._storeFile();
|
|
65
|
+
if (!fs.existsSync(file)) return false;
|
|
66
|
+
try {
|
|
67
|
+
const raw = fs.readFileSync(file, "utf8");
|
|
68
|
+
const parsed = JSON.parse(raw);
|
|
69
|
+
if (parsed.deviceSecret) this.data.deviceSecret = parsed.deviceSecret;
|
|
70
|
+
const decrypted = this._localDecrypt(parsed.payload || "{}");
|
|
71
|
+
const payload = JSON.parse(decrypted);
|
|
72
|
+
this.data.identityKey = payload.identityKey || null;
|
|
73
|
+
this.data.signedPreKey = payload.signedPreKey || null;
|
|
74
|
+
this.data.oneTimePreKeys = payload.oneTimePreKeys || [];
|
|
75
|
+
this.data.sessions = payload.sessions || {};
|
|
76
|
+
this._loaded = true;
|
|
77
|
+
return true;
|
|
78
|
+
} catch (_) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
save() {
|
|
84
|
+
if (!fs.existsSync(this.storePath)) {
|
|
85
|
+
fs.mkdirSync(this.storePath, { recursive: true });
|
|
86
|
+
}
|
|
87
|
+
if (!this.data.deviceSecret) {
|
|
88
|
+
this.data.deviceSecret = crypto.randomBytes(32).toString("base64");
|
|
89
|
+
}
|
|
90
|
+
const payload = JSON.stringify({
|
|
91
|
+
identityKey: this.data.identityKey,
|
|
92
|
+
signedPreKey: this.data.signedPreKey,
|
|
93
|
+
oneTimePreKeys: this.data.oneTimePreKeys,
|
|
94
|
+
sessions: this.data.sessions
|
|
95
|
+
});
|
|
96
|
+
const encrypted = this._localEncrypt(payload);
|
|
97
|
+
fs.writeFileSync(this._storeFile(), JSON.stringify({ deviceSecret: this.data.deviceSecret, payload: encrypted }, null, 2), "utf8");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
hasIdentityKey() { return !!this.data.identityKey; }
|
|
101
|
+
|
|
102
|
+
getIdentityKey() {
|
|
103
|
+
const ik = this.data.identityKey;
|
|
104
|
+
if (!ik) return null;
|
|
105
|
+
return {
|
|
106
|
+
privateKey: Buffer.from(ik.privateKey, "base64"),
|
|
107
|
+
publicKey: Buffer.from(ik.publicKey, "base64"),
|
|
108
|
+
publicKeyRaw: Buffer.from(ik.publicKeyRaw, "base64")
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
setIdentityKey(keyPair) {
|
|
113
|
+
this.data.identityKey = {
|
|
114
|
+
privateKey: keyPair.privateKey.toString("base64"),
|
|
115
|
+
publicKey: keyPair.publicKey.toString("base64"),
|
|
116
|
+
publicKeyRaw: keyPair.publicKeyRaw.toString("base64")
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
getSignedPreKey() {
|
|
121
|
+
const spk = this.data.signedPreKey;
|
|
122
|
+
if (!spk) return null;
|
|
123
|
+
return {
|
|
124
|
+
privateKey: Buffer.from(spk.privateKey, "base64"),
|
|
125
|
+
publicKey: Buffer.from(spk.publicKey, "base64"),
|
|
126
|
+
publicKeyRaw: Buffer.from(spk.publicKeyRaw, "base64"),
|
|
127
|
+
signature: Buffer.from(spk.signature, "base64"),
|
|
128
|
+
keyId: spk.keyId
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
setSignedPreKey(keyPair) {
|
|
133
|
+
this.data.signedPreKey = {
|
|
134
|
+
privateKey: keyPair.privateKey.toString("base64"),
|
|
135
|
+
publicKey: keyPair.publicKey.toString("base64"),
|
|
136
|
+
publicKeyRaw: keyPair.publicKeyRaw.toString("base64"),
|
|
137
|
+
signature: keyPair.signature ? keyPair.signature.toString("base64") : "",
|
|
138
|
+
keyId: keyPair.keyId || 1
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
getOneTimePreKeys() {
|
|
143
|
+
return (this.data.oneTimePreKeys || []).map(k => ({
|
|
144
|
+
privateKey: Buffer.from(k.privateKey, "base64"),
|
|
145
|
+
publicKey: Buffer.from(k.publicKey, "base64"),
|
|
146
|
+
publicKeyRaw: Buffer.from(k.publicKeyRaw, "base64"),
|
|
147
|
+
keyId: k.keyId
|
|
148
|
+
}));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
addOneTimePreKey(keyPair) {
|
|
152
|
+
this.data.oneTimePreKeys = this.data.oneTimePreKeys || [];
|
|
153
|
+
this.data.oneTimePreKeys.push({
|
|
154
|
+
privateKey: keyPair.privateKey.toString("base64"),
|
|
155
|
+
publicKey: keyPair.publicKey.toString("base64"),
|
|
156
|
+
publicKeyRaw: keyPair.publicKeyRaw.toString("base64"),
|
|
157
|
+
keyId: keyPair.keyId
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
consumeOneTimePreKey(keyId) {
|
|
162
|
+
const idx = (this.data.oneTimePreKeys || []).findIndex(k => k.keyId === keyId);
|
|
163
|
+
if (idx < 0) return null;
|
|
164
|
+
const k = this.data.oneTimePreKeys.splice(idx, 1)[0];
|
|
165
|
+
return {
|
|
166
|
+
privateKey: Buffer.from(k.privateKey, "base64"),
|
|
167
|
+
publicKey: Buffer.from(k.publicKey, "base64"),
|
|
168
|
+
publicKeyRaw: Buffer.from(k.publicKeyRaw, "base64"),
|
|
169
|
+
keyId: k.keyId
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
hasSession(threadID) { return !!this.data.sessions[threadID]; }
|
|
174
|
+
|
|
175
|
+
getSession(threadID) { return this.data.sessions[threadID] || null; }
|
|
176
|
+
|
|
177
|
+
setSession(threadID, serializedSession) { this.data.sessions[threadID] = serializedSession; }
|
|
178
|
+
|
|
179
|
+
deleteSession(threadID) { delete this.data.sessions[threadID]; }
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
module.exports = { E2EEStore };
|
package/src/e2ee.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var utils = require("../utils");
|
|
4
|
+
|
|
5
|
+
module.exports = function (defaultFuncs, api, ctx) {
|
|
6
|
+
return function editMessage(text, messageID, callback) {
|
|
7
|
+
var { promise, callback: cb } = utils.wrapCallback(callback);
|
|
8
|
+
|
|
9
|
+
var mqttClient = ctx.mqttClient || global.mqttClient;
|
|
10
|
+
|
|
11
|
+
if (mqttClient) {
|
|
12
|
+
ctx.wsReqNumber += 1;
|
|
13
|
+
ctx.wsTaskNumber += 1;
|
|
14
|
+
|
|
15
|
+
var queryPayload = {
|
|
16
|
+
message_id: messageID,
|
|
17
|
+
text: text
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
var query = {
|
|
21
|
+
failure_count: null,
|
|
22
|
+
label: "742",
|
|
23
|
+
payload: JSON.stringify(queryPayload),
|
|
24
|
+
queue_name: "edit_message",
|
|
25
|
+
task_id: ctx.wsTaskNumber
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
var context = {
|
|
29
|
+
app_id: "2220391788200892",
|
|
30
|
+
payload: JSON.stringify({
|
|
31
|
+
data_trace_id: null,
|
|
32
|
+
epoch_id: String((BigInt(Date.now()) << 22n)),
|
|
33
|
+
tasks: [query],
|
|
34
|
+
version_id: "6903494529735864"
|
|
35
|
+
}),
|
|
36
|
+
request_id: ctx.wsReqNumber,
|
|
37
|
+
type: 3
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
mqttClient.publish("/ls_req", JSON.stringify(context), { qos: 1, retain: false }, function (err) {
|
|
41
|
+
if (err) return cb(err);
|
|
42
|
+
cb(null, true);
|
|
43
|
+
});
|
|
44
|
+
} else {
|
|
45
|
+
defaultFuncs.post("https://www.facebook.com/messaging/edit_message/", ctx.jar, {
|
|
46
|
+
message_id: messageID,
|
|
47
|
+
body: text
|
|
48
|
+
})
|
|
49
|
+
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
|
50
|
+
.then(function (resData) { if (resData.error) throw resData; cb(null, true); })
|
|
51
|
+
.catch(function (err) { cb(err); });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return promise;
|
|
55
|
+
};
|
|
56
|
+
};
|