记一次Typora macOS版破解

0x00 起因

Typora是一款非常好用的Markdown编辑器,但从1.0版本开始转为付费软件。Windows版有现成的基于Electron的破解方案(asar解包+JS Hook注入),但macOS版是原生Cocoa应用,不是Electron,没有app.asar可以解包。

网上找到的其他破解方案为修改前端中的e.hasActivated="true"==e.hasActivated部分实现验证绕过,但在实践后发现此种方法似乎不再适用于mac OS版的Typora。

于是在claude的帮助下逆向一下。

注:本文仅学习交流技术原理,请购买正版许可证支持正版

0x01 初步分析

先看看Typora.app的结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Typora.app/
├── Contents/
│ ├── MacOS/
│ │ └── Typora ← Mach-O universal binary (x86_64 + arm64)
│ ├── Frameworks/
│ │ └── Sparkle.framework
│ └── Resources/
│ ├── TypeMark/
│ │ ├── appsrc/
│ │ │ └── main.js
│ │ ├── page-dist/
│ │ │ ├── license.html
│ │ │ └── static/js/LicenseIndex.*.js
│ │ └── ...
│ └── ...

没有Electron Framework,没有app.asar。file命令确认是原生Mach-O二进制:

1
2
$ file /Applications/Typora.app/Contents/MacOS/Typora
Mach-O universal binary with 2 architectures: [x86_64] [arm64]

代码签名是adhoc(无正式签名),这意味着patch后重签名很方便:

1
2
$ codesign -d --verbose /Applications/Typora.app
Signature=adhoc

0x02 寻找突破口

strings在二进制中搜索关键信息:

1
2
3
4
$ strings Typora | grep -i "api/client"
api/client/renew
api/client/activate
api/client/deactivate

有激活API。再找找公钥:

1
2
3
4
5
6
7
$ strings Typora | grep -A8 "BEGIN PUBLIC KEY"
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA7nVoGCHqIMJyqgALEUrc
5JJhap0+HtJqzPE04pz4y+nrOmY7/12f3HvZyyoRsxKdXTZbO0wEHFIh0cRqsuaJ
...
DwIDAQAB
-----END PUBLIC KEY-----

RSA 2048公钥,出现了2次(x86_64和arm64各一份)。这个公钥用来验证激活签名。

再看看有没有离线激活相关的:

1
2
3
$ strings Typora | grep -i "offlineActivation"
offlineActivation:callback:
offlineActivationMachineCode

有离线激活入口。一种思路:替换二进制中的公钥为自己的,然后用自己的私钥签名生成Activation Token

0x03 逆向激活流程

前端JS层

page-dist/static/js/LicenseIndex.*.js中找到离线激活的调用逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// token 格式检查:必须以 + 开头,# 结尾
if ("+" == t[0] || "#" == t[t.length - 1]) { ... }

// 去掉首尾标记
t = t.substr(1, t.length - 2)

// macOS (window.webkit) 特殊处理
n = t.split("|") // 用 | 分割
a = r[0] // 第一部分:base64编码的JSON
i = r[1] // 第二部分:签名
o = JSON.parse(window.atob(a)) // base64解码并解析JSON
o.sig = i // 把签名加入字典
t = JSON.stringify(o) // 重新序列化

// 传给原生
window.Setting.invokeWithCallback("offlineActivation", t)

所以Token格式为:+base64(JSON)|signature_base64#

原生层:反汇编分析

nm找到关键方法:

1
2
3
4
5
$ nm Typora | grep -i "verifySig\|offlineActivation\|Crypto"
-[LicenseManager offlineActivation:callback:]
-[LicenseManager verifySig:]
-[LicenseManager writeLicenseInfo:with:from:]
+[Crypto verify:with:]

otool反汇编offlineActivation:callback:

1
2
3
4
1. [Utils jsonStringToObject: t]     → 解析JSON为NSDictionary
2. 检查是否为NSDictionary
3.dict["email"] 和 dict["license"]
4. 调用 [self writeLicenseInfo:email with:license from:dict]

