From fb3534553b0a6fe8a1c30e26cfebc6bfdf1bec91 Mon Sep 17 00:00:00 2001 From: Stefan Hacker Date: Sun, 26 Apr 2026 12:09:39 +0200 Subject: [PATCH] initial aubox skeleton: web-UI, kirin DLOAD, firmware library MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - FastAPI Web-UI auf 127.0.0.1:8080 mit Geräte-Live-Erkennung, sandboxed File-Browser, Firmware-Library (SQLite + Auto-Identifikation) - Huawei update.app Parser: extrahiert Hardware-ID, Section-Layout, BOOT/SYSTEM-Vorhandensein direkt aus den Headern - Kirin Download-Mode: hisi-idt-Protokoll-Implementation gegen pyusb - USB-Erkennung für Huawei (DLOAD/Fastboot-D), Google, MediaTek, Qualcomm EDL - Huawei-P10-Lite-Workflow (eRecovery + Testpoint-DLOAD-Pfade) - Docker-Compose mit USB-Passthrough (Major 189) für Re-Enumeration - udev-Regeln + Setup-Script für Debian/Ubuntu/Pi-OS Co-Authored-By: Claude Opus 4.7 (1M context) --- .dockerignore | 8 ++ Dockerfile | 33 +++++ README.md | 84 ++++++++++++ aubox/__init__.py | 1 + aubox/__main__.py | 3 + aubox/cli.py | 119 +++++++++++++++++ aubox/filebrowse.py | 70 ++++++++++ aubox/kirin.py | 144 ++++++++++++++++++++ aubox/library/__init__.py | 9 ++ aubox/library/db.py | 94 +++++++++++++ aubox/library/huawei.py | 148 ++++++++++++++++++++ aubox/library/identify.py | 92 +++++++++++++ aubox/p10lite.py | 93 +++++++++++++ aubox/usb.py | 82 ++++++++++++ aubox/web/__init__.py | 0 aubox/web/app.py | 163 +++++++++++++++++++++++ aubox/web/static/app.js | 50 +++++++ aubox/web/static/style.css | 136 +++++++++++++++++++ aubox/web/templates/_devices.html | 20 +++ aubox/web/templates/base.html | 27 ++++ aubox/web/templates/browse.html | 36 +++++ aubox/web/templates/devices.html | 9 ++ aubox/web/templates/firmware.html | 38 ++++++ aubox/web/templates/firmware_detail.html | 23 ++++ aubox/web/templates/index.html | 39 ++++++ aubox/web/templates/p10lite.html | 15 +++ docker-compose.yml | 28 ++++ docs/huawei-p10-lite.md | 119 +++++++++++++++++ firmware/README.md | 49 +++++++ loaders/README.md | 59 ++++++++ loaders/kirin/.gitkeep | 0 requirements.txt | 4 + scripts/run-docker.sh | 30 +++++ scripts/setup-linux.sh | 40 ++++++ udev/51-android-unlock.rules | 18 +++ 35 files changed, 1883 insertions(+) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 aubox/__init__.py create mode 100644 aubox/__main__.py create mode 100644 aubox/cli.py create mode 100644 aubox/filebrowse.py create mode 100644 aubox/kirin.py create mode 100644 aubox/library/__init__.py create mode 100644 aubox/library/db.py create mode 100644 aubox/library/huawei.py create mode 100644 aubox/library/identify.py create mode 100644 aubox/p10lite.py create mode 100644 aubox/usb.py create mode 100644 aubox/web/__init__.py create mode 100644 aubox/web/app.py create mode 100644 aubox/web/static/app.js create mode 100644 aubox/web/static/style.css create mode 100644 aubox/web/templates/_devices.html create mode 100644 aubox/web/templates/base.html create mode 100644 aubox/web/templates/browse.html create mode 100644 aubox/web/templates/devices.html create mode 100644 aubox/web/templates/firmware.html create mode 100644 aubox/web/templates/firmware_detail.html create mode 100644 aubox/web/templates/index.html create mode 100644 aubox/web/templates/p10lite.html create mode 100644 docker-compose.yml create mode 100644 docs/huawei-p10-lite.md create mode 100644 firmware/README.md create mode 100644 loaders/README.md create mode 100644 loaders/kirin/.gitkeep create mode 100644 requirements.txt create mode 100755 scripts/run-docker.sh create mode 100755 scripts/setup-linux.sh create mode 100644 udev/51-android-unlock.rules diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..8c7733e --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +.venv/ +__pycache__/ +*.pyc +loaders/ +.git/ +docs/ +*.md +!README.md diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3d6c2d4 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,33 @@ +# Container für aubox. Funktioniert auf x86_64 und arm64 (Pi 4) gleichermaßen. +# +# Bauen: +# docker build -t aubox . +# +# Start mit USB-Passthrough siehe scripts/run-docker.sh. +FROM debian:bookworm-slim + +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update && apt-get install -y --no-install-recommends \ + python3 \ + python3-pip \ + android-tools-adb \ + android-tools-fastboot \ + libusb-1.0-0 \ + usbutils \ + ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +COPY requirements.txt ./ +RUN pip3 install --no-cache-dir --break-system-packages -r requirements.txt + +COPY aubox/ ./aubox/ + +# loaders/ und firmware/ werden zur Laufzeit als Volumes gemountet — nicht ins +# Image kopieren. Damit sind Hersteller-Binaries nie im Image-Layer. + +EXPOSE 8080 +ENTRYPOINT ["python3", "-m", "aubox"] +CMD ["web", "--host", "0.0.0.0", "--port", "8080"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..dcd4114 --- /dev/null +++ b/README.md @@ -0,0 +1,84 @@ +# android-unlock-and-more-box + +Open-Source-Werkzeugkasten für Android-Recovery (FRP, Bootloader-Tasks, NV-Reparatur). +Läuft auf jedem Linux-PC oder Pi 4 — der PC ist in der Regel die bessere Wahl +(schneller bei CRC, schneller bei Firmware-Dumps, mehr RAM). Pi 4 ist nett +wenn man ein dediziertes Werkstatt-Gerät will. Kein Voodoo, keine Wunder — +Aggregation öffentlicher Protokolle und Workflows. + +## Status + +Pre-Alpha. Aktueller Fokus: **Huawei P10 Lite (WAS-LX1, Kirin 658)** ohne +funktionierenden Bootloader-Unlock und ohne IMEI/NV. + +Siehe [docs/huawei-p10-lite.md](docs/huawei-p10-lite.md). + +## Was das hier ist — und was nicht + +**Ist:** +- Lokale Web-UI (FastAPI, kein Build-Step, kein Node) +- Geräte-Erkennung über USB-VID:PID, live im Browser +- Datei-Browser sandboxed auf die Firmware-Library +- SQLite-Index der Firmware-Library mit Auto-Identifikation + (Huawei `update.app` voll, MTK/Samsung als Stub) +- Wrapper um `adb`, `fastboot`, `pyusb` +- Implementierung des Kirin-Download-Mode-Protokolls (hisi-idt) +- Geräte-spezifische Workflows mit klaren Schritt-für-Schritt-Anweisungen +- Läuft auf jedem Linux-System: PC, Pi 4, Container + +**Ist nicht:** +- Ersatz für Octopus/UnlockTool/Z3X — die Datenbanken und Loader-Sammlungen + dort sind das eigentliche Asset. Hier baust du dir das selbst auf. +- Magic-Exploit-Sammlung. Wenn ein SoC keinen öffentlichen BROM-Bypass hat + (z.B. Kirin 658), braucht es leider Hersteller-Loader, die du selbst + beschaffen musst (siehe `loaders/README.md`). + +## Zwei Wege zum Start + +### A) Nativ (einfachste Variante) + +Funktioniert auf Debian/Ubuntu/Mint/Pi OS gleichermaßen. + +```bash +sudo bash scripts/setup-linux.sh # einmalig: Pakete + udev + venv +source .venv/bin/activate +python -m aubox detect +python -m aubox p10lite frp-remove --method erecovery +``` + +### B) Web-UI im Container (empfohlen) + +```bash +sudo bash scripts/setup-linux.sh # udev-Regeln müssen auf dem Host liegen +docker compose up -d +``` + +Browser auf . Fertig. Smartphone per USB anstecken, +Geräte-Seite zeigt es live mit dem erkannten Modus. + +Was die Compose-Datei macht: +- `./firmware` ↔ `/firmware` (read-write, hier liegt deine Firmware-Library + SQLite-Index) +- `./loaders` ↔ `/loaders` (read-only, Hersteller-Loader) +- `/dev/bus/usb` ↔ `/dev/bus/usb` (komplett, damit Re-Enumeration funktioniert) +- Port 8080 nur an 127.0.0.1 gebunden — nichts geht ins LAN raus + +### C) Container per Wrapper-Script (für CLI-Nutzung) + +```bash +./scripts/run-docker.sh detect +./scripts/run-docker.sh p10lite frp-remove --method dload-erase +``` + +### PC vs. Pi 4 — kurze Entscheidungshilfe + +| Kriterium | Linux-PC | Pi 4 | +|-------------------------|--------------------|----------------------------| +| Geschwindigkeit | viel schneller | ok | +| Firmware-Dumps (>4 GB) | spürbar besser | langsam (USB shared) | +| Dediziertes Werkstatt-Gerät | nein | ja, kann immer laufen | +| Strom/Mobilität | schlechter | besser | +| USB-3-Bandbreite | volles xHCI | ~250 MB/s effektiv | + +## Lizenz + +Privat, noch nicht entschieden. diff --git a/aubox/__init__.py b/aubox/__init__.py new file mode 100644 index 0000000..f102a9c --- /dev/null +++ b/aubox/__init__.py @@ -0,0 +1 @@ +__version__ = "0.0.1" diff --git a/aubox/__main__.py b/aubox/__main__.py new file mode 100644 index 0000000..eb53e2f --- /dev/null +++ b/aubox/__main__.py @@ -0,0 +1,3 @@ +from .cli import main + +raise SystemExit(main()) diff --git a/aubox/cli.py b/aubox/cli.py new file mode 100644 index 0000000..5b8ed4a --- /dev/null +++ b/aubox/cli.py @@ -0,0 +1,119 @@ +"""CLI-Einstiegspunkt: `python -m aubox `.""" +from __future__ import annotations + +import argparse +import sys +import time + +from . import p10lite, usb +from .kirin import KirinDload + + +def cmd_detect(_args: argparse.Namespace) -> int: + devs = usb.scan() + if not devs: + print("Kein bekanntes Hersteller-Gerät angeschlossen.") + print("Probiere `lsusb` für die rohe Liste.") + return 1 + for d in devs: + print(d) + if d.mode.notes: + print(f" {d.mode.notes}") + return 0 + + +def cmd_p10lite_frp(args: argparse.Namespace) -> int: + if args.method == "erecovery": + print(p10lite.erecovery_instructions()) + return 0 + + if args.method == "dload-erase": + try: + plan = p10lite.dload_erase_plan() + except FileNotFoundError as e: + print(str(e), file=sys.stderr) + return 2 + + print("Plan:") + for stage in plan["stages"]: + print(f" - {stage['label']}: {stage['file']} -> 0x{stage['addr']:08x}") + print() + print("Bitte jetzt Testpoint setzen und USB anschließen.") + print("Warte auf Kirin DLOAD (12d1:1100)...") + + for _ in range(60): + if usb.find_first("kirin-dload") is not None: + break + time.sleep(0.5) + else: + print("Timeout: kein Kirin DLOAD erschienen.", file=sys.stderr) + return 3 + + print("Gerät erkannt. Sende Loader-Stages...") + with KirinDload() as k: + for stage in plan["stages"]: + print(f" -> {stage['label']}") + k.send_loader(stage["file"], stage["addr"]) + # Zwischen den Stages re-enumeriert das Gerät evtl. + time.sleep(2) + + print("Loader-Stages gesendet. Erwarteter Folgemodus:", + plan["post"]) + print("Nächster Schritt (manuell, bis Workflow ausgereift):") + print(" fastboot devices") + print(f" fastboot erase {plan['frp_partition']}") + return 0 + + print(f"Methode {args.method!r} noch nicht implementiert.", file=sys.stderr) + return 2 + + +def cmd_web(args: argparse.Namespace) -> int: + import uvicorn + uvicorn.run( + "aubox.web.app:app", + host=args.host, + port=args.port, + reload=args.reload, + log_level="info", + ) + return 0 + + +def build_parser() -> argparse.ArgumentParser: + p = argparse.ArgumentParser(prog="aubox") + sub = p.add_subparsers(dest="cmd", required=True) + + sp_detect = sub.add_parser("detect", help="Angeschlossene Geräte erkennen") + sp_detect.set_defaults(func=cmd_detect) + + sp_web = sub.add_parser("web", help="Lokale Web-UI starten") + sp_web.add_argument("--host", default="127.0.0.1", + help="Bind-Adresse (Default: 127.0.0.1, im Container 0.0.0.0)") + sp_web.add_argument("--port", type=int, default=8080) + sp_web.add_argument("--reload", action="store_true", + help="Auto-Reload bei Code-Änderungen (Entwicklung)") + sp_web.set_defaults(func=cmd_web) + + sp_p10 = sub.add_parser("p10lite", help="Workflows für Huawei P10 Lite") + p10_sub = sp_p10.add_subparsers(dest="p10_cmd", required=True) + + sp_frp = p10_sub.add_parser("frp-remove", help="Google-Account-Sperre entfernen") + sp_frp.add_argument( + "--method", + choices=["erecovery", "dload-erase", "dload-flash"], + default="erecovery", + help="Welcher Pfad? Default: erecovery (sicherster, kein Loader nötig)", + ) + sp_frp.set_defaults(func=cmd_p10lite_frp) + + return p + + +def main(argv: list[str] | None = None) -> int: + args = build_parser().parse_args(argv) + return args.func(args) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/aubox/filebrowse.py b/aubox/filebrowse.py new file mode 100644 index 0000000..5dfdbab --- /dev/null +++ b/aubox/filebrowse.py @@ -0,0 +1,70 @@ +"""Sandbox-fähiger Datei-Browser. + +Egal welcher Pfad reinkommt — er wird gegen ein konfiguriertes Root-Verzeichnis +geprüft. Path-Traversal (`../..`) ist damit ausgeschlossen. +""" +from __future__ import annotations + +from dataclasses import dataclass +from pathlib import Path + + +class PathEscapeError(PermissionError): + """Pfad würde aus dem erlaubten Root ausbrechen.""" + + +@dataclass +class Entry: + name: str + rel_path: str # relativ zum Root, mit '/' Separator + is_dir: bool + size: int # Bytes (0 für Dirs) + + +def safe_resolve(root: Path, rel: str) -> Path: + """Pfad relativ zum Root auflösen und verifizieren, dass er drin bleibt.""" + root = root.resolve() + rel_clean = rel.lstrip("/").strip() + target = (root / rel_clean).resolve() + try: + target.relative_to(root) + except ValueError as e: + raise PathEscapeError(f"Pfad {rel!r} verlässt Root {root}") from e + return target + + +def list_dir(root: Path, rel: str = "") -> tuple[Path, list[Entry]]: + """Inhalt eines Verzeichnisses listen, Dirs zuerst, dann Files alphabetisch.""" + target = safe_resolve(root, rel) + if not target.exists(): + raise FileNotFoundError(target) + if not target.is_dir(): + raise NotADirectoryError(target) + + entries: list[Entry] = [] + for child in sorted(target.iterdir(), key=lambda p: (not p.is_dir(), p.name.lower())): + if child.name.startswith("."): + continue + rel_child = str(child.relative_to(root.resolve())).replace("\\", "/") + try: + size = child.stat().st_size if child.is_file() else 0 + except OSError: + size = 0 + entries.append(Entry( + name=child.name, + rel_path=rel_child, + is_dir=child.is_dir(), + size=size, + )) + return target, entries + + +def breadcrumbs(rel: str) -> list[tuple[str, str]]: + """Liste von (Label, Pfad) für die Anzeige als Breadcrumb-Navigation.""" + parts = [p for p in rel.split("/") if p] + out = [("/", "")] + cur = "" + for p in parts: + cur = f"{cur}/{p}" if cur else p + out.append((p, cur)) + return out diff --git a/aubox/kirin.py b/aubox/kirin.py new file mode 100644 index 0000000..a98246e --- /dev/null +++ b/aubox/kirin.py @@ -0,0 +1,144 @@ +"""Kirin Download-Mode (HiSilicon) — hisi-idt-Protokoll. + +Wenn ein Kirin-Gerät per Testpoint zur Erde gezogen und gebootet wird, +landet es im "DLOAD"-Modus mit USB-VID:PID 12d1:1100. In diesem Modus +hat es selbst noch keinen Speicher initialisiert — es wartet darauf, +dass der Host einen *xloader* (kleiner sekundärer Bootloader) per USB +schickt. Erst danach kommt der nächste Stage-Loader (`usb_loader.bin`), +und dann fährt das Gerät als erweiterter Fastboot hoch. + +Das Protokoll dafür heißt im Huawei-Kosmos schlicht "hisi-idt". Es ist +nicht öffentlich dokumentiert, aber seit Jahren bekannt aus dem +hisi-idt.py-Skript, das u.a. im Kirin-Tools-Umfeld kursiert. Hier eine +saubere Implementation gegen pyusb. + +WICHTIG: + Die Loader-Dateien (`hisi-sec_usb_xloader.bin`, `usb_loader.bin`) + sind Huawei-signierte Binaries. Du musst sie selbst beschaffen + und in `loaders/kirin//` ablegen. Siehe loaders/README.md. + +Diese Implementation orientiert sich am öffentlichen Protokoll-Wissen. +Auf realer P10-Lite-Hardware vor produktivem Einsatz validieren. +""" +from __future__ import annotations + +import struct +import time +from pathlib import Path + +import usb.core +import usb.util + +KIRIN_DLOAD_VID = 0x12D1 +KIRIN_DLOAD_PID = 0x1100 + +# Protokoll-Konstanten (hisi-idt) +HEAD_TAG = 0xFE +TAIL_TAG = 0xFE +TYPE_DATA = 0xDA +TYPE_HEAD = 0xA5 +CHUNK = 0x800 # 2 KiB Payload pro Frame +TIMEOUT_MS = 5000 + + +def _crc16_xmodem(data: bytes) -> int: + """CRC-16/XMODEM (Polynom 0x1021, init 0).""" + crc = 0 + for b in data: + crc ^= b << 8 + for _ in range(8): + if crc & 0x8000: + crc = (crc << 1) ^ 0x1021 + else: + crc <<= 1 + crc &= 0xFFFF + return crc + + +def _frame(seq: int, payload: bytes, frame_type: int) -> bytes: + """Ein hisi-idt-Frame bauen. + + Layout: + HEAD (1) | TYPE (1) | SEQ (2 BE) | PAYLOAD | CRC16 (2 BE) | TAIL (1) + """ + body = struct.pack(">BBH", HEAD_TAG, frame_type, seq) + payload + crc = _crc16_xmodem(body[1:]) # CRC ohne HEAD + return body + struct.pack(">HB", crc, TAIL_TAG) + + +class KirinDload: + """Verbindung zum Kirin im Download-Mode.""" + + def __init__(self) -> None: + self.dev: usb.core.Device | None = None + self.ep_out = None + self.ep_in = None + + def open(self) -> None: + dev = usb.core.find(idVendor=KIRIN_DLOAD_VID, idProduct=KIRIN_DLOAD_PID) + if dev is None: + raise RuntimeError( + f"Kein Kirin-DLOAD-Gerät gefunden " + f"({KIRIN_DLOAD_VID:04x}:{KIRIN_DLOAD_PID:04x}). " + f"Testpoint sitzen? USB-Kabel datentauglich?" + ) + if dev.is_kernel_driver_active(0): + dev.detach_kernel_driver(0) + dev.set_configuration() + cfg = dev.get_active_configuration() + intf = cfg[(0, 0)] + self.ep_out = usb.util.find_descriptor( + intf, + custom_match=lambda e: usb.util.endpoint_direction(e.bEndpointAddress) + == usb.util.ENDPOINT_OUT, + ) + self.ep_in = usb.util.find_descriptor( + intf, + custom_match=lambda e: usb.util.endpoint_direction(e.bEndpointAddress) + == usb.util.ENDPOINT_IN, + ) + if self.ep_out is None: + raise RuntimeError("Kein OUT-Endpunkt gefunden — falsche USB-Konfiguration?") + self.dev = dev + + def close(self) -> None: + if self.dev is not None: + usb.util.dispose_resources(self.dev) + self.dev = None + + def __enter__(self) -> "KirinDload": + self.open() + return self + + def __exit__(self, *exc) -> None: + self.close() + + def send_loader(self, path: Path, load_addr: int) -> None: + """Loader-Binary in einer Folge von Frames an das Gerät senden. + + Args: + path: Pfad zur xloader/usb_loader-Datei. + load_addr: Zieladresse im SRAM. Pro SoC unterschiedlich. + Für Kirin 658 (P10 Lite, xloader) typischerweise 0x07012000. + In der Doku des SoCs verifizieren. + """ + data = path.read_bytes() + self._send_header(load_addr, len(data)) + seq = 1 + for offset in range(0, len(data), CHUNK): + chunk = data[offset : offset + CHUNK] + self._write(_frame(seq, chunk, TYPE_DATA)) + seq = (seq + 1) & 0xFFFF + # Kleines Delay — Kirin akzeptiert zu schnelle Bursts nicht zuverlässig + time.sleep(0.001) + # Sequenz abschließen: einige Implementationen senden ein finales Frame + # mit Länge 0. Bei Bedarf hier ergänzen, je nach SoC-Verhalten. + + def _send_header(self, addr: int, size: int) -> None: + """Header-Frame: sagt dem Gerät, wohin und wieviel.""" + payload = struct.pack(">II", addr, size) + self._write(_frame(0, payload, TYPE_HEAD)) + + def _write(self, buf: bytes) -> None: + assert self.dev is not None and self.ep_out is not None + self.ep_out.write(buf, timeout=TIMEOUT_MS) diff --git a/aubox/library/__init__.py b/aubox/library/__init__.py new file mode 100644 index 0000000..79f6f7b --- /dev/null +++ b/aubox/library/__init__.py @@ -0,0 +1,9 @@ +"""Firmware-Library: SQLite-Index + Format-Identifikation. + +Öffentliche API: + db.connect(path) -> sqlite3.Connection + db.upsert_firmware(conn, record) + db.list_firmware(conn, ...) + identify.identify(path) -> FirmwareInfo | None +""" +from . import db, identify # noqa: F401 diff --git a/aubox/library/db.py b/aubox/library/db.py new file mode 100644 index 0000000..03e79fd --- /dev/null +++ b/aubox/library/db.py @@ -0,0 +1,94 @@ +"""SQLite-Index der Firmware-Library. + +Die DB-Datei wandert standardmäßig nach `/firmware.db`, +damit sie zur Library gehört und beim Mounten automatisch da ist. +""" +from __future__ import annotations + +import sqlite3 +from contextlib import contextmanager +from pathlib import Path +from typing import Iterator + +SCHEMA = """ +CREATE TABLE IF NOT EXISTS firmware ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + rel_path TEXT NOT NULL UNIQUE, -- relativ zum firmware-Root + size INTEGER NOT NULL, + mtime REAL NOT NULL, + sha256 TEXT, -- erst nach erstem Hash gefüllt + format TEXT, -- 'huawei-update.app', 'mtk-scatter', 'unknown', ... + vendor TEXT, + model TEXT, -- z.B. 'WAS-LX1' + soc TEXT, -- z.B. 'kirin-658' + version TEXT, -- z.B. '8.0.0.367' + region TEXT, -- z.B. 'C432' + extra_json TEXT, -- alles weitere als JSON + added_at REAL DEFAULT (strftime('%s','now')), + last_seen_at REAL DEFAULT (strftime('%s','now')) +); +CREATE INDEX IF NOT EXISTS idx_fw_model ON firmware(model); +CREATE INDEX IF NOT EXISTS idx_fw_format ON firmware(format); +""" + + +def connect(db_path: Path) -> sqlite3.Connection: + db_path.parent.mkdir(parents=True, exist_ok=True) + conn = sqlite3.connect(db_path) + conn.row_factory = sqlite3.Row + conn.executescript(SCHEMA) + return conn + + +@contextmanager +def open_db(db_path: Path) -> Iterator[sqlite3.Connection]: + """Connection-Lifecycle für FastAPI-Handler: öffnet, gibt zurück, schließt.""" + conn = connect(db_path) + try: + yield conn + finally: + conn.close() + + +@contextmanager +def transaction(conn: sqlite3.Connection) -> Iterator[sqlite3.Connection]: + try: + yield conn + conn.commit() + except Exception: + conn.rollback() + raise + + +def upsert(conn: sqlite3.Connection, rec: dict) -> int: + """Einfügen oder bei Konflikt auf rel_path aktualisieren.""" + cols = ["rel_path", "size", "mtime", "sha256", "format", + "vendor", "model", "soc", "version", "region", "extra_json"] + placeholders = ",".join(f":{c}" for c in cols) + update = ",".join(f"{c}=excluded.{c}" for c in cols if c != "rel_path") + sql = (f"INSERT INTO firmware ({','.join(cols)}) VALUES ({placeholders}) " + f"ON CONFLICT(rel_path) DO UPDATE SET {update}, " + f"last_seen_at=strftime('%s','now')") + cur = conn.execute(sql, {c: rec.get(c) for c in cols}) + return cur.lastrowid or 0 + + +def list_all(conn: sqlite3.Connection) -> list[sqlite3.Row]: + return list(conn.execute( + "SELECT * FROM firmware ORDER BY vendor, model, version" + )) + + +def get_by_id(conn: sqlite3.Connection, fw_id: int) -> sqlite3.Row | None: + cur = conn.execute("SELECT * FROM firmware WHERE id = ?", (fw_id,)) + return cur.fetchone() + + +def delete_missing(conn: sqlite3.Connection, present_rel_paths: set[str]) -> int: + """Einträge entfernen, die beim letzten Scan nicht mehr da waren.""" + cur = conn.execute("SELECT id, rel_path FROM firmware") + to_delete = [row["id"] for row in cur if row["rel_path"] not in present_rel_paths] + if to_delete: + conn.executemany("DELETE FROM firmware WHERE id = ?", + [(i,) for i in to_delete]) + return len(to_delete) diff --git a/aubox/library/huawei.py b/aubox/library/huawei.py new file mode 100644 index 0000000..fd994da --- /dev/null +++ b/aubox/library/huawei.py @@ -0,0 +1,148 @@ +"""Parser für Huawei `update.app`-Container. + +Format-Beschreibung (öffentlich aus splituapp / Huawei Update Extractor): + + Datei beginnt mit 0x5C Bytes Padding/Header, danach folgen Section-Header. + Jeder Section-Header ist 98 Bytes: + + uint32 magic = 0xA55AAA55 + uint32 header_length = 98 + uint32 unknown1 + char[8] hardware_id z.B. "WAS-LX1" + uint32 file_sequence + uint32 file_size + char[16] file_date z.B. "2018.04.16" + char[16] file_time z.B. "10:23:45" + char[16] file_type z.B. "BOOT", "SYSTEM", "USERDATA", "CRC" + char[16] blank1 + uint16 header_checksum + uint16 block_size + uint16 blank2 + + Danach `file_size` Bytes Section-Daten, gefolgt von einer CRC-Tabelle + (eine uint16 pro `block_size` Bytes Daten), dann 4-Byte-Alignment auf + die nächste Section-Header-Position. +""" +from __future__ import annotations + +import struct +from dataclasses import dataclass, field +from pathlib import Path + +UPDATE_MAGIC = 0xA55AAA55 +HEADER_FMT = " str: + return b.split(b"\x00", 1)[0].decode("ascii", errors="replace").strip() + + +def parse(path: Path, max_sections: int = 500) -> UpdateAppInfo | None: + """Header der ersten N Sections einlesen, ohne das ganze Image zu lesen.""" + if not path.is_file(): + return None + if path.stat().st_size < 0x100: + return None + + with path.open("rb") as f: + # Magic suchen — sitzt meist bei 0x5C, manche Builds variieren + head = f.read(0x200) + idx = head.find(struct.pack(" SoC-Mapping. Wird mit der Zeit erweitert; was hier nicht steht, +# kommt als 'unknown' raus und der User kann es manuell pflegen. +HUAWEI_MODEL_SOC = { + "WAS-LX1": "kirin-658", + "WAS-LX2": "kirin-658", + "WAS-LX3": "kirin-658", + "PRA-LX1": "kirin-655", # P8 Lite 2017 + "VTR-L09": "kirin-960", # P10 + "VTR-L29": "kirin-960", + "VKY-L09": "kirin-960", # P10 Plus + "EML-L09": "kirin-970", # P20 + "EML-L29": "kirin-970", + "CLT-L29": "kirin-970", # P20 Pro + "ANE-LX1": "kirin-710", # P20 Lite +} + + +def soc_for(hardware_id: str) -> str | None: + return HUAWEI_MODEL_SOC.get(hardware_id.upper()) diff --git a/aubox/library/identify.py b/aubox/library/identify.py new file mode 100644 index 0000000..8b0487d --- /dev/null +++ b/aubox/library/identify.py @@ -0,0 +1,92 @@ +"""Format-Erkennung. Dispatcher: schaut auf Magic + Dateinamen, leitet an +den passenden Parser weiter. Liefert ein einheitliches Dict mit den Feldern, +die in die DB gehen. + +Aktuell unterstützt: + - Huawei update.app (vollständig) + +Geplant (Stubs als Hinweis): + - MediaTek scatter (txt + bin) + - Samsung Odin (.tar.md5 mit AP/BL/CP/CSC) + - Qualcomm rawprogram*.xml +""" +from __future__ import annotations + +import json +import struct +from pathlib import Path + +from . import huawei + +HUAWEI_MAGIC = struct.pack(" bytes: + try: + with path.open("rb") as f: + return f.read(n) + except OSError: + return b"" + + +def identify(path: Path, root: Path) -> dict: + """Rückgabe: dict für db.upsert. Enthält mindestens rel_path, size, mtime, + format. Erkennungs-Misserfolg -> format='unknown'.""" + rel = str(path.resolve().relative_to(root.resolve())).replace("\\", "/") + stat = path.stat() + base: dict = { + "rel_path": rel, + "size": stat.st_size, + "mtime": stat.st_mtime, + "sha256": None, + "format": "unknown", + "vendor": None, + "model": None, + "soc": None, + "version": None, + "region": None, + "extra_json": None, + } + + head = _peek(path) + + # Huawei update.app — Magic taucht innerhalb der ersten 0x100 Bytes auf + if HUAWEI_MAGIC in head[:0x200]: + info = huawei.parse(path) + if info is not None: + base.update( + format="huawei-update.app", + vendor="Huawei", + model=info.hardware_id or None, + soc=huawei.soc_for(info.hardware_id) if info.hardware_id else None, + version=info.earliest_date, # bessere Quelle wenn verfügbar + region=_region_from_filename(path.name), + extra_json=json.dumps({ + "section_count": info.section_count, + "has_boot": info.has_boot, + "has_system": info.has_system, + "first_sections": [ + s.type for s in info.sections[:10] + ], + }), + ) + return base + + # Fallback: Filename-Heuristik (MTK scatter etc. — TODO) + name = path.name.lower() + if name.startswith("mt") and name.endswith(".txt"): + base["format"] = "mtk-scatter-candidate" + elif name.endswith(".tar.md5"): + base["format"] = "samsung-odin-candidate" + base["vendor"] = "Samsung" + + return base + + +def _region_from_filename(name: str) -> str | None: + """Huawei-Region oft im Filename: '...C432B198...' -> 'C432'.""" + upper = name.upper() + for marker in ("C432", "C636", "C185", "C233", "C605", "C10", "C461"): + if marker in upper: + return marker + return None diff --git a/aubox/p10lite.py b/aubox/p10lite.py new file mode 100644 index 0000000..1cbb20d --- /dev/null +++ b/aubox/p10lite.py @@ -0,0 +1,93 @@ +"""Workflows für Huawei P10 Lite (WAS-LX1, Kirin 658, EMUI 8.x). + +Drei FRP-Removal-Pfade in absteigender Erfolgswahrscheinlichkeit: + +1. erecovery — eRecovery + update.app von SD-Karte. Kein Loader nötig. + Kein Bootloader-Unlock nötig. Funktioniert in der Regel. +2. dload-erase — Testpoint -> Kirin DLOAD -> xloader -> frp-Partition leeren. + Braucht passende Loader-Files (siehe loaders/README.md). +3. dload-flash — Testpoint -> Kirin DLOAD -> kompletter Reflash via update.app. + Maximaler Eingriff, höchstes Risiko, bricht Garantie endgültig. + +Was hier *nicht* funktioniert: +- ADB/Fastboot-FRP-Tricks: das Gerät ist im Setup-Wizard, ADB ist aus. +- TalkBack/YouTube-Tricks: von Google/Huawei seit Jahren gepatcht. +- Bootloader-Unlock-Code von Huawei: seit Juli 2018 abgeschaltet. +""" +from __future__ import annotations + +from pathlib import Path + +LOADER_ROOT = Path(__file__).resolve().parent.parent / "loaders" / "kirin" / "kirin960_lite" +# Kirin 658 wird intern oft mit dem Kirin-960-Loader-Set gefahren (Lite-Variante). +# Für 100%-Sicherheit: Loader vom exakten WAS-LX1-Firmware-Dump verwenden. + +XLOADER_NAME = "hisi-sec_usb_xloader.bin" +USB_LOADER_NAME = "usb_loader.bin" +FASTBOOT_BIN_NAME = "fastboot.bin" + +# SRAM-Adressen für die Loader-Stages (öffentlich aus Kirin-Reverse-Engineering) +# Im Zweifel mit dem Image abgleichen — manche WAS-Builds nutzen 0x07012000. +XLOADER_ADDR = 0x07012000 +USB_LOADER_ADDR = 0x07012000 + + +def erecovery_instructions() -> str: + """Schritt-für-Schritt für den eRecovery-Pfad. Pure Anleitung, kein Code.""" + return """ +eRecovery-Methode (ohne Loader, ohne Bootloader-Unlock) +======================================================== + +Du brauchst: +- Eine FAT32-formatierte SD-Karte, max. 32 GB +- Die *exakte* update.app deiner Region (z.B. WAS-LX1 EMEA C432) + Bezug: huaweifirm.com, hovatek.com, stockromhuawei.com +- USB-Ladegerät (kein Datenkabel nötig) + +Schritte: +1. Auf der SD-Karte einen Ordner `dload` (klein!) anlegen. +2. update.app dort hineinkopieren. Pfad: SD:/dload/update.app +3. SD-Karte in das ausgeschaltete P10 Lite stecken. +4. Beim Einschalten gleichzeitig halten: Power + Volume-Up + Volume-Down. + Halten bis das Huawei-Logo erscheint, dann nur noch Power weiter, andere lösen. +5. eRecovery erkennt die SD-Karte und bietet "Software-Update" / "SD-Karten-Update". + Bestätigen. +6. Nach Abschluss bootet das Gerät neu, FRP-Partition ist neu geschrieben, + Setup-Assistent startet ohne alten Google-Account. + +Wenn eRecovery die Datei nicht erkennt: +- Region passt nicht (häufigster Fehler) +- update.app ist Multi-Part (.app + .lst). Beide nach /dload kopieren. +- SD-Karte nicht FAT32 oder > 32 GB +""" + + +def dload_erase_plan(loader_dir: Path = LOADER_ROOT) -> dict: + """Plant den Testpoint -> DLOAD -> frp-erase-Pfad. + + Liefert ein Dict mit Pfaden und Adressen, das vom CLI ausgeführt wird. + Wirft FileNotFoundError, wenn die Loader-Files fehlen — dann landet der + User bei loaders/README.md und weiß, was zu tun ist. + """ + xloader = loader_dir / XLOADER_NAME + usb_loader = loader_dir / USB_LOADER_NAME + + missing = [str(p) for p in (xloader, usb_loader) if not p.is_file()] + if missing: + raise FileNotFoundError( + "Folgende Loader-Files fehlen:\n - " + + "\n - ".join(missing) + + "\nSiehe loaders/README.md für Beschaffung." + ) + + return { + "stages": [ + {"file": xloader, "addr": XLOADER_ADDR, "label": "xloader"}, + {"file": usb_loader, "addr": USB_LOADER_ADDR, "label": "usb_loader"}, + ], + "post": "huawei-fastboot-d", # erwarteter Modus nach dem Loaden + "frp_partition": "frp", + # Größe der FRP-Partition auf P10 Lite: meist 1 MiB. Beim ersten echten + # Lauf gegen die ptable des Geräts gegenchecken (gpt-Dump via fastboot). + "frp_size_bytes": 1024 * 1024, + } diff --git a/aubox/usb.py b/aubox/usb.py new file mode 100644 index 0000000..9267679 --- /dev/null +++ b/aubox/usb.py @@ -0,0 +1,82 @@ +"""USB-Geräte-Erkennung über VID:PID. + +Erkennt die wichtigsten Hersteller-Modi und ordnet sie einem semantischen +Status zu, damit Workflows wissen, in welchem Modus das Gerät gerade ist. +""" +from __future__ import annotations + +from dataclasses import dataclass + +import usb.core +import usb.util + + +@dataclass(frozen=True) +class DeviceMode: + label: str + vendor: str + notes: str = "" + + +# (vid, pid) -> Modus +KNOWN: dict[tuple[int, int], DeviceMode] = { + # Huawei / Kirin + (0x12D1, 0x1100): DeviceMode("kirin-dload", "Huawei", + "Kirin Download-Mode, wartet auf xloader via hisi-idt"), + (0x12D1, 0x1052): DeviceMode("huawei-fastboot-d", "Huawei", + "Erweiterter Fastboot-Mode (nach xloader)"), + (0x12D1, 0x3609): DeviceMode("kirin-dload-alt", "Huawei", + "Kirin DLOAD alternative PID"), + (0x12D1, 0x107E): DeviceMode("huawei-hisuite", "Huawei", + "HiSuite/MTP-Modus, kein Recovery-Zugriff"), + # Google / Android Standard + (0x18D1, 0x4EE0): DeviceMode("fastboot", "Google", "Standard-Fastboot"), + (0x18D1, 0x4EE7): DeviceMode("adb", "Google", "ADB"), + # MediaTek + (0x0E8D, 0x0003): DeviceMode("mtk-preloader", "MediaTek", + "Preloader (DA-Mode möglich)"), + (0x0E8D, 0x2000): DeviceMode("mtk-brom", "MediaTek", + "BROM — Ziel für mtkclient/kamakiri"), + # Qualcomm EDL + (0x05C6, 0x9008): DeviceMode("qc-edl", "Qualcomm", + "Emergency Download (Firehose-fähig)"), +} + + +@dataclass +class FoundDevice: + vid: int + pid: int + mode: DeviceMode + bus: int + address: int + + def __str__(self) -> str: + return (f"[{self.bus:03d}:{self.address:03d}] " + f"{self.vid:04x}:{self.pid:04x} {self.mode.vendor} " + f"{self.mode.label}") + + +def scan() -> list[FoundDevice]: + """Alle bekannten Hersteller-Modi auflisten, die gerade angeschlossen sind.""" + found: list[FoundDevice] = [] + for dev in usb.core.find(find_all=True): + key = (dev.idVendor, dev.idProduct) + mode = KNOWN.get(key) + if mode is None: + continue + found.append(FoundDevice( + vid=dev.idVendor, + pid=dev.idProduct, + mode=mode, + bus=dev.bus, + address=dev.address, + )) + return found + + +def find_first(label: str) -> FoundDevice | None: + for d in scan(): + if d.mode.label == label: + return d + return None diff --git a/aubox/web/__init__.py b/aubox/web/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/aubox/web/app.py b/aubox/web/app.py new file mode 100644 index 0000000..4785705 --- /dev/null +++ b/aubox/web/app.py @@ -0,0 +1,163 @@ +"""FastAPI-App. Lokale Web-UI für aubox. + +Start: + uvicorn aubox.web.app:app --host 127.0.0.1 --port 8080 +oder über CLI: + python -m aubox web +""" +from __future__ import annotations + +import os +from pathlib import Path + +from fastapi import FastAPI, HTTPException, Request +from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse +from fastapi.staticfiles import StaticFiles +from fastapi.templating import Jinja2Templates + +from .. import filebrowse, p10lite, usb +from ..library import db as fwdb +from ..library import identify as fwid + +WEB_ROOT = Path(__file__).resolve().parent +FIRMWARE_ROOT = Path(os.environ.get("AUBOX_FIRMWARE_ROOT", "./firmware")).resolve() +LOADER_ROOT = Path(os.environ.get("AUBOX_LOADER_ROOT", "./loaders")).resolve() +DB_PATH = FIRMWARE_ROOT / "firmware.db" + +app = FastAPI(title="aubox") +app.mount("/static", StaticFiles(directory=WEB_ROOT / "static"), name="static") +templates = Jinja2Templates(directory=WEB_ROOT / "templates") + + +def _human_size(n: int) -> str: + for unit in ("B", "KB", "MB", "GB", "TB"): + if n < 1024: + return f"{n:.1f} {unit}" if unit != "B" else f"{n} B" + n /= 1024 + return f"{n:.1f} PB" + + +templates.env.filters["humansize"] = _human_size + + +@app.get("/", response_class=HTMLResponse) +async def index(request: Request): + devices = usb.scan() + with fwdb.open_db(DB_PATH) as conn: + fw_count = conn.execute("SELECT COUNT(*) FROM firmware").fetchone()[0] + return templates.TemplateResponse("index.html", { + "request": request, + "devices": devices, + "fw_count": fw_count, + "firmware_root": FIRMWARE_ROOT, + "loader_root": LOADER_ROOT, + }) + + +# ---------- Devices -------------------------------------------------------- + +@app.get("/devices", response_class=HTMLResponse) +async def devices_page(request: Request): + return templates.TemplateResponse("devices.html", {"request": request}) + + +@app.get("/api/devices/html", response_class=HTMLResponse) +async def devices_partial(request: Request): + return templates.TemplateResponse("_devices.html", { + "request": request, + "devices": usb.scan(), + }) + + +# ---------- Firmware Library ---------------------------------------------- + +@app.get("/firmware", response_class=HTMLResponse) +async def firmware_page(request: Request): + with fwdb.open_db(DB_PATH) as conn: + rows = fwdb.list_all(conn) + return templates.TemplateResponse("firmware.html", { + "request": request, + "firmware": rows, + "firmware_root": FIRMWARE_ROOT, + }) + + +@app.post("/firmware/scan") +async def firmware_scan(): + """Walkt FIRMWARE_ROOT, identifiziert jede Datei, schreibt in DB.""" + if not FIRMWARE_ROOT.is_dir(): + raise HTTPException(404, f"Firmware-Root {FIRMWARE_ROOT} nicht gefunden") + + seen: set[str] = set() + added = updated = 0 + with fwdb.open_db(DB_PATH) as conn: + with fwdb.transaction(conn): + for path in FIRMWARE_ROOT.rglob("*"): + if not path.is_file(): + continue + if path.name == "firmware.db" or path.name.startswith("."): + continue + rec = fwid.identify(path, FIRMWARE_ROOT) + seen.add(rec["rel_path"]) + existed = conn.execute( + "SELECT 1 FROM firmware WHERE rel_path = ?", + (rec["rel_path"],), + ).fetchone() + fwdb.upsert(conn, rec) + if existed: + updated += 1 + else: + added += 1 + removed = fwdb.delete_missing(conn, seen) + + return JSONResponse({ + "scanned": len(seen), + "added": added, + "updated": updated, + "removed": removed, + }) + + +@app.get("/firmware/{fw_id}", response_class=HTMLResponse) +async def firmware_detail(request: Request, fw_id: int): + with fwdb.open_db(DB_PATH) as conn: + row = fwdb.get_by_id(conn, fw_id) + if row is None: + raise HTTPException(404) + return templates.TemplateResponse("firmware_detail.html", { + "request": request, + "fw": row, + "firmware_root": FIRMWARE_ROOT, + }) + + +# ---------- File Browser (sandboxed auf FIRMWARE_ROOT) -------------------- + +@app.get("/browse", response_class=HTMLResponse) +async def browse(request: Request, path: str = ""): + try: + target, entries = filebrowse.list_dir(FIRMWARE_ROOT, path) + except (filebrowse.PathEscapeError, FileNotFoundError, NotADirectoryError) as e: + raise HTTPException(400, str(e)) + return templates.TemplateResponse("browse.html", { + "request": request, + "rel": path, + "entries": entries, + "crumbs": filebrowse.breadcrumbs(path), + "firmware_root": FIRMWARE_ROOT, + }) + + +# ---------- Workflows ------------------------------------------------------ + +@app.get("/workflows/p10lite", response_class=HTMLResponse) +async def workflow_p10lite(request: Request): + return templates.TemplateResponse("p10lite.html", { + "request": request, + "instructions": p10lite.erecovery_instructions(), + }) + + +@app.get("/health") +async def health(): + return {"ok": True} diff --git a/aubox/web/static/app.js b/aubox/web/static/app.js new file mode 100644 index 0000000..287ca69 --- /dev/null +++ b/aubox/web/static/app.js @@ -0,0 +1,50 @@ +// Live-Refresh für Container mit data-refresh="" und data-interval="" +function startAutoRefresh() { + document.querySelectorAll("[data-refresh]").forEach((el) => { + const url = el.dataset.refresh; + const interval = parseInt(el.dataset.interval || "2000", 10); + const tick = async () => { + try { + const r = await fetch(url, { headers: { "Accept": "text/html" } }); + if (r.ok) el.innerHTML = await r.text(); + } catch (e) { /* netzkurz weg, nicht weiter schlimm */ } + }; + tick(); + setInterval(tick, interval); + }); +} + +// Forms mit data-action posten und Antwort in data-target rendern +function wireScanForms() { + document.querySelectorAll("form[data-action]").forEach((form) => { + form.addEventListener("submit", async (ev) => { + ev.preventDefault(); + const btn = form.querySelector("button[type=submit]"); + const target = document.querySelector(form.dataset.target); + btn.disabled = true; + if (target) target.textContent = "läuft…"; + try { + const r = await fetch(form.dataset.action, { method: "POST" }); + const j = await r.json().catch(() => null); + if (target) { + if (j) { + target.textContent = `gescannt: ${j.scanned} · neu: ${j.added} · aktualisiert: ${j.updated} · entfernt: ${j.removed}`; + } else { + target.textContent = r.ok ? "fertig" : "Fehler"; + } + } + // Seite neu laden, damit die Tabelle die neuen Einträge zeigt + if (r.ok) setTimeout(() => location.reload(), 800); + } catch (e) { + if (target) target.textContent = "Fehler: " + e; + } finally { + btn.disabled = false; + } + }); + }); +} + +document.addEventListener("DOMContentLoaded", () => { + startAutoRefresh(); + wireScanForms(); +}); diff --git a/aubox/web/static/style.css b/aubox/web/static/style.css new file mode 100644 index 0000000..07fd745 --- /dev/null +++ b/aubox/web/static/style.css @@ -0,0 +1,136 @@ +:root { + --bg: #1c1f24; + --bg-card: #262a31; + --fg: #e6e6e6; + --muted: #8a8f98; + --accent: #6cb4ff; + --accent-hover: #8fc8ff; + --border: #353a44; + --ok: #7adf7a; + --warn: #ffb86c; +} + +* { box-sizing: border-box; } + +body { + margin: 0; + font-family: system-ui, -apple-system, "Segoe UI", sans-serif; + background: var(--bg); + color: var(--fg); + line-height: 1.5; +} + +header { + background: var(--bg-card); + border-bottom: 1px solid var(--border); + padding: 1rem 2rem; + display: flex; + align-items: center; + gap: 2rem; +} + +header h1 { margin: 0; font-size: 1.4rem; } +header h1 a { color: var(--fg); text-decoration: none; } + +nav { display: flex; gap: 1.5rem; } +nav a { color: var(--muted); text-decoration: none; font-weight: 500; } +nav a:hover { color: var(--accent); } + +main { max-width: 1200px; margin: 0 auto; padding: 2rem; } + +footer { + text-align: center; padding: 2rem; color: var(--muted); + border-top: 1px solid var(--border); margin-top: 4rem; +} + +a { color: var(--accent); } +a:hover { color: var(--accent-hover); } + +code, pre { + font-family: "JetBrains Mono", Menlo, Consolas, monospace; + font-size: 0.9em; +} +pre { + background: var(--bg-card); + padding: 1rem; + border-radius: 6px; + overflow-x: auto; + border: 1px solid var(--border); +} +code { background: var(--bg-card); padding: 0.1em 0.4em; border-radius: 3px; } + +.cards { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 1rem; + margin: 2rem 0; +} +.card { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 8px; + padding: 1.5rem; +} +.card h2 { margin-top: 0; font-size: 1rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.05em; } +.card .big { font-size: 2.5rem; font-weight: 700; margin: 0.5rem 0; } + +.info { margin-top: 3rem; } +.info ul { list-style: none; padding: 0; } +.info li { margin: 0.5rem 0; } + +table.grid { + width: 100%; + border-collapse: collapse; + background: var(--bg-card); + border-radius: 8px; + overflow: hidden; +} +table.grid th, table.grid td { + text-align: left; + padding: 0.75rem 1rem; + border-bottom: 1px solid var(--border); +} +table.grid th { background: rgba(255,255,255,0.04); font-weight: 600; } +table.grid tbody tr:last-child td { border-bottom: none; } +table.grid tbody tr:hover { background: rgba(255,255,255,0.03); } + +table.kv { border-collapse: collapse; } +table.kv th { text-align: left; padding: 0.5rem 1rem 0.5rem 0; color: var(--muted); font-weight: 500; } +table.kv td { padding: 0.5rem 0; } + +button { + background: var(--accent); + color: #0d1117; + border: 0; + padding: 0.6rem 1.2rem; + border-radius: 6px; + font-weight: 600; + cursor: pointer; + font-size: 0.95rem; +} +button:hover { background: var(--accent-hover); } +button:disabled { opacity: 0.5; cursor: wait; } + +.empty { + background: var(--bg-card); + border: 1px dashed var(--border); + padding: 1.5rem; + border-radius: 8px; + color: var(--muted); + text-align: center; +} + +.muted { color: var(--muted); } + +.crumbs { + background: var(--bg-card); + padding: 0.6rem 1rem; + border-radius: 6px; + margin-bottom: 1rem; + border: 1px solid var(--border); + font-family: "JetBrains Mono", Menlo, monospace; +} +.crumbs a { color: var(--fg); text-decoration: none; } +.crumbs a:hover { color: var(--accent); } + +#scan-result { margin-left: 1rem; color: var(--muted); } diff --git a/aubox/web/templates/_devices.html b/aubox/web/templates/_devices.html new file mode 100644 index 0000000..c4d1740 --- /dev/null +++ b/aubox/web/templates/_devices.html @@ -0,0 +1,20 @@ +{% if not devices %} +

Kein bekanntes Hersteller-Gerät am USB.

+{% else %} + + + + + + {% for d in devices %} + + + + + + + + {% endfor %} + +
Bus:AddrVID:PIDHerstellerModusHinweis
{{ "%03d"|format(d.bus) }}:{{ "%03d"|format(d.address) }}{{ "%04x"|format(d.vid) }}:{{ "%04x"|format(d.pid) }}{{ d.mode.vendor }}{{ d.mode.label }}{{ d.mode.notes }}
+{% endif %} diff --git a/aubox/web/templates/base.html b/aubox/web/templates/base.html new file mode 100644 index 0000000..22b3190 --- /dev/null +++ b/aubox/web/templates/base.html @@ -0,0 +1,27 @@ + + + + +{% block title %}aubox{% endblock %} + + + +
+

aubox

+ +
+
+ {% block content %}{% endblock %} +
+
+ aubox · lokale Web-UI +
+ + + diff --git a/aubox/web/templates/browse.html b/aubox/web/templates/browse.html new file mode 100644 index 0000000..51a1392 --- /dev/null +++ b/aubox/web/templates/browse.html @@ -0,0 +1,36 @@ +{% extends "base.html" %} +{% block title %}Dateien · aubox{% endblock %} +{% block content %} +

Datei-Browser

+

Sandboxed auf {{ firmware_root }} — Path-Traversal blockiert.

+ + + +{% if not entries %} +

Verzeichnis ist leer.

+{% else %} + + + + {% for e in entries %} + + + + + + {% endfor %} + +
NameTypGröße
+ {% if e.is_dir %} + {{ e.name }}/ + {% else %} + {{ e.name }} + {% endif %} + {{ "DIR" if e.is_dir else "FILE" }}{{ e.size|humansize if not e.is_dir else "—" }}
+{% endif %} +{% endblock %} diff --git a/aubox/web/templates/devices.html b/aubox/web/templates/devices.html new file mode 100644 index 0000000..1d7258c --- /dev/null +++ b/aubox/web/templates/devices.html @@ -0,0 +1,9 @@ +{% extends "base.html" %} +{% block title %}Geräte · aubox{% endblock %} +{% block content %} +

Angeschlossene Geräte

+

Aktualisiert sich alle 2 Sekunden.

+
+ Lade… +
+{% endblock %} diff --git a/aubox/web/templates/firmware.html b/aubox/web/templates/firmware.html new file mode 100644 index 0000000..6851c6b --- /dev/null +++ b/aubox/web/templates/firmware.html @@ -0,0 +1,38 @@ +{% extends "base.html" %} +{% block title %}Firmware · aubox{% endblock %} +{% block content %} +

