initial aubox skeleton: web-UI, kirin DLOAD, firmware library

- 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) <noreply@anthropic.com>
This commit is contained in:
Stefan Hacker 2026-04-26 12:09:39 +02:00
parent d0386b3c53
commit fb3534553b
35 changed files with 1883 additions and 0 deletions

8
.dockerignore Normal file
View File

@ -0,0 +1,8 @@
.venv/
__pycache__/
*.pyc
loaders/
.git/
docs/
*.md
!README.md

33
Dockerfile Normal file
View File

@ -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"]

84
README.md Normal file
View File

@ -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 <http://127.0.0.1:8080>. 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.

1
aubox/__init__.py Normal file
View File

@ -0,0 +1 @@
__version__ = "0.0.1"

3
aubox/__main__.py Normal file
View File

@ -0,0 +1,3 @@
from .cli import main
raise SystemExit(main())

119
aubox/cli.py Normal file
View File

@ -0,0 +1,119 @@
"""CLI-Einstiegspunkt: `python -m aubox <command>`."""
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())

70
aubox/filebrowse.py Normal file
View File

@ -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

144
aubox/kirin.py Normal file
View File

@ -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/<soc>/` 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)

View File

@ -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

94
aubox/library/db.py Normal file
View File

@ -0,0 +1,94 @@
"""SQLite-Index der Firmware-Library.
Die DB-Datei wandert standardmäßig nach `<firmware_root>/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)

148
aubox/library/huawei.py Normal file
View File

@ -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 = "<III8sII16s16s16s16sHHH"
HEADER_SIZE = 98
@dataclass
class Section:
sequence: int
size: int
date: str
time: str
type: str
@dataclass
class UpdateAppInfo:
hardware_id: str
section_count: int
sections: list[Section] = field(default_factory=list)
has_boot: bool = False
has_system: bool = False
earliest_date: str | None = None
def _cstr(b: bytes) -> 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("<I", UPDATE_MAGIC))
if idx < 0:
return None
f.seek(idx)
sections: list[Section] = []
hardware_id = ""
earliest: str | None = None
has_boot = has_system = False
for _ in range(max_sections):
buf = f.read(HEADER_SIZE)
if len(buf) < HEADER_SIZE:
break
(magic, _hlen, _u, hw, seq, size,
date, time_, ftype, _b, _crc, blksz, _b2) = struct.unpack(HEADER_FMT, buf)
if magic != UPDATE_MAGIC:
break
hw_str = _cstr(hw)
type_str = _cstr(ftype)
date_str = _cstr(date)
time_str = _cstr(time_)
if not hardware_id and hw_str:
hardware_id = hw_str
if earliest is None or (date_str and date_str < earliest):
if date_str:
earliest = date_str
if type_str.upper() in ("BOOT", "FASTBOOT", "XLOADER", "BOOTLOADER"):
has_boot = True
if type_str.upper() in ("SYSTEM", "SYSTEM_IMAGE"):
has_system = True
sections.append(Section(
sequence=seq, size=size, date=date_str,
time=time_str, type=type_str,
))
# Daten + CRC + Padding überspringen
if blksz <= 0:
break
crc_size = ((size + blksz - 1) // blksz) * 2
cur_after_data = f.tell() + size + crc_size
pad = (-cur_after_data) % 4
f.seek(size + crc_size + pad, 1)
if not sections:
return None
return UpdateAppInfo(
hardware_id=hardware_id,
section_count=len(sections),
sections=sections,
has_boot=has_boot,
has_system=has_system,
earliest_date=earliest,
)
# ---------------------------------------------------------------------------
# Modell -> 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())

92
aubox/library/identify.py Normal file
View File

@ -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("<I", huawei.UPDATE_MAGIC)
def _peek(path: Path, n: int = 0x200) -> 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

93
aubox/p10lite.py Normal file
View File

@ -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,
}

82
aubox/usb.py Normal file
View File

@ -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

0
aubox/web/__init__.py Normal file
View File

163
aubox/web/app.py Normal file
View File

@ -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}

50
aubox/web/static/app.js Normal file
View File

@ -0,0 +1,50 @@
// Live-Refresh für Container mit data-refresh="<url>" und data-interval="<ms>"
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();
});

