闲的无聊,看到有这么一个Electron 桌面端,试试练练手
## 1. 环境与工具
目标软件 | ProcessOn v5.14.16 桌面端 |
框架 | Electron |
asar 路径 | `*\ProcessOn\resources\app.asar`
## 2. 第一步:解包 asar
2.1解包后的目录结构:
processon_extracted/
├── node_modules/
│ ├── font-list/ # 原生模块(需 unpack)
│ └── jszip/ # 原生模块(需 unpack)
├── out/
│ ├── main/
│ │ └── index.js # 主进程(74KB,含 RSA 密钥对)
│ ├── preload/
│ │ └── index.js # preload 脚本
│ └── renderer/
│ ├── login.html # 登录页(不要改!)
│ ├── index.html # 主界面
│ └── src/
│ ├── assets/
│ │ ├── user.js # ⭐ 核心:用户模块(173KB)
│ │ ├── js/pc/
│ │ │ ├── home.js # ⭐ 首页:会员标签显示
│ │ │ ├── login.js # 登录逻辑
│ │ │ ├── server/server.js # API 通信
│ │ │ ├── saveWindow.js # 保存窗口
│ │ │ └── ...
│ │ └── js/plug_in/
│ │ └── inject.js # 权限对象定义
│ └── v6_editor/ # 编辑器
└── package.json
2.2 审计验证模块:
1.RSA 私钥硬编码在客户端 | main/index.js class C
2.Electron 安全配置缺失(nodeIntegration等) | main/index.js BrowserWindow
3.授权码本地明文存储 | %APPDATA%\ProcessOn\data\code\code.txt
4.用户 token localStorage 明文 | processon_token, processon_ws_token
5.IPC 无认证(任意 channel 可调) | preload/index.js
6.privilege 数据 localStorage 明文 | localStorage.privilege
7.CSP 配置宽松 | main/index.js
其中RSA 密钥对 main/index.js` 中 “class C” 包含完整的 RSA 密钥对(硬编码在客户端),用于:授权码的 RSA 加密,授权码的验证【*重大问题】
3.分析验证逻辑权限体系
ProcessOn 的会员权限通过 3次实现:
1: getUser() → 用户基础信息
API: /api/pc/user/info
返回: user.expireTime, user.member 等
消费方: home.js (首页标签), saveWindow.js
2: getUserPrivilege() → 权限详情
API: /api/pc/user/privilege
返回: privilege.member, fileCount 等
消费方: inject.js (编辑器功能限制)
3: inject.js 权限对象映射
memberPrivilege → 会员权限(100000图形、导出等)
noMemberPrivilege → 免费版限制(60图形、无导出等)
判断: window.isMember()
3.1 getUser(用户基础信息)
// user.js
getUser: function(userId, callback, options) {
userId = userId || UserOperation.getUk();
if (pcUtil.isOnline()) {
ServerUtil.userInfo(function(res) {
if (res && 200 == res.code && res.data.user) {
let data = res.data;
let user = {
id: data.user.userId,
...data.user, // 包含 expireTime, member 等
totalFileCount: data.auth.fileCount,
auth: data.auth
};
DBUtil.put("users", user, () => { callback(user) });
}
});
} else {
// 离线读本地 DB
DBUtil.findOne("users", userId, (user) => { callback(user || false) });
}
}
用户会员判断关键点
UserOperation.getUser(false, user => {
window.user = user;
pageContext.updataFileNum((data) => {
const totalInfo = Object.assign(user, data);
// 关键判断:终身会员?
const isLongMember = user.expireTime
&& new Date(user.expireTime).getTime() > new Date('2100/01/01').getTime();
if (isLongMember) {
// → 显示 "终身版"
} else if (user.member && expireTime <= 2100/01/01) {
// → 显示 "个人版"
} else {
// → 显示 "免费版"
}
});
});
在这里我遇到一个坑,“isLongMember ”要求 expireTime > 2100/01/01`!用 `2099-12-31` 不够!这跟正常的判断有点出入 厚礼蟹!
3.2 getUserPrivilege(权限详情)
// user.js
getUserPrivilege: function(userId, callback) {
if (!userId || userId == "") userId = UserOperation.getUk();
if (pcUtil.isOnline()) {
ServerUtil.userPrivilege(function(res) {
let data = res.data;
callback({...data.privilege, totalFileCount: data.privilege.fileCount})
});
} else {
callback(JSON.parse(localStorage.getItem("privilege")))
}
}
API 端点: GET /api/pc/user/privilege
3.3 inject.js 权限对象
// inject.js — 会员权限(功能不受限)
const memberPrivilege = {
flowShapeNum: 100000, // 图形数量
mindShapeNum: Infinity,
download: true, // 导出
AIGC: true, // AI 功能
canvasNum: 30, // 画布数量
historyNum: 999999, // 历史版本
// ...
}
// 免费版权限(功能受限)
const noMemberPrivilege = {
flowShapeNum: 60, // 只能 60 个图形!
mindShapeNum: 80,
download: false, // 无导出
AIGC: false, // 无 AI
canvasNum: 1, // 单画布
historyNum: 5,
// ...
}
判断函数:
// isMember 检查 privilege 对象
window.isMember() → privilege.member || privilege.orgMember
4.实施 Patch 修改三个点位
修改点 1:getUserPrivilege(user.js)— 编辑器功能权限
// 原始:
getUserPrivilege:function(e,t=function(){}){e&&""!=e||(e=UserOperation.getUk()),pcUtil.isOnline()?ServerUtil$1.userPrivilege((function(e){let i=e.data;t({...i.privilege,totalFileCount:i.privilege.fileCount})})):t(JSON.parse(localStorage.getItem("privilege")))}
// Patch 后:
getUserPrivilege:function(e,t=function(){}){e&&""!=e||(e=UserOperation.getUk()),pcUtil.isOnline()?ServerUtil$1.userPrivilege((function(e){let i=e.data;i.privilege.expireTime='2099-12-31 23:59:59',i.privilege.orgExpireTime='2100-01-01 23:59:59',i.privilege.inActivity=true,i.privilege.createChartStatus=true,i.privilege.member=true,i.privilege.orgMember=true,t({...i.privilege,totalFileCount:i.privilege.fileCount})})):t(JSON.parse(localStorage.getItem("privilege")))}
注意* 踩坑点 :保留 `ServerUtil$1.userPrivilege` API 调用,在回调拿到真实数据后再覆盖。这样 `privilege` 对象的其他字段(如 `fileCount` 的真实值、`userId` 等)不会丢失
修改点 2:getUser(user.js)— 用户基础信息
// 原始:
DBUtil.put("users",n,(()=>{t(n)}))
// Patch 后:
n.member=true;n.orgMember=true;n.expireTime="2099-12-31 23:59:59";n.orgExpireTime="2099-12-31 23:59:59";n.fileCount=999999;n.totalFileCount=999999;DBUtil.put("users",n,(()=>{t(n)}))
修改点 3:home.js — totalInfo 合并后覆盖
// 原始:
const totalInfo = Object.assign(user, data)
// Patch 后:
const totalInfo = Object.assign(user, data);totalInfo.member=true;totalInfo.orgMember=true;totalInfo.expireTime="2099-12-31 23:59:59";totalInfo.fileCount=999999;totalInfo.totalFileCount=999999
注意* 踩坑点 :因为 `Object.assign(user, data)` 中 `data` 可能覆盖 `user` 的属性(API 返回的 `data` 是最新的),所以需要在合并后再覆盖一次。
5 打包
打包
npx asar pack "***** app.asar“
-----------------------------------
成品效果图:
看了一下功能都齐全,ai功能也是正常的!
这里说一下,其实Electron 真的很简单,只要会点 JS 玩这个东西 不是手拿把掐? 拿到文件就先解包,解了包就 DEV tools
如果你有搞不懂的程序,可以发我,抽空 帮你研究写个教程,记得给我加评分!
|