writeLicenseInfo:with:from:的逻辑:

1
2
3
1. [self verifySig: dict]            → 验证签名
2. 如果验证失败 → 返回false
3. 验证成功 → 保存license信息

签名验证逻辑

反汇编[LicenseManager verifySig:],核心流程:

1
2
3
4
5
1. dict.allKeys → sortedArrayUsingSelector:compare:  (按字母排序)
2. 遍历排序后的key,跳过 "sig""oldSig""oldFinger"
3. 对每个key:取值 → [StringUtils ensureString:] → stringByAppendingString: 拼接
4. 最终得到一个拼接字符串
5. 调用 [Crypto verify:拼接字符串 with:dict["sig"]]

反汇编[Crypto verify:with:]

1
2
3
4
5
6
7
1. 第一个参数(data):[NSString dataUsingEncoding:NSUTF8StringEncoding]
2. 第二个参数(sig):[NSData initWithBase64EncodedString:options:]
3. SecItemImport 导入硬编码的PEM公钥
4. SecVerifyTransformCreate 创建验证transform
5. SecTransformSetAttribute: kSecDigestTypeAttribute = kSecDigestSHA2
6. SecTransformSetAttribute: kSecDigestLengthAttribute = 256
7. SecTransformExecute 执行验证

确认:SHA256 + PKCS1v15 签名验证

0x04 关键坑点:atob编码问题

这里掉了一个非常隐蔽的坑。

JS前端用window.atob()解码base64,但atob返回的是Latin1 binary string。如果JSON中包含非ASCII字符(比如中文设备名),UTF-8多字节序列会被按Latin1逐字节解码为乱码:

1
2
原始:letr的MacBook Air     (UTF-8: E7 9A 84)
atob后:letrçMacBook Air (Latin1: ç=E7, =9A, =84)

这个乱码后的字符串被JSON.parse解析,然后传给原生。原生拿到的值就是乱码的。

所以签名时必须用atob乱码后的值来拼接,否则签名验证永远不会通过:

1
2
3
4
5
6
7
8
9
// 模拟浏览器 atob 行为
const atobResult = Buffer.from(jsonBase64, 'base64').toString('latin1');
const parsedAfterAtob = JSON.parse(atobResult);

// 用乱码后的值构造签名数据
const sortedKeys = Object.keys(parsedAfterAtob)
.filter(k => !['sig','oldSig','oldFinger'].includes(k))
.sort();
const dataToSign = sortedKeys.map(k => String(parsedAfterAtob[k])).join('');

这个问题调试了很久才发现,是整个破解过程中最隐蔽的一环。

0x05 实现

完整的破解流程:

1. 生成RSA 2048密钥对

1
2
3
4
5
const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', {
modulusLength: 2048,
publicKeyEncoding: { type: 'spki', format: 'pem' },
privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
});

2. 替换二进制中的公钥

新公钥格式化为与原始公钥完全相同的字节长度(450字节),然后在二进制中搜索并替换:

1
2
3
4
5
6
7
8
let binaryData = fs.readFileSync(binPath);
while (true) {
const idx = binaryData.indexOf(originalKeyBuf, offset);
if (idx === -1) break;
newKeyBuf.copy(binaryData, idx);
offset = idx + newKeyBuf.length;
}
fs.writeFileSync(binPath, binaryData);

两个架构(x86_64 + arm64)各替换一处,共2处。

3. 生成Activation Token

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const ACT_ENTITY = { deviceId, fingerprint, email, license, version, date, type };
const licenseJSON = JSON.stringify(ACT_ENTITY);
const jsonBase64 = Buffer.from(licenseJSON, 'utf8').toString('base64');

// 关键:模拟atob乱码
const atobResult = Buffer.from(jsonBase64, 'base64').toString('latin1');
const parsed = JSON.parse(atobResult);

// 按key排序拼接值(跳过sig/oldSig/oldFinger)
const dataToSign = Object.keys(parsed)
.filter(k => !skipKeys.has(k)).sort()
.map(k => String(parsed[k])).join('');