136
aubox/web/static/style.css Normal file
View File

@ -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); }

View File

@ -0,0 +1,20 @@
{% if not devices %}
<p class="empty">Kein bekanntes Hersteller-Gerät am USB.</p>
{% else %}
<table class="grid">
<thead>
<tr><th>Bus:Addr</th><th>VID:PID</th><th>Hersteller</th><th>Modus</th><th>Hinweis</th></tr>
</thead>
<tbody>
{% for d in devices %}
<tr>
<td>{{ "%03d"|format(d.bus) }}:{{ "%03d"|format(d.address) }}</td>
<td><code>{{ "%04x"|format(d.vid) }}:{{ "%04x"|format(d.pid) }}</code></td>
<td>{{ d.mode.vendor }}</td>
<td><strong>{{ d.mode.label }}</strong></td>
<td>{{ d.mode.notes }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}

View File

@ -0,0 +1,27 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<title>{% block title %}aubox{% endblock %}</title>
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<header>
<h1><a href="/">aubox</a></h1>
<nav>
<a href="/">Übersicht</a>
<a href="/devices">Geräte</a>
<a href="/firmware">Firmware</a>
<a href="/browse">Dateien</a>
<a href="/workflows/p10lite">P10 Lite</a>
</nav>
</header>
<main>
{% block content %}{% endblock %}
</main>
<footer>
<small>aubox · lokale Web-UI</small>
</footer>
<script src="/static/app.js"></script>
</body>
</html>

View File

