3105-mcp
by bbaobaob
README.md
# 3105-extract
Công cụ **CLI + MCP server** dùng để xem, mở khóa, giải nén, kiểm tra và tạo ra
**gói patch `.3105`** — định dạng patch của ứng dụng iOS
[3105](https://github.com/YangJiiii/3105), dùng để sửa dữ liệu bên trong container
của app và App Group **trên chính thiết bị của bạn**.
Viết lại bằng Node.js thuần cho Termux/Linux/macOS, nên bạn làm được mọi thứ ngay
trong terminal — hoặc để một **AI agent** làm thay bạn qua MCP server cục bộ.
```
3105PATCH\0 + binary plist envelope + AES-256-GCM wrapped key & payload
```
---
## 1. Giới thiệu
### 1.1 `3105` và `.3105` là gì?
**3105** là một ứng dụng iOS cho phép bạn gom nhiều thay đổi vào một gói patch, rồi
áp gói đó lên container của một app hoặc một App Group **trên máy bạn** — không cần
jailbreak, không cần máy tính, không có gì gửi lên máy chủ của Apple.
Một **gói patch `.3105`** là một file nhị phân, tự chứa, gồm 3 phần:
```
+--------------------+------------------------------+-------------------------------+
| 3105 P A T C H \0 | bplist00 envelope | (ciphertext nằm bên trong) |
| 10 byte ASCII | binary property list | |
+--------------------+------------------------------+-------------------------------+
```
1. **Magic `3105PATCH\0`** — 10 byte đầu tiên: 9 ký tự ASCII `3105PATCH` cộng một
byte NUL (`00`). Magic **không** nằm trong plist, nên mọi trình đọc đều phải kiểm
tra 10 byte này trước đã. Đây là cách nhanh nhất để biết "đây có phải file
`.3105` không".
2. **Envelope** — một **binary property list** (`bplist00`) chứa `schemaVersion`,
`packageID`, `isPasswordProtected`, `keyFingerprint`, `wrappedContentKey`,
`kdfSalt`, `kdfIterations`, `encryptedPayload`. Phần này **luôn đọc được không
cần mật khẩu**.
3. **Payload đã mã hóa** — blob `AES-256-GCM` chứa toàn bộ nội dung patch (danh sách
rule + dữ liệu thay thế), chỉ mở được khi có content key.
Mật khẩu chỉ bảo vệ **content key**, không bảo vệ envelope. Đó là lý do `info` và
`scan` chạy được trên file có mật khẩu mà không cần nhập gì.
Chi tiết byte-level của toàn bộ định dạng: **[docs/FORMAT.md](docs/FORMAT.md)**.
### 1.2 Công cụ này làm được gì?
| Khả năng | Nghĩa là |
| --- | --- |
| **Inspect — Xem** | Đọc metadata công khai mà **không cần mật khẩu**: magic, schema version, package ID, cờ có mật khẩu, tham số KDF, key fingerprint. |
| **Unlock — Mở khóa** | Dẫn xuất content key từ mật khẩu rồi đối chiếu với `keyFingerprint` trong envelope để biết đúng/sai mật khẩu. |
| **Extract — Giải nén** | Giải mã gói thành một *workspace* có thể sửa tay (`settings.json` + `Application/…` + `AppGroup/…`). |
| **Verify — Kiểm tra** | Kiểm tra GCM tag, SHA-256 của từng rule, tính hợp lệ của `packageID`, `bundleID`, `relativePath`. |
| **Create — Tạo** | `pack` ngược workspace thành `.3105` hợp lệ (có hoặc không có mật khẩu), rồi `verify` lại để chắc chắn nó đọc lại được nguyên vẹn. |
Hai điểm vào:
| Điểm vào | Là gì |
| --- | --- |
| `3105x` | CLI — dùng cho terminal, script, pipeline. |
| `3105-mcp` | MCP server trên stdio — cho OpenCode, Claude Code, mọi MCP client khác. |
Công cụ này chỉ **đọc và ghi file patch package**. Nó không chạm vào container của app,
không cần jailbreak, và không gọi lên máy chủ nào. Chính ứng dụng 3105 trên máy bạn
mới là thứ áp patch. Xem [mục 10](#10-an-toàn).
---
## 2. Yêu cầu & cài đặt
| Cần | Chi tiết |
| --- | --- |
| **Node.js** | `>= 18` (đã chạy thử trên Node 24/26 trong Termux) |
| **npm** | đi kèm Node |
| **Không cần gì khác** | không có native module, không gọi mạng, không cần công cụ của Apple |
### 2.1 Trên Termux
```bash
pkg update
pkg install nodejs-lts git
node -v # >= v18
npm -v
```
Đảm bảo `$PREFIX/bin` nằm trong `PATH` (thường đã sẵn, nhưng thêm vào `~/.bashrc`
cho chắc):
```bash
echo 'export PATH="$PREFIX/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc
```
Dùng **Node của Termux**, đừng dùng bản Node cài từ Play Store / máy tính: global
npm prefix của Termux là `$PREFIX/lib/node_modules` và binary nằm ở `$PREFIX/bin`.
### 2.2 Cài từ git
```bash
npm i -g git+https://github.com/bbaobaob/3105-extract.git
```
### 2.3 Cài từ tarball
Không cần git, không cần registry — hữu ích khi chuyển máy:
```bash
# trên máy đã có source
npm install
npm pack # -> 3105-extract-1.0.0.tgz
# trên máy nhận file .tgz
npm i -g ./3105-extract-1.0.0.tgz
```
### 2.4 Kiểm tra đã cài được
```bash
3105x --help
```
Một dòng đầu tiên phải là:
```
3105x - inspect, unlock, extract, verify and create 3105 patch packages
```
Kiểm tra nối MCP server (in dòng `ready:` ra stderr rồi thoát):
```bash
3105-mcp </dev/null
# [3105-mcp] ready: 3105-extract 1.0.0 on stdio (protocol 2024-11-05)
```
> Nếu `3105x: command not found`, xem [docs/INSTALL.md](docs/INSTALL.md) mục 7.
---
## 3. Bắt đầu nhanh
Các file mẫu nằm ở `test/samples/` và **tất cả dùng mật khẩu `JFL`**. Ba lệnh bạn
dùng nhiều nhất:
```bash
cd /path/to/3105-extract
# 1. Xem thông tin — KHÔNG cần mật khẩu
3105x info test/samples/bIscreen.3105
# 2. Xem patch này sửa những gì
3105x list test/samples/bIscreen.3105 --password JFL
# 3. Giải mã ra thư mục để sửa tay
3105x extract test/samples/bIscreen.3105 --password JFL -o ./work/bIscreen
```
**1. `info`** — không cần mật khẩu:
```
file test/samples/bIscreen.3105
packageID 9D4CC6CD-CDB7-45CE-AB02-EACC1E87BA06
schemaVersion 9 (key AAD v9)
protected yes, PBKDF2-HMAC-SHA256 x250000
keyFingerprint feda3fcfef0c8d9d075582b28ffcd1d21cadf169be978baef1f9789a27a29ed8
fileSize 1738 bytes
(project is locked: pass --password to read it)
```
**2. `list`** — xem toàn bộ rule:
```
9D4CC6CD-CDB7-45CE-AB02-EACC1E87BA06 (schema 9)
bundles com.zerone.hidesktop
appGroups group.hidesktop
RULES (2)
CONTAINER BUNDLE PATH OPERATION FILE SIZE SHA256
--------- -------------------- ---------------------------------------------- ---------- -------------------------- ---- ----------------
appGroup group.hidesktop Library/Preferences/group.hidesktop.plist mergePlist group.hidesktop.plist 247B 39461d6213c2141c
app com.zerone.hidesktop Library/Preferences/com.zerone.hidesktop.plist mergePlist com.zerone.hidesktop.plist 147B 71d2574ef2463934
DIRECTORIES (0)
(none)
```
**3. `extract`** — giải mã ra workspace:
```
extracted bIscreen (9D4CC6CD-CDB7-45CE-AB02-EACC1E87BA06, schema 9) to ./work/bIscreen
3 files:
AppGroup/group.hidesktop/Library/Preferences/group.hidesktop.plist
Application/com.zerone.hidesktop/Library/Preferences/com.zerone.hidesktop.plist
manifest.json
```
Sau đó: sửa file trong `./work/bIscreen`, `pack` lại, `verify` lại. Chi tiết ở
mục 4.
---
## 4. Chi tiết từng lệnh
Có **12 subcommand**. Quy ước chung:
- `--json` có thể đặt ở **bất kỳ vị trí nào** trên dòng lệnh; nó in ra **đúng một
object JSON** ra stdout, thay cho output dễ đọc.
- Mọi lỗi in ra **stderr** và **exit code 1**. Thành công là **exit code 0**.
- `3105x --help` in usage và exit **0**; gọi `3105x` không có subcommand in usage và
exit **1**; subcommand lạ in usage kèm lỗi và exit **1**.
- Có thể dùng `--flag=value` hoặc `--flag value`. `-o` là dạng ngắn của
`--output`.
- `3105x --version` in `3105x on Node <version>` (exit 0). Chỉ có tác dụng khi
**không** kèm subcommand.
- **Không có** cờ `--deep` và **không có** `-p`. Mật khẩu chỉ nhận qua
`--password <P>`. Dùng sai cờ sẽ báo `unknown flag`.
### 4.1 `info` — metadata của envelope
Đọc phần công khai của gói. **Không cần mật khẩu.** Nếu gói không có mật khẩu, lệnh
in luôn cả metadata project; nếu có mật khẩu, thêm `--password` mới đọc được phần
đó.
**Cú pháp**
```
3105x info <file> [--password P] [--json]
```
| Flag | Bắt buộc? | Ý nghĩa |
| --- | --- | --- |
| `<file>` | có | Đường dẫn tới file `.3105`. |
| `--password P` | không | Mật khẩu, chỉ cần khi muốn đọc metadata project. |
| `--json` | không | In một object JSON thay cho bảng text. |
**Ví dụ 1 — không mật khẩu**
```console
$ 3105x info test/samples/bIscreen.3105
file test/samples/bIscreen.3105
packageID 9D4CC6CD-CDB7-45CE-AB02-EACC1E87BA06
schemaVersion 9 (key AAD v9)
protected yes, PBKDF2-HMAC-SHA256 x250000
keyFingerprint feda3fcfef0c8d9d075582b28ffcd1d21cadf169be978baef1f9789a27a29ed8
fileSize 1738 bytes
(project is locked: pass --password to read it)
```
**Ví dụ 2 — có mật khẩu, thấy thêm metadata project**
```console
$ 3105x info test/samples/bIscreen.3105 --password JFL
file test/samples/bIscreen.3105
packageID 9D4CC6CD-CDB7-45CE-AB02-EACC1E87BA06
schemaVersion 9 (key AAD v9)
protected yes, PBKDF2-HMAC-SHA256 x250000
keyFingerprint feda3fcfef0c8d9d075582b28ffcd1d21cadf169be978baef1f9789a27a29ed8
fileSize 1738 bytes
project bIscreen
author b
private true
createdAt 2026-09-07T09:07:16.337Z
updatedAt 2026-09-07T09:07:16.337Z
rules 2
directories 0
bundles com.zerone.hidesktop
appGroups group.hidesktop
settings 0 entries
```
**Lỗi thường gặp**
```
3105x: cannot read nope.3105: ENOENT: no such file or directory, open 'nope.3105' [IO]
3105x: not a 3105 patch file (missing "3105PATCH\0" magic) [UNSUPPORTED_FORMAT]
3105x: authentication failed: wrong password or corrupted patch data [DECRYPT_FAILED]
```
> Trong thông báo thật, `\0` ở trên là **một byte NUL thật** (byte `00` cuối magic).
> Ở đây ghi bằng `\0` cho dễ đọc.
---
### 4.2 `list` — toàn bộ rule, thư mục và bundle
Giải mã gói rồi liệt kê từng rule (bundle, loại container, đường dẫn, operation, kích
thước, SHA-256) cùng danh sách bundle / App Group. **Cần mật khẩu** nếu gói có mật
khẩu.
**Cú pháp**
```
3105x list <file> [--password P] [--json]
```
| Flag | Bắt buộc? | Ý nghĩa |
| --- | --- | --- |
| `<file>` | có | Đường dẫn tới file `.3105`. |
| `--password P` | tuỳ | Bắt buộc với gói có mật khẩu. |
| `--json` | không | In một object JSON. |
**Ví dụ 1 — dạng bảng**
```console
$ 3105x list test/samples/bWidgy.3105 --password JFL
8BD2B5BC-EC53-4192-A7EF-EB1DD33D2AC7 (schema 9)
bundles -
appGroups group.woodsign.widgy
RULES (1)
CONTAINER BUNDLE PATH OPERATION FILE SIZE SHA256
--------- -------------------- -------------------------------- --------- ------------------ ----- ----------------
appGroup group.woodsign.widgy widgyDefaults/tiny-storage.plist replace tiny-storage.plist 1088B 64bbb55b93719d73
DIRECTORIES (1)
group.woodsign.widgy/widgyDefaults
```
**Ví dụ 2 — dạng JSON**
```console
$ 3105x list test/samples/bIscreen.3105 --password JFL --json
{
"file": "test/samples/bIscreen.3105",
"packageID": "9D4CC6CD-CDB7-45CE-AB02-EACC1E87BA06",
"schemaVersion": 9,
"bundles": ["com.zerone.hidesktop"],
"appGroups": ["group.hidesktop"],
"directories": [],
"rules": [
{
"id": "F5046C99-F94C-48EB-99CA-DB15F5633999",
"bundleID": "group.hidesktop",
"containerKind": "appGroup",
"relativePath": "Library/Preferences/group.hidesktop.plist",
"operation": "mergePlist",
"replacementFilename": "group.hidesktop.plist",
"bytes": 247,
"sha256": "39461d6213c2141c0d352e94a29650604bbf5f61889bc970a573c78f6d38fe45"
}
]
}
```
Ghi chú: `containerKind` là `app` (container của app, đọc ra là `application`) hoặc
`appGroup`. `operation` là `replace`, `mergePlist` hoặc `mergeJson`.
**Lỗi thường gặp**
```
3105x: this patch is password protected: a password is required [PASSWORD_REQUIRED]
3105x: authentication failed: wrong password or corrupted patch data [DECRYPT_FAILED]
```
---
### 4.3 `unlock` — kiểm tra mật khẩu, lấy content key
Dẫn xuất content key AES-256 từ mật khẩu, in ra dạng hex, và so với `keyFingerprint`
trong envelope. Đây là cách **rẻ nhất** để biết mật khẩu đúng hay sai mà không phải
giải mã cả payload. `fingerprint` phải là `MATCH`, nếu không lệnh **exit 1**.
**Cú pháp**
```
3105x unlock <file> --password P [--json]
```
| Flag | Bắt buộc? | Ý nghĩa |
| --- | --- | --- |
| `<file>` | có | Đường dẫn tới file `.3105`. |
| `--password P` | **có** | Không có cờ rút gọn. |
| `--json` | không | In một object JSON. |
**Ví dụ**
```console
$ 3105x unlock test/samples/bIscreen.3105 --password JFL
packageID 9D4CC6CD-CDB7-45CE-AB02-EACC1E87BA06
schemaVersion 9
keyAADVersion 9
contentKey 3301d19e4724a1c54a13dbb392cee77c1699459b909888b05df4a1fe0718ef3e
keyFingerprint feda3fcfef0c8d9d075582b28ffcd1d21cadf169be978baef1f9789a27a29ed8
fingerprint MATCH
```
> `contentKey` là **khoá bí mật của gói patch**. Không in nó ra log, tin nhắn hay
> issue công khai. Chỉ dùng `unlock` để *kiểm tra* rồi bỏ qua dòng đó.
**Lỗi thường gặp**
```
3105x: --password needs a value [USAGE]
3105x: authentication failed: wrong password or corrupted patch data [DECRYPT_FAILED]
fingerprint MISMATCH # exit 1 — sai mật khẩu hoặc package bị sửa
```
---
### 4.4 `extract` — giải mã ra workspace
Giải mã toàn bộ gói và ghi ra thư mục có thể sửa tay. Thư mục đích được tạo nếu
chưa có; mọi thành phần đường dẫn đều được kiểm tra nên không thể ghi ra ngoài
`-o`.
**Cú pháp**
```
3105x extract <file> -o DIR [--password P] [--json]
```
| Flag | Bắt buộc? | Ý nghĩa |
| --- | --- | --- |
| `<file>` | có | Đường dẫn tới file `.3105`. |
| `-o DIR` / `--output DIR` | **có** | Thư mục đích. |
| `--password P` | tuỳ | Bắt buộc với gói có mật khẩu. |
| `--json` | không | In một object JSON. |
Cấu trúc workspace:
```
work/bIscreen/
manifest.json # manifest trích xuất
settings.json # chỉ có khi project có settings
Application/com.zerone.hidesktop/Library/Preferences/com.zerone.hidesktop.plist
AppGroup/group.hidesktop/Library/Preferences/group.hidesktop.plist
```
**Ví dụ 1 — giải nén một gói**
```console
$ 3105x extract test/samples/bIscreen.3105 --password JFL -o ./work/bIscreen
extracted bIscreen (9D4CC6CD-CDB7-45CE-AB02-EACC1E87BA06, schema 9) to ./work/bIscreen
3 files:
AppGroup/group.hidesktop/Library/Preferences/group.hidesktop.plist
Application/com.zerone.hidesktop/Library/Preferences/com.zerone.hidesktop.plist
manifest.json
```
**Ví dụ 2 — dạng JSON**
```console
$ 3105x extract test/samples/bIscreen.3105 --password JFL -o ./work/x --json
{
"file": "test/samples/bIscreen.3105",
"outDir": "/data/data/com.termux/files/home/work/x",
"packageID": "9D4CC6CD-CDB7-45CE-AB02-EACC1E87BA06",
"schemaVersion": 9,
"written": [
"AppGroup/group.hidesktop/Library/Preferences/group.hidesktop.plist",
"Application/com.zerone.hidesktop/Library/Preferences/com.zerone.hidesktop.plist",
"manifest.json"
]
}
```
Ghi chú: `settings.json` **chỉ** được ghi khi project có ít nhất một phần tử
settings — nên `bIscreen` (0 entries) chỉ ra 3 file ở trên.
**Lỗi thường gặp**
```
3105x: missing required option -o/--output [USAGE]
3105x: -o needs a value [USAGE]
3105x: missing required argument <file> [USAGE]
```
---
### 4.5 `pack` — đóng gói workspace thành `.3105`
Đọc ngược một workspace rồi ghi ra file `.3105`. Giữ nguyên `packageID`, `createdAt`,
`updatedAt`, id của rule và `operation` khi có trong `manifest.json`; phần thiếu sẽ
tự sinh. Thư mục cha của file đích được tạo tự động.
**Cú pháp**
```
3105x pack DIR -o FILE [--password P] [--schema 3|9] [--name N] [--author A]
[--iterations N] [--private] [--json]
```
| Flag | Bắt buộc? | Ý nghĩa |
| --- | --- | --- |
| `DIR` | có | Workspace (output của `extract`). |
| `-o FILE` / `--output FILE` | **có** | File `.3105` đích. |
| `--password P` | không | Đặt mật khẩu. Bỏ trống ⇒ gói **không** mật khẩu. |
| `--schema 3\|9` | không | Ép `schemaVersion`. Mặc định: tự suy ra (xem mục 9). |
| `--name N` | không | Ghi đè `project.name`. |
| `--author A` | không | Ghi đè `project.author`. |
| `--iterations N` | không | Số vòng PBKDF2. Mặc định `250000`, hợp lệ `100000..1000000`. |
| `--private` | không | Đặt `isPrivate = true` (gói private ⇒ phải có mật khẩu). |
| `--json` | không | In một object JSON. |
**Ví dụ 1 — pack có mật khẩu, đổi tên**
```console
$ 3105x pack ./work/bIscreen -o ./out/bIscreen-patched.3105 --password JFL \
--name "bIscreen (mine)" --author "tac gia"
packed 2 rules from ./work/bIscreen
file ./out/bIscreen-patched.3105
packageID 9D4CC6CD-CDB7-45CE-AB02-EACC1E87BA06
schemaVersion 9
protected yes
kdfIterations 250000
keyFingerprint 6d412976398530f6964c77506348855eb34ce77827ff034699f4f0c9057e201b
fileSize 1768 bytes
```
**Ví dụ 2 — pack không mật khẩu, ép schema 9**
```console
$ 3105x pack ./work/bIscreen -o ./out/plain.3105 --schema 9
packed 2 rules from ./work/bIscreen
file ./out/plain.3105
packageID 9D4CC6CD-CDB7-45CE-AB02-EACC1E87BA06
schemaVersion 9
protected no
kdfIterations -
keyFingerprint 9ad8a4ae830be35ef0d78316c3901b46803443283fa5e015235d311947a62b20
fileSize 1644 bytes
```
Sau khi pack, **luôn** `verify` lại (mục 4.6).
**Lỗi thường gặp**
```
3105x: cannot read workspace /tmp/nope: ENOENT: ... [WORKSPACE_NOT_FOUND]
3105x: --schema must be 3 or 9 [USAGE]
3105x: kdfIterations must be an integer between 100000 and 1000000, got 100 [VALIDATION]
3105x: manifest.json is not valid JSON: ... [VALIDATION]
```
---
### 4.6 `verify` — kiểm tra toàn vẹn
Chạy toàn bộ kiểm tra: magic → envelope → `keyFingerprint` → tham số KDF → mở khoá
content key → so fingerprint → giải mã payload → parse plist → kiểm tra cấu trúc
project → so `packageID` → so SHA-256 của **từng** rule. In từng bước với `[ok]` hoặc
`[!!]`. `ok` khi tất cả pass, `FAILED` khi có bước fail — và **exit 1**.
**Cú pháp**
```
3105x verify <file> [--password P] [--json]
```
| Flag | Bắt buộc? | Ý nghĩa |
| --- | --- | --- |
| `<file>` | có | Đường dẫn tới file `.3105`. |
| `--password P` | tuỳ | Không có nó thì các bước sau `keyUnwrap` sẽ fail. |
| `--json` | không | In `{ file, ok, checks[], errors[] }`. |
**Ví dụ 1 — verify thành công**
```console
$ 3105x verify test/samples/bIscreen.3105 --password JFL
OK test/samples/bIscreen.3105
[ok] magic: 3105PATCH\0 magic present
[ok] envelope: envelope parsed (schemaVersion 9, packageID 9D4CC6CD-CDB7-45CE-AB02-EACC1E87BA06)
[ok] keyFingerprint: keyFingerprint is 32 bytes
[ok] kdfIterations: kdfIterations 250000 (allowed 100000..1000000)
[ok] kdfSalt: kdfSalt is 16 bytes
[ok] wrappedKey: wrappedContentKey is 60 bytes
[ok] keyUnwrap: content key unwrapped
[ok] contentKeyFingerprint: contentKey fingerprint e57818b33ac98cf4430d0100d369e924fa9f85d8351e1322cf14b9e68b8e695c
[ok] payloadDecrypt: payload decrypted (1301 bytes)
[ok] payloadPlist: payload is a binary plist dictionary
[ok] projectStructure: project bIscreen with 2 rule(s)
[ok] packageIDMatch: project id 9D4CC6CD-CDB7-45CE-AB02-EACC1E87BA06 vs envelope 9D4CC6CD-CDB7-45CE-AB02-EACC1E87BA06
[ok] ruleDigests: 2 rule digest(s) match
```
**Ví dụ 2 — verify thiếu mật khẩu**
```console
$ 3105x verify test/samples/bIscreen.3105; echo "exit=$?"
FAILED test/samples/bIscreen.3105
[ok] magic: 3105PATCH\0 magic present
...
[ok] wrappedKey: wrappedContentKey is 60 bytes
[!!] keyUnwrap: password required for a protected package: rerun with --password
exit=1
```
**Lỗi thường gặp**
| Dòng `[!!]` | Nguyên nhân |
| --- | --- |
| `keyUnwrap: password required…` | Thiếu `--password`. |
| `contentKeyFingerprint: …` | Sai mật khẩu hoặc package bị sửa. |
| `payloadDecrypt: …` | Sai mật khẩu, AAD sai, hoặc dữ liệu hỏng. |
| `ruleDigests: rule … has no replacementDigests entry` | Payload bị sửa tay mà chưa `pack` lại. |
---
### 4.7 `read` — đọc nội dung một rule
In `replacementData` của **một** rule mà không ghi gì ra đĩa. Nếu nội dung là UTF-8
hợp lệ thì in dạng text; ngược lại in `base64(N bytes):` kèm chuỗi base64.
**Cú pháp**
```
3105x read <file> <relativePath> [--password P] [--json]
```
| Flag | Bắt buộc? | Ý nghĩa |
| --- | --- | --- |
| `<file>` | có | Đường dẫn tới file `.3105`. |
| `<relativePath>` | có | Đường dẫn như `list` in ra. |
| `--password P` | tuỳ | Bắt buộc với gói có mật khẩu. |
| `--json` | không | In một object JSON, kèm `encoding` và `content`. |
Nếu `relativePath` không khớp, tool thử lại bằng `replacementFilename`.
**Ví dụ 1 — payload là text thì in thẳng nội dung**
```console
$ 3105x read test/samples/bPVZ.3105 "Documents/userdata/user1_json.dat" --password JFL
{"NewAchievementUnlcoked":false,"NoAdsPackActive":true,"NoAdsPackExpiry":0,"ReplayingLevel":-1,"SpeedBoostFTUDone":false…
...,"version":126,"zengardenTutorialComplete":false,"zombiesKilled":0}
```
(Nội dung thật dài 69530 byte; ở trên chỉ cắt hai đầu cho dễ đọc.)
**Ví dụ 2 — payload là bplist thì ra base64**
```console
$ 3105x read test/samples/bIscreen.3105 \
"Library/Preferences/group.hidesktop.plist" --password JFL
base64(247 bytes):
YnBsaXN0MDDSAQIDBFxIaVdpZGdldFZpcHNcZ3JvdXBfaGFzVmlwTxCneyJpc1RyeSI6ZmFsc2UsInByb2R1Y3RJRCI6...
```
Muốn đọc nội dung plist cho dễ hiểu: `3105x read … --json` rồi chuyển base64 sang file
rồi dùng `3105x convert` (mục 4.12), hoặc đơn giản hơn là `extract` rồi `convert`.
**Lỗi thường gặp**
```
3105x: no rule with relativePath "nope.plist"; available: Library/Preferences/group.hidesktop.plist, Library/Preferences/com.zerone.hidesktop.plist [USAGE]
3105x: missing required argument <relativePathInPayload> [USAGE]
```
---
### 4.8 `settings` — nội dung `settings.json`
In phần settings của project (mảng các trường có kiểu mà người dùng điền, dùng cho
placeholder `{{…}}`). Không có settings thì in `[]`.
**Cú pháp**
```
3105x settings <file> --password P [--json]
```
| Flag | Bắt buộc? | Ý nghĩa |
| --- | --- | --- |
| `<file>` | có | Đường dẫn tới file `.3105`. |
| `--password P` | **có** | Luôn cần để giải mã payload. |
| `--json` | không | In `{ file, packageID, schemaVersion, settings }`. |
**Ví dụ**
```console
$ 3105x settings test/samples/bIscreen.3105 --password JFL
[]
$ 3105x settings test/samples/bIscreen.3105 --password JFL --json
{
"file": "test/samples/bIscreen.3105",
"packageID": "9D4CC6CD-CDB7-45CE-AB02-EACC1E87BA06",
"schemaVersion": 9,
"settings": []
}
```
**Lỗi thường gặp**
```
3105x: this patch is password protected: a password is required [PASSWORD_REQUIRED]
```
---
### 4.9 `scan` — tóm tắt cả thư mục
Liệt kê mọi `*.3105` trong một thư mục (không đệ quy vào thư mục con) và in envelope
của từng cái. **Không cần mật khẩu**; nếu có `--password` thì cột `NAME`/`PRIVATE`
cũng được điền. File không đọc được hiện `error` thay vì làm hỏng cả lần quét.
**Cú pháp**
```
3105x scan <dir> [--json]
```
| Flag | Bắt buộc? | Ý nghĩa |
| --- | --- | --- |
| `<dir>` | có | Thư mục chứa file `.3105`. |
| `--password P` | không | **Không cần** để đọc envelope. Nếu có, `scan` còn dùng nó để điền thêm cột `NAME` / `PRIVATE`. |
| `--json` | không | In `{ dir, count, patches[] }`. |
**Ví dụ 1 — không mật khẩu**
```console
$ 3105x scan test/samples
NAME FILE SCHEMA PRIVATE FINGERPRINT SIZE
---- ------------- ------ ------- ---------------- -------
- bFIMO.3105 3 - ab86caeeae9ef486 170460B
- bIscreen.3105 9 - feda3fcfef0c8d9d 1738B
- bPVZ.3105 3 - 5f8f9e04c95591d3 70615B
- bWidgy.3105 9 - c161e6b8e2a32b07 2266B
```
**Ví dụ 2 — có mật khẩu thì `NAME` và `PRIVATE` được điền**
```console
$ 3105x scan test/samples --password JFL
NAME FILE SCHEMA PRIVATE FINGERPRINT SIZE
-------- ------------- ------ ------- ---------------- -------
b'FIMO bFIMO.3105 3 yes ab86caeeae9ef486 170460B
bIscreen bIscreen.3105 9 yes feda3fcfef0c8d9d 1738B
b'PVZ bPVZ.3105 3 yes 5f8f9e04c95591d3 70615B
b'Widgy bWidgy.3105 9 yes c161e6b8e2a32b07 2266B
```
Thư mục không có file `.3105`: in `no *.3105 files in <dir>` và exit **0**.
**Lỗi thường gặp**
```
3105x: cannot read directory /tmp/nope: ENOENT: ... [IO]
```
---
### 4.10 `batch-extract` — giải nén cả thư mục
Giải nén mọi `*.3105` trong thư mục bằng **một** mật khẩu, mỗi gói ra một thư mục con
đặt theo tên file (bỏ đuôi `.3105`). Cái nào lỗi được ghi riêng; **exit 1** nếu có
bất kỳ cái nào lỗi.
**Cú pháp**
```
3105x batch-extract <dir> -o DIR [--password P] [--json]
```
| Flag | Bắt buộc? | Ý nghĩa |
| --- | --- | --- |
| `<dir>` | có | Thư mục chứa file `.3105`. |
| `-o DIR` / `--output DIR` | **có** | Thư mục đích. |
| `--password P` | tuỳ | Bắt buộc nếu có gói có mật khẩu. |
| `--json` | không | In `{ dir, outDir, extracted[], errors[] }`. |
**Ví dụ**
```console
$ 3105x batch-extract test/samples -o ./work/batch --password JFL
extracted 4/4 patches to ./work/batch
bFIMO.3105 -> ./work/batch/bFIMO (2 files)
bIscreen.3105 -> ./work/batch/bIscreen (3 files)
bPVZ.3105 -> ./work/batch/bPVZ (2 files)
bWidgy.3105 -> ./work/batch/bWidgy (2 files)
```
Trong thư mục trộn lẫn gói có mật khẩu và gói không mật khẩu với **hai** mật khẩu
khác nhau thì `batch-extract` không phù hợp — chạy `extract` cho từng gói.
**Lỗi thường gặp**
```
3105x: missing required option -o/--output [USAGE]
# và các dòng kiểu:
bPVZ.3105 -> FAILED: authentication failed: wrong password or corrupted patch data
```
---
### 4.11 `merge-preview` — xem trước kết quả merge
Áp **đúng ngữ nghĩa merge của 3105** cho một file: lấy `replacementData` của rule làm
payload, truyền cùng nội dung hiện tại của file đích (base64) vào merge engine, rồi
trả về byte đã merge (base64) + `notes`. **Dry-run hoàn toàn, không ghi gì ra đĩa.**
**Cú pháp**
```
3105x merge-preview <file> --password P --target <relativePath>
--content-base64 B64 [--json]
```
| Flag | Bắt buộc? | Ý nghĩa |
| --- | --- | --- |
| `<file>` | có | File `.3105`. |
| `--password P` | **có** | Giải mã payload. |
| `--target <relativePath>` | **có** | Đường dẫn rule cần merge. |
| `--content-base64 B64` | **có** | Nội dung **hiện tại** của file đích, mã hoá base64. |
| `--json` | không | In một object JSON. |
**Ví dụ**
```console
$ 3105x merge-preview test/samples/bIscreen.3105 --password JFL \
--target "Library/Preferences/com.zerone.hidesktop.plist" \
--content-base64 "$(printf '<plist/>' | base64 -w0)"
com.zerone.hidesktop/Library/Preferences/com.zerone.hidesktop.plist (mergePlist)
format plist
target 8 bytes
merged 385 bytes sha256 e75329878b62e3b7af52e03887124f0d8d1a7a27a4b43cb16859026de972bfd7
notes:
- target could not be parsed (<plist/> has no root value); merging into an empty target
- target Library/Preferences/com.zerone.hidesktop.plist is empty: the payload becomes the result
merged (base64):
PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPCFET0NUWVBFIHBsaXN0IFBVQkxJQyAi...
```
`notes` cho biết merge engine làm gì: key mới được thêm, key trùng bị ghi đè, dict
lồng nhau gộp đệ quy, mảng và giá trị vô hướng bị thay trọn vẹn, và các directive
`%3105_Remove%` / `%3105_Overwrite%` / `%3105_MatchPrefix%…`.
**Lỗi thường gặp**
```
3105x: missing required option --target [USAGE]
3105x: missing required option --content-base64 [USAGE]
3105x: --content-base64 must be non-empty base64 [USAGE]
```
---
### 4.12 `convert` — chuyển đổi plist ⇄ JSON ⇄ XML
Chuyển một **payload file độc lập** (không phải file `.3105`) giữa binary plist,
XML plist và JSON. Đây là công cụ để *nhìn* nội dung plist mà không cần mở app.
**Cú pháp**
```
3105x convert <file> [--from auto|plist|json|xml] [--to json|xml|plist] [--json]
```
| Flag | Bắt buộc? | Ý nghĩa |
| --- | --- | --- |
| `<file>` | có | File payload cần chuyển. |
| `--from ...` | không | Mặc định `auto` (dò `bplist00` / `<?xml` / `<plist` / JSON). `plist` hiểu cả binary lẫn XML. |
| `--to ...` | không | Mặc định `json`. Chỉ nhận `json`, `xml`, `plist`. |
| `--json` | không | In kết quả dạng object JSON kèm `contentBase64`. |
Không phải file `.3105`, không cần mật khẩu.
**Ví dụ 1 — binary plist ra XML**
```console
$ 3105x convert group.bplist --to xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>HiWidgetVips</key>
<data>eyJpc1RyeSI6ZmFsc2UsInByb2R1Y3RJRCI6ImNvbS56ZXJvbmUuaGlkZXNrdG9wLmZvcmV2ZXIzIiwi…</data>
<key>group_hasVip</key>
<true/>
</dict>
</plist>
```
(`<data>` là **một dòng dài**, ở trên đã cắt ngắn. XML do `convert` sinh ra có key
đã sắp xếp, nên đầu ra ổn định — thuận tiện để diff.)
**Ví dụ 2 — ra binary plist thì nhận base64**
```console
$ 3105x convert group.bplist --to plist --json
{
"file": "./group.bplist",
"from": "bplist",
"to": "plist",
"bytes": 247,
"sha256": "39461d6213c2141c0d352e94a29650604bbf5f61889bc970a573c78f6d38fe45",
"content": null,
"contentBase64": "YnBsaXN0MDDSAQIDBFxIaVdpZGdldFZpcHNcZ3JvdXBfaGFzVmlwTxCneyJpc1RyeSI6..."
}
```
Không có `--json` thì `convert` in thẳng kết quả ra stdout, nên đổi định dạng bằng
shell:
```bash
3105x convert group.bplist --to xml > group.xml
3105x convert group.xml --to json
```
**Lỗi thường gặp**
```
3105x: --to must be plist, xml or json [USAGE]
3105x: input is neither a binary plist, an XML plist nor JSON [VALIDATION]
```
---
## 5. MCP server cho AI agent
`3105-mcp` là một **MCP server trên stdio** (JSON-RPC 2.0, mỗi frame một dòng). Nhờ vậy
một AI agent (OpenCode, Claude Code, …) có thể tự gọi đúng công cụ thay vì tự viết code
mã hoá.
Server **không có flag nào**. Đừng truyền `--help`. Chạy `3105-mcp </dev/null` để
kiểm tra nó khởi động được không.
### 5.1 Đăng ký vào OpenCode
Sửa `~/.config/opencode/opencode.json` (thêm hoặc gộp vào khối `mcp` sẵn có):
```json
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"3105": {
"type": "local",
"command": [
"node",
"/data/data/com.termux/files/home/src/3105-extract/bin/3105-mcp.mjs"
],
"enabled": true
}
}
}
```
Lưu ý:
- `command` là **mảng argv**, không phải chuỗi shell — không cần quote, không dùng
được `&&`.
- Nếu OpenCode chạy ngoài Termux shell của bạn, `node` phải resolve được theo `PATH`
của tiến trình đó. An toàn nhất là dùng đường dẫn tuyệt đối:
`/data/data/com.termux/files/usr/bin/node`.
- Sửa xong **khởi động lại** OpenCode (config chỉ đọc lúc startup).
- Muốn dùng bản global cài bằng npm, trỏ thẳng vào binary:
```json
"command": ["3105-mcp"]
```
Với Claude Code:
```bash
claude mcp add 3105 -- node /data/data/com.termux/files/home/src/3105-extract/bin/3105-mcp.mjs
```
### 5.2 Danh sách 14 tools
| Tool | Làm gì | Cần mật khẩu |
| --- | --- | --- |
| `3105_patch_info` | Metadata envelope của một gói: magic, schema, packageID, cờ mật khẩu, fingerprint, kích thước, sha256. Trả cả metadata project nếu gói không có mật khẩu. | Không |
| `3105_batch_info` | Metadata envelope cho **nhiều gói trong một lệnh** (tối đa 200 path). File hỏng được báo `ok:false` chứ không làm hỏng cả lệnh. | Không |
| `3105_patch_scan` | Quét thư mục `*.3105`, báo envelope từng cái. | Không |
| `3105_patch_unlock` | Dẫn xuất content key (hex) + kết quả so fingerprint. Dùng để xác nhận mật khẩu trước khi giải nén. | Có |
| `3105_patch_list` | Giải mã và liệt kê **mọi rule**: bundleID, containerKind, relativePath, operation, kích thước, sha256, cùng bundles/appGroups/settings và tổng. | Có |
| `3105_patch_settings` | Nội dung `settings.json` của gói. | Có |
| `3105_patch_extract_file` | Đọc **một** `replacementData` mà không ghi gì. Trả text UTF-8 nếu là text, ngược lại base64 (hoặc luôn base64 với `base64:true`). | Có |
| `3105_patch_extract` | Giải mã toàn bộ workspace (`settings.json`, `manifest.json`, `Application/…`, `AppGroup/…`), trả list file đã ghi kèm kích thước. | Có |
| `3105_patch_batch_extract` | Giải nén mọi gói trong thư mục bằng một mật khẩu, một thư mục con mỗi gói, tách success/failure. | Có |
| `3105_patch_pack` | Workspace → `.3105` mới (tuỳ chọn password, `schemaVersion`, `kdfIterations`, `name`, `author`), rồi **tự verify** kết quả. | Tuỳ chọn |
| `3105_patch_verify` | Kiểm tra đầy đủ: magic, envelope, AAD/derivation, xác thực payload và **mọi** digest của rule. Trả từng bước kèm cờ ok. | Tuỳ chọn |
| `3105_patch_merge_preview` | Dry-run ngữ nghĩa merge 3105 cho một file; trả byte đã merge (base64), format và notes. Không ghi đĩa. | Có |
| `3105_convert` | Chuyển payload file: JSON ⇄ XML plist ⇄ binary plist. `from=auto` dò `bplist00` / XML / JSON. `{"$data":"<base64>"}` là cách biểu diễn plist `<data>`. | Không |
| `3105_format_guide` | Cheat-sheet định dạng nhúng sẵn: magic, envelope, PBKDF2 250k/16B, AES-256-GCM, hai chuỗi AAD, cấu trúc payload, layout workspace. Không có tham số. | Không |
### 5.3 Khi nào nên để agent "chơi" (defer) với công cụ này
Nên gọi MCP tools khi:
- người dùng nhắc tới `.3105`, "patch 3105", "giải mã patch", "patch này sửa gì",
"tạo patch 3105", "bplist patch package";
- cần biết một patch **làm gì** mà không muốn đọc tay binary plist;
- cần giải nén hàng loạt gói trong một thư mục;
- cần tạo patch mới rồi **kiểm chứng** nó đọc lại được;
- cần xem nội dung plist/JSON của một payload cho dễ đọc.
Nên **defer** (bảo agent tự làm / hỏi lại) khi:
- người dùng hỏi về app 3105 nhưng **không** phải về file `.3105`;
- file không có magic `3105PATCH\0` — đó không phải patch 3105, đừng cố;
- người dùng muốn áp patch lên container của **app của người khác** hoặc thiết bị
không phải của họ;
- chưa có mật khẩu — hãy hỏi, đừng đoán;
- cần chỉnh sửa byte thô của file `.3105` — hãy dùng `extract` → sửa → `pack`.
---
## 6. Tab completion
`3105x` **không tự** bật completion. Cách nhanh nhất gõ `u` rồi `Tab` để ra `unlock`:
```console
$ 3105x u<Tab>
unlock
$ 3105x <Tab>
batch-extract convert extract info list merge-preview pack read scan settings unlock verify
```
### 6.1 Cách dùng
File completion nằm ở `completions/3105x.bash` trong repo, và **có** được đóng gói
kèm vào tarball khi `npm pack`. Source nó từ `~/.bashrc`:
```bash
echo 'source /data/data/com.termux/files/home/src/3105-extract/completions/3105x.bash' >> ~/.bashrc
source ~/.bashrc
```
Nếu bạn cài global bằng npm và không giữ checkout source, tìm file đó trong bản cài:
```bash
find "$(npm root -g)/3105-extract" -name '3105x.bash'
# $(npm root -g)/3105-extract/completions/3105x.bash
```
Sau khi source, kiểm tra:
```bash
complete -p 3105x
# complete -F _3105x 3105x
```
Cơ chế: `3105x u<Tab>` → `unlock`; `3105x pack --<Tab>` → danh sách flag hợp lệ **của
đúng subcommand đó** (`scan` không có `--password`, `pack` có đủ 8 flag); đối số file
thì hoàn thành theo filesystem, và `scan` / `batch-extract` / `pack` hoàn thành theo
**thư mục**.
### 6.2 Biết trước
Bản `completions/3105x.bash` hiện tại gợi ý thêm hai thứ mà CLI **không** chấp nhận:
- `help` như một subcommand (CLI chỉ có `--help`).
- `--out` cho `extract` / `pack` / `batch-extract` (CLI chỉ nhận `--output`, viết tắt là
`-o`).
Gõ sai sẽ báo `unknown flag` / `unknown command` và exit 1 — vô hại, nhưng đừng lấy
các gợi ý đó làm chuẩn. Danh sách cờ đúng nằm ở [mục 4](#4-chi-tiết-từng-lệnh).
Nếu bản cài của bạn không có thư mục `completions/`, đây là một đoạn tương đương đã
kiểm thử, bạn có thể dán thẳng vào `~/.bashrc`:
```bash
_3105x() {
local cur prev sub flags
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
# Các flag có giá trị: hoàn thành bằng đường dẫn.
case "$prev" in
-o|--output|--password|--schema|--name|--author|--iterations|--target|--content-base64|--from|--to)
COMPREPLY=( $(compgen -f -- "$cur") ); return 0 ;;
esac
sub="${COMP_WORDS[1]}"
case "$sub" in
unlock|settings|info|list|extract|verify|read) flags="--password --json --help" ;;
pack) flags="-o --output --password --schema --name --author --iterations --private --json --help" ;;
batch-extract) flags="-o --output --password --json --help" ;;
merge-preview) flags="--password --target --content-base64 --json --help" ;;
convert) flags="--from --to --json --help" ;;
scan) flags="--json --help" ;;
*) flags="--json --help --version" ;;
esac
# Đang gõ một flag: chỉ gợi ý các flag hợp lệ cho subcommand đó.
if [[ "$cur" == -* ]]; then
COMPREPLY=( $(compgen -W "$flags" -- "$cur") ); return 0
fi
# Chưa chọn subcommand: gợi ý 12 subcommand.
if [[ "$COMP_CWORD" -eq 1 ]]; then
COMPREPLY=( $(compgen -W "info list unlock extract pack verify read settings scan batch-extract merge-preview convert" -- "$cur") )
return 0
fi
# Đã chọn subcommand: thư mục hay file.
case "$sub" in
scan|batch-extract|pack) COMPREPLY=( $(compgen -d -- "$cur") ) ;;
*) COMPREPLY=( $(compgen -f -- "$cur") ) ;;
esac
return 0
}
complete -F _3105x 3105x
```
---
## 7. Scripts hữu ích
### `install-skills.sh` — cài 2 skill cho OpenCode
Script này chép (không chạy) hai skill OpenCode vào `~/.config/opencode/skills/`:
| Skill | Dùng khi nào |
| --- | --- |
| `3105-patch` | Xem / mở khóa / giải nén / kiểm tra một patch có sẵn. |
| `3105-patch-author` | Tự tạo patch mới và kiểm tra trước khi chia sẻ. |
Cách dùng:
```bash
cd /path/to/3105-extract
./install-skills.sh # cài / làm mới cả hai skill
./install-skills.sh --dest DIR # cài vào thư mục khác
./install-skills.sh --remove # gỡ ra
./install-skills.sh --help # in hướng dẫn
```
Mặc định đích là `$HOME/.config/opencode/skills`, hoặc `$OPENCODE_SKILLS_DIR` nếu bạn
đặt biến môi trường đó. Sau khi cài, **khởi động lại OpenCode** để nó nạp skill.
Làm thủ công (tương đương):
```bash
mkdir -p ~/.config/opencode/skills
cp -r skills/3105-patch ~/.config/opencode/skills/
cp -r skills/3105-patch-author ~/.config/opencode/skills/
```
---
## 8. Định dạng file
Tóm tắt ba ý, chi tiết byte-level ở **[docs/FORMAT.md](docs/FORMAT.md)**:
- **Magic** — 10 byte đầu: ASCII `3105PATCH` + byte NUL (`62 33 31 30 35 50 41 54 43
48 00`). Nằm **ngoài** plist, phải kiểm tra trước khi parse.
- **Envelope** — binary property list (`bplist00`) chứa `schemaVersion`,
`keyAADVersion`, `packageID`, `isPasswordProtected`, `kdfSalt`, `kdfIterations`,
`wrappedContentKey` / `publicContentKey`, `keyFingerprint`, `encryptedPayload`.
Đọc được không cần mật khẩu.
- **Payload** — `AES-256-GCM` với `contentKey` sinh từ
`PBKDF2-HMAC-SHA256(password, kdfSalt, 250000, 32)`. Payload là một binary plist:
`{ project: { … }, replacementDigests: { <rule id>: data(32) } }`.
Sơ đồ pipeline mã hoá (đọc ngược chiều khi giải mã):
```
password (UTF-8)
│
PBKDF2-HMAC-SHA256(password, kdfSalt, kdfIterations, dkLen=32)
│
wrappingKey (32B)
│
AES-256-GCM open(wrappedContentKey, wrappingKey, "3105PATCH/v{n}/key/{packageID}")
│
contentKey (32B)
│ ← so SHA-256(contentKey) với keyFingerprint (ngoài mã hoá)
│
AES-256-GCM open(encryptedPayload, contentKey, "3105PATCH/v{n}/payload/{packageID}")
│
payload (binary plist) → project + replacementDigests
```
Hai chuỗi AAD phải khớp **từng byte**; `keyAADVersion` (mặc định bằng
`schemaVersion`) quyết định phiên bản của chuỗi AAD phía key.
Áp dụng patch — app làm gì với từng rule:
| `operation` | Tác dụng |
| --- | --- |
| `replace` | Ghi đè toàn bộ file tại `relativePath`. |
| `mergePlist` | Gộp dict đệ quy vào plist đích. |
| `mergeJson` | Gộp như trên với JSON; mảng và giá trị vô hướng bị thay trọn vẹn, không gộp. |
Thêm các directive trong payload: `%3105_Remove%`, `%3105_Overwrite%`,
`%3105_MatchPrefix%<prefix>`, và placeholder `{{field}}` lấy từ `settings.json`.
---
## 9. Ghi chú về `schemaVersion`
Có **hai** hệ schema, nhưng **cùng một envelope**:
- **v1 – v3 — có trong mã nguồn công khai** (github.com/YangJiiii/3105, tag v1.0.0 …
v2.0): `latestSchemaVersion = 3`, `minimumSchemaVersion = 1`. v2 thêm
`keyAADVersion` + `directories` / `bundleIdentifiers`; v3 thêm `author` +
`isPrivate` (private ⇒ cần mật khẩu ⇒ từ v3 trở lên). Magic, cách serialize và
hệ mã hoá **giống hệt** nhau suốt v1–v3.
- **v4 – v9 — chỉ có trong binary app 2.0 đã phát hành**: các trường `settings`,
ánh xạ App Group, `containerKind`, `operation`. Mã nguồn công khai chưa có.
Envelope **không đổi một byte nào**: cùng magic, cùng khoá envelope, cùng
PBKDF2/AES-GCM, cùng cơ chế AAD. Gói thật từ app đó mang `schemaVersion = 9` và
`keyAADVersion = 9`.
Tool này hiểu payload v3 và v9, và chuẩn hoá chúng về một model thống nhất khi đọc
nên `extract` → sửa → `pack` không mất thông tin.
**Chọn schema khi `pack`:**
| Tình huống | Nên dùng |
| --- | --- |
| Chỉ `replace`, không có App Group, không có settings | `--schema 3` — tương thích đọc rộng nhất. |
| Cần `mergePlist` / `mergeJson`, `containerKind`, App Group, `settings` | `--schema 9` (bắt buộc — các trường này không tồn tại ở v3). |
| Không chắc | Bỏ trống `--schema`: tool tự suy ra (có dữ liệu v9 hoặc operation ≠ `replace` ⇒ 9, ngược lại 3). |
Một ứng dụng cũ hơn gói nó mở sẽ **từ chối ở cổng envelope** (`unsupportedVersion`).
Nếu gói của bạn không thật sự cần trường riêng của v9, hãy xuất `--schema 3`.
---
## 10. An toàn
- **Chỉ dùng với thiết bị và dữ liệu của chính bạn.** Công cụ này đọc và ghi *file
patch package*; nó **không** chạm vào container của app, **không** cần jailbreak và
**không** gọi lên máy chủ nào của Apple. Chính ứng dụng 3105 trên máy bạn mới là
thứ áp patch vào container.
- **Sao lưu trước khi áp patch.** Áp patch ghi đè dữ liệu trong container của app.
Export dữ liệu gốc ra một bản sao trước đã.
- **Mật khẩu không được lưu ở đâu.** `3105-extract` không có config, không có keyring,
không có cache: mật khẩu chỉ tồn tại trong bộ nhớ của tiến trình và biến mất khi
tiến trình kết thúc. Nhưng nó **có** xuất hiện trong:
- **lịch sử shell** — vì nằm trên dòng lệnh. Xoá bằng `history -d <số>` hoặc
`history -c`, và tránh `ps` lộ tham số khi chạy dài.
- **log của công cụ khác** (bao gồm transcript của AI agent) — đừng bảo agent in
ra mật khẩu.
- MCP: **mỗi tool call tự mang tham số `password` của riêng nó**, không có mật khẩu
mặc định toàn cục, và không có nơi nào lưu lại.
- **`unlock` in ra content key.** Đó là khoá bí mật của gói patch. Dùng nó để *kiểm
tra* rồi bỏ qua; không lưu, không chia sẻ.
- **Xem `.3105` của người khác là input không tin cậy.** Nó là một plist sẽ được ghi
vào thư mục dữ liệu của một app, với đường dẫn và `bundleID` do người khác chọn.
- **`verify` trước khi chia sẻ** bất kỳ gói nào bạn tự build.
- Chỉ có 2 dependency (`bplist-parser`, `bplist-creator`), không có native module, không
có telemetry, không có network call.
---
## 11. Đóng góp & phát triển
### Chạy test
```bash
npm install
npm test
```
Bộ test giải mã **package `.3105` thật**. Các file mẫu là **riêng tư và không được
commit** (`.gitignore` loại `test/samples/*.3105`), nên hãy bỏ file của bạn vào
`test/samples/` — bất kỳ tên nào kết thúc bằng `.3105` — và đặt mật khẩu trong
`test/run-tests.mjs`. **Không có mẫu thì bộ test báo skip sạch và vẫn xanh:**
```
SKIP no .3105 samples in test/samples (private, not committed)
```
### Cấu trúc thư mục
```
3105-extract/
├── bin/
│ ├── 3105x.mjs # CLI entry point
│ └── 3105-mcp.mjs # MCP stdio server (JSON-RPC 2.0 thuần, không SDK)
├── src/
│ ├── cli.mjs # phân tích argv + 12 lệnh + output text/JSON
│ ├── format.mjs # magic, parse/build envelope binary plist
│ ├── crypto.mjs # PBKDF2, AES-256-GCM, unwrap content key, fingerprint
│ ├── model.mjs # validate / normalize / denormalize project v3 <-> v9
│ ├── pack.mjs # inspect, decodePatch, encodePatch, verifyPatch
│ ├── workspace.mjs # đọc & ghi workspace (safeJoin chống path traversal)
│ ├── merge.mjs # ngữ nghĩa merge 3105 + chuyển đổi plist/JSON/XML
│ └── mcp/tools.mjs # định nghĩa 14 MCP tool + FORMAT_GUIDE
├── docs/
│ ├── FORMAT.md # đặc tả định dạng ở mức byte
│ └── INSTALL.md # cài đặt sâu, MCP, troubleshooting
├── skills/
│ ├── 3105-patch/ # skill OpenCode: xem / giải nén / kiểm tra
│ └── 3105-patch-author/ # skill OpenCode: tạo patch mới
├── completions/3105x.bash # bash completion cho 3105x
├── test/
│ ├── run-tests.mjs
│ └── samples/*.3105 # riêng tư, không commit
├── install-skills.sh
├── package.json # ESM, Node >= 18, bin: 3105x + 3105-mcp
└── README.md
```
### Tài liệu khác
| Tài liệu | Nội dung |
| --- | --- |
| [docs/FORMAT.md](docs/FORMAT.md) | Đặc tả byte-level: khoá envelope, PBKDF2/AES-GCM + chuỗi AAD, schema project v3/v9, layout workspace, ngữ nghĩa merge, ví dụ giải mã một gói v9 thật, công thức ghi file, bẫy thường gặp. |
| [docs/INSTALL.md](docs/INSTALL.md) | Cài đặt sâu: Termux, 3 cách cài, smoke test, đăng ký MCP, kiểm soát `schemaVersion`, gỡ cài đặt, bảng xử lý sự cố. |
| [skills/3105-patch/SKILL.md](skills/3105-patch/SKILL.md) | Skill OpenCode cho luồng xem / giải nén / kiểm tra. |
| [skills/3105-patch-author/SKILL.md](skills/3105-patch-author/SKILL.md) | Skill OpenCode cho luồng tạo và kiểm chứng patch. |
## Giấy phép
MITThis server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues