"""Builds the password-protected download page for one APK (TASK-066). The APK is encrypted with AES-256-GCM under a key derived from the password with PBKDF2-SHA256, so the files can sit on a public static host: the page asks for the password and decrypts in the browser with WebCrypto. version.json beside it is plain; the app reads it to know a newer build is out. python tools/publish-download.py --notes "what changed" The password is read from UBGITEA_DOWNLOAD_PASSWORD, or asked for. It is never written anywhere. versionCode and versionName are read from app/build.gradle.kts. """ import argparse import base64 import getpass import hashlib import html import json import os import re import sys from pathlib import Path from cryptography.hazmat.primitives.ciphers.aead import AESGCM ITERATIONS = 310_000 ENCRYPTED_NAME = "ubgitea.apk.enc" def gradle_version(root: Path): text = (root / "app" / "build.gradle.kts").read_text(encoding="utf-8") code = re.search(r"versionCode\s*=\s*(\d+)", text) name = re.search(r'versionName\s*=\s*"([^"]+)"', text) if not code or not name: sys.exit("versionCode / versionName not found in app/build.gradle.kts") return int(code.group(1)), name.group(1) def main(): parser = argparse.ArgumentParser() parser.add_argument("apk", type=Path) parser.add_argument("out", type=Path) parser.add_argument("--notes", default="") args = parser.parse_args() password = os.environ.get("UBGITEA_DOWNLOAD_PASSWORD") or getpass.getpass("Download password: ") if not password: sys.exit("A password is required.") code, name = gradle_version(Path(__file__).resolve().parent.parent) data = args.apk.read_bytes() salt = os.urandom(16) iv = os.urandom(12) key = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt, ITERATIONS, 32) sealed = AESGCM(key).encrypt(iv, data, None) args.out.mkdir(parents=True, exist_ok=True) (args.out / ENCRYPTED_NAME).write_bytes(sealed) release = {"versionCode": code, "versionName": name, "notes": args.notes} (args.out / "version.json").write_text(json.dumps(release, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") meta = { "salt": base64.b64encode(salt).decode(), "iv": base64.b64encode(iv).decode(), "iterations": ITERATIONS, "file": ENCRYPTED_NAME, "fileName": f"UBGitea-{name}.apk", "size": len(data), } page = PAGE.replace("{{META}}", json.dumps(meta)) \ .replace("{{VERSION}}", html.escape(name)) \ .replace("{{NOTES}}", html.escape(args.notes)) \ .replace("{{SIZE}}", f"{len(data) / 1024 / 1024:.1f} MB") (args.out / "index.html").write_text(page, encoding="utf-8") print(f"UBGitea {name} (versionCode {code}) -> {args.out}") PAGE = """ UBGitea 다운로드

UBGitea

버전 {{VERSION}} · {{SIZE}}
{{NOTES}}

  1. 받은 APK 파일을 엽니다.
  2. "출처를 알 수 없는 앱" 설치를 이 브라우저에 허용합니다.
  3. 설치(업데이트)를 누릅니다. 로그인과 설정은 그대로 남습니다.
""" if __name__ == "__main__": main()