// SHA256 + PKCS1v15 签名
const signature = crypto.sign('SHA256', Buffer.from(dataToSign, 'utf8'), {
key: privateKey, padding: crypto.constants.RSA_PKCS1_PADDING
});

// 组装token
const token = '+' + jsonBase64 + '|' + signature.toString('base64') + '#';

4. Patch 验证服务器地址

仅替换公钥还不够。Typora 启动后会调用 [LicenseManager renew]store.typora.io/api/client/renew 发请求。如果服务器返回了响应,Typora 会用返回的数据再次调用 writeLicenseInfo:with:from:verifySig:。但服务器返回的数据是用官方私钥签名的,我们替换后的公钥验证不通过 → 调用 unfillLicense → 掉激活。

反汇编 renew 的 block_invoke 确认了这个流程:

1
2
3
4
5
6
7
8
9
10
// renew 回调
if (response["success"].boolValue) {
// 用服务器返回的数据验证签名
if (![self writeLicenseInfo:email with:license from:responseDict]) {
[self unfillLicense]; // 验证失败 → 撤销激活!
}
} else {
// 请求失败 → 只递增重试计数器,不撤销
failedCounts++;
}

解决方案:直接在二进制中把服务器 URL 替换为 https://127.0.0.1,让 renew 请求永远连不上。请求失败时 Typora 不会撤销激活。

1
2
3
4
5
6
7
8
9
10
const urlReplacements = [
{ from: 'https://store.typora.io', to: 'https://127.0.0.1' },
{ from: 'https://dian.typora.com.cn', to: 'https://127.0.0.1' }
];
for (const { from, to } of urlReplacements) {
const fromBuf = Buffer.from(from, 'utf8');
const toBuf = Buffer.alloc(fromBuf.length, 0); // null 填充剩余
Buffer.from(to, 'utf8').copy(toBuf);
// 在 binaryData 中搜索替换...
}

两个 URL 各出现 2 次(x86_64 + arm64),共 patch 4 处。这样所有修改都在 app 内部,不需要改 /etc/hosts,回滚时还原 .bak 即可。

5. 重新签名应用

1
codesign --force --deep -s - /Applications/Typora.app

6. 在Typora中激活

打开Typora → 帮助 → 我的许可证 → 离线激活 → 粘贴Token → 激活成功

0x06 总结

对比项 Windows版 macOS版
架构 Electron 原生Cocoa + WebKit
破解方式 asar解包 + JS Hook 二进制公钥替换 + URL patch
签名验证 JS层crypto.publicDecrypt 原生SecVerifyTransformCreate
激活方式 自动(Hook拦截API) 手动(生成Token离线激活)
防掉激活 Hook拦截renew请求 Patch服务器URL为localhost

macOS版的难点在于:

  1. 需要反汇编理解原生验证逻辑
  2. 需要发现atob编码导致的数据不一致问题
  3. 需要精确匹配公钥字节长度
  4. 需要发现renew导致掉激活的问题并patch服务器URL

最终写成了一个脚本,一键完成patch + token生成。

完整脚本 (mac.js)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
#!/usr/bin/env node
/**
* Typora macOS 激活脚本
* 原理:替换二进制中的 RSA 公钥,用自己的私钥签名生成 Activation Token
*/

const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const { spawnSync } = require('child_process');
const readlineSync = require('readline-sync');
const os = require('os');