@ -0,0 +1,36 @@
{% extends "base.html" %}
{% block title %}Dateien · aubox{% endblock %}
{% block content %}
<h2>Datei-Browser</h2>
<p class="muted">Sandboxed auf <code>{{ firmware_root }}</code> — Path-Traversal blockiert.</p>
<nav class="crumbs">
{% for label, p in crumbs %}
<a href="/browse?path={{ p }}">{{ label }}</a>
{% if not loop.last %} / {% endif %}
{% endfor %}
</nav>
{% if not entries %}
<p class="empty">Verzeichnis ist leer.</p>
{% else %}
<table class="grid">
<thead><tr><th>Name</th><th>Typ</th><th>Größe</th></tr></thead>
<tbody>
{% for e in entries %}
<tr>
<td>
{% if e.is_dir %}
<a href="/browse?path={{ e.rel_path }}">{{ e.name }}/</a>
{% else %}
{{ e.name }}
{% endif %}
</td>
<td>{{ "DIR" if e.is_dir else "FILE" }}</td>
<td>{{ e.size|humansize if not e.is_dir else "—" }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,9 @@
{% extends "base.html" %}
{% block title %}Geräte · aubox{% endblock %}
{% block content %}
<h2>Angeschlossene Geräte</h2>
<p>Aktualisiert sich alle 2 Sekunden.</p>
<div id="devices" data-refresh="/api/devices/html" data-interval="2000">
Lade…
</div>
{% endblock %}

View File

@ -0,0 +1,38 @@
{% extends "base.html" %}
{% block title %}Firmware · aubox{% endblock %}
{% block content %}
<h2>Firmware-Library</h2>
<p>Quelle: <code>{{ firmware_root }}</code></p>
<form id="scan-form" data-action="/firmware/scan" data-target="#scan-result">
<button type="submit">Library scannen</button>
<span id="scan-result"></span>
</form>
{% if not firmware %}
<p class="empty">Noch keine Einträge. Lege Firmware-Dateien unter
<code>{{ firmware_root }}</code> ab und klicke "Library scannen".</p>
{% else %}
<table class="grid">
<thead>
<tr>
<th>Vendor</th><th>Modell</th><th>SoC</th><th>Region</th>
<th>Format</th><th>Größe</th><th>Pfad</th>
</tr>
</thead>
<tbody>
{% for fw in firmware %}
<tr>
<td>{{ fw.vendor or "—" }}</td>
<td><a href="/firmware/{{ fw.id }}">{{ fw.model or "—" }}</a></td>
<td>{{ fw.soc or "—" }}</td>
<td>{{ fw.region or "—" }}</td>
<td>{{ fw.format }}</td>
<td>{{ fw.size|humansize }}</td>
<td><code>{{ fw.rel_path }}</code></td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,23 @@
{% extends "base.html" %}
{% block title %}{{ fw.model or fw.rel_path }} · aubox{% endblock %}
{% block content %}
<h2>{{ fw.model or "Unbekannt" }} <small>{{ fw.format }}</small></h2>
<table class="kv">
<tr><th>Pfad</th><td><code>{{ fw.rel_path }}</code></td></tr>
<tr><th>Vendor</th><td>{{ fw.vendor or "—" }}</td></tr>
<tr><th>Modell</th><td>{{ fw.model or "—" }}</td></tr>
<tr><th>SoC</th><td>{{ fw.soc or "—" }}</td></tr>
<tr><th>Region</th><td>{{ fw.region or "—" }}</td></tr>
<tr><th>Version</th><td>{{ fw.version or "—" }}</td></tr>
<tr><th>Größe</th><td>{{ fw.size|humansize }}</td></tr>
<tr><th>SHA-256</th><td><code>{{ fw.sha256 or "noch nicht berechnet" }}</code></td></tr>
</table>
{% if fw.extra_json %}
<h3>Format-spezifische Daten</h3>
<pre>{{ fw.extra_json }}</pre>
{% endif %}
<p><a href="/firmware">← zurück</a></p>
{% endblock %}

View File

@ -0,0 +1,39 @@
{% extends "base.html" %}
{% block title %}aubox · Übersicht{% endblock %}
{% block content %}
<section class="cards">
<article class="card">
<h2>Geräte</h2>
<p class="big">{{ devices|length }}</p>
<p>{{ "angeschlossen" if devices else "keins erkannt" }}</p>
<p><a href="/devices">öffnen →</a></p>
</article>
<article class="card">
<h2>Firmware-Library</h2>
<p class="big">{{ fw_count }}</p>
<p>Einträge in der Datenbank</p>
<p><a href="/firmware">öffnen →</a></p>
</article>
<article class="card">
<h2>Dateien</h2>
<p>Sandbox-Browser für die Firmware-Library</p>
<p><a href="/browse">öffnen →</a></p>
</article>
<article class="card">
<h2>Huawei P10 Lite</h2>
<p>FRP-Removal · WAS-LX1 · Kirin 658</p>
<p><a href="/workflows/p10lite">öffnen →</a></p>
</article>
</section>
<section class="info">
<h3>Aktive Pfade im Container</h3>
<ul>
<li><strong>Firmware:</strong> <code>{{ firmware_root }}</code></li>
<li><strong>Loader:</strong> <code>{{ loader_root }}</code></li>
</ul>
</section>
{% endblock %}

View File

@ -0,0 +1,15 @@
{% extends "base.html" %}
{% block title %}Huawei P10 Lite · aubox{% endblock %}
{% block content %}
<h2>Huawei P10 Lite — FRP-Removal</h2>
<p class="muted">WAS-LX1 · Kirin 658 · EMUI 8.x</p>
<h3>Methode 1: eRecovery + SD-Karte (empfohlen)</h3>
<pre>{{ instructions }}</pre>
<h3>Methode 2: Testpoint + Kirin DLOAD</h3>
<p>Über CLI vorbereitet, Web-UI-Button folgt:</p>
<pre>python -m aubox p10lite frp-remove --method dload-erase</pre>
<p>Voraussetzung: Loader-Files in <code>/loaders/kirin/kirin960_lite/</code>
(siehe Loader-README im Repo).</p>
{% endblock %}

28
docker-compose.yml Normal file
View File

@ -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

119
docs/huawei-p10-lite.md Normal file
View File

@ -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)

49
firmware/README.md Normal file
View File

@ -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.

59
loaders/README.md Normal file
View File

@ -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 `<install>/loaders/` oder
`<install>/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`.

0
loaders/kirin/.gitkeep Normal file
View File

4
requirements.txt Normal file
View File

@ -0,0 +1,4 @@
pyusb>=1.2.1
fastapi>=0.110
uvicorn[standard]>=0.27
jinja2>=3.1

30
scripts/run-docker.sh Executable file
View File

@ -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" "$@"

40
scripts/setup-linux.sh Executable file
View File

@ -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"

View File

@ -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"