liangzimixin 0.3.46 → 0.3.47
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/dist/index.cjs +157 -19
- package/dist/index.d.cts +19 -0
- package/dist/setup-entry.cjs +157 -19
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -2233,7 +2233,7 @@ var require_websocket = __commonJS({
|
|
|
2233
2233
|
var http = require("http");
|
|
2234
2234
|
var net = require("net");
|
|
2235
2235
|
var tls = require("tls");
|
|
2236
|
-
var { randomBytes:
|
|
2236
|
+
var { randomBytes: randomBytes4, createHash: createHash3 } = require("crypto");
|
|
2237
2237
|
var { Duplex, Readable } = require("stream");
|
|
2238
2238
|
var { URL: URL2 } = require("url");
|
|
2239
2239
|
var PerMessageDeflate2 = require_permessage_deflate();
|
|
@@ -2763,7 +2763,7 @@ var require_websocket = __commonJS({
|
|
|
2763
2763
|
}
|
|
2764
2764
|
}
|
|
2765
2765
|
const defaultPort = isSecure ? 443 : 80;
|
|
2766
|
-
const key =
|
|
2766
|
+
const key = randomBytes4(16).toString("base64");
|
|
2767
2767
|
const request = isSecure ? https.request : http.request;
|
|
2768
2768
|
const protocolSet = /* @__PURE__ */ new Set();
|
|
2769
2769
|
let perMessageDeflate;
|
|
@@ -2893,7 +2893,7 @@ var require_websocket = __commonJS({
|
|
|
2893
2893
|
abortHandshake(websocket, socket, "Invalid Upgrade header");
|
|
2894
2894
|
return;
|
|
2895
2895
|
}
|
|
2896
|
-
const digest =
|
|
2896
|
+
const digest = createHash3("sha1").update(key + GUID).digest("base64");
|
|
2897
2897
|
if (res.headers["sec-websocket-accept"] !== digest) {
|
|
2898
2898
|
abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
|
|
2899
2899
|
return;
|
|
@@ -3260,7 +3260,7 @@ var require_websocket_server = __commonJS({
|
|
|
3260
3260
|
var EventEmitter2 = require("events");
|
|
3261
3261
|
var http = require("http");
|
|
3262
3262
|
var { Duplex } = require("stream");
|
|
3263
|
-
var { createHash:
|
|
3263
|
+
var { createHash: createHash3 } = require("crypto");
|
|
3264
3264
|
var extension2 = require_extension();
|
|
3265
3265
|
var PerMessageDeflate2 = require_permessage_deflate();
|
|
3266
3266
|
var subprotocol2 = require_subprotocol();
|
|
@@ -3561,7 +3561,7 @@ var require_websocket_server = __commonJS({
|
|
|
3561
3561
|
);
|
|
3562
3562
|
}
|
|
3563
3563
|
if (this._state > RUNNING) return abortHandshake(socket, 503);
|
|
3564
|
-
const digest =
|
|
3564
|
+
const digest = createHash3("sha1").update(key + GUID).digest("base64");
|
|
3565
3565
|
const headers = [
|
|
3566
3566
|
"HTTP/1.1 101 Switching Protocols",
|
|
3567
3567
|
"Upgrade: websocket",
|
|
@@ -17836,8 +17836,8 @@ async function uploadMedia(params) {
|
|
|
17836
17836
|
const { readFile: readFile3 } = await import("fs/promises");
|
|
17837
17837
|
buffer = await readFile3(file2);
|
|
17838
17838
|
}
|
|
17839
|
-
const { createHash:
|
|
17840
|
-
const fileHash =
|
|
17839
|
+
const { createHash: createHash3 } = await import("crypto");
|
|
17840
|
+
const fileHash = createHash3("md5").update(buffer).digest("hex");
|
|
17841
17841
|
log2.debug("upload:fileHash", { fileName, fileHash });
|
|
17842
17842
|
const maxBytes = maxFileSizeMb * 1024 * 1024;
|
|
17843
17843
|
if (buffer.length > maxBytes) {
|
|
@@ -18017,6 +18017,65 @@ function inferMimeType(fileName) {
|
|
|
18017
18017
|
const ext = path.extname(fileName).toLowerCase();
|
|
18018
18018
|
return MIME_MAP[ext] || "application/octet-stream";
|
|
18019
18019
|
}
|
|
18020
|
+
function getImageDimensions(buffer) {
|
|
18021
|
+
try {
|
|
18022
|
+
if (buffer.length < 24) return void 0;
|
|
18023
|
+
if (buffer[0] === 137 && buffer[1] === 80 && buffer[2] === 78 && buffer[3] === 71) {
|
|
18024
|
+
return {
|
|
18025
|
+
width: buffer.readUInt32BE(16),
|
|
18026
|
+
height: buffer.readUInt32BE(20)
|
|
18027
|
+
};
|
|
18028
|
+
}
|
|
18029
|
+
if (buffer[0] === 71 && buffer[1] === 73 && buffer[2] === 70) {
|
|
18030
|
+
return {
|
|
18031
|
+
width: buffer.readUInt16LE(6),
|
|
18032
|
+
height: buffer.readUInt16LE(8)
|
|
18033
|
+
};
|
|
18034
|
+
}
|
|
18035
|
+
if (buffer[0] === 66 && buffer[1] === 77 && buffer.length >= 26) {
|
|
18036
|
+
return {
|
|
18037
|
+
width: buffer.readInt32LE(18),
|
|
18038
|
+
height: Math.abs(buffer.readInt32LE(22))
|
|
18039
|
+
};
|
|
18040
|
+
}
|
|
18041
|
+
if (buffer[0] === 82 && buffer[1] === 73 && buffer[8] === 87 && buffer[9] === 69) {
|
|
18042
|
+
if (buffer[12] === 86 && buffer[13] === 80 && buffer[14] === 56 && buffer[15] === 32) {
|
|
18043
|
+
if (buffer.length >= 30) {
|
|
18044
|
+
return {
|
|
18045
|
+
width: buffer.readUInt16LE(26) & 16383,
|
|
18046
|
+
height: buffer.readUInt16LE(28) & 16383
|
|
18047
|
+
};
|
|
18048
|
+
}
|
|
18049
|
+
}
|
|
18050
|
+
if (buffer[12] === 86 && buffer[13] === 80 && buffer[14] === 56 && buffer[15] === 76) {
|
|
18051
|
+
if (buffer.length >= 25) {
|
|
18052
|
+
const bits = buffer.readUInt32LE(21);
|
|
18053
|
+
return {
|
|
18054
|
+
width: (bits & 16383) + 1,
|
|
18055
|
+
height: (bits >> 14 & 16383) + 1
|
|
18056
|
+
};
|
|
18057
|
+
}
|
|
18058
|
+
}
|
|
18059
|
+
}
|
|
18060
|
+
if (buffer[0] === 255 && buffer[1] === 216) {
|
|
18061
|
+
let offset = 2;
|
|
18062
|
+
while (offset < buffer.length - 8) {
|
|
18063
|
+
if (buffer[offset] !== 255) break;
|
|
18064
|
+
const marker = buffer[offset + 1];
|
|
18065
|
+
if (marker >= 192 && marker <= 195 || marker >= 197 && marker <= 199 || marker >= 201 && marker <= 203 || marker >= 205 && marker <= 207) {
|
|
18066
|
+
return {
|
|
18067
|
+
height: buffer.readUInt16BE(offset + 5),
|
|
18068
|
+
width: buffer.readUInt16BE(offset + 7)
|
|
18069
|
+
};
|
|
18070
|
+
}
|
|
18071
|
+
const segLen = buffer.readUInt16BE(offset + 2);
|
|
18072
|
+
offset += 2 + segLen;
|
|
18073
|
+
}
|
|
18074
|
+
}
|
|
18075
|
+
} catch {
|
|
18076
|
+
}
|
|
18077
|
+
return void 0;
|
|
18078
|
+
}
|
|
18020
18079
|
async function resolveAndUploadMedia(params) {
|
|
18021
18080
|
const {
|
|
18022
18081
|
mediaUrl,
|
|
@@ -18067,7 +18126,26 @@ async function resolveAndUploadMedia(params) {
|
|
|
18067
18126
|
}
|
|
18068
18127
|
const fileType = detectFileType(fileName);
|
|
18069
18128
|
const mimeType = inferMimeType(fileName);
|
|
18070
|
-
|
|
18129
|
+
const ext = path.extname(fileName).replace(".", "").toLowerCase();
|
|
18130
|
+
let imageDimensions;
|
|
18131
|
+
if (fileType === "image") {
|
|
18132
|
+
imageDimensions = getImageDimensions(buffer);
|
|
18133
|
+
log3.info("media:imageDimensions", { fileName, ...imageDimensions });
|
|
18134
|
+
}
|
|
18135
|
+
log3.info("media:fileType", { fileName, fileType, mimeType, ext });
|
|
18136
|
+
let encryptionMeta;
|
|
18137
|
+
if (!params.skipEncrypt) {
|
|
18138
|
+
try {
|
|
18139
|
+
const originalSize = buffer.length;
|
|
18140
|
+
const result = await messagePipe.encryptFile(buffer);
|
|
18141
|
+
buffer = result.encryptedBuffer;
|
|
18142
|
+
encryptionMeta = { keyId: result.keyId, iv: result.iv };
|
|
18143
|
+
log3.info("media:encrypted", { fileName, originalSize, encryptedSize: buffer.length });
|
|
18144
|
+
} catch (err) {
|
|
18145
|
+
log3.error("media:encrypt failed", { fileName, error: err.message });
|
|
18146
|
+
throw err;
|
|
18147
|
+
}
|
|
18148
|
+
}
|
|
18071
18149
|
let uploadResult;
|
|
18072
18150
|
try {
|
|
18073
18151
|
uploadResult = await uploadMedia({
|
|
@@ -18092,11 +18170,17 @@ async function resolveAndUploadMedia(params) {
|
|
|
18092
18170
|
const msgType = fileType;
|
|
18093
18171
|
let contentPayload;
|
|
18094
18172
|
if (fileType === "file") {
|
|
18095
|
-
contentPayload = { fileId: uploadResult.fileKey, fileName, size: uploadResult.fileSize,
|
|
18173
|
+
contentPayload = { fileId: uploadResult.fileKey, fileName, size: uploadResult.fileSize, ext };
|
|
18096
18174
|
} else if (fileType === "image") {
|
|
18097
|
-
contentPayload = {
|
|
18175
|
+
contentPayload = {
|
|
18176
|
+
fileId: uploadResult.fileKey,
|
|
18177
|
+
width: imageDimensions?.width ?? 0,
|
|
18178
|
+
height: imageDimensions?.height ?? 0,
|
|
18179
|
+
altText: fileName,
|
|
18180
|
+
ext
|
|
18181
|
+
};
|
|
18098
18182
|
} else {
|
|
18099
|
-
contentPayload = { fileId: uploadResult.fileKey,
|
|
18183
|
+
contentPayload = { fileId: uploadResult.fileKey, ext };
|
|
18100
18184
|
}
|
|
18101
18185
|
await messagePipe.sendMessage({
|
|
18102
18186
|
chatId,
|
|
@@ -18104,12 +18188,15 @@ async function resolveAndUploadMedia(params) {
|
|
|
18104
18188
|
// 私聊场景: chatId 即为用户 ID
|
|
18105
18189
|
msgType,
|
|
18106
18190
|
content: JSON.stringify(contentPayload),
|
|
18107
|
-
skipEncrypt: params.skipEncrypt
|
|
18191
|
+
skipEncrypt: params.skipEncrypt,
|
|
18192
|
+
encryptionMeta
|
|
18193
|
+
// 文件加密的 keyId/iv → sendMessage 用于构建 extra
|
|
18108
18194
|
});
|
|
18109
18195
|
log3.info("media:sent", {
|
|
18110
18196
|
chatId,
|
|
18111
18197
|
fileId: uploadResult.fileKey,
|
|
18112
|
-
msgType
|
|
18198
|
+
msgType,
|
|
18199
|
+
encrypted: Boolean(encryptionMeta)
|
|
18113
18200
|
});
|
|
18114
18201
|
return {
|
|
18115
18202
|
channel: CHANNEL_ID,
|
|
@@ -20097,6 +20184,49 @@ var MessagePipe = class _MessagePipe {
|
|
|
20097
20184
|
async injectRawFrame(rawData) {
|
|
20098
20185
|
return this.handleInbound(rawData);
|
|
20099
20186
|
}
|
|
20187
|
+
/**
|
|
20188
|
+
* 加密文件 Buffer — 按 10MB 分片加密,返回加密后的 Buffer + 密钥信息。
|
|
20189
|
+
* 用于文件上传前的加密处理。
|
|
20190
|
+
*
|
|
20191
|
+
* 如果 CryptoEngine 为透传模式,直接返回原始 buffer。
|
|
20192
|
+
*
|
|
20193
|
+
* @param buffer - 原始文件 Buffer
|
|
20194
|
+
* @returns { encryptedBuffer, keyId, iv } — 加密后的 buffer + 密钥标识 + 初始化向量
|
|
20195
|
+
*/
|
|
20196
|
+
async encryptFile(buffer) {
|
|
20197
|
+
const iv = (0, import_node_crypto3.randomBytes)(8).toString("hex");
|
|
20198
|
+
const chunkSize = 10 * 1024 * 1024;
|
|
20199
|
+
const totalChunks = Math.ceil(buffer.length / chunkSize);
|
|
20200
|
+
let keyId = "";
|
|
20201
|
+
let sessionKey = "";
|
|
20202
|
+
let fillKey = "";
|
|
20203
|
+
const encryptedChunks = [];
|
|
20204
|
+
for (let i = 0; i < totalChunks; i++) {
|
|
20205
|
+
const start = i * chunkSize;
|
|
20206
|
+
const end = Math.min(start + chunkSize, buffer.length);
|
|
20207
|
+
const chunk = buffer.subarray(start, end);
|
|
20208
|
+
const options = keyId ? { keyId, sessionKey, fillKey } : void 0;
|
|
20209
|
+
const result = await this.crypto.encryptFileChunk(chunk, iv, options);
|
|
20210
|
+
encryptedChunks.push(result.fileBuffer);
|
|
20211
|
+
keyId = result.keyId;
|
|
20212
|
+
sessionKey = result.sessionKey;
|
|
20213
|
+
fillKey = result.fillKey;
|
|
20214
|
+
log21.debug("\u{1F512} \u6587\u4EF6\u5206\u7247\u52A0\u5BC6", {
|
|
20215
|
+
chunk: `${i + 1}/${totalChunks}`,
|
|
20216
|
+
originalSize: chunk.length,
|
|
20217
|
+
encryptedSize: result.fileBuffer.length,
|
|
20218
|
+
keyId
|
|
20219
|
+
});
|
|
20220
|
+
}
|
|
20221
|
+
const encryptedBuffer = Buffer.concat(encryptedChunks);
|
|
20222
|
+
log21.info("\u{1F512} \u6587\u4EF6\u52A0\u5BC6\u5B8C\u6210", {
|
|
20223
|
+
originalSize: buffer.length,
|
|
20224
|
+
encryptedSize: encryptedBuffer.length,
|
|
20225
|
+
totalChunks,
|
|
20226
|
+
keyId
|
|
20227
|
+
});
|
|
20228
|
+
return { encryptedBuffer, keyId, iv };
|
|
20229
|
+
}
|
|
20100
20230
|
/** 文件类消息类型集合 — 这些类型的加密消息保留原始 content(文件元数据) */
|
|
20101
20231
|
static FILE_MSG_TYPES = /* @__PURE__ */ new Set(["image", "file", "voice", "video"]);
|
|
20102
20232
|
/**
|
|
@@ -20144,11 +20274,15 @@ var MessagePipe = class _MessagePipe {
|
|
|
20144
20274
|
const isFileMessage = _MessagePipe.FILE_MSG_TYPES.has(msg.msgType);
|
|
20145
20275
|
if (!msg.skipEncrypt && this.quantumAccount) {
|
|
20146
20276
|
if (this.encryptionMode === "quantum_only") {
|
|
20147
|
-
|
|
20148
|
-
|
|
20277
|
+
if (isFileMessage && msg.encryptionMeta) {
|
|
20278
|
+
extra = _MessagePipe.buildFileEncryptExtra(msg.encryptionMeta.iv, msg.encryptionMeta.keyId);
|
|
20279
|
+
log21.debug("\u{1F512} \u51FA\u7AD9\u6587\u4EF6\u6D88\u606F\u5DF2\u6807\u8BB0\u52A0\u5BC6 (quantum_only)", { sessionId: msg.encryptionMeta.keyId, msgType: msg.msgType });
|
|
20280
|
+
} else if (isFileMessage) {
|
|
20281
|
+
const { keyId, iv } = await this.crypto.encrypt(msg.content);
|
|
20149
20282
|
extra = _MessagePipe.buildFileEncryptExtra(iv, keyId);
|
|
20150
|
-
log21.debug("\u{1F512} \u51FA\u7AD9\u6587\u4EF6\u6D88\u606F\u5DF2\u6807\u8BB0\u52A0\u5BC6 (quantum_only)", { sessionId: keyId, msgType: msg.msgType });
|
|
20283
|
+
log21.debug("\u{1F512} \u51FA\u7AD9\u6587\u4EF6\u6D88\u606F\u5DF2\u6807\u8BB0\u52A0\u5BC6 (quantum_only, fallback)", { sessionId: keyId, msgType: msg.msgType });
|
|
20151
20284
|
} else {
|
|
20285
|
+
const { ciphertext, keyId, iv } = await this.crypto.encrypt(msg.content);
|
|
20152
20286
|
extra = JSON.stringify({
|
|
20153
20287
|
cryptoIv: iv,
|
|
20154
20288
|
encryptMsg: ciphertext,
|
|
@@ -20159,11 +20293,15 @@ var MessagePipe = class _MessagePipe {
|
|
|
20159
20293
|
}
|
|
20160
20294
|
} else {
|
|
20161
20295
|
try {
|
|
20162
|
-
|
|
20163
|
-
|
|
20296
|
+
if (isFileMessage && msg.encryptionMeta) {
|
|
20297
|
+
extra = _MessagePipe.buildFileEncryptExtra(msg.encryptionMeta.iv, msg.encryptionMeta.keyId);
|
|
20298
|
+
log21.debug("\u{1F512} \u51FA\u7AD9\u6587\u4EF6\u6D88\u606F\u5DF2\u6807\u8BB0\u52A0\u5BC6 (quantum_and_plain)", { sessionId: msg.encryptionMeta.keyId, msgType: msg.msgType });
|
|
20299
|
+
} else if (isFileMessage) {
|
|
20300
|
+
const { keyId, iv } = await this.crypto.encrypt(msg.content);
|
|
20164
20301
|
extra = _MessagePipe.buildFileEncryptExtra(iv, keyId);
|
|
20165
|
-
log21.debug("\u{1F512} \u51FA\u7AD9\u6587\u4EF6\u6D88\u606F\u5DF2\u6807\u8BB0\u52A0\u5BC6 (quantum_and_plain)", { sessionId: keyId, msgType: msg.msgType });
|
|
20302
|
+
log21.debug("\u{1F512} \u51FA\u7AD9\u6587\u4EF6\u6D88\u606F\u5DF2\u6807\u8BB0\u52A0\u5BC6 (quantum_and_plain, fallback)", { sessionId: keyId, msgType: msg.msgType });
|
|
20166
20303
|
} else {
|
|
20304
|
+
const { ciphertext, keyId, iv } = await this.crypto.encrypt(msg.content);
|
|
20167
20305
|
extra = JSON.stringify({
|
|
20168
20306
|
cryptoIv: iv,
|
|
20169
20307
|
encryptMsg: ciphertext,
|
package/dist/index.d.cts
CHANGED
|
@@ -191,6 +191,11 @@ interface OutboundMessage {
|
|
|
191
191
|
replyToMessageId?: string;
|
|
192
192
|
/** 跳过出站加密 — 用于"思考中"等无需加密的系统提示消息 */
|
|
193
193
|
skipEncrypt?: boolean;
|
|
194
|
+
/** 文件加密元数据 — 文件已在上传前加密时传入,sendMessage 直接使用此 keyId/iv 构建 extra */
|
|
195
|
+
encryptionMeta?: {
|
|
196
|
+
keyId: string;
|
|
197
|
+
iv: string;
|
|
198
|
+
};
|
|
194
199
|
}
|
|
195
200
|
/**
|
|
196
201
|
* 加密策略枚举 — 根据消息类型选择不同的加密等级
|
|
@@ -786,6 +791,20 @@ declare class MessagePipe {
|
|
|
786
791
|
* TODO: 验证通过后移除此方法
|
|
787
792
|
*/
|
|
788
793
|
injectRawFrame(rawData: string | Buffer): Promise<void>;
|
|
794
|
+
/**
|
|
795
|
+
* 加密文件 Buffer — 按 10MB 分片加密,返回加密后的 Buffer + 密钥信息。
|
|
796
|
+
* 用于文件上传前的加密处理。
|
|
797
|
+
*
|
|
798
|
+
* 如果 CryptoEngine 为透传模式,直接返回原始 buffer。
|
|
799
|
+
*
|
|
800
|
+
* @param buffer - 原始文件 Buffer
|
|
801
|
+
* @returns { encryptedBuffer, keyId, iv } — 加密后的 buffer + 密钥标识 + 初始化向量
|
|
802
|
+
*/
|
|
803
|
+
encryptFile(buffer: Buffer): Promise<{
|
|
804
|
+
encryptedBuffer: Buffer;
|
|
805
|
+
keyId: string;
|
|
806
|
+
iv: string;
|
|
807
|
+
}>;
|
|
789
808
|
/** 文件类消息类型集合 — 这些类型的加密消息保留原始 content(文件元数据) */
|
|
790
809
|
private static readonly FILE_MSG_TYPES;
|
|
791
810
|
/**
|
package/dist/setup-entry.cjs
CHANGED
|
@@ -2233,7 +2233,7 @@ var require_websocket = __commonJS({
|
|
|
2233
2233
|
var http = require("http");
|
|
2234
2234
|
var net = require("net");
|
|
2235
2235
|
var tls = require("tls");
|
|
2236
|
-
var { randomBytes:
|
|
2236
|
+
var { randomBytes: randomBytes4, createHash: createHash3 } = require("crypto");
|
|
2237
2237
|
var { Duplex, Readable } = require("stream");
|
|
2238
2238
|
var { URL: URL2 } = require("url");
|
|
2239
2239
|
var PerMessageDeflate2 = require_permessage_deflate();
|
|
@@ -2763,7 +2763,7 @@ var require_websocket = __commonJS({
|
|
|
2763
2763
|
}
|
|
2764
2764
|
}
|
|
2765
2765
|
const defaultPort = isSecure ? 443 : 80;
|
|
2766
|
-
const key =
|
|
2766
|
+
const key = randomBytes4(16).toString("base64");
|
|
2767
2767
|
const request = isSecure ? https.request : http.request;
|
|
2768
2768
|
const protocolSet = /* @__PURE__ */ new Set();
|
|
2769
2769
|
let perMessageDeflate;
|
|
@@ -2893,7 +2893,7 @@ var require_websocket = __commonJS({
|
|
|
2893
2893
|
abortHandshake(websocket, socket, "Invalid Upgrade header");
|
|
2894
2894
|
return;
|
|
2895
2895
|
}
|
|
2896
|
-
const digest =
|
|
2896
|
+
const digest = createHash3("sha1").update(key + GUID).digest("base64");
|
|
2897
2897
|
if (res.headers["sec-websocket-accept"] !== digest) {
|
|
2898
2898
|
abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
|
|
2899
2899
|
return;
|
|
@@ -3260,7 +3260,7 @@ var require_websocket_server = __commonJS({
|
|
|
3260
3260
|
var EventEmitter2 = require("events");
|
|
3261
3261
|
var http = require("http");
|
|
3262
3262
|
var { Duplex } = require("stream");
|
|
3263
|
-
var { createHash:
|
|
3263
|
+
var { createHash: createHash3 } = require("crypto");
|
|
3264
3264
|
var extension2 = require_extension();
|
|
3265
3265
|
var PerMessageDeflate2 = require_permessage_deflate();
|
|
3266
3266
|
var subprotocol2 = require_subprotocol();
|
|
@@ -3561,7 +3561,7 @@ var require_websocket_server = __commonJS({
|
|
|
3561
3561
|
);
|
|
3562
3562
|
}
|
|
3563
3563
|
if (this._state > RUNNING) return abortHandshake(socket, 503);
|
|
3564
|
-
const digest =
|
|
3564
|
+
const digest = createHash3("sha1").update(key + GUID).digest("base64");
|
|
3565
3565
|
const headers = [
|
|
3566
3566
|
"HTTP/1.1 101 Switching Protocols",
|
|
3567
3567
|
"Upgrade: websocket",
|
|
@@ -3928,8 +3928,8 @@ async function uploadMedia(params) {
|
|
|
3928
3928
|
const { readFile: readFile3 } = await import("fs/promises");
|
|
3929
3929
|
buffer = await readFile3(file2);
|
|
3930
3930
|
}
|
|
3931
|
-
const { createHash:
|
|
3932
|
-
const fileHash =
|
|
3931
|
+
const { createHash: createHash3 } = await import("crypto");
|
|
3932
|
+
const fileHash = createHash3("md5").update(buffer).digest("hex");
|
|
3933
3933
|
log2.debug("upload:fileHash", { fileName, fileHash });
|
|
3934
3934
|
const maxBytes = maxFileSizeMb * 1024 * 1024;
|
|
3935
3935
|
if (buffer.length > maxBytes) {
|
|
@@ -4109,6 +4109,65 @@ function inferMimeType(fileName) {
|
|
|
4109
4109
|
const ext = path.extname(fileName).toLowerCase();
|
|
4110
4110
|
return MIME_MAP[ext] || "application/octet-stream";
|
|
4111
4111
|
}
|
|
4112
|
+
function getImageDimensions(buffer) {
|
|
4113
|
+
try {
|
|
4114
|
+
if (buffer.length < 24) return void 0;
|
|
4115
|
+
if (buffer[0] === 137 && buffer[1] === 80 && buffer[2] === 78 && buffer[3] === 71) {
|
|
4116
|
+
return {
|
|
4117
|
+
width: buffer.readUInt32BE(16),
|
|
4118
|
+
height: buffer.readUInt32BE(20)
|
|
4119
|
+
};
|
|
4120
|
+
}
|
|
4121
|
+
if (buffer[0] === 71 && buffer[1] === 73 && buffer[2] === 70) {
|
|
4122
|
+
return {
|
|
4123
|
+
width: buffer.readUInt16LE(6),
|
|
4124
|
+
height: buffer.readUInt16LE(8)
|
|
4125
|
+
};
|
|
4126
|
+
}
|
|
4127
|
+
if (buffer[0] === 66 && buffer[1] === 77 && buffer.length >= 26) {
|
|
4128
|
+
return {
|
|
4129
|
+
width: buffer.readInt32LE(18),
|
|
4130
|
+
height: Math.abs(buffer.readInt32LE(22))
|
|
4131
|
+
};
|
|
4132
|
+
}
|
|
4133
|
+
if (buffer[0] === 82 && buffer[1] === 73 && buffer[8] === 87 && buffer[9] === 69) {
|
|
4134
|
+
if (buffer[12] === 86 && buffer[13] === 80 && buffer[14] === 56 && buffer[15] === 32) {
|
|
4135
|
+
if (buffer.length >= 30) {
|
|
4136
|
+
return {
|
|
4137
|
+
width: buffer.readUInt16LE(26) & 16383,
|
|
4138
|
+
height: buffer.readUInt16LE(28) & 16383
|
|
4139
|
+
};
|
|
4140
|
+
}
|
|
4141
|
+
}
|
|
4142
|
+
if (buffer[12] === 86 && buffer[13] === 80 && buffer[14] === 56 && buffer[15] === 76) {
|
|
4143
|
+
if (buffer.length >= 25) {
|
|
4144
|
+
const bits = buffer.readUInt32LE(21);
|
|
4145
|
+
return {
|
|
4146
|
+
width: (bits & 16383) + 1,
|
|
4147
|
+
height: (bits >> 14 & 16383) + 1
|
|
4148
|
+
};
|
|
4149
|
+
}
|
|
4150
|
+
}
|
|
4151
|
+
}
|
|
4152
|
+
if (buffer[0] === 255 && buffer[1] === 216) {
|
|
4153
|
+
let offset = 2;
|
|
4154
|
+
while (offset < buffer.length - 8) {
|
|
4155
|
+
if (buffer[offset] !== 255) break;
|
|
4156
|
+
const marker = buffer[offset + 1];
|
|
4157
|
+
if (marker >= 192 && marker <= 195 || marker >= 197 && marker <= 199 || marker >= 201 && marker <= 203 || marker >= 205 && marker <= 207) {
|
|
4158
|
+
return {
|
|
4159
|
+
height: buffer.readUInt16BE(offset + 5),
|
|
4160
|
+
width: buffer.readUInt16BE(offset + 7)
|
|
4161
|
+
};
|
|
4162
|
+
}
|
|
4163
|
+
const segLen = buffer.readUInt16BE(offset + 2);
|
|
4164
|
+
offset += 2 + segLen;
|
|
4165
|
+
}
|
|
4166
|
+
}
|
|
4167
|
+
} catch {
|
|
4168
|
+
}
|
|
4169
|
+
return void 0;
|
|
4170
|
+
}
|
|
4112
4171
|
async function resolveAndUploadMedia(params) {
|
|
4113
4172
|
const {
|
|
4114
4173
|
mediaUrl,
|
|
@@ -4159,7 +4218,26 @@ async function resolveAndUploadMedia(params) {
|
|
|
4159
4218
|
}
|
|
4160
4219
|
const fileType = detectFileType(fileName);
|
|
4161
4220
|
const mimeType = inferMimeType(fileName);
|
|
4162
|
-
|
|
4221
|
+
const ext = path.extname(fileName).replace(".", "").toLowerCase();
|
|
4222
|
+
let imageDimensions;
|
|
4223
|
+
if (fileType === "image") {
|
|
4224
|
+
imageDimensions = getImageDimensions(buffer);
|
|
4225
|
+
log3.info("media:imageDimensions", { fileName, ...imageDimensions });
|
|
4226
|
+
}
|
|
4227
|
+
log3.info("media:fileType", { fileName, fileType, mimeType, ext });
|
|
4228
|
+
let encryptionMeta;
|
|
4229
|
+
if (!params.skipEncrypt) {
|
|
4230
|
+
try {
|
|
4231
|
+
const originalSize = buffer.length;
|
|
4232
|
+
const result = await messagePipe.encryptFile(buffer);
|
|
4233
|
+
buffer = result.encryptedBuffer;
|
|
4234
|
+
encryptionMeta = { keyId: result.keyId, iv: result.iv };
|
|
4235
|
+
log3.info("media:encrypted", { fileName, originalSize, encryptedSize: buffer.length });
|
|
4236
|
+
} catch (err) {
|
|
4237
|
+
log3.error("media:encrypt failed", { fileName, error: err.message });
|
|
4238
|
+
throw err;
|
|
4239
|
+
}
|
|
4240
|
+
}
|
|
4163
4241
|
let uploadResult;
|
|
4164
4242
|
try {
|
|
4165
4243
|
uploadResult = await uploadMedia({
|
|
@@ -4184,11 +4262,17 @@ async function resolveAndUploadMedia(params) {
|
|
|
4184
4262
|
const msgType = fileType;
|
|
4185
4263
|
let contentPayload;
|
|
4186
4264
|
if (fileType === "file") {
|
|
4187
|
-
contentPayload = { fileId: uploadResult.fileKey, fileName, size: uploadResult.fileSize,
|
|
4265
|
+
contentPayload = { fileId: uploadResult.fileKey, fileName, size: uploadResult.fileSize, ext };
|
|
4188
4266
|
} else if (fileType === "image") {
|
|
4189
|
-
contentPayload = {
|
|
4267
|
+
contentPayload = {
|
|
4268
|
+
fileId: uploadResult.fileKey,
|
|
4269
|
+
width: imageDimensions?.width ?? 0,
|
|
4270
|
+
height: imageDimensions?.height ?? 0,
|
|
4271
|
+
altText: fileName,
|
|
4272
|
+
ext
|
|
4273
|
+
};
|
|
4190
4274
|
} else {
|
|
4191
|
-
contentPayload = { fileId: uploadResult.fileKey,
|
|
4275
|
+
contentPayload = { fileId: uploadResult.fileKey, ext };
|
|
4192
4276
|
}
|
|
4193
4277
|
await messagePipe.sendMessage({
|
|
4194
4278
|
chatId,
|
|
@@ -4196,12 +4280,15 @@ async function resolveAndUploadMedia(params) {
|
|
|
4196
4280
|
// 私聊场景: chatId 即为用户 ID
|
|
4197
4281
|
msgType,
|
|
4198
4282
|
content: JSON.stringify(contentPayload),
|
|
4199
|
-
skipEncrypt: params.skipEncrypt
|
|
4283
|
+
skipEncrypt: params.skipEncrypt,
|
|
4284
|
+
encryptionMeta
|
|
4285
|
+
// 文件加密的 keyId/iv → sendMessage 用于构建 extra
|
|
4200
4286
|
});
|
|
4201
4287
|
log3.info("media:sent", {
|
|
4202
4288
|
chatId,
|
|
4203
4289
|
fileId: uploadResult.fileKey,
|
|
4204
|
-
msgType
|
|
4290
|
+
msgType,
|
|
4291
|
+
encrypted: Boolean(encryptionMeta)
|
|
4205
4292
|
});
|
|
4206
4293
|
return {
|
|
4207
4294
|
channel: CHANNEL_ID,
|
|
@@ -19141,6 +19228,49 @@ var MessagePipe = class _MessagePipe {
|
|
|
19141
19228
|
async injectRawFrame(rawData) {
|
|
19142
19229
|
return this.handleInbound(rawData);
|
|
19143
19230
|
}
|
|
19231
|
+
/**
|
|
19232
|
+
* 加密文件 Buffer — 按 10MB 分片加密,返回加密后的 Buffer + 密钥信息。
|
|
19233
|
+
* 用于文件上传前的加密处理。
|
|
19234
|
+
*
|
|
19235
|
+
* 如果 CryptoEngine 为透传模式,直接返回原始 buffer。
|
|
19236
|
+
*
|
|
19237
|
+
* @param buffer - 原始文件 Buffer
|
|
19238
|
+
* @returns { encryptedBuffer, keyId, iv } — 加密后的 buffer + 密钥标识 + 初始化向量
|
|
19239
|
+
*/
|
|
19240
|
+
async encryptFile(buffer) {
|
|
19241
|
+
const iv = (0, import_node_crypto3.randomBytes)(8).toString("hex");
|
|
19242
|
+
const chunkSize = 10 * 1024 * 1024;
|
|
19243
|
+
const totalChunks = Math.ceil(buffer.length / chunkSize);
|
|
19244
|
+
let keyId = "";
|
|
19245
|
+
let sessionKey = "";
|
|
19246
|
+
let fillKey = "";
|
|
19247
|
+
const encryptedChunks = [];
|
|
19248
|
+
for (let i = 0; i < totalChunks; i++) {
|
|
19249
|
+
const start = i * chunkSize;
|
|
19250
|
+
const end = Math.min(start + chunkSize, buffer.length);
|
|
19251
|
+
const chunk = buffer.subarray(start, end);
|
|
19252
|
+
const options = keyId ? { keyId, sessionKey, fillKey } : void 0;
|
|
19253
|
+
const result = await this.crypto.encryptFileChunk(chunk, iv, options);
|
|
19254
|
+
encryptedChunks.push(result.fileBuffer);
|
|
19255
|
+
keyId = result.keyId;
|
|
19256
|
+
sessionKey = result.sessionKey;
|
|
19257
|
+
fillKey = result.fillKey;
|
|
19258
|
+
log12.debug("\u{1F512} \u6587\u4EF6\u5206\u7247\u52A0\u5BC6", {
|
|
19259
|
+
chunk: `${i + 1}/${totalChunks}`,
|
|
19260
|
+
originalSize: chunk.length,
|
|
19261
|
+
encryptedSize: result.fileBuffer.length,
|
|
19262
|
+
keyId
|
|
19263
|
+
});
|
|
19264
|
+
}
|
|
19265
|
+
const encryptedBuffer = Buffer.concat(encryptedChunks);
|
|
19266
|
+
log12.info("\u{1F512} \u6587\u4EF6\u52A0\u5BC6\u5B8C\u6210", {
|
|
19267
|
+
originalSize: buffer.length,
|
|
19268
|
+
encryptedSize: encryptedBuffer.length,
|
|
19269
|
+
totalChunks,
|
|
19270
|
+
keyId
|
|
19271
|
+
});
|
|
19272
|
+
return { encryptedBuffer, keyId, iv };
|
|
19273
|
+
}
|
|
19144
19274
|
/** 文件类消息类型集合 — 这些类型的加密消息保留原始 content(文件元数据) */
|
|
19145
19275
|
static FILE_MSG_TYPES = /* @__PURE__ */ new Set(["image", "file", "voice", "video"]);
|
|
19146
19276
|
/**
|
|
@@ -19188,11 +19318,15 @@ var MessagePipe = class _MessagePipe {
|
|
|
19188
19318
|
const isFileMessage = _MessagePipe.FILE_MSG_TYPES.has(msg.msgType);
|
|
19189
19319
|
if (!msg.skipEncrypt && this.quantumAccount) {
|
|
19190
19320
|
if (this.encryptionMode === "quantum_only") {
|
|
19191
|
-
|
|
19192
|
-
|
|
19321
|
+
if (isFileMessage && msg.encryptionMeta) {
|
|
19322
|
+
extra = _MessagePipe.buildFileEncryptExtra(msg.encryptionMeta.iv, msg.encryptionMeta.keyId);
|
|
19323
|
+
log12.debug("\u{1F512} \u51FA\u7AD9\u6587\u4EF6\u6D88\u606F\u5DF2\u6807\u8BB0\u52A0\u5BC6 (quantum_only)", { sessionId: msg.encryptionMeta.keyId, msgType: msg.msgType });
|
|
19324
|
+
} else if (isFileMessage) {
|
|
19325
|
+
const { keyId, iv } = await this.crypto.encrypt(msg.content);
|
|
19193
19326
|
extra = _MessagePipe.buildFileEncryptExtra(iv, keyId);
|
|
19194
|
-
log12.debug("\u{1F512} \u51FA\u7AD9\u6587\u4EF6\u6D88\u606F\u5DF2\u6807\u8BB0\u52A0\u5BC6 (quantum_only)", { sessionId: keyId, msgType: msg.msgType });
|
|
19327
|
+
log12.debug("\u{1F512} \u51FA\u7AD9\u6587\u4EF6\u6D88\u606F\u5DF2\u6807\u8BB0\u52A0\u5BC6 (quantum_only, fallback)", { sessionId: keyId, msgType: msg.msgType });
|
|
19195
19328
|
} else {
|
|
19329
|
+
const { ciphertext, keyId, iv } = await this.crypto.encrypt(msg.content);
|
|
19196
19330
|
extra = JSON.stringify({
|
|
19197
19331
|
cryptoIv: iv,
|
|
19198
19332
|
encryptMsg: ciphertext,
|
|
@@ -19203,11 +19337,15 @@ var MessagePipe = class _MessagePipe {
|
|
|
19203
19337
|
}
|
|
19204
19338
|
} else {
|
|
19205
19339
|
try {
|
|
19206
|
-
|
|
19207
|
-
|
|
19340
|
+
if (isFileMessage && msg.encryptionMeta) {
|
|
19341
|
+
extra = _MessagePipe.buildFileEncryptExtra(msg.encryptionMeta.iv, msg.encryptionMeta.keyId);
|
|
19342
|
+
log12.debug("\u{1F512} \u51FA\u7AD9\u6587\u4EF6\u6D88\u606F\u5DF2\u6807\u8BB0\u52A0\u5BC6 (quantum_and_plain)", { sessionId: msg.encryptionMeta.keyId, msgType: msg.msgType });
|
|
19343
|
+
} else if (isFileMessage) {
|
|
19344
|
+
const { keyId, iv } = await this.crypto.encrypt(msg.content);
|
|
19208
19345
|
extra = _MessagePipe.buildFileEncryptExtra(iv, keyId);
|
|
19209
|
-
log12.debug("\u{1F512} \u51FA\u7AD9\u6587\u4EF6\u6D88\u606F\u5DF2\u6807\u8BB0\u52A0\u5BC6 (quantum_and_plain)", { sessionId: keyId, msgType: msg.msgType });
|
|
19346
|
+
log12.debug("\u{1F512} \u51FA\u7AD9\u6587\u4EF6\u6D88\u606F\u5DF2\u6807\u8BB0\u52A0\u5BC6 (quantum_and_plain, fallback)", { sessionId: keyId, msgType: msg.msgType });
|
|
19210
19347
|
} else {
|
|
19348
|
+
const { ciphertext, keyId, iv } = await this.crypto.encrypt(msg.content);
|
|
19211
19349
|
extra = JSON.stringify({
|
|
19212
19350
|
cryptoIv: iv,
|
|
19213
19351
|
encryptMsg: ciphertext,
|