Firmware-Library

+

Quelle: {{ firmware_root }}

+ +
+ + +
+ +{% if not firmware %} +

Noch keine Einträge. Lege Firmware-Dateien unter +{{ firmware_root }} ab und klicke "Library scannen".

+{% else %} + + + + + + + + + {% for fw in firmware %} + + + + + + + + + + {% endfor %} + +
VendorModellSoCRegionFormatGrößePfad
{{ fw.vendor or "—" }}{{ fw.model or "—" }}{{ fw.soc or "—" }}{{ fw.region or "—" }}{{ fw.format }}{{ fw.size|humansize }}{{ fw.rel_path }}
+{% endif %} +{% endblock %} diff --git a/aubox/web/templates/firmware_detail.html b/aubox/web/templates/firmware_detail.html new file mode 100644 index 0000000..1fd49ee --- /dev/null +++ b/aubox/web/templates/firmware_detail.html @@ -0,0 +1,23 @@ +{% extends "base.html" %} +{% block title %}{{ fw.model or fw.rel_path }} · aubox{% endblock %} +{% block content %} +

{{ fw.model or "Unbekannt" }} {{ fw.format }}

+ + + + + + + + + + +
Pfad{{ fw.rel_path }}
Vendor{{ fw.vendor or "—" }}
Modell{{ fw.model or "—" }}
SoC{{ fw.soc or "—" }}
Region{{ fw.region or "—" }}
Version{{ fw.version or "—" }}
Größe{{ fw.size|humansize }}
SHA-256{{ fw.sha256 or "noch nicht berechnet" }}
+ +{% if fw.extra_json %} +

