From 4082b651c5a5fa7aba03788da6ddb4113d91c791 Mon Sep 17 00:00:00 2001 From: pk5ls20 Date: Wed, 23 Oct 2024 06:14:48 +0800 Subject: [PATCH] feat & fix: add video msg pack & upload, fix some bugs in uploading c2c elements --- src/core/apis/packet.ts | 10 +- src/core/packet/highway/session.ts | 157 ++++++++++++++- src/core/packet/msg/element.ts | 51 +++-- src/core/packet/packer.ts | 179 +++++++++++++++++- src/core/packet/utils/crypto/hash.ts | 19 ++ .../packet/utils/crypto/sha1Stream.test.ts | 19 ++ src/core/packet/utils/crypto/sha1Stream.ts | 118 ++++++++++++ .../utils/crypto/sha1StreamBytesTransform.ts | 53 ++++++ 8 files changed, 586 insertions(+), 20 deletions(-) create mode 100644 src/core/packet/utils/crypto/sha1Stream.test.ts create mode 100644 src/core/packet/utils/crypto/sha1Stream.ts create mode 100644 src/core/packet/utils/crypto/sha1StreamBytesTransform.ts diff --git a/src/core/apis/packet.ts b/src/core/apis/packet.ts index fb930e95..b1bdeed8 100644 --- a/src/core/apis/packet.ts +++ b/src/core/apis/packet.ts @@ -12,7 +12,7 @@ import { LogWrapper } from "@/common/log"; import { SendLongMsgResp } from "@/core/packet/proto/message/action"; import { PacketMsg } from "@/core/packet/msg/message"; import { OidbSvcTrpcTcp0x6D6Response } from "@/core/packet/proto/oidb/Oidb.0x6D6"; -import { PacketMsgPicElement } from "@/core/packet/msg/element"; +import { PacketMsgPicElement, PacketMsgVideoElement } from "@/core/packet/msg/element"; interface OffsetType { @@ -118,7 +118,13 @@ export class NTQQPacketApi { if (e instanceof PacketMsgPicElement) { reqList.push(this.packetSession?.highwaySession.uploadImage({ chatType: groupUin ? ChatType.KCHATTYPEGROUP : ChatType.KCHATTYPEC2C, - peerUid: String(groupUin) ? String(groupUin) : this.core.selfInfo.uid + peerUid: groupUin ? String(groupUin) : this.core.selfInfo.uid + }, e)); + } + if (e instanceof PacketMsgVideoElement) { + reqList.push(this.packetSession?.highwaySession.uploadVideo({ + chatType: groupUin ? ChatType.KCHATTYPEGROUP : ChatType.KCHATTYPEC2C, + peerUid: groupUin ? String(groupUin) : this.core.selfInfo.uid }, e)); } } diff --git a/src/core/packet/highway/session.ts b/src/core/packet/highway/session.ts index 54384840..b13f30a7 100644 --- a/src/core/packet/highway/session.ts +++ b/src/core/packet/highway/session.ts @@ -8,9 +8,10 @@ import { HttpConn0x6ff_501Response } from "@/core/packet/proto/action/action"; import { PacketHighwayClient } from "@/core/packet/highway/client"; import { NTV2RichMediaResp } from "@/core/packet/proto/oidb/common/Ntv2.RichMediaResp"; import { OidbSvcTrpcTcpBaseRsp } from "@/core/packet/proto/oidb/OidbBase"; -import { PacketMsgPicElement } from "@/core/packet/msg/element"; +import { PacketMsgPicElement, PacketMsgVideoElement } from "@/core/packet/msg/element"; import { NTV2RichMediaHighwayExt } from "@/core/packet/proto/highway/highway"; import { int32ip2str, oidbIpv4s2HighwayIpv4s } from "@/core/packet/highway/utils"; +import { calculateSha1StreamBytes } from "@/core/packet/utils/crypto/hash"; export const BlockSize = 1024 * 1024; @@ -94,6 +95,17 @@ export class PacketHighwaySession { } } + async uploadVideo(peer: Peer, video: PacketMsgVideoElement): Promise { + await this.checkAvailable(); + if (peer.chatType === ChatType.KCHATTYPEGROUP) { + await this.uploadGroupVideoReq(Number(peer.peerUid), video); + } else if (peer.chatType === ChatType.KCHATTYPEC2C) { + await this.uploadC2CVideoReq(peer.peerUid, video); + } else { + throw new Error(`[Highway] unsupported chatType: ${peer.chatType}`); + } + } + private async uploadGroupImageReq(groupUin: number, img: PacketMsgPicElement): Promise { const preReq = await this.packer.packUploadGroupImgReq(groupUin, img); const preRespRaw = await this.packetClient.sendPacket('OidbSvcTrpcTcp.0x11c4_100', preReq, true); @@ -103,7 +115,7 @@ export class PacketHighwaySession { const preRespData = new NapProtoMsg(NTV2RichMediaResp).decode(preResp.body); const ukey = preRespData.upload.uKey; if (ukey && ukey != "") { - this.logger.logDebug(`[Highway] get upload ukey: ${ukey}, need upload!`); + this.logger.logDebug(`[Highway] uploadGroupImageReq get upload ukey: ${ukey}, need upload!`); const index = preRespData.upload.msgInfo.msgInfoBody[0].index; const sha1 = Buffer.from(index.info.fileSha1, 'hex'); const md5 = Buffer.from(index.info.fileHash, 'hex'); @@ -121,13 +133,13 @@ export class PacketHighwaySession { }); await this.packetHighwayClient.upload( 1004, - fs.createReadStream(img.path, { highWaterMark: BlockSize }), + fs.createReadStream(img.path, {highWaterMark: BlockSize}), img.size, md5, extend ); } else { - this.logger.logDebug(`[Highway] get upload invalid ukey ${ukey}, don't need upload!`); + this.logger.logDebug(`[Highway] uploadGroupImageReq get upload invalid ukey ${ukey}, don't need upload!`); } img.msgInfo = preRespData.upload.msgInfo; // img.groupPicExt = new NapProtoMsg(CustomFace).decode(preRespData.tcpUpload.compatQMsg) @@ -142,7 +154,7 @@ export class PacketHighwaySession { const preRespData = new NapProtoMsg(NTV2RichMediaResp).decode(preResp.body); const ukey = preRespData.upload.uKey; if (ukey && ukey != "") { - this.logger.logDebug(`[Highway] get upload ukey: ${ukey}, need upload!`); + this.logger.logDebug(`[Highway] uploadC2CImageReq get upload ukey: ${ukey}, need upload!`); const index = preRespData.upload.msgInfo.msgInfoBody[0].index; const sha1 = Buffer.from(index.info.fileSha1, 'hex'); const md5 = Buffer.from(index.info.fileHash, 'hex'); @@ -160,12 +172,145 @@ export class PacketHighwaySession { }); await this.packetHighwayClient.upload( 1003, - fs.createReadStream(img.path, { highWaterMark: BlockSize }), + fs.createReadStream(img.path, {highWaterMark: BlockSize}), img.size, md5, extend ); + } else { + this.logger.logDebug(`[Highway] uploadC2CImageReq get upload invalid ukey ${ukey}, don't need upload!`); } img.msgInfo = preRespData.upload.msgInfo; } + + private async uploadGroupVideoReq(groupUin: number, video: PacketMsgVideoElement): Promise { + const preReq = await this.packer.packUploadGroupVideoReq(groupUin, video); + const preRespRaw = await this.packetClient.sendPacket('OidbSvcTrpcTcp.0x11ea_100', preReq, true); + const preResp = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode( + Buffer.from(preRespRaw.hex_data, 'hex') + ); + const preRespData = new NapProtoMsg(NTV2RichMediaResp).decode(preResp.body); + const ukey = preRespData.upload.uKey; + if (ukey && ukey != "") { + this.logger.logDebug(`[Highway] uploadGroupVideoReq get upload video ukey: ${ukey}, need upload!`); + const index = preRespData.upload.msgInfo.msgInfoBody[0].index; + const md5 = Buffer.from(index.info.fileHash, 'hex'); + const extend = new NapProtoMsg(NTV2RichMediaHighwayExt).encode({ + fileUuid: index.fileUuid, + uKey: ukey, + network: { + ipv4S: oidbIpv4s2HighwayIpv4s(preRespData.upload.ipv4S) + }, + msgInfoBody: preRespData.upload.msgInfo.msgInfoBody, + blockSize: BlockSize, + hash: { + fileSha1: await calculateSha1StreamBytes(video.filePath!) + } + }) + await this.packetHighwayClient.upload( + 1005, + fs.createReadStream(video.filePath!, {highWaterMark: BlockSize}), + +video.fileSize!, + md5, + extend + ); + } else { + this.logger.logDebug(`[Highway] uploadGroupVideoReq get upload invalid ukey ${ukey}, don't need upload!`); + } + const subFile = preRespData.upload.subFileInfos[0]; + if (subFile.uKey && subFile.uKey != "") { + this.logger.logDebug(`[Highway] uploadGroupVideoReq get upload video thumb ukey: ${subFile.uKey}, need upload!`); + const index = preRespData.upload.msgInfo.msgInfoBody[1].index; + const md5 = Buffer.from(index.info.fileHash, 'hex'); + const sha1 = Buffer.from(index.info.fileSha1, 'hex'); + const extend = new NapProtoMsg(NTV2RichMediaHighwayExt).encode({ + fileUuid: index.fileUuid, + uKey: subFile.uKey, + network: { + ipv4S: oidbIpv4s2HighwayIpv4s(subFile.ipv4S) + }, + msgInfoBody: preRespData.upload.msgInfo.msgInfoBody, + blockSize: BlockSize, + hash: { + fileSha1: [sha1] + } + }); + await this.packetHighwayClient.upload( + 1006, + fs.createReadStream(video.thumbPath!, {highWaterMark: BlockSize}), + +video.thumbSize!, + md5, + extend + ); + } else { + this.logger.logDebug(`[Highway] uploadGroupVideoReq get upload invalid thumb ukey ${subFile.uKey}, don't need upload!`); + } + video.msgInfo = preRespData.upload.msgInfo; + } + + private async uploadC2CVideoReq(peerUid: string, video: PacketMsgVideoElement): Promise { + const preReq = await this.packer.packUploadC2CVideoReq(peerUid, video); + const preRespRaw = await this.packetClient.sendPacket('OidbSvcTrpcTcp.0x11e9_100', preReq, true); + console.log(preRespRaw); + const preResp = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode( + Buffer.from(preRespRaw.hex_data, 'hex') + ); + const preRespData = new NapProtoMsg(NTV2RichMediaResp).decode(preResp.body); + const ukey = preRespData.upload.uKey; + if (ukey && ukey != "") { + this.logger.logDebug(`[Highway] uploadC2CVideoReq get upload video ukey: ${ukey}, need upload!`); + const index = preRespData.upload.msgInfo.msgInfoBody[0].index; + const md5 = Buffer.from(index.info.fileHash, 'hex'); + const extend = new NapProtoMsg(NTV2RichMediaHighwayExt).encode({ + fileUuid: index.fileUuid, + uKey: ukey, + network: { + ipv4S: oidbIpv4s2HighwayIpv4s(preRespData.upload.ipv4S) + }, + msgInfoBody: preRespData.upload.msgInfo.msgInfoBody, + blockSize: BlockSize, + hash: { + fileSha1: await calculateSha1StreamBytes(video.filePath!) + } + }) + await this.packetHighwayClient.upload( + 1001, + fs.createReadStream(video.filePath!, {highWaterMark: BlockSize}), + +video.fileSize!, + md5, + extend + ); + } else { + this.logger.logDebug(`[Highway] uploadC2CVideoReq get upload invalid ukey ${ukey}, don't need upload!`); + } + const subFile = preRespData.upload.subFileInfos[0]; + if (subFile.uKey && subFile.uKey != "") { + this.logger.logDebug(`[Highway] uploadC2CVideoReq get upload video thumb ukey: ${subFile.uKey}, need upload!`); + const index = preRespData.upload.msgInfo.msgInfoBody[1].index; + const md5 = Buffer.from(index.info.fileHash, 'hex'); + const sha1 = Buffer.from(index.info.fileSha1, 'hex'); + const extend = new NapProtoMsg(NTV2RichMediaHighwayExt).encode({ + fileUuid: index.fileUuid, + uKey: subFile.uKey, + network: { + ipv4S: oidbIpv4s2HighwayIpv4s(subFile.ipv4S) + }, + msgInfoBody: preRespData.upload.msgInfo.msgInfoBody, + blockSize: BlockSize, + hash: { + fileSha1: [sha1] + } + }); + await this.packetHighwayClient.upload( + 1002, + fs.createReadStream(video.thumbPath!, {highWaterMark: BlockSize}), + +video.thumbSize!, + md5, + extend + ); + } else { + this.logger.logDebug(`[Highway] uploadC2CVideoReq get upload invalid thumb ukey ${subFile.uKey}, don't need upload!`); + } + video.msgInfo = preRespData.upload.msgInfo; + } } diff --git a/src/core/packet/msg/element.ts b/src/core/packet/msg/element.ts index 81d1401f..9f7cc562 100644 --- a/src/core/packet/msg/element.ts +++ b/src/core/packet/msg/element.ts @@ -44,7 +44,7 @@ export abstract class IPacketMsgElement { } toPreview(): string { - return '[nya~]'; + return '[暂不支持该消息类型喵~]'; } } @@ -84,19 +84,15 @@ export class PacketMsgAtElement extends PacketMsgTextElement { text: { str: this.text, pbReserve: new NapProtoMsg(MentionExtra).encode({ - type: this.atAll ? 1 : 2, - uin: 0, - field5: 0, - uid: this.targetUid, - } + type: this.atAll ? 1 : 2, + uin: 0, + field5: 0, + uid: this.targetUid, + } ) } }]; } - - toPreview(): string { - return `@${this.targetUid} ${this.text}`; - } } export class PacketMsgPicElement extends IPacketMsgElement { @@ -189,7 +185,7 @@ export class PacketMsgReplyElement extends IPacketMsgElement { } toPreview(): string { - return "[回复]"; + return "[回复消息]"; } } @@ -285,8 +281,41 @@ export class PacketMsgMarkFaceElement extends IPacketMsgElement { + fileSize?: string; + filePath?: string; + thumbSize?: number; + thumbPath?: string; + fileMd5?: string; + thumbMd5?: string; + thumbWidth?: number; + thumbHeight?: number; + msgInfo: NapProtoEncodeStructType | null = null; + constructor(element: SendVideoElement) { super(element); + this.fileSize = element.videoElement.fileSize; + this.filePath = element.videoElement.filePath; + this.thumbSize = element.videoElement.thumbSize; + this.thumbPath = element.videoElement.thumbPath?.get(0); + this.fileMd5 = element.videoElement.videoMd5 + this.thumbMd5 = element.videoElement.thumbMd5; + this.thumbWidth = element.videoElement.thumbWidth; + this.thumbHeight = element.videoElement.thumbHeight; + } + + buildElement(): NapProtoEncodeStructType[] { + assert(this.msgInfo !== null, 'msgInfo is null, expected not null'); + return [{ + commonElem: { + serviceType: 48, + pbElem: new NapProtoMsg(MsgInfo).encode(this.msgInfo), + businessType: 21, + } + }]; + } + + toPreview(): string { + return "[视频]"; } } diff --git a/src/core/packet/packer.ts b/src/core/packet/packer.ts index cb53db5c..087b7fd0 100644 --- a/src/core/packet/packer.ts +++ b/src/core/packet/packer.ts @@ -11,7 +11,7 @@ import { NTV2RichMediaReq } from "@/core/packet/proto/oidb/common/Ntv2.RichMedia import { HttpConn0x6ff_501 } from "@/core/packet/proto/action/action"; import { LongMsgResult, SendLongMsgReq } from "@/core/packet/proto/message/action"; import { PacketMsgBuilder } from "@/core/packet/msg/builder"; -import { PacketMsgPicElement } from "@/core/packet/msg/element"; +import { PacketMsgPicElement, PacketMsgVideoElement } from "@/core/packet/msg/element"; import { LogWrapper } from "@/common/log"; import { PacketMsg } from "@/core/packet/msg/message"; import { OidbSvcTrpcTcp0x6D6 } from "@/core/packet/proto/oidb/Oidb.0x6D6"; @@ -289,6 +289,183 @@ export class PacketPacker { return this.toHexStr(this.packOidbPacket(0x11c5, 100, req, true, false)); } + async packUploadGroupVideoReq(groupUin: number, video: PacketMsgVideoElement): Promise { + if (!video.filePath || !video.thumbPath) throw new Error("video.filePath or video.thumbPath is empty"); + if (!video.fileSize || !video.thumbSize) throw new Error("video.fileSize or video.thumbSize is empty"); + const videoSha1 = await calculateSha1(video.filePath ?? ""); + const videoThumbSha1 = await calculateSha1(video.thumbPath ?? ""); + const req = new NapProtoMsg(NTV2RichMediaReq).encode({ + reqHead: { + common: { + requestId: 3, + command: 100 + }, + scene: { + requestType: 2, + businessType: 2, + sceneType: 2, + group: { + groupUin: groupUin + }, + }, + client: { + agentType: 2 + } + }, + upload: { + uploadInfo: [ + { + fileInfo: { + fileSize: +video.fileSize, + fileHash: video.fileMd5, + fileSha1: this.toHexStr(videoSha1), + fileName: "nya.mp4", + type: { + type: 2, + picFormat: 0, + videoFormat: 0, + voiceFormat: 0 + }, + height: 0, + width: 0, + time: 0, + original: 0 + }, + subFileType: 0 + }, { + fileInfo: { + fileSize: +video.thumbSize, + fileHash: video.thumbMd5, + fileSha1: this.toHexStr(videoThumbSha1), + fileName: "nya.jpg", + type: { + type: 1, + picFormat: 0, + videoFormat: 0, + voiceFormat: 0 + }, + height: video.thumbHeight, + width: video.thumbWidth, + time: 0, + original: 0 + }, + subFileType: 100 + } + ], + tryFastUploadCompleted: true, + srvSendMsg: false, + clientRandomId: crypto.randomBytes(8).readBigUInt64BE() & BigInt('0x7FFFFFFFFFFFFFFF'), + compatQMsgSceneType: 2, + extBizInfo: { + pic : { + bizType: 0, + textSummary: "Nya~", + }, + video: { + bytesPbReserve: Buffer.from([0x80, 0x01, 0x00]), + }, + ptt: { + bytesPbReserve: Buffer.alloc(0), + bytesReserve: Buffer.alloc(0), + bytesGeneralFlags: Buffer.alloc(0), + } + }, + clientSeq: 0, + noNeedCompatMsg: false + } + }); + return this.toHexStr(this.packOidbPacket(0x11EA, 100, req, true, false)); + } + + async packUploadC2CVideoReq(peerUin: string, video: PacketMsgVideoElement): Promise { + if (!video.filePath || !video.thumbPath) throw new Error("video.filePath or video.thumbPath is empty"); + if (!video.fileSize || !video.thumbSize) throw new Error("video.fileSize or video.thumbSize is empty"); + const videoSha1 = await calculateSha1(video.filePath ?? ""); + const videoThumbSha1 = await calculateSha1(video.thumbPath ?? ""); + const req = new NapProtoMsg(NTV2RichMediaReq).encode({ + reqHead: { + common: { + requestId: 3, + command: 100 + }, + scene: { + requestType: 2, + businessType: 2, + sceneType: 1, + c2C: { + accountType: 2, + targetUid: peerUin + } + }, + client: { + agentType: 2 + } + }, + upload: { + uploadInfo: [ + { + fileInfo: { + fileSize: +video.fileSize, + fileHash: video.fileMd5, + fileSha1: this.toHexStr(videoSha1), + fileName: "nya.mp4", + type: { + type: 2, + picFormat: 0, + videoFormat: 0, + voiceFormat: 0 + }, + height: 0, + width: 0, + time: 0, + original: 0 + }, + subFileType: 0 + }, { + fileInfo: { + fileSize: +video.thumbSize, + fileHash: video.thumbMd5, + fileSha1: this.toHexStr(videoThumbSha1), + fileName: "nya.jpg", + type: { + type: 1, + picFormat: 0, + videoFormat: 0, + voiceFormat: 0 + }, + height: video.thumbHeight, + width: video.thumbWidth, + time: 0, + original: 0 + }, + subFileType: 100 + } + ], + tryFastUploadCompleted: true, + srvSendMsg: false, + clientRandomId: crypto.randomBytes(8).readBigUInt64BE() & BigInt('0x7FFFFFFFFFFFFFFF'), + compatQMsgSceneType: 2, + extBizInfo: { + pic : { + bizType: 0, + textSummary: "Nya~", + }, + video: { + bytesPbReserve: Buffer.from([0x80, 0x01, 0x00]), + }, + ptt: { + bytesPbReserve: Buffer.alloc(0), + bytesReserve: Buffer.alloc(0), + bytesGeneralFlags: Buffer.alloc(0), + } + }, + clientSeq: 0, + noNeedCompatMsg: false + } + }); + return this.toHexStr(this.packOidbPacket(0x11E9, 100, req, true, false)); + } + packGroupFileDownloadReq(groupUin: number, fileUUID: string): PacketHexStr { return this.toHexStr( this.packOidbPacket(0x6D6, 2, new NapProtoMsg(OidbSvcTrpcTcp0x6D6).encode({ diff --git a/src/core/packet/utils/crypto/hash.ts b/src/core/packet/utils/crypto/hash.ts index 53901e6c..51eaf4ec 100644 --- a/src/core/packet/utils/crypto/hash.ts +++ b/src/core/packet/utils/crypto/hash.ts @@ -2,6 +2,7 @@ import * as crypto from 'crypto'; import * as stream from 'stream'; import * as fs from 'fs'; +import {CalculateStreamBytesTransform} from "@/core/packet/utils/crypto/sha1StreamBytesTransform"; function sha1Stream(readable: stream.Readable) { return new Promise((resolve, reject) => { @@ -14,3 +15,21 @@ export function calculateSha1(filePath: string): Promise { const readable = fs.createReadStream(filePath); return sha1Stream(readable); } + +export function calculateSha1StreamBytes(filePath: string): Promise { + return new Promise((resolve, reject) => { + const readable = fs.createReadStream(filePath); + const calculateStreamBytes = new CalculateStreamBytesTransform(); + const byteArrayList: Buffer[] = []; + calculateStreamBytes.on('data', (chunk: Buffer) => { + byteArrayList.push(chunk); + }); + calculateStreamBytes.on('end', () => { + resolve(byteArrayList); + }); + calculateStreamBytes.on('error', (err) => { + reject(err); + }); + readable.pipe(calculateStreamBytes); + }); +} diff --git a/src/core/packet/utils/crypto/sha1Stream.test.ts b/src/core/packet/utils/crypto/sha1Stream.test.ts new file mode 100644 index 00000000..b8c9ef7f --- /dev/null +++ b/src/core/packet/utils/crypto/sha1Stream.test.ts @@ -0,0 +1,19 @@ +import crypto from 'crypto'; +import assert from 'assert'; +import { Sha1Stream } from './sha1Stream'; + +function testSha1Stream() { + for (let i = 0; i < 100000; i++) { + const randomLength = Math.floor(Math.random() * 1024); + const randomData = crypto.randomBytes(randomLength); + const sha1Stream = new Sha1Stream(); + sha1Stream.update(randomData); + const hash = sha1Stream.final(); + const expectedDigest = crypto.createHash('sha1').update(randomData).digest(); + assert.strictEqual(hash.toString('hex'), expectedDigest.toString('hex')); + console.log(`Test ${i + 1}: Passed`); + } + console.log('All tests passed successfully.'); +} + +testSha1Stream(); diff --git a/src/core/packet/utils/crypto/sha1Stream.ts b/src/core/packet/utils/crypto/sha1Stream.ts new file mode 100644 index 00000000..f1c344df --- /dev/null +++ b/src/core/packet/utils/crypto/sha1Stream.ts @@ -0,0 +1,118 @@ +export class Sha1Stream { + readonly Sha1BlockSize = 64; + readonly Sha1DigestSize = 20; + private readonly _padding = Buffer.concat([Buffer.from([0x80]), Buffer.alloc(63)]); + private readonly _state = new Uint32Array(5); + private readonly _count = new Uint32Array(2); + private readonly _buffer = Buffer.allocUnsafe(this.Sha1BlockSize); + private readonly _w = new Uint32Array(80); + + constructor() { + this.reset(); + } + + private reset(): void { + this._state[0] = 0x67452301; + this._state[1] = 0xEFCDAB89; + this._state[2] = 0x98BADCFE; + this._state[3] = 0x10325476; + this._state[4] = 0xC3D2E1F0; + this._count[0] = 0; + this._count[1] = 0; + this._buffer.fill(0); + } + + private rotateLeft(v: number, o: number): number { + return ((v << o) | (v >>> (32 - o))) >>> 0; + } + + private transform(chunk: Buffer, offset: number): void { + const w = this._w; + const view = new DataView(chunk.buffer, chunk.byteOffset + offset, 64); + + for (let i = 0; i < 16; i++) { + w[i] = view.getUint32(i * 4, false); + } + + for (let i = 16; i < 80; i++) { + w[i] = this.rotateLeft(w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16], 1) >>> 0; + } + + let a = this._state[0]; + let b = this._state[1]; + let c = this._state[2]; + let d = this._state[3]; + let e = this._state[4]; + + for (let i = 0; i < 80; i++) { + const [f, k] = (i < 20) ? [(b & c) | ((~b) & d), 0x5A827999] : + (i < 40) ? [b ^ c ^ d, 0x6ED9EBA1] : + (i < 60) ? [(b & c) | (b & d) | (c & d), 0x8F1BBCDC] : + [b ^ c ^ d, 0xCA62C1D6]; + const temp = (this.rotateLeft(a, 5) + f + k + e + w[i]) >>> 0; + e = d; + d = c; + c = this.rotateLeft(b, 30) >>> 0; + b = a; + a = temp; + } + + this._state[0] = (this._state[0] + a) >>> 0; + this._state[1] = (this._state[1] + b) >>> 0; + this._state[2] = (this._state[2] + c) >>> 0; + this._state[3] = (this._state[3] + d) >>> 0; + this._state[4] = (this._state[4] + e) >>> 0; + } + + public update(data: Buffer, len?: number): void { + let index = ((this._count[0] >>> 3) & 0x3F) >>> 0; + const dataLen = len ?? data.length; + this._count[0] = (this._count[0] + (dataLen << 3)) >>> 0; + + if (this._count[0] < (dataLen << 3)) this._count[1] = (this._count[1] + 1) >>> 0; + + this._count[1] = (this._count[1] + (dataLen >>> 29)) >>> 0; + + let partLen = (this.Sha1BlockSize - index) >>> 0; + let i = 0; + + if (dataLen >= partLen) { + data.copy(this._buffer, index, 0, partLen); + this.transform(this._buffer, 0); + for (i = partLen; (i + this.Sha1BlockSize) <= dataLen; i = (i + this.Sha1BlockSize) >>> 0) { + this.transform(data, i); + } + index = 0; + } + + data.copy(this._buffer, index, i, dataLen); + } + + public hash(bigEndian: boolean = true): Buffer { + const digest = Buffer.allocUnsafe(this.Sha1DigestSize); + if (bigEndian) { + for (let i = 0; i < 5; i++) digest.writeUInt32BE(this._state[i], i * 4); + } else { + for (let i = 0; i < 5; i++) digest.writeUInt32LE(this._state[i], i * 4); + } + return digest; + } + + public final(): Buffer { + const digest = Buffer.allocUnsafe(this.Sha1DigestSize); + const bits = Buffer.allocUnsafe(8) + bits.writeUInt32BE(this._count[1], 0); + bits.writeUInt32BE(this._count[0], 4); + + let index = ((this._count[0] >>> 3) & 0x3F) >>> 0; + const padLen = ((index < 56) ? (56 - index) : (120 - index)) >>> 0; + this.update(this._padding, padLen); + this.update(bits); + + for (let i = 0; i < 5; i++) { + digest.writeUInt32BE(this._state[i], i * 4); + } + + return digest; + } +} diff --git a/src/core/packet/utils/crypto/sha1StreamBytesTransform.ts b/src/core/packet/utils/crypto/sha1StreamBytesTransform.ts new file mode 100644 index 00000000..38fd66eb --- /dev/null +++ b/src/core/packet/utils/crypto/sha1StreamBytesTransform.ts @@ -0,0 +1,53 @@ +import * as stream from "node:stream"; +import {Sha1Stream} from "@/core/packet/utils/crypto/sha1Stream"; + +export class CalculateStreamBytesTransform extends stream.Transform { + private readonly blockSize = 1024 * 1024; + private sha1: Sha1Stream; + private buffer: Buffer; + private bytesRead: number; + private readonly byteArrayList: Buffer[]; + + constructor() { + super(); + this.sha1 = new Sha1Stream(); + this.buffer = Buffer.alloc(0); + this.bytesRead = 0; + this.byteArrayList = []; + } + + _transform(chunk: Buffer, _: BufferEncoding, callback: stream.TransformCallback): void { + try { + this.buffer = Buffer.concat([this.buffer, chunk]); + let offset = 0; + while (this.buffer.length - offset >= this.sha1.Sha1BlockSize) { + const block = this.buffer.subarray(offset, offset + this.sha1.Sha1BlockSize); + this.sha1.update(block); + offset += this.sha1.Sha1BlockSize; + this.bytesRead += this.sha1.Sha1BlockSize; + if (this.bytesRead % this.blockSize === 0) { + const digest = this.sha1.hash(false); + this.byteArrayList.push(Buffer.from(digest)); + } + } + this.buffer = this.buffer.subarray(offset); + callback(null); + } catch (err) { + callback(err as Error); + } + } + + _flush(callback: stream.TransformCallback): void { + try { + if (this.buffer.length > 0) this.sha1.update(this.buffer); + const finalDigest = this.sha1.final(); + this.byteArrayList.push(Buffer.from(finalDigest)); + for (const digest of this.byteArrayList) { + this.push(digest); + } + callback(null); + } catch (err) { + callback(err as Error); + } + } +}