Skip to main content
Glama
bbaobaob

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

MIT