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:
parent
d0386b3c53
commit
fb3534553b
|
|
@ -0,0 +1,8 @@
|
|||
.venv/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
loaders/
|
||||
.git/
|
||||
docs/
|
||||
*.md
|
||||
!README.md
|
||||
|
|
@ -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"]
|
||||
|
|
@ -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.
|
||||
|
|
@ -0,0 +1 @@
|
|||
__version__ = "0.0.1"
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
from .cli import main
|
||||
|
||||
raise SystemExit(main())
|
||||
|
|
@ -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())
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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())
|
||||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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,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}
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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); }
|
||||
|
|
@ -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 %}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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 %}
|
||||
|
|
@ -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 %}
|
||||
|
|
@ -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 %}
|
||||
|
|
@ -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 %}
|
||||
|
|
@ -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 %}
|
||||
|
|
@ -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 %}
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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.
|
||||
|
|
@ -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,0 +1,4 @@
|
|||
pyusb>=1.2.1
|
||||
fastapi>=0.110
|
||||
uvicorn[standard]>=0.27
|
||||
jinja2>=3.1
|
||||
|
|
@ -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" "$@"
|
||||
|
|
@ -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"
|
||||
|
|
@ -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"
|
||||
Loading…
Reference in New Issue