Format-spezifische Daten

+
{{ fw.extra_json }}
+{% endif %} + +

← zurück

+{% endblock %} diff --git a/aubox/web/templates/index.html b/aubox/web/templates/index.html new file mode 100644 index 0000000..2b39a4b --- /dev/null +++ b/aubox/web/templates/index.html @@ -0,0 +1,39 @@ +{% extends "base.html" %} +{% block title %}aubox · Übersicht{% endblock %} +{% block content %} +
+
+

Geräte

+

{{ devices|length }}

+

{{ "angeschlossen" if devices else "keins erkannt" }}

+

öffnen →

+
+ +
+

Firmware-Library

+

{{ fw_count }}

+

Einträge in der Datenbank

+

öffnen →

+
+ +
+

Dateien

+

Sandbox-Browser für die Firmware-Library

+

öffnen →

+
+ +
+

Huawei P10 Lite

+

FRP-Removal · WAS-LX1 · Kirin 658

+

öffnen →

+
+
+ +
+

Aktive Pfade im Container

+
    +
  • Firmware: {{ firmware_root }}
  • +
  • Loader: {{ loader_root }}
  • +
+
+{% endblock %} diff --git a/aubox/web/templates/p10lite.html b/aubox/web/templates/p10lite.html new file mode 100644 index 0000000..b63ee89 --- /dev/null +++ b/aubox/web/templates/p10lite.html @@ -0,0 +1,15 @@ +{% extends "base.html" %} +{% block title %}Huawei P10 Lite · aubox{% endblock %} +{% block content %} +

