Merge pull request #80 from aynakeya/master

Video Decryption Prototype
This commit is contained in:
乐平 2023-12-02 12:26:50 +08:00 committed by GitHub
commit 3f56e8ce6e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 4859 additions and 33 deletions

4745
electron/decrypt.js Normal file

File diff suppressed because one or more lines are too long

View File

@ -18,21 +18,7 @@ export default function initIPC() {
ipcMain.handle('invoke_启动服务', async (event, arg) => { ipcMain.handle('invoke_启动服务', async (event, arg) => {
return startServer({ return startServer({
interceptCallback: phase => async (req, res) => { win: win,
if (phase === 'response' && res?._data?.headers?.['content-type'] == 'video/mp4') {
const fixUrl = {}
if(req.fullUrl().includes("video.qq.com")){
fixUrl.fixUrl = req.fullUrl().replace(/\/20302\//g, '/20304/');
fixUrl.hdUrl = fixUrl.fixUrl.replace(/(\?|&)(?!(encfilekey=|token=))[^&]+/g, '');
}
win?.webContents?.send?.('VIDEO_CAPTURE', {
url: req.fullUrl(),
size: res?._data?.headers?.['content-length'] ?? 0,
...fixUrl
});
}
},
setProxyErrorCallback: err => { setProxyErrorCallback: err => {
console.log('开启代理失败', err); console.log('开启代理失败', err);
}, },
@ -49,9 +35,10 @@ export default function initIPC() {
return result?.[0]; return result?.[0];
}); });
ipcMain.handle('invoke_下载视频', async (event, { url, savePath }) => { ipcMain.handle('invoke_下载视频', async (event, { url, decodeKey, savePath }) => {
console.log(url,decodeKey);
return downloadFile( return downloadFile(
url, url,decodeKey,
`${savePath}/${Date.now()}.mp4`, `${savePath}/${Date.now()}.mp4`,
throttle(value => win?.webContents?.send?.('e_进度变化', value), 1000), throttle(value => win?.webContents?.send?.('e_进度变化', value), 1000),
).catch(err => { ).catch(err => {

View File

@ -11,8 +11,66 @@ if (process.platform === 'win32') {
process.env.OPENSSL_CONF = CONFIG.OPEN_SSL_CNF_PATH; process.env.OPENSSL_CONF = CONFIG.OPEN_SSL_CNF_PATH;
} }
const injection_script =`
(function () {
if (window.wvds !== undefined) {
return
}
let receiver_url = "https://aaaa.com"
function send_response_if_is_video(response) {
if (response == undefined) {
return;
}
if (response["err_msg"] != "H5ExtTransfer:ok") {
return;
}
let value = JSON.parse(response["jsapi_resp"]["resp_json"]);
if (value["object"] == undefined || value["object"]["object_desc"] == undefined || value["object"]["object_desc"]["media"].length == 0) {
return
}
let media = value["object"]["object_desc"]["media"][0]
let video_data = {
"decode_key": media["decode_key"],
"url": media["url"]+media["url_token"],
"size": media["file_size"],
"description": value["object"]["object_desc"]["description"].trim(),
"uploader": value["object"]["nickname"]
}
fetch(receiver_url, {
method: 'POST',
mode: 'no-cors',
body: JSON.stringify(video_data),
}).then((resp) => {
console.log(\`video data for \${video_data["description"]} sent\`);
});
}
function wrapper(name,origin) {
console.log(\`injecting \${name}\`);
return function() {
let cmdName = arguments[0];
if (arguments.length == 3) {
let original_callback = arguments[2];
arguments[2] = async function () {
if (arguments.length == 1) {
send_response_if_is_video(arguments[0]);
}
return await original_callback.apply(this, arguments);
}
}
let result = origin.apply(this,arguments);
return result;
}
}
console.log(\`------- Invoke WechatVideoDownloader Service ---------\`);
window.WeixinJSBridge.invoke = wrapper("WeixinJSBridge.invoke",window.WeixinJSBridge.invoke);
window.wvds = true;
})()
`;
export async function startServer({ export async function startServer({
interceptCallback = f => f => f, win,
setProxyErrorCallback = f => f, setProxyErrorCallback = f => f,
}) { }) {
const port = await getPort(); const port = await getPort();
@ -40,15 +98,27 @@ export async function startServer({
proxy.intercept( proxy.intercept(
{ {
phase: 'request', phase: 'request',
hostname: 'aaaa.com',
as: 'json'
},
(req, res) => {
res.string = "ok";
res.statusCode = 200;
win?.webContents?.send?.('VIDEO_CAPTURE', req.json)
}, },
interceptCallback('request'),
); );
proxy.intercept( proxy.intercept(
{ {
phase: 'response', phase: 'response',
hostname: 'res.wx.qq.com',
as: "string"
},
async (req, res) => {
if (req.url.includes("polyfills.publish")) {
res.string = res.string + "\n" + injection_script;
}
}, },
interceptCallback('response'),
); );
}); });
} }

View File

@ -2,6 +2,8 @@ import { get } from 'axios';
import { app, dialog, shell } from 'electron'; import { app, dialog, shell } from 'electron';
import semver from 'semver'; import semver from 'semver';
import fs from 'fs'; import fs from 'fs';
import {getDecryptionArray} from './decrypt';
import {Transform } from 'stream';
// packageUrl 需要包含 { "version": "1.0.0" } 结构 // packageUrl 需要包含 { "version": "1.0.0" } 结构
function checkUpdate( function checkUpdate(
@ -28,7 +30,27 @@ function checkUpdate(
.catch(err => {}); .catch(err => {});
} }
function downloadFile(url, fullFileName, progressCallback) {
function xorTransform(decryptionArray) {
let processedBytes = 0;
return new Transform({
transform(chunk, encoding, callback) {
if (processedBytes < decryptionArray.length) {
let remaining = Math.min(decryptionArray.length - processedBytes, chunk.length);
for (let i = 0; i < remaining; i++) {
chunk[i] = chunk[i] ^ decryptionArray[processedBytes + i];
}
processedBytes += remaining;
}
this.push(chunk);
callback();
}
});
}
function downloadFile(url,decodeKey, fullFileName, progressCallback) {
const xorStream = xorTransform(getDecryptionArray(decodeKey));
return get(url, { return get(url, {
responseType: 'stream', responseType: 'stream',
headers: { headers: {
@ -47,7 +69,7 @@ function downloadFile(url, fullFileName, progressCallback) {
data.on('error', err => reject(err)); data.on('error', err => reject(err));
data.pipe( data.pipe(xorStream).pipe(
fs.createWriteStream(fullFileName).on('finish', () => { fs.createWriteStream(fullFileName).on('finish', () => {
resolve({ resolve({
fullFileName, fullFileName,

View File

@ -43,9 +43,9 @@ function App() {
dataSource={captureList} dataSource={captureList}
columns={[ columns={[
{ {
title: '视频地址(捕获中……)', title: '视频标题(捕获中……)',
dataIndex: 'url', dataIndex: 'description',
key: 'url', key: 'description',
render: value => value, render: value => value,
ellipsis: true, ellipsis: true,
}, },
@ -61,7 +61,7 @@ function App() {
dataIndex: 'action', dataIndex: 'action',
key: 'action', key: 'action',
width: '210px', width: '210px',
render: (_, { url, hdUrl, fixUrl, fullFileName, }) => ( render: (_, { url, decodeKey, hdUrl, fixUrl, fullFileName, }) => (
<div> <div>
{fullFileName ? ( {fullFileName ? (
<Button <Button
@ -80,7 +80,7 @@ function App() {
icon={<DownloadOutlined />} icon={<DownloadOutlined />}
type="primary" type="primary"
onClick={() => { onClick={() => {
send({ type: 'e_下载', url: (hdUrl || url) }); send({ type: 'e_下载', url: (hdUrl || url), decodeKey: decodeKey });
}} }}
size="small" size="small"
> >

View File

@ -161,8 +161,8 @@ export default createMachine(
.finally(() => send('e_重新检测')); .finally(() => send('e_重新检测'));
}, },
invoke_启动服务: (context, event) => send => { invoke_启动服务: (context, event) => send => {
const fnDealVideoCapture = (eName, { url, size, ...other }) => { const fnDealVideoCapture = (eName, { url, size, description, decode_key, ...other }) => {
send({ type: 'e_视频捕获', url, size, ...other }); send({ type: 'e_视频捕获', url, size, description, decodeKey: decode_key, ...other });
}; };
ipcRenderer ipcRenderer
@ -190,11 +190,12 @@ export default createMachine(
.catch(() => send('e_取消')); .catch(() => send('e_取消'));
}, },
invoke_下载视频: invoke_下载视频:
({ currentUrl, savePath }) => ({ currentUrl, savePath, decodeKey }) =>
send => { send => {
ipcRenderer ipcRenderer
.invoke('invoke_下载视频', { .invoke('invoke_下载视频', {
url: currentUrl, url: currentUrl,
decodeKey: decodeKey,
savePath, savePath,
}) })
.then(({ fullFileName }) => { .then(({ fullFileName }) => {
@ -217,8 +218,8 @@ export default createMachine(
}, },
}, },
actions: { actions: {
action_视频捕获: actions.assign(({ captureList }, { size, url, ...other }) => { action_视频捕获: actions.assign(({ captureList }, { url, size, description, decodeKey, ...other }) => {
captureList.push({ size, url, prettySize: prettyBytes(+size), ...other }); captureList.push({ size, url, prettySize: prettyBytes(+size), description, decodeKey, ...other });
return { return {
captureList: uniqBy(captureList, 'url'), captureList: uniqBy(captureList, 'url'),
@ -229,9 +230,10 @@ export default createMachine(
captureList: [], captureList: [],
}; };
}), }),
action_设置当前地址: actions.assign((_, { url }) => { action_设置当前地址: actions.assign((_, { url,decodeKey }) => {
return { return {
currentUrl: url, currentUrl: url,
decodeKey: decodeKey,
}; };
}), }),
action_存储下载位置: actions.assign((_, { data }) => { action_存储下载位置: actions.assign((_, { data }) => {