// ================================
// 配置
// ================================
const CONFIG_FILE = path.join(os.homedir(), '.typora_crack_config.json');
const DEFAULT_APP_PATH = '/Applications/Typora.app';
const ORIGINAL_PUB_KEY = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA7nVoGCHqIMJyqgALEUrc
5JJhap0+HtJqzPE04pz4y+nrOmY7/12f3HvZyyoRsxKdXTZbO0wEHFIh0cRqsuaJ
PyaOOPbA0BsalofIAY3mRhQQ3vSf+rn3g+w0S+udWmKV9DnmJlpWqizFajU4T/E4
5ZgMNcXt3E1ips32rdbTR0Nnen9PVITvrbJ3l6CI2BFBImZQZ2P8N+LsqfJsqyVV
wDkt3mHAVxV7FZbfYWG+8FDSuKQHaCmvgAtChx9hwl3J6RekkqDVa6GIV13D23LS
qdk0Jb521wFJi/V6QAK6SLBiby5gYN6zQQ5RQpjXtR53MwzTdiAzGEuKdOtrY2Me
DwIDAQAB
-----END PUBLIC KEY-----`;

// ================================
// 工具函数
// ================================
function getBinaryPath(appPath) {
return path.join(appPath, 'Contents', 'MacOS', 'Typora');
}

function readCache() {
try {
if (fs.existsSync(CONFIG_FILE)) {
const data = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
if (data.path && fs.existsSync(getBinaryPath(data.path))) {
return data;
}
}
} catch (e) {}
return { path: null, machineCode: null, email: null };
}

function saveCache(obj) {
try { fs.writeFileSync(CONFIG_FILE, JSON.stringify(obj, null, 2), 'utf8'); } catch (e) {}
}

function getNowDateStr() {
const now = new Date();
const dd = String(now.getDate()).padStart(2, '0');
const mm = String(now.getMonth() + 1).padStart(2, '0');
return `${mm}/${dd}/${now.getFullYear()}`;
}

// ================================
// 选择 Typora 路径
// ================================
function chooseTyporaPath() {
const cache = readCache();

// 1. 缓存
if (cache.path) {
console.log(`\n✅ 检测到缓存路径: ${cache.path}`);
const use = readlineSync.question('是否使用此路径?[Y/n]:').trim().toLowerCase();
if (use !== 'n' && use !== 'no') return cache.path;
}

// 2. 默认路径
if (fs.existsSync(getBinaryPath(DEFAULT_APP_PATH))) {
console.log(`\n✅ 检测到默认路径: ${DEFAULT_APP_PATH}`);
const use = readlineSync.question('是否使用此路径?[Y/n]:').trim().toLowerCase();
if (use !== 'n' && use !== 'no') {
saveCache({ ...cache, path: DEFAULT_APP_PATH });
return DEFAULT_APP_PATH;
}
}

// 3. 文件夹选择
try {
const output = spawnSync('osascript', ['-e',
'POSIX path of (choose folder with prompt "请选择 Typora.app 所在目录")'
], { encoding: 'utf8' }).stdout.trim();
if (output) {
let p = path.resolve(output.replace(/\/$/, ''));
if (!p.endsWith('.app')) p = path.join(p, 'Typora.app');
if (fs.existsSync(getBinaryPath(p))) {
saveCache({ ...cache, path: p });
return p;
}
}
} catch (e) {}

// 4. 手动输入
while (true) {
const input = readlineSync.question('请输入 Typora.app 路径:').trim();
if (!input) continue;
const p = path.resolve(input);
if (fs.existsSync(getBinaryPath(p))) {
saveCache({ ...cache, path: p });
return p;
}
console.log('❌ 路径无效,未找到 Typora 二进制');
}
}

// ================================
// 获取机器码和邮箱
// ================================
function getMachineCodeAndEmail() {
const cache = readCache();

if (cache.machineCode && cache.email) {
console.log(`\n✅ 检测到缓存的激活信息:`);
console.log(` 机器码: ${cache.machineCode.substring(0, 30)}...`);
console.log(` 邮箱: ${cache.email}`);
const use = readlineSync.question('是否使用此信息?[Y/n]:').trim().toLowerCase();
if (use !== 'n' && use !== 'no') {
return { machineCode: cache.machineCode, email: cache.email };
}
}

let machineCode, email;
while (true) {
machineCode = readlineSync.question('\n请输入机器码: ').trim();
email = readlineSync.question('请输入邮箱: ').trim();
if (!machineCode) { console.log('❌ 机器码不能为空'); continue; }
if (!email) { console.log('❌ 邮箱不能为空'); continue; }
break;
}

saveCache({ ...cache, machineCode, email });
return { machineCode, email };
}

// ================================
// 回滚还原
// ================================
function rollback(appPath) {
console.log('\n🔄 开始回滚还原...');
const binPath = getBinaryPath(appPath);
const bakPath = binPath + '.bak';

spawnSync('killall', ['Typora'], { stdio: 'ignore' });

if (fs.existsSync(bakPath)) {
fs.copyFileSync(bakPath, binPath);
fs.rmSync(bakPath, { force: true });
console.log('✅ 已还原 Typora 二进制');
} else {
console.log('⚠️ 未找到备份文件,无需回滚');
return;
}

spawnSync('defaults', ['delete', 'abnerworks.Typora', 'typora-license'], { stdio: 'ignore' });
spawnSync('codesign', ['--force', '--deep', '-s', '-', appPath], { stdio: 'ignore' });
console.log('✅ 已清理授权信息并重新签名');
console.log('\n🎉 回滚完成!');
}

// ================================
// 主流程
// ================================
(function main() {
process.stdout.write('\x1Bc');
console.log('=================================');
console.log(' Typora macOS 激活脚本');
console.log('=================================');

// 选择路径
const appPath = chooseTyporaPath();
const binPath = getBinaryPath(appPath);
const bakPath = binPath + '.bak';

// 检查是否已 patch
if (fs.existsSync(bakPath)) {
console.log('\n⚠️ 检测到已激活过(二进制已 patch)');
const choice = readlineSync.question('[R]回滚还原 / [P]重新Patch / [Q]退出:').trim().toUpperCase();
if (choice === 'R') { rollback(appPath); process.exit(0); }
if (choice !== 'P') { process.exit(0); }
// 还原后重新 patch
console.log('还原原始二进制...');
fs.copyFileSync(bakPath, binPath);
fs.rmSync(bakPath, { force: true });
spawnSync('defaults', ['delete', 'abnerworks.Typora', 'typora-license'], { stdio: 'ignore' });
}

// 获取机器码
const { machineCode, email } = getMachineCodeAndEmail();
const mc = JSON.parse(Buffer.from(machineCode, 'base64').toString('utf8'));
console.log(`\ndeviceId: ${mc.l}`);
console.log(`fingerprint: ${mc.i}`);
console.log(`version: ${mc.v}`);

// 关闭 Typora
spawnSync('killall', ['Typora'], { stdio: 'ignore' });

const nowDateStr = getNowDateStr();

// ====== 一、生成密钥对 ======
console.log('\n一、生成密钥对...');
const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', {
modulusLength: 2048,
publicKeyEncoding: { type: 'spki', format: 'pem' },
privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
});

// 格式化公钥为与原始公钥相同的字节长度
const pubKeyBase64 = publicKey
.replace('-----BEGIN PUBLIC KEY-----', '')
.replace('-----END PUBLIC KEY-----', '')
.replace(/\s/g, '');
const lines = [];
for (let i = 0; i < pubKeyBase64.length; i += 64) {
lines.push(pubKeyBase64.slice(i, i + 64));
}
const formattedPubKey = '-----BEGIN PUBLIC KEY-----\n' + lines.join('\n') + '\n-----END PUBLIC KEY-----';
const newKeyBuf = Buffer.from(formattedPubKey, 'utf8');
const originalKeyBuf = Buffer.from(ORIGINAL_PUB_KEY, 'utf8');

if (newKeyBuf.length !== originalKeyBuf.length) {
console.log(`❌ 公钥长度不匹配: 期望 ${originalKeyBuf.length}, 实际 ${newKeyBuf.length}`);
process.exit(1);
}
console.log(`✅ 密钥对生成成功 (${newKeyBuf.length} 字节)`);

// ====== 二、Patch 二进制 ======
console.log('\n二、Patch 二进制...');
fs.copyFileSync(binPath, bakPath);

let binaryData = fs.readFileSync(binPath);
let offset = 0, replacements = 0;
while (true) {
const idx = binaryData.indexOf(originalKeyBuf, offset);
if (idx === -1) break;
newKeyBuf.copy(binaryData, idx);
offset = idx + newKeyBuf.length;
replacements++;
}

if (replacements === 0) {
console.log('❌ 未在二进制中找到原始公钥,可能版本不兼容');
fs.copyFileSync(bakPath, binPath);
fs.rmSync(bakPath, { force: true });
process.exit(1);
}

// 同时 patch 验证服务器 URL,防止 renew 请求导致掉激活
const urlReplacements = [
{ from: 'https://store.typora.io', to: 'https://127.0.0.1' },
{ from: 'https://dian.typora.com.cn', to: 'https://127.0.0.1' }
];
let urlPatched = 0;
for (const { from, to } of urlReplacements) {
const fromBuf = Buffer.from(from, 'utf8');
const toBuf = Buffer.alloc(fromBuf.length, 0); // null 填充
Buffer.from(to, 'utf8').copy(toBuf);
let off = 0;
while (true) {
const idx = binaryData.indexOf(fromBuf, off);
if (idx === -1) break;
toBuf.copy(binaryData, idx);
off = idx + toBuf.length;
urlPatched++;
}
}

fs.writeFileSync(binPath, binaryData);
console.log(`✅ 已替换 ${replacements} 处公钥, ${urlPatched} 处服务器地址`);

// ====== 三、生成 Activation Token ======
console.log('\n三、生成 Activation Token...');

const ACT_ENTITY = {
deviceId: mc.l,
fingerprint: mc.i,
email: email,
license: "Cracked_By_DreamNya&Letr",
version: mc.v,
date: nowDateStr,
type: "DreamNya"
};

// Token 格式: +base64(JSON)|signature#
// 关键:浏览器 atob() 把 UTF-8 多字节字符按 Latin1 解码
// 原生验证时用的是 atob 乱码后的值,所以签名必须基于乱码后的数据
const licenseJSON = JSON.stringify(ACT_ENTITY);
const jsonBase64 = Buffer.from(licenseJSON, 'utf8').toString('base64');

// 模拟浏览器 atob:UTF-8 bytes → Latin1 string
const atobResult = Buffer.from(jsonBase64, 'base64').toString('latin1');
const parsedAfterAtob = JSON.parse(atobResult);

// 按 key 字母排序,跳过 sig/oldSig/oldFinger,拼接值
const skipKeys = new Set(['sig', 'oldSig', 'oldFinger']);
const sortedKeys = Object.keys(parsedAfterAtob).filter(k => !skipKeys.has(k)).sort();
const dataToSign = sortedKeys.map(k => String(parsedAfterAtob[k])).join('');

// SHA256 + PKCS1v15 签名
const signature = crypto.sign('SHA256', Buffer.from(dataToSign, 'utf8'), {
key: privateKey,
padding: crypto.constants.RSA_PKCS1_PADDING
});

const activationToken = '+' + jsonBase64 + '|' + signature.toString('base64') + '#';
console.log('✅ Token 生成成功');

// ====== 四、重新签名 ======
console.log('\n四、重新签名应用...');
const signResult = spawnSync('codesign', ['--force', '--deep', '-s', '-', appPath], {
encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe']
});
if (signResult.status !== 0) {
console.log(`⚠️ 签名警告: ${(signResult.stderr || '').trim()}`);
} else {
console.log('✅ 应用重新签名成功');
}

// ====== 完成 ======
console.log('\n🎉 全部完成!');
console.log('\n使用方法:');
console.log('1. 打开 Typora → 帮助 → 我的许可证');
console.log('2. 点击「获取 Activation Token」进入离线激活页面');
console.log('3. 在 Activation Token 框中粘贴以下内容:\n');
console.log(activationToken);
console.log('\n(Token 已复制到剪贴板)');

try { spawnSync('pbcopy', [], { input: activationToken, encoding: 'utf8' }); } catch (e) {}
})();

仅做技术交流之用,请支持正版软件

在此感谢[讨论]代码整理【Typora激活劫持 支持到1.13.6】 提供参考!


记一次Typora macOS版破解
https://www.letr7.com/2026/05/29/cracking-typora-macos/
作者
letr
发布于
2026年5月29日
更新于
2026年5月29日
许可协议