TASK-066: the download page checks the password before fetching
A wrong password fetched the encrypted APK before being refused, and a phone browser can keep that fetch as a download. A small sealed check is opened first; nothing is fetched when it fails. Agent: main (opus) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
69556b1e23
commit
a02a987946
@ -44,4 +44,8 @@ to host it in a folder of the existing `choimy0204.github.io` site.
|
|||||||
|
|
||||||
## Review findings
|
## Review findings
|
||||||
|
|
||||||
None yet.
|
- 2026-10-07, product owner: a wrong password still downloaded. In a desktop browser a wrong
|
||||||
|
password got nothing, but the page fetched the encrypted APK before checking the password,
|
||||||
|
and a phone browser can keep that fetch as a download (still encrypted, not installable). The
|
||||||
|
page now opens a small sealed check with the password first and fetches nothing when it is
|
||||||
|
wrong; tried in the browser: a wrong password made no request, the right one downloaded.
|
||||||
|
|||||||
@ -25,6 +25,7 @@ from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|||||||
|
|
||||||
ITERATIONS = 310_000
|
ITERATIONS = 310_000
|
||||||
ENCRYPTED_NAME = "ubgitea.apk.enc"
|
ENCRYPTED_NAME = "ubgitea.apk.enc"
|
||||||
|
CHECK_TEXT = b"UBGitea"
|
||||||
|
|
||||||
|
|
||||||
def gradle_version(root: Path):
|
def gradle_version(root: Path):
|
||||||
@ -53,6 +54,10 @@ def main():
|
|||||||
iv = os.urandom(12)
|
iv = os.urandom(12)
|
||||||
key = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt, ITERATIONS, 32)
|
key = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt, ITERATIONS, 32)
|
||||||
sealed = AESGCM(key).encrypt(iv, data, None)
|
sealed = AESGCM(key).encrypt(iv, data, None)
|
||||||
|
# A few sealed bytes the page opens first: a wrong password is refused before the APK is
|
||||||
|
# fetched at all, so nothing is downloaded with it.
|
||||||
|
check_iv = os.urandom(12)
|
||||||
|
check = AESGCM(key).encrypt(check_iv, CHECK_TEXT, None)
|
||||||
|
|
||||||
args.out.mkdir(parents=True, exist_ok=True)
|
args.out.mkdir(parents=True, exist_ok=True)
|
||||||
(args.out / ENCRYPTED_NAME).write_bytes(sealed)
|
(args.out / ENCRYPTED_NAME).write_bytes(sealed)
|
||||||
@ -62,6 +67,8 @@ def main():
|
|||||||
"salt": base64.b64encode(salt).decode(),
|
"salt": base64.b64encode(salt).decode(),
|
||||||
"iv": base64.b64encode(iv).decode(),
|
"iv": base64.b64encode(iv).decode(),
|
||||||
"iterations": ITERATIONS,
|
"iterations": ITERATIONS,
|
||||||
|
"check": base64.b64encode(check).decode(),
|
||||||
|
"checkIv": base64.b64encode(check_iv).decode(),
|
||||||
"file": ENCRYPTED_NAME,
|
"file": ENCRYPTED_NAME,
|
||||||
"fileName": f"UBGitea-{name}.apk",
|
"fileName": f"UBGitea-{name}.apk",
|
||||||
"size": len(data),
|
"size": len(data),
|
||||||
@ -138,23 +145,24 @@ document.getElementById("form").addEventListener("submit", async event => {
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
go.disabled = true;
|
go.disabled = true;
|
||||||
try {
|
try {
|
||||||
say("파일을 받는 중…");
|
say("비밀번호 확인 중…");
|
||||||
const response = await fetch(META.file, { cache: "no-store" });
|
|
||||||
if (!response.ok) throw new Error("fetch");
|
|
||||||
const sealed = await response.arrayBuffer();
|
|
||||||
say("여는 중…");
|
|
||||||
const password = new TextEncoder().encode(document.getElementById("password").value);
|
const password = new TextEncoder().encode(document.getElementById("password").value);
|
||||||
const base = await crypto.subtle.importKey("raw", password, "PBKDF2", false, ["deriveKey"]);
|
const base = await crypto.subtle.importKey("raw", password, "PBKDF2", false, ["deriveKey"]);
|
||||||
const key = await crypto.subtle.deriveKey(
|
const key = await crypto.subtle.deriveKey(
|
||||||
{ name: "PBKDF2", salt: b64(META.salt), iterations: META.iterations, hash: "SHA-256" },
|
{ name: "PBKDF2", salt: b64(META.salt), iterations: META.iterations, hash: "SHA-256" },
|
||||||
base, { name: "AES-GCM", length: 256 }, false, ["decrypt"]);
|
base, { name: "AES-GCM", length: 256 }, false, ["decrypt"]);
|
||||||
let apk;
|
|
||||||
try {
|
try {
|
||||||
apk = await crypto.subtle.decrypt({ name: "AES-GCM", iv: b64(META.iv) }, key, sealed);
|
await crypto.subtle.decrypt({ name: "AES-GCM", iv: b64(META.checkIv) }, key, b64(META.check));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
say("비밀번호가 맞지 않습니다.", true);
|
say("비밀번호가 맞지 않습니다.", true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
say("파일을 받는 중…");
|
||||||
|
const response = await fetch(META.file, { cache: "no-store" });
|
||||||
|
if (!response.ok) throw new Error("fetch");
|
||||||
|
const sealed = await response.arrayBuffer();
|
||||||
|
say("여는 중…");
|
||||||
|
const apk = await crypto.subtle.decrypt({ name: "AES-GCM", iv: b64(META.iv) }, key, sealed);
|
||||||
const url = URL.createObjectURL(new Blob([apk], { type: "application/vnd.android.package-archive" }));
|
const url = URL.createObjectURL(new Blob([apk], { type: "application/vnd.android.package-archive" }));
|
||||||
const link = document.createElement("a");
|
const link = document.createElement("a");
|
||||||
link.href = url;
|
link.href = url;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user