Huawei P10 Lite — FRP-Removal

+

WAS-LX1 · Kirin 658 · EMUI 8.x

+ +

Methode 1: eRecovery + SD-Karte (empfohlen)

+
{{ instructions }}
+ +

Methode 2: Testpoint + Kirin DLOAD

+

Über CLI vorbereitet, Web-UI-Button folgt:

+
python -m aubox p10lite frp-remove --method dload-erase
+

Voraussetzung: Loader-Files in /loaders/kirin/kirin960_lite/ +(siehe Loader-README im Repo).

+{% endblock %} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..124e7b5 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,28 @@ +# Lokaler Start: docker compose up +# Web-UI: http://127.0.0.1:8080 +# +# USB-Passthrough komplett, damit Re-Enumeration funktioniert +# (Kirin DLOAD -> Huawei Fastboot-D nach xloader). + +services: + aubox: + build: . + container_name: aubox + ports: + # 127.0.0.1 only — Web-UI nicht ans LAN exponieren + - "127.0.0.1:8080:8080" + volumes: + # Firmware-Bibliothek: Host-Verzeichnis ./firmware -> /firmware im Container + - ./firmware:/firmware + # Loader-Files (read-only, sicherheitshalber) + - ./loaders:/loaders:ro + # USB-Geräte komplett, damit Hot-Plug + Re-Enumeration durchgeht + - /dev/bus/usb:/dev/bus/usb + device_cgroup_rules: + # USB-Major 189 — alle USB-Geräte für den Container freigeben + - 'c 189:* rmw' + environment: + AUBOX_FIRMWARE_ROOT: /firmware + AUBOX_LOADER_ROOT: /loaders + command: ["web", "--host", "0.0.0.0", "--port", "8080"] + restart: unless-stopped diff --git a/docs/huawei-p10-lite.md b/docs/huawei-p10-lite.md new file mode 100644 index 0000000..26b00d6 --- /dev/null +++ b/docs/huawei-p10-lite.md @@ -0,0 +1,119 @@ +# Huawei P10 Lite — FRP entfernen ohne IMEI / ohne Bootloader-Unlock + +Modell: **WAS-LX1** (EU/EMEA), Kirin 658 (Hi3660 Lite), EMUI 8.x. + +## Ausgangslage + +- Google-Account in den FRP-Records vergessen +- Bootloader gelockt (Huawei hat 07/2018 die Codes abgeschaltet) +- Keine valide IMEI im NV-Speicher +- Gerät startet bei jedem Reset wieder im Setup-Wizard mit Account-Abfrage +- Bekannte Tricks (TalkBack-Browser, YouTube-Account-Switch) sind seit + Jahren von Google und Huawei gepatcht + +## Drei Pfade, in dieser Reihenfolge probieren + +### Pfad 1 — eRecovery + SD-Karte (empfohlen, kein Loader nötig) + +Erfolgsquote bei intaktem Gerät: hoch. Risiko: niedrig. +Funktioniert *immer ohne* Bootloader-Unlock. + +```bash +python -m aubox p10lite frp-remove --method erecovery +``` + +Das gibt dir die Schritt-für-Schritt-Anleitung aus. Kernpunkte nochmal: + +1. SD-Karte FAT32, max. 32 GB +2. Ordner `dload` (klein!) anlegen +3. *Region-passende* `update.app` da rein. Bei Multi-Part auch die `.lst`. +4. Tasten: Power + Vol+ + Vol- gleichzeitig beim Einschalten +5. eRecovery erkennt SD und bietet Update an + +Region-Codes im Dateinamen der update.app: +- C432 = EMEA (Deutschland/EU Open-Market) +- C636 = Asia Pacific +- C185 = Middle East +- C233 = Italien (TIM) +- usw. + +**Wichtig:** Falsche Region = Anti-Rollback-Trigger oder Reject. +Im Zweifel die Region aus dem Aufkleber im Akkuschacht / hinter der +SIM-Schublade ablesen (steht als CLT bzw. C-Code drauf). + +### Pfad 2 — Testpoint + Kirin DLOAD + frp-Erase + +Wenn eRecovery nicht startet (defekte System-Partition o.ä.), der +nächste Schritt. Du machst das eh schon teilweise: Testpoint kurz auf +Masse legen, dann USB anstecken — Gerät erscheint als 12d1:1100. + +**Was du brauchst:** + +- `hisi-sec_usb_xloader.bin` (signierter Stage-1-Loader, Kirin 960 lite / 658) +- `usb_loader.bin` (Stage 2) +- Optional: `fastboot.bin` für volles Reflash +- Diese Files müssen passend zum WAS-LX1-Build sein. Aus einer + matching update.app extrahierbar (siehe `loaders/README.md`). + +**Ablauf:** + +```bash +# Loader-Files in loaders/kirin/kirin960_lite/ ablegen, dann: +python -m aubox p10lite frp-remove --method dload-erase +``` + +Was das Tool macht: + +1. Wartet auf USB-VID:PID 12d1:1100 (Kirin DLOAD). +2. Sendet xloader via hisi-idt nach SRAM-Adresse 0x07012000. +3. Sendet usb_loader. +4. Gerät re-enumeriert als Huawei Fastboot-D (12d1:1052). +5. Zeigt dir den nächsten manuellen Schritt: + ``` + fastboot erase frp + fastboot reboot + ``` + +**Warum noch manuell ab Schritt 5:** +Der `fastboot erase`-Aufruf hängt vom genauen Partitionsnamen ab +(`frp`, `frpcache`, manchmal nur in `userdata` enthalten). Beim +ersten echten Lauf solltest du erst `fastboot getvar all` machen +und das ptable prüfen, bevor du blind erase fährst. + +### Pfad 3 — Kompletter Reflash via DLOAD + +Wenn Pfad 2 nichts bringt (frp-Partition leer und Sperre bleibt = +Sperre liegt anderswo, evtl. in `persist` oder im Account-Manager-DB). +Dann hilft nur ein voller Wipe + Reflash via DLOAD und passender +update.app. Das macht das Tool noch nicht — kommt später. + +## Was *garantiert* nicht funktioniert + +- **TalkBack-Trick** im Setup-Wizard: Google hat das bei den + Sicherheits-Patches 2019+ entfernt. EMUI 8 mit Patch von 2019 + oder neuer = tot. +- **YouTube-App-Update-Trick**: dito, gepatcht. +- **HiSuite-FRP-Reset**: braucht USB-Debugging, das im Setup aus ist. +- **OEM-Unlock-Code aus dem Internet** für 2018+ generierte Codes: + Server gegen, hat Huawei aktiv abgedreht. +- **Magisk-FRP-Module**: setzen gerootetes System voraus → Henne/Ei. + +## IMEI-Frage + +IMEI weg ist ein zweites, separates Problem. Auch wenn FRP entsperrt +ist, ohne IMEI hast du kein funktionierendes Mobilfunk-Modul. +Reparatur: + +- NV-Backup vom selben Modell (WAS-LX1) → einfach restoren via + Potato_NV oder DC-Unlocker +- Ohne Backup: NV neu generieren mit dem auf dem Aufkleber + abgelesenen IMEI. Das Tool kann das später bekommen + (`aubox p10lite imei-write --imei <15 digits>`), aber dafür + muss erst der DLOAD-Pfad solide laufen. + +## Quellen / Weiterlesen + +- XDA WAS-LX1 Forum (alte Threads von 2018-2020 sind die Goldgrube) +- hovatek.com — Huawei-spezifische Tutorials +- Potato_NV (mkasu/potatonv auf GitHub) +- HCU-Client-Reverse-Engineering-Diskussionen (4PDA-Foren) diff --git a/firmware/README.md b/firmware/README.md new file mode 100644 index 0000000..49dddad --- /dev/null +++ b/firmware/README.md @@ -0,0 +1,49 @@ +# Firmware-Library + +Hier liegen die Firmware-Dateien, die aubox indexiert. Im Container ist +das Verzeichnis als `/firmware` gemountet. + +## Layout-Vorschlag + +Ablage frei wählbar — die Library scannt rekursiv und identifiziert über +Magic-Bytes, nicht über Pfad. Trotzdem hilfreich: + +``` +firmware/ +├── huawei/ +│ ├── WAS-LX1/ +│ │ ├── EMUI8_C432_8.0.0.367.app +│ │ └── EMUI8_C432_8.0.0.367.app.sha256 +│ └── EML-L29/ +├── samsung/ +│ └── SM-G960F_xx.tar.md5 +├── mediatek/ +└── firmware.db # ← SQLite-Index, wird beim ersten Scan erzeugt +``` + +## Was wird erkannt + +- **Huawei `update.app`**: Hardware-ID, Region, Section-Layout, BOOT/SYSTEM + vorhanden? — voll automatisch +- **MediaTek scatter**: aktuell nur Kandidaten-Erkennung über Filename +- **Samsung Odin (`*.tar.md5`)**: aktuell nur Kandidaten-Erkennung + +Mehr Parser folgen, sobald der jeweilige Workflow gebraucht wird. + +## Datenbank + +Die SQLite-Datei (`firmware.db`) ist Teil der Library. Sie wandert mit +einem Backup oder Sync mit. Schema steht in `aubox/library/db.py`. + +## Git und Größe + +Standardmäßig sind die Binaries in `.gitignore` ausgenommen (typisch +2-8 GB pro update.app). Wenn du sie trotzdem ins Repo legen willst: + +- **git-lfs** ist die einzig sinnvolle Option — `git lfs track "*.app"` etc. +- Direktes `git add` einer 4-GB-Datei macht den Repo-Pull für andere zur Hölle. +- Ein Backup-Repo extra für Firmware (LFS) ist sauberer als alles in einem. + +Die `firmware.db` selbst ist klein (KB) und kann ohne LFS committet werden, +falls du das Inventory teilen willst — sie enthält nur Metadaten, nicht +die Binaries. diff --git a/loaders/README.md b/loaders/README.md new file mode 100644 index 0000000..2618f43 --- /dev/null +++ b/loaders/README.md @@ -0,0 +1,59 @@ +# Loader-Files + +Hier kommen die hersteller-signierten Stage-Loader rein, ohne die im +Kirin-Download-Mode nichts geht. Diese Dateien sind **nicht** im +Repo — du musst sie selbst beschaffen. Drei Wege: + +## Aus einer passenden update.app extrahieren (sauberster Weg) + +Eine update.app ist ein Container, in dem alle Partitions-Images + +einige Loader liegen. Werkzeug: `splituapp` oder `Huawei Update Extractor`. + +```bash +pip install splituapp +splituapp -f update.app +``` + +In den extrahierten Files suchst du nach: + +- `xloader.img` / `hisi-sec_usb_xloader.bin` +- `fastboot.img` -> wird zu `fastboot.bin` +- Manche Builds: `usb_loader.bin` separat + +Vorteil: signiert + zur Hardware passend. Garantiert kompatibel. + +## Aus Tool-Distributionen rausziehen + +Tools wie HCU-Client, DC-Unlocker, manche Octopus-Plugins liefern +Loader-Sets im Installer mit. In `/loaders/` oder +`/data/`. Nach Filenamen wie oben suchen. Hash gegen einen +update.app-Auszug abgleichen, falls möglich — du willst kein +manipuliertes Binary auf den Phone-eMMC schreiben. + +## Aus Forum-Mirrors + +XDA-WAS-LX1-Threads, hovatek-Forum, 4PDA-Threads. Risiko: Manipuliert. +Immer SHA-256 gegen mehrere Quellen prüfen. Wenn nur eine Quelle, lass +es sein und nimm den update.app-Weg. + +## Erwartete Struktur + +``` +loaders/ +└── kirin/ + └── kirin960_lite/ # für Kirin 658 (P10 Lite) + ├── hisi-sec_usb_xloader.bin + ├── usb_loader.bin + └── fastboot.bin # optional +``` + +Pro SoC eigenes Unterverzeichnis. Wenn du später z.B. einen Kirin 970 +hinzufügst: `loaders/kirin/kirin970/`. + +## Rechtliches + +Diese Loader-Files sind urheberrechtlich Huawei. Aus *eigenen +Geräten*-Firmware extrahieren ist in DE/EU für private Reparatur +unproblematisch (Recht auf Reparatur, EU 2024/1799). Weiterverbreiten +ist es nicht — also: nicht ins Repo committen, nicht öffentlich +spiegeln. Das `loaders/`-Verzeichnis steht deshalb in `.gitignore`. diff --git a/loaders/kirin/.gitkeep b/loaders/kirin/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..eeaaed0 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +pyusb>=1.2.1 +fastapi>=0.110 +uvicorn[standard]>=0.27 +jinja2>=3.1 diff --git a/scripts/run-docker.sh b/scripts/run-docker.sh new file mode 100755 index 0000000..1df94b1 --- /dev/null +++ b/scripts/run-docker.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +# aubox im Container starten, mit USB-Passthrough so dass Re-Enumeration +# (Kirin DLOAD -> Huawei Fastboot-D) korrekt durchläuft. +# +# Wichtig: Wir mounten /dev/bus/usb komplett (nicht ein einzelnes Device), +# damit nach Re-Enumeration das neue Device sichtbar ist. Dazu ist die +# device-cgroup-rule für USB (Major 189) nötig. +# +# udev-Regeln müssen auf dem HOST liegen, nicht im Container. Falls noch +# nicht installiert, einmalig: sudo bash scripts/setup-linux.sh + +set -euo pipefail + +PROJECT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +IMAGE="${AUBOX_IMAGE:-aubox}" + +# Image bauen, falls nicht vorhanden +if ! docker image inspect "$IMAGE" >/dev/null 2>&1; then + echo "Image $IMAGE nicht gefunden — baue..." + docker build -t "$IMAGE" "$PROJECT_DIR" +fi + +# USB-Major-Nummer für cgroup-rule +USB_MAJOR=189 + +exec docker run --rm -it \ + --device-cgroup-rule="c ${USB_MAJOR}:* rmw" \ + -v /dev/bus/usb:/dev/bus/usb \ + -v "${PROJECT_DIR}/loaders:/app/loaders:ro" \ + "$IMAGE" "$@" diff --git a/scripts/setup-linux.sh b/scripts/setup-linux.sh new file mode 100755 index 0000000..32626aa --- /dev/null +++ b/scripts/setup-linux.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +# Setup für Debian/Ubuntu/Mint (inkl. Raspberry Pi OS). Muss als root laufen. +set -euo pipefail + +if [[ $EUID -ne 0 ]]; then + echo "Bitte mit sudo aufrufen." >&2 + exit 1 +fi + +apt-get update +apt-get install -y \ + android-tools-adb \ + android-tools-fastboot \ + libusb-1.0-0 \ + libusb-1.0-0-dev \ + python3 \ + python3-pip \ + python3-venv \ + usbutils + +# udev-Regel installieren, damit der normale User ohne sudo auf die +# Hersteller-spezifischen USB-Modi zugreifen kann (Kirin DLOAD, fastboot etc.) +install -m 0644 udev/51-android-unlock.rules /etc/udev/rules.d/ +udevadm control --reload-rules +udevadm trigger + +# Den aufrufenden User in die plugdev-Gruppe stecken +TARGET_USER="${SUDO_USER:-$USER}" +if [[ -n "$TARGET_USER" && "$TARGET_USER" != "root" ]]; then + usermod -aG plugdev "$TARGET_USER" +fi + +# Python-Deps in eine venv im Projektverzeichnis +PROJECT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +sudo -u "$TARGET_USER" python3 -m venv "$PROJECT_DIR/.venv" +sudo -u "$TARGET_USER" "$PROJECT_DIR/.venv/bin/pip" install -r "$PROJECT_DIR/requirements.txt" + +echo +echo "Setup fertig. Einmal ab- und neu anmelden, damit plugdev greift." +echo "Dann: source .venv/bin/activate && python -m aubox detect" diff --git a/udev/51-android-unlock.rules b/udev/51-android-unlock.rules new file mode 100644 index 0000000..779a19d --- /dev/null +++ b/udev/51-android-unlock.rules @@ -0,0 +1,18 @@ +# Huawei — alle Hersteller-Modi, plugdev-User darf rauf +# 12d1 = Huawei +SUBSYSTEM=="usb", ATTR{idVendor}=="12d1", MODE="0660", GROUP="plugdev" + +# Kirin DLOAD (Download-Mode nach Testpoint) +# Typische PIDs: 1100 (DLOAD), 1052 (Fastboot-D), 3609 (manche Kirin) +SUBSYSTEM=="usb", ATTR{idVendor}=="12d1", ATTR{idProduct}=="1100", MODE="0660", GROUP="plugdev", SYMLINK+="kirin_dload" +SUBSYSTEM=="usb", ATTR{idVendor}=="12d1", ATTR{idProduct}=="1052", MODE="0660", GROUP="plugdev", SYMLINK+="huawei_fastbootd" +SUBSYSTEM=="usb", ATTR{idVendor}=="12d1", ATTR{idProduct}=="3609", MODE="0660", GROUP="plugdev", SYMLINK+="kirin_dload_alt" + +# Generisch: Android Fastboot (18d1 = Google) +SUBSYSTEM=="usb", ATTR{idVendor}=="18d1", MODE="0660", GROUP="plugdev" + +# MediaTek Preloader / BROM (für später) +SUBSYSTEM=="usb", ATTR{idVendor}=="0e8d", MODE="0660", GROUP="plugdev" + +# Qualcomm 9008 EDL (für später) +SUBSYSTEM=="usb", ATTR{idVendor}=="05c6", ATTR{idProduct}=="9008", MODE="0660", GROUP="plugdev"