009: Magisk ship pack through US-S5-1 + US-S4 OpenSSH/Jelly/Gallery2.

Release-keys/AVB, CVE clean+hi batches, Yama/slabrand, Pro branding,
bundled apps, and AXP-class product surface docs/scripts.
This commit is contained in:
sunfish-admin committed 2026-10-05 10:42:00 +02:00
1 parent fd793b9471
commit deaaf42caa
129 files changed
+11769 -141

No files matched your search

@@ -0,0 +1,222 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Attribute: AXP GOLDEN ConnectivityCheckPreferenceController (Settings.apk),
* cloned for LOS 23.2 without axp.os / captiveportal.axpos.org endpoints.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*/
package com.android.settings.network;
import android.content.ContentResolver;
import android.content.Context;
import android.provider.Settings;
import androidx.preference.ListPreference;
import androidx.preference.Preference;
import androidx.preference.PreferenceScreen;
import com.android.settings.R;
import com.android.settings.core.BasePreferenceController;
import com.android.settings.core.PreferenceControllerMixin;
import com.android.settingslib.RestrictedLockUtils;
import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
import com.android.settingslib.RestrictedLockUtilsInternal;
import com.android.settingslib.core.lifecycle.events.OnResume;
import android.os.UserHandle;
import android.os.UserManager;
/** Internet connectivity check provider list (captive portal URLs). */
public class ConnectivityCheckPreferenceController extends BasePreferenceController
implements PreferenceControllerMixin, Preference.OnPreferenceChangeListener, OnResume {
private static final String KEY = "connectivity_check_settings";
private static final int DISABLED = 0;
private static final int AMAZON = 1;
private static final int CLOUDFLARE = 2;
private static final int GOOGLE = 3;
private static final int HUAWEI = 4;
private static final int KUKETZ = 5;
private static final int MICROSOFT = 6;
private static final int OPENSUSE = 7;
private static final int UBUNTU = 8;
private ListPreference mConnectivityPreference;
public ConnectivityCheckPreferenceController(Context context) {
super(context, KEY);
}
@Override
public int getAvailabilityStatus() {
return isDisabledByAdmin() ? DISABLED_FOR_USER : AVAILABLE;
}
@Override
public String getPreferenceKey() {
return KEY;
}
@Override
public void displayPreference(PreferenceScreen screen) {
ListPreference pref = new ListPreference(screen.getContext());
pref.setKey(KEY);
pref.setOrder(30);
pref.setIcon(R.drawable.ic_settings_language);
pref.setTitle(R.string.connectivity_check_title);
pref.setSummary(R.string.connectivity_check_summary);
pref.setEntries(R.array.connectivity_check_entries);
pref.setEntryValues(R.array.connectivity_check_values);
pref.setOnPreferenceChangeListener(this);
if (mConnectivityPreference == null) {
screen.addPreference(pref);
mConnectivityPreference = pref;
}
super.displayPreference(screen);
updatePreferenceState();
}
private void updatePreferenceState() {
if (mConnectivityPreference == null) {
return;
}
ContentResolver cr = mContext.getContentResolver();
int mode = Settings.Global.getInt(cr, Settings.Global.CAPTIVE_PORTAL_MODE, 1);
if (mode == 0) {
mConnectivityPreference.setValueIndex(DISABLED);
return;
}
String http = Settings.Global.getString(cr, Settings.Global.CAPTIVE_PORTAL_HTTP_URL);
int idx = GOOGLE;
if ("http://fireoscaptiveportal.com/generate_204".equals(http)) {
idx = AMAZON;
} else if ("http://cp.cloudflare.com".equals(http)) {
idx = CLOUDFLARE;
} else if ("http://connectivitycheck.gstatic.com/generate_204".equals(http)) {
idx = GOOGLE;
} else if ("http://connectivitycheck.platform.hicloud.com/generate_204".equals(http)) {
idx = HUAWEI;
} else if ("http://captiveportal.kuketz.de".equals(http)) {
idx = KUKETZ;
} else if ("http://edge-http.microsoft.com/captiveportal/generate_204".equals(http)) {
idx = MICROSOFT;
} else if ("http://conncheck.opensuse.org".equals(http)) {
idx = OPENSUSE;
} else if ("http://connectivity-check.ubuntu.com".equals(http)) {
idx = UBUNTU;
}
mConnectivityPreference.setValueIndex(idx);
}
private void setCaptivePortalURLs(ContentResolver cr, int value) {
final String httpKey = Settings.Global.CAPTIVE_PORTAL_HTTP_URL;
final String httpsKey = Settings.Global.CAPTIVE_PORTAL_HTTPS_URL;
final String fallbackKey = Settings.Global.CAPTIVE_PORTAL_FALLBACK_URL;
final String otherKey = Settings.Global.CAPTIVE_PORTAL_OTHER_FALLBACK_URLS;
final String modeKey = Settings.Global.CAPTIVE_PORTAL_MODE;
String http;
String https;
String fallback;
String other;
int mode = 1;
switch (value) {
case DISABLED:
// Match GOLDEN: write Google URLs then mode=0
http = "http://connectivitycheck.gstatic.com/generate_204";
https = "https://www.google.com/generate_204";
fallback = "http://www.google.com/gen_204";
other = "http://play.googleapis.com/generate_204";
mode = 0;
break;
case AMAZON:
http = "http://fireoscaptiveportal.com/generate_204";
https = "https://fireoscaptiveportal.com/generate_204";
fallback = http;
other = http;
break;
case CLOUDFLARE:
http = "http://cp.cloudflare.com";
https = "https://cp.cloudflare.com";
fallback = http;
other = http;
break;
case HUAWEI:
http = "http://connectivitycheck.platform.hicloud.com/generate_204";
https = "https://connectivitycheck.platform.hicloud.com/generate_204";
fallback = http;
other = http;
break;
case KUKETZ:
http = "http://captiveportal.kuketz.de";
https = "https://captiveportal.kuketz.de";
fallback = http;
other = http;
break;
case MICROSOFT:
http = "http://edge-http.microsoft.com/captiveportal/generate_204";
https = http;
fallback = http;
other = http;
break;
case OPENSUSE:
http = "http://conncheck.opensuse.org";
https = "https://conncheck.opensuse.org";
fallback = http;
other = http;
break;
case UBUNTU:
http = "http://connectivity-check.ubuntu.com";
https = "https://connectivity-check.ubuntu.com";
fallback = http;
other = http;
break;
case GOOGLE:
default:
http = "http://connectivitycheck.gstatic.com/generate_204";
https = "https://www.google.com/generate_204";
fallback = "http://www.google.com/gen_204";
other = "http://play.googleapis.com/generate_204";
break;
}
Settings.Global.putString(cr, httpKey, http);
Settings.Global.putString(cr, httpsKey, https);
Settings.Global.putString(cr, fallbackKey, fallback);
Settings.Global.putString(cr, otherKey, other);
Settings.Global.putInt(cr, modeKey, mode);
}
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
if (KEY.equals(preference.getKey())) {
setCaptivePortalURLs(
mContext.getContentResolver(), Integer.parseInt((String) newValue));
return true;
}
return false;
}
@Override
public void onResume() {
updatePreferenceState();
if (mConnectivityPreference != null && mConnectivityPreference.getValue() != null) {
setCaptivePortalURLs(
mContext.getContentResolver(),
Integer.parseInt(mConnectivityPreference.getValue()));
}
}
private EnforcedAdmin getEnforcedAdmin() {
return RestrictedLockUtilsInternal.checkIfRestrictionEnforced(
mContext, UserManager.DISALLOW_CONFIG_PRIVATE_DNS, UserHandle.myUserId());
}
private boolean isDisabledByAdmin() {
return getEnforcedAdmin() != null;
}
}
+58
View File
@@ -0,0 +1,58 @@
# 009 AXP LOS23 port — patch / packaging README
**Track**: Magisk AXP resurrection on LOS 23.2 sunfish (`AXP_Resurection`).
**Not ship**: KernelSU / SusFS (`007` standby / optional recycle only).
## Apply order (host tree under `/srv/rom/src/lineage/`)
1. **Google stack** — `vendor/sunfish-a16/gms.mk` + `Android.mk` (microG, GSF,
Phonesky). Host APKs: `/srv/rom/prebuilts/{microg,phonesky}/` (never Git).
2. **Signature spoofing** — `vendor/sunfish-a16/patches/005-sigspoof/`
(`apply_005_sigspoof.py`). Attribute: LOS restricted spoofing + AXP/005.
3. **Call recording unlock** — `vendor/sunfish-a16/patches/008-call-rec-flag-secure/`
(`extendrom_call_recording`). Attribute: EXTENDROM `F_CALLREC`.
4. **Magisk preroot** — EXTENDROM-class boot/`init_boot` patch using Magisk APK
from `/srv/rom/prebuilts/magisk/` (see `docs/009-magisk-preinstall-runbook.md`).
Stub: `vendor/sunfish-a16/magisk.mk`. Attribute: Magisk upstream + sfX
extendrom Magisk patcher. **Host tree currently has no `vendor/extendrom`**
— must vendor minimal patcher or checkout extendrom before first Magisk bacon.
5. **Bundled apps** — `/srv/rom/prebuilts/axp-apps/` + `apps.mk` (Aurora,
Droidify, Fossify Gallery). Symlink APKs into `vendor/sunfish-a16/prebuilt/`.
6. **Private DNS presets** — `apply_009_private_dns_providers.py` (GOLDEN list).
7. **Internet connectivity check** — `apply_009_connectivity_check.py` (GOLDEN
list **minus** AXP.OS / `axpos.org`).
8. **Disable for user apps** — `apply_009_disable_user_apps.py` (SPA gate).
9. **Userspace hardening / signing** — staged: see `signing/README.md`,
`hardening/README.md` (US-S4-1 OpenSSH trim), and
`docs/009-hardening-signing-checklist.md` (US-S1 keys → US-S2 wire →
US-S3 AVB → US-S4+ harden). Bootloader lock = wont.
10. **Kernel hardening** — LOS23 `msm-4.14` + map in
`docs/009-kernel-hardening-map.md` (FR-017). Stage 1:
`vendor/sunfish-a16/patches/009-kernel-hardening/apply_stage1.sh`
(AXP Graphene/AH allowlist). Priority: AXP → LOS → Iodé → optional
Graphene; no Divest hunt; no extra location lockdown.
## Product includes
Wire Magisk-resurrection product from `vendor/sunfish-a16/` into
`lineage_sunfish` (same pattern as `004`/`005`). Keep Phonesky priv-app path.
**No FakeStore. No KernelSU product packages.**
## Deferred
- Banking Gate E / Island validation (owner future).
- Localization AXP-vs-LOS compare (`docs/009-localization-axp-vs-los.md` stub).
## Host paths (rom-dev01)
| What | Path |
|------|------|
| Magisk APK | `/srv/rom/prebuilts/magisk/Magisk-v30.7.apk` |
| EXTENDROM Magisk patcher | `/srv/rom/references/extendrom/config/magisk/` |
| Kernel source | `/srv/rom/src/lineage/kernel/google/msm-4.14` |
| Product include (gms) | `device/google/sunfish/lineage_sunfish.mk` → `vendor/sunfish-a16/gms.mk` |
| Vanilla baseline | `/srv/rom/artifacts/003-vanilla/20260914-lineage-23.2-sunfish/` |
| Optional KSU boots | `/srv/rom/artifacts/007-kernelsu/` (non-ship) |
**Gap:** `vendor/extendrom` is **not** checked out in the lineage tree yet.
Magisk bacon blocked on T011 (symlink/checkout + PREROOT or slim wire).
@@ -0,0 +1,114 @@
#!/usr/bin/env python3
"""009: port GOLDEN Internet connectivity check UI (sans axp.os).
Copies ConnectivityCheckPreferenceController, adds string/array resources,
wires into NetworkDashboardFragment. Idempotent.
"""
from __future__ import annotations
import pathlib
import shutil
import sys
MARKER = "sunfish-a16-009-connectivity-check"
ENTRIES = """ <!-- {m}: GOLDEN connectivity providers minus AXP.OS -->
<string-array name="connectivity_check_entries" translatable="false">
<item>Disabled</item>
<item>Amazon Fire OS (US)</item>
<item>Cloudflare (US)</item>
<item>Google (US) [default]</item>
<item>Huawei (CN)</item>
<item>Kuketz (DE)</item>
<item>Microsoft Edge (US)</item>
<item>openSUSE (DE)</item>
<item>Ubuntu (UK)</item>
</string-array>
<string-array name="connectivity_check_values" translatable="false">
<item>0</item>
<item>1</item>
<item>2</item>
<item>3</item>
<item>4</item>
<item>5</item>
<item>6</item>
<item>7</item>
<item>8</item>
</string-array>
""".format(
m=MARKER
)
STRINGS = f""" <!-- {MARKER} -->
<string name="connectivity_check_title" translatable="false">Internet connectivity check</string>
<string name="connectivity_check_summary" translatable="false">HTTP endpoints to use for performing internet connectivity checks.</string>
"""
def ensure_arrays(path: pathlib.Path) -> None:
text = path.read_text(encoding="utf-8")
if "connectivity_check_entries" in text:
return
if "</resources>" not in text:
raise SystemExit(f"FAIL: no </resources> in {path}")
path.write_text(text.replace("</resources>", ENTRIES + "</resources>", 1), encoding="utf-8")
def ensure_strings(path: pathlib.Path) -> None:
text = path.read_text(encoding="utf-8")
if "connectivity_check_title" in text:
return
path.write_text(text.replace("</resources>", STRINGS + "</resources>", 1), encoding="utf-8")
def wire_fragment(path: pathlib.Path) -> None:
text = path.read_text(encoding="utf-8")
if "ConnectivityCheckPreferenceController" in text:
return
if "controllers.add(privateDnsPreferenceController);" not in text:
raise SystemExit("FAIL: expected PrivateDns add site missing")
text = text.replace(
"controllers.add(privateDnsPreferenceController);",
"controllers.add(privateDnsPreferenceController);\n"
f" // {MARKER}\n"
" controllers.add(new ConnectivityCheckPreferenceController(context));",
1,
)
path.write_text(text, encoding="utf-8")
def main() -> int:
root = pathlib.Path(sys.argv[1] if len(sys.argv) > 1 else ".")
here = pathlib.Path(__file__).resolve().parent
src = here / "ConnectivityCheckPreferenceController.java"
dst = root / (
"packages/apps/Settings/src/com/android/settings/network/"
"ConnectivityCheckPreferenceController.java"
)
arrays = root / "packages/apps/Settings/res/values/arrays.xml"
# Prefer cm_strings for non-translated titles; fall back to strings.xml end
cm = root / "packages/apps/Settings/res/values/cm_strings.xml"
frag = root / (
"packages/apps/Settings/src/com/android/settings/network/NetworkDashboardFragment.java"
)
for p in (src, arrays, cm, frag):
if not p.is_file():
print(f"FAIL: missing {p}", file=sys.stderr)
return 1
if dst.is_file() and MARKER.split("-")[0] and "sans axp" in dst.read_text(encoding="utf-8"):
# already copied
pass
else:
dst.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(src, dst)
ensure_arrays(arrays)
ensure_strings(cm)
wire_fragment(frag)
print(f"OK: connectivity check wired -> {dst}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,52 @@
#!/usr/bin/env python3
"""009: allow classic Disable for non-system apps (A16 Archive era).
LOS AppDisableButton returns null for !isSystemApp. Graphene/AXP-class UX
keeps Disable usable for user apps. Attribute: AXP/owner preference over Archive.
Idempotent.
"""
from __future__ import annotations
import pathlib
import sys
MARKER = "sunfish-a16-009-disable-user-apps"
CANDIDATES = [
pathlib.Path("packages/apps/Settings/src/com/android/settings/spa/app/appinfo/AppDisableButton.kt"),
]
def main() -> int:
root = pathlib.Path(sys.argv[1] if len(sys.argv) > 1 else ".")
path = None
for rel in CANDIDATES:
p = root / rel
if p.is_file():
path = p
break
if path is None:
print("FAIL: AppDisableButton.kt not found", file=sys.stderr)
return 1
text = path.read_text(encoding="utf-8")
if MARKER in text:
print(f"OK: already applied ({path})")
return 0
old = " if (!app.isSystemApp) return null\n"
if old not in text:
print("FAIL: expected isSystemApp gate not found", file=sys.stderr)
return 2
new = (
f" // {MARKER}: allow Disable for user apps (not only system)\n"
f" // if (!app.isSystemApp) return null\n"
)
path.write_text(text.replace(old, new, 1), encoding="utf-8")
print(f"OK: patched {path}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,161 @@
#!/usr/bin/env python3
"""US-S4-2 — drop Lineage Jelly + Gallery2 to match AXP.OS PRO product surface.
Oracle: AXP.OS-22.2-20260324-PRO-sunfish.zip product/app has neither Jelly nor
Gallery2 (browser stub + FossifyGallery_FD live on system_ext). See
docs/009-axp-pro-product-apps-20260324.md.
FossifyGallery already declares LOCAL_OVERRIDES_PACKAGES := Gallery Gallery2
Glimpse, but Gallery2 still landed in product/app on the 162840Z ship — same
class of failure as OpenSSH filter-out. Patch the Lineage PRODUCT_PACKAGES
add sites and add belt-and-suspenders stubs.
Usage (from ANDROID_BUILD_TOP):
python3 vendor/sunfish-a16/patches/009-axp-los23-port/apply_009_drop_jelly_gallery2.py
"""
from __future__ import annotations
import os
import re
import sys
from pathlib import Path
MARKER = "sunfish-a16-009-uss4-2-jelly-gallery2"
# Common Lineage shapes (tolerant).
JELLY_BLOCK = re.compile(
r"# Browser\n"
r"PRODUCT_PACKAGES \+= \\\n"
r" Jelly\n",
re.MULTILINE,
)
# ExactCalculator + Jelly list (common Lineage shape) → keep ExactCalculator only.
JELLY_WITH_CALC = re.compile(
r"(PRODUCT_PACKAGES \+= \\\n"
r" ExactCalculator) \\\n"
r" Jelly\n",
re.MULTILINE,
)
GALLERY2_IN_LIST = re.compile(
r"( DeskClock \\\n)"
r" Gallery2 \\\n",
re.MULTILINE,
)
def patch_text(text: str, path: Path) -> tuple[str, list[str]]:
notes: list[str] = []
bad = f"# Jelly stripped ({MARKER})" in text or f"# Gallery2 stripped ({MARKER})" in text
has_pkg = bool(re.search(r"(?m)^[ \t]*Jelly( \\)?$", text)) or bool(
re.search(r"(?m)^[ \t]*Gallery2( \\)?$", text)
)
if MARKER in text and not bad and not has_pkg:
notes.append("already")
return text, notes
orig = text
text, n = JELLY_BLOCK.subn(
f"# Browser (Jelly) omitted ({MARKER}) — AXP PRO uses noDefaultBrowserWebView\n",
text,
count=1,
)
if n:
notes.append(f"jelly_block={n}")
text, n = JELLY_WITH_CALC.subn(
rf"\1\n",
text,
count=1,
)
if n:
notes.append(f"jelly_calc={n}")
text, n = GALLERY2_IN_LIST.subn(
rf"\1",
text,
count=1,
)
if n:
notes.append(f"gallery2_list={n}")
# Remove leftover broken comment-only continuations from older applicator.
text2 = re.sub(
r"^[ \t]*# (?:Jelly|Gallery2) stripped \(" + re.escape(MARKER) + r"\)\n",
"",
text,
flags=re.MULTILINE,
)
if text2 != text:
notes.append("scrub_bad_comments")
text = text2
if text == orig and not notes:
return orig, notes
if MARKER not in text and text != orig:
text = f"# {MARKER}\n" + text
return text, notes
def main() -> int:
top = Path(os.environ.get("ANDROID_BUILD_TOP", ".")).resolve()
candidates = [
top / "vendor/lineage/config/common.mk",
top / "vendor/lineage/config/common_mobile.mk",
top / "vendor/lineage/config/common_full.mk",
top / "vendor/lineage/config/telephony.mk",
top / "build/make/target/product/handheld_product.mk",
top / "build/make/target/product/handheld_system.mk",
top / "build/make/target/product/generic_system.mk",
]
# Also scan device trees that often force Gallery2
for p in (top / "device").rglob("*.mk") if (top / "device").is_dir() else []:
if p.name in ("device.mk", "lineage.mk", "lineage_sunfish.mk", "aosp_sunfish.mk"):
candidates.append(p)
touched = 0
still_present = []
for mk in candidates:
if not mk.is_file():
continue
text = mk.read_text(encoding="utf-8", errors="replace")
if "Jelly" not in text and "Gallery2" not in text:
continue
# Ignore mention-only in comments/docs after a clean strip.
has_pkg = bool(re.search(r"(?m)^[ \t]*Jelly( \\)?$", text)) or bool(
re.search(r"(?m)^[ \t]*Gallery2( \\)?$", text)
)
bad = f"# Jelly stripped ({MARKER})" in text or f"# Gallery2 stripped ({MARKER})" in text
if not has_pkg and not bad:
continue
new, notes = patch_text(text, mk)
if notes == ["already"]:
print(f"OK: already applied ({mk})")
touched += 1
continue
if new == text:
print(f"WARN: {mk} mentions Jelly/Gallery2 but no pattern matched: {notes}")
still_present.append(str(mk))
continue
mk.write_text(new, encoding="utf-8")
print(f"OK: patched {mk} ({', '.join(notes)})")
touched += 1
if still_present:
print(
"FAIL: Jelly/Gallery2 still present, patterns missed: "
+ ", ".join(still_present),
file=sys.stderr,
)
return 1
if touched == 0:
print("OK: Jelly/Gallery2 already absent from PRODUCT_PACKAGES sites")
return 0
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,99 @@
#!/usr/bin/env python3
"""US-S4-1 — strip OpenSSH/rsync/procmem packages from Lineage common.mk.
AXP.OS PRO sunfish (A15) product/system lack sshd/ssh/rsync/procmem binaries but
still ship init.openssh.rc. PRODUCT_PACKAGES filter-out via inherit-product did
*not* remove them from the 20261002T125259Z ship image — patch the source block
instead (same place Lineage adds them).
Keeps PRODUCT_COPY_FILES for init.openssh.rc (AXP surface).
Usage:
python3 vendor/sunfish-a16/patches/009-axp-los23-port/apply_009_drop_openssh_packages.py
"""
from __future__ import annotations
import os
import re
import sys
from pathlib import Path
MARKER = "sunfish-a16-009-uss4-openssh"
OPENSSH_PACKAGES = re.compile(
r"# Openssh\n"
r"PRODUCT_PACKAGES \+= \\\n"
r"(?: \S+ \\\n)*"
r" start-ssh\n",
re.MULTILINE,
)
RSYNC_PACKAGES = re.compile(
r"# rsync\n"
r"PRODUCT_PACKAGES \+= \\\n"
r" rsync\n",
re.MULTILINE,
)
PROCMEM_DEBUG = re.compile(
r"# These packages are excluded from user builds\n"
r"PRODUCT_PACKAGES_DEBUG \+= \\\n"
r" procmem\n"
r"\n"
r"ifneq \(\$\(TARGET_BUILD_VARIANT\),user\)\n"
r"PRODUCT_ARTIFACT_PATH_REQUIREMENT_ALLOWED_LIST \+= \\\n"
r" system/bin/procmem\n"
r"endif\n",
re.MULTILINE,
)
def main() -> int:
top = Path(os.environ.get("ANDROID_BUILD_TOP", ".")).resolve()
mk = top / "vendor/lineage/config/common.mk"
if not mk.is_file():
print(f"FAIL: missing {mk}", file=sys.stderr)
return 1
text = mk.read_text()
if MARKER in text:
print(f"OK: already applied ({mk})")
return 0
orig = text
text, n1 = OPENSSH_PACKAGES.subn(
f"# Openssh packages stripped ({MARKER}) — keep init.openssh.rc below\n",
text,
count=1,
)
text, n2 = RSYNC_PACKAGES.subn(
f"# rsync stripped ({MARKER})\n",
text,
count=1,
)
text, n3 = PROCMEM_DEBUG.subn(
f"# procmem debug package stripped ({MARKER})\n",
text,
count=1,
)
if n1 != 1 or n2 != 1:
print(
f"FAIL: Openssh/rsync block match n1={n1} n2={n2} n3={n3}",
file=sys.stderr,
)
return 1
# procmem block optional if layout drifted
if n3 == 0:
print("WARN: procmem debug block not matched (layout drift)")
if "init.openssh.rc" not in text:
print("FAIL: init.openssh.rc COPY_FILES missing after patch", file=sys.stderr)
return 1
mk.write_text(text)
print(f"OK: patched {mk} (openssh_pkgs={n1} rsync={n2} procmem={n3})")
return 0 if text != orig else 1
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,498 @@
#!/usr/bin/env python3
"""009: expand Private DNS presets to AXP GOLDEN list (no axp.os).
Clones GOLDEN Settings radios/hostnames into LOS PrivateDnsModeDialogPreference
(which already has a Cloudflare stub). Idempotent via marker comments.
"""
from __future__ import annotations
import pathlib
import re
import sys
MARKER = "sunfish-a16-009-private-dns"
# key, UI label, DoT hostname — GOLDEN order (cloudflare hostname = security.*)
PROVIDERS = [
("adguard", "AdGuard (CT)", "dns.adguard.com"),
("appliedprivacy", "AppliedPrivacy (AT)", "dot1.applied-privacy.net"),
("cira", "CIRA (CA)", "protected.canadianshield.cira.ca"),
("controld", "Control D (CA)", "p2.freedns.controld.com"),
("cleanbrowsing", "CleanBrowsing (US)", "security-filter-dns.cleanbrowsing.org"),
("cznic", "CZ.NIC (CZ)", "odvr.nic.cz"),
("cloudflare", "Cloudflare (US)", "security.cloudflare-dns.com"),
("dnszero", "DNS0 (FR)", "zero.dns0.eu"),
("dnssb", "DNS.SB (DE)", "dot.sb"),
("google", "Google (US)", "dns.google"),
("mullvad", "Mullvad (SE)", "base.dns.mullvad.net"),
("quadnine", "Quad9 (CH)", "dns.quad9.net"),
("restena", "Restena (LU)", "kaitain.restena.lu"),
("switch", "SWITCH (CH)", "dns.switch.ch"),
("uncensoreddns", "UncensoredDNS (DK)", "anycast.censurfridns.dk"),
]
def patch_layout(path: pathlib.Path) -> bool:
text = path.read_text(encoding="utf-8")
if MARKER in text:
return False
# Insert provider radios after off, before opportunistic (keep provider+hostname)
includes = []
for key, _, _ in PROVIDERS:
includes.append(
f' <!-- {MARKER} -->\n'
f' <include\n'
f' android:id="@+id/private_dns_mode_{key}"\n'
f' layout="@layout/preference_widget_dialog_radiobutton"/>\n'
)
block = "".join(includes)
# Remove ALL existing cloudflare includes (re-added once via PROVIDERS)
text2 = re.sub(
r'\s*(?:<!--.*?-->\s*)?<include\s*\n\s*android:id="@\+id/private_dns_mode_cloudflare"\s*\n\s*layout="@layout/preference_widget_dialog_radiobutton"/>\s*',
"\n",
text,
flags=re.DOTALL,
)
text2 = re.sub(
r'\s*<include\s+android:id="@\+id/private_dns_mode_cloudflare"[^/]*/>\s*',
"\n",
text2,
)
needle = (
' <include\n'
' android:id="@+id/private_dns_mode_opportunistic"\n'
' layout="@layout/preference_widget_dialog_radiobutton"/>'
)
if needle not in text2:
# alternate formatting
needle = 'android:id="@+id/private_dns_mode_opportunistic"'
idx = text2.find(needle)
if idx < 0:
raise SystemExit(f"FAIL: opportunistic radio not in {path}")
# find start of include element
start = text2.rfind("<include", 0, idx)
text2 = text2[:start] + block + text2[start:]
else:
text2 = text2.replace(needle, block + needle, 1)
path.write_text(text2, encoding="utf-8")
return True
def patch_cm_strings(path: pathlib.Path) -> bool:
text = path.read_text(encoding="utf-8")
if MARKER in text and "private_dns_hostname_quadnine" in text:
return False
# Replace/extend cloudflare hostname to GOLDEN security.cloudflare-dns.com
text = re.sub(
r'<string name="private_dns_hostname_cloudflare"[^>]*>.*?</string>',
'<string name="private_dns_hostname_cloudflare" translatable="false">'
"security.cloudflare-dns.com</string>",
text,
count=1,
)
text = re.sub(
r'<string name="private_dns_mode_cloudflare"[^>]*>.*?</string>',
'<string name="private_dns_mode_cloudflare" translatable="false">'
"Cloudflare (US)</string>",
text,
count=1,
)
lines = [f" <!-- {MARKER}: AXP GOLDEN Private DNS presets -->"]
for key, label, host in PROVIDERS:
if key == "cloudflare":
continue # already present
lines.append(
f' <string name="private_dns_mode_{key}" translatable="false">{label}</string>'
)
lines.append(
f' <string name="private_dns_hostname_{key}" translatable="false">{host}</string>'
)
insert = "\n".join(lines) + "\n"
if "</resources>" not in text:
raise SystemExit(f"FAIL: no </resources> in {path}")
text = text.replace("</resources>", insert + "</resources>", 1)
path.write_text(text, encoding="utf-8")
return True
def write_preference_java(path: pathlib.Path) -> None:
"""Rewrite PrivateDnsModeDialogPreference with data-driven GOLDEN presets."""
# Keep copyright / package; replace body after class declaration via full file.
providers_java = ",\n".join(
f' new Provider("{key}", R.id.private_dns_mode_{key}, '
f"R.string.private_dns_mode_{key}, R.string.private_dns_hostname_{key})"
for key, _, _ in PROVIDERS
)
# Assign custom mode ints starting at 4 (LOS cloudflare used 4)
mode_consts = "\n".join(
f" private static final int PRIVATE_DNS_MODE_{key.upper()} = {4 + i};"
for i, (key, _, _) in enumerate(PROVIDERS)
)
map_puts = "\n".join(
f" PRIVATE_DNS_MAP.put(PRIVATE_DNS_MODE_{key.upper()}, "
f"R.id.private_dns_mode_{key});"
for key, _, _ in PROVIDERS
)
content = f'''/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.network;
import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE_OFF;
import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE_OPPORTUNISTIC;
import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE_PROVIDER_HOSTNAME;
import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
import android.app.Dialog;
import android.app.settings.SettingsEnums;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.ConnectivitySettingsManager;
import android.os.UserHandle;
import android.os.UserManager;
import android.text.Editable;
import android.text.TextWatcher;
import android.text.method.LinkMovementMethod;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.TextView;
import androidx.annotation.VisibleForTesting;
import androidx.appcompat.app.AlertDialog;
import androidx.preference.PreferenceViewHolder;
import com.android.settings.R;
import com.android.settings.overlay.FeatureFactory;
import com.android.settings.utils.AnnotationSpan;
import com.android.settingslib.CustomDialogPreferenceCompat;
import com.android.settingslib.HelpUtils;
import com.android.settingslib.RestrictedLockUtils;
import com.android.settingslib.RestrictedLockUtilsInternal;
import com.google.android.material.textfield.TextInputLayout;
import com.google.common.net.InternetDomainName;
import java.util.HashMap;
import java.util.Map;
/**
* Dialog to set the Private DNS.
* {MARKER}: AXP GOLDEN provider presets (hostnames from GOLDEN Settings.apk).
*/
public class PrivateDnsModeDialogPreference extends CustomDialogPreferenceCompat implements
RadioGroup.OnCheckedChangeListener, TextWatcher {{
public static final String ANNOTATION_URL = "url";
private static final String TAG = "PrivateDnsModeDialog";
private static final Map<Integer, Integer> PRIVATE_DNS_MAP;
{mode_consts}
private static final Provider[] PROVIDERS = new Provider[] {{
{providers_java}
}};
static {{
PRIVATE_DNS_MAP = new HashMap<>();
PRIVATE_DNS_MAP.put(PRIVATE_DNS_MODE_OFF, R.id.private_dns_mode_off);
PRIVATE_DNS_MAP.put(PRIVATE_DNS_MODE_OPPORTUNISTIC, R.id.private_dns_mode_opportunistic);
PRIVATE_DNS_MAP.put(PRIVATE_DNS_MODE_PROVIDER_HOSTNAME, R.id.private_dns_mode_provider);
{map_puts}
}}
private static final class Provider {{
final String key;
final int radioId;
final int labelRes;
final int hostnameRes;
Provider(String key, int radioId, int labelRes, int hostnameRes) {{
this.key = key;
this.radioId = radioId;
this.labelRes = labelRes;
this.hostnameRes = hostnameRes;
}}
}}
@VisibleForTesting
TextInputLayout mHostnameLayout;
@VisibleForTesting
EditText mHostnameText;
@VisibleForTesting
RadioGroup mRadioGroup;
@VisibleForTesting
int mMode;
public PrivateDnsModeDialogPreference(Context context) {{
super(context);
}}
public PrivateDnsModeDialogPreference(Context context, AttributeSet attrs) {{
super(context, attrs);
}}
public PrivateDnsModeDialogPreference(Context context, AttributeSet attrs, int defStyleAttr) {{
super(context, attrs, defStyleAttr);
}}
public PrivateDnsModeDialogPreference(Context context, AttributeSet attrs, int defStyleAttr,
int defStyleRes) {{
super(context, attrs, defStyleAttr, defStyleRes);
}}
private final AnnotationSpan.LinkInfo mUrlLinkInfo = new AnnotationSpan.LinkInfo(
ANNOTATION_URL, (widget) -> {{
final Context context = widget.getContext();
final Intent intent = HelpUtils.getHelpIntent(context,
context.getString(R.string.help_uri_private_dns),
context.getClass().getName());
if (intent != null) {{
try {{
widget.startActivityForResult(intent, 0);
}} catch (ActivityNotFoundException e) {{
Log.w(TAG, "Activity was not found for intent, " + intent.toString());
}}
}}
}});
private static int modeForRadioId(int radioId) {{
if (radioId == R.id.private_dns_mode_off) return PRIVATE_DNS_MODE_OFF;
if (radioId == R.id.private_dns_mode_opportunistic) return PRIVATE_DNS_MODE_OPPORTUNISTIC;
if (radioId == R.id.private_dns_mode_provider) return PRIVATE_DNS_MODE_PROVIDER_HOSTNAME;
for (int i = 0; i < PROVIDERS.length; i++) {{
if (PROVIDERS[i].radioId == radioId) {{
return 4 + i;
}}
}}
return PRIVATE_DNS_MODE_OPPORTUNISTIC;
}}
private static Provider providerForMode(int mode) {{
int idx = mode - 4;
if (idx >= 0 && idx < PROVIDERS.length) {{
return PROVIDERS[idx];
}}
return null;
}}
@Override
public void onBindViewHolder(PreferenceViewHolder holder) {{
super.onBindViewHolder(holder);
if (isDisabledByAdmin()) {{
holder.itemView.setEnabled(true);
}}
setSaveButtonListener();
}}
@Override
protected void onBindDialogView(View view) {{
final Context context = getContext();
mMode = ConnectivitySettingsManager.getPrivateDnsMode(context);
if (mMode == PRIVATE_DNS_MODE_PROVIDER_HOSTNAME) {{
final String host = ConnectivitySettingsManager.getPrivateDnsHostname(context);
for (int i = 0; i < PROVIDERS.length; i++) {{
if (host != null && host.equals(context.getString(PROVIDERS[i].hostnameRes))) {{
mMode = 4 + i;
break;
}}
}}
}}
mRadioGroup = view.findViewById(R.id.private_dns_radio_group);
mRadioGroup.check(PRIVATE_DNS_MAP.getOrDefault(mMode, R.id.private_dns_mode_opportunistic));
mRadioGroup.setOnCheckedChangeListener(this);
final RadioButton offRadioButton = view.findViewById(R.id.private_dns_mode_off);
offRadioButton.setText(com.android.settingslib.R.string.private_dns_mode_off);
for (Provider p : PROVIDERS) {{
final RadioButton rb = view.findViewById(p.radioId);
if (rb != null) {{
rb.setText(p.labelRes);
}}
}}
final RadioButton opportunisticRadioButton =
view.findViewById(R.id.private_dns_mode_opportunistic);
opportunisticRadioButton.setText(
com.android.settingslib.R.string.private_dns_mode_opportunistic);
final RadioButton providerRadioButton = view.findViewById(R.id.private_dns_mode_provider);
providerRadioButton.setText(com.android.settingslib.R.string.private_dns_mode_provider);
mHostnameLayout = view.findViewById(R.id.private_dns_mode_provider_hostname_layout);
mHostnameText = view.findViewById(R.id.private_dns_mode_provider_hostname);
if (mHostnameText != null) {{
mHostnameText.setText(ConnectivitySettingsManager.getPrivateDnsHostname(context));
mHostnameText.addTextChangedListener(this);
}}
final TextView helpTextView = view.findViewById(R.id.private_dns_help_info);
helpTextView.setMovementMethod(LinkMovementMethod.getInstance());
final Intent helpIntent = HelpUtils.getHelpIntent(context,
context.getString(R.string.help_uri_private_dns),
context.getClass().getName());
final AnnotationSpan.LinkInfo linkInfo = new AnnotationSpan.LinkInfo(context,
ANNOTATION_URL, helpIntent);
if (linkInfo.isActionable()) {{
helpTextView.setText(AnnotationSpan.linkify(
context.getText(R.string.private_dns_help_message), linkInfo));
}} else {{
helpTextView.setText("");
}}
updateDialogInfo();
}}
@Override
public void onCheckedChanged(RadioGroup group, int checkedId) {{
mMode = modeForRadioId(checkedId);
updateDialogInfo();
}}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {{
}}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {{
}}
@Override
public void afterTextChanged(Editable s) {{
updateDialogInfo();
}}
@Override
public void performClick() {{
EnforcedAdmin enforcedAdmin = getEnforcedAdmin();
if (enforcedAdmin == null) {{
super.performClick();
}} else {{
RestrictedLockUtils.sendShowAdminSupportDetailsIntent(getContext(), enforcedAdmin);
}}
}}
private EnforcedAdmin getEnforcedAdmin() {{
return RestrictedLockUtilsInternal.checkIfRestrictionEnforced(
getContext(), UserManager.DISALLOW_CONFIG_PRIVATE_DNS, UserHandle.myUserId());
}}
private boolean isDisabledByAdmin() {{
return getEnforcedAdmin() != null;
}}
private void updateDialogInfo() {{
final boolean modeProvider = PRIVATE_DNS_MODE_PROVIDER_HOSTNAME == mMode;
if (mHostnameLayout != null) {{
mHostnameLayout.setEnabled(modeProvider);
mHostnameLayout.setErrorEnabled(false);
}}
}}
private void setSaveButtonListener() {{
View.OnClickListener onClickListener = v -> doSaveButton();
DialogInterface.OnShowListener onShowListener = dialog -> {{
if (dialog == null) {{
Log.e(TAG, "The DialogInterface is null!");
return;
}}
Button saveButton = ((AlertDialog) dialog).getButton(DialogInterface.BUTTON_POSITIVE);
if (saveButton == null) {{
Log.e(TAG, "Can't get the save button!");
return;
}}
saveButton.setOnClickListener(onClickListener);
}};
setOnShowListener(onShowListener);
}}
@VisibleForTesting
void doSaveButton() {{
Context context = getContext();
int modeToSet = mMode;
Provider preset = providerForMode(mMode);
if (preset != null) {{
ConnectivitySettingsManager.setPrivateDnsHostname(context,
context.getString(preset.hostnameRes));
modeToSet = PRIVATE_DNS_MODE_PROVIDER_HOSTNAME;
}} else if (mMode == PRIVATE_DNS_MODE_PROVIDER_HOSTNAME) {{
if (mHostnameLayout == null || mHostnameText == null) {{
Log.e(TAG, "Can't find hostname resources!");
return;
}}
if (mHostnameText.getText().isEmpty()) {{
mHostnameLayout.setError(context.getString(R.string.private_dns_field_require));
Log.w(TAG, "The hostname is empty!");
return;
}}
if (!InternetDomainName.isValid(mHostnameText.getText().toString())) {{
mHostnameLayout.setError(context.getString(R.string.private_dns_hostname_invalid));
Log.w(TAG, "The hostname is invalid!");
return;
}}
ConnectivitySettingsManager.setPrivateDnsHostname(context,
mHostnameText.getText().toString());
}}
ConnectivitySettingsManager.setPrivateDnsMode(context, modeToSet);
FeatureFactory.getFeatureFactory().getMetricsFeatureProvider()
.action(context, SettingsEnums.ACTION_PRIVATE_DNS_MODE, modeToSet);
Dialog dialog = getDialog();
if (dialog != null) {{
dialog.dismiss();
}}
}}
}}
'''
# Fix f-string doubled braces - we used {{ for literal braces in the template
# But MARKER was intended as comment - replace
content = content.replace("{MARKER}", MARKER)
path.write_text(content, encoding="utf-8")
def main() -> int:
root = pathlib.Path(sys.argv[1] if len(sys.argv) > 1 else ".")
layout = root / "packages/apps/Settings/res/layout/private_dns_mode_dialog.xml"
cm = root / "packages/apps/Settings/res/values/cm_strings.xml"
java = root / (
"packages/apps/Settings/src/com/android/settings/network/"
"PrivateDnsModeDialogPreference.java"
)
for p in (layout, cm, java):
if not p.is_file():
print(f"FAIL: missing {p}", file=sys.stderr)
return 1
if MARKER in java.read_text(encoding="utf-8") and "quadnine" in layout.read_text(
encoding="utf-8"
):
print("OK: private DNS already applied")
return 0
patch_layout(layout)
patch_cm_strings(cm)
write_preference_java(java)
print(f"OK: private DNS providers applied ({len(PROVIDERS)} presets)")
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,41 @@
# Apply early TARGET_UNOFFICIAL_BUILD_ID to device product makefile.
# Must run before breakfast: inherit-product of vendor/lineage parses version.mk
# immediately; vendor/extra inherit-product is too late.
#
# Usage (on rom-dev01 tree):
# python3 vendor/sunfish-a16/patches/009-axp-los23-port/apply_009_unofficial_build_id.py
from __future__ import annotations
from pathlib import Path
import os
import sys
MARKER = "TARGET_UNOFFICIAL_BUILD_ID := AXP-Resurection-Pro"
NEEDLE = "# Inherit some common Lineage stuff.\n"
INSERT = (
"# sunfish-a16: must precede vendor/lineage version.mk (via inherit below)\n"
f"{MARKER}\n\n"
)
def main() -> int:
top = Path(os.environ.get("ANDROID_BUILD_TOP", ".")).resolve()
mk = top / "device/google/sunfish/lineage_sunfish.mk"
if not mk.is_file():
print(f"FAIL: missing {mk}", file=sys.stderr)
return 1
text = mk.read_text()
if MARKER in text:
print(f"OK: already applied ({mk})")
return 0
if NEEDLE not in text:
print(f"FAIL: needle not found in {mk}", file=sys.stderr)
return 1
mk.write_text(text.replace(NEEDLE, INSERT + NEEDLE, 1))
print(f"OK: patched {mk}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,67 @@
# US-S4-1 — drop Lineage product OpenSSH binaries / rsync / procmem
**Track**: AXP Resurection Pro (Magisk), unlocked sunfish.
**Not**: slim variant; not bootloader lock; not Yama (kernel).
## AXP reference (mandatory)
Oracle: `AXP.OS-22.2-20260324-PRO-sunfish.zip` → `payload.bin` → `product.img`
(+ GOLDEN `ro.lineage.version=22.2-2026.003-sunfish-pro`, Android 15):
| Path | AXP PRO (A15) |
|------|----------------|
| `/bin/sshd`, `ssh`, `scp`, `sftp`, `ssh-keygen`, `start-ssh` | **absent** |
| `/bin/rsync`, `/bin/procmem` | **absent** |
| `/etc/init/init.openssh.rc` | **present** (`mkdir /data/ssh*`) |
### Was this an A16 Lineage add?
**No.** Lineage `vendor/lineage/config/common.mk` has shipped the Openssh
block since 2012; lineage-20 / 21 / 22.2 all still include it. AXP PRO on
A15 therefore **actively omitted** the binaries (same lineage generation
still lists them upstream). Public `AXP.OS/android_vendor_lineage`
`upstream_*` mirrors keep the Lineage Openssh block — use the **PRO zip /
GOLDEN**, not that mirror, as product oracle.
## What we ship
Match AXP PRO product surface by stripping Lineage `common.mk` package
adds (see `apply_009_drop_openssh_packages.py`) — a plain
`PRODUCT_PACKAGES := $(filter-out …)` inherit did **not** drop `sshd` from
ship `20261002T125259Z`:
- Remove packages: `scp`, `sftp`, `ssh`, `sshd`, `sshd_config`, `ssh-keygen`,
`start-ssh`, `rsync`, `procmem` (debug)
- **Keep** `init.openssh.rc` copy (AXP has it)
Remote shell / file transfer on this track remains **adb** (+ Magisk); the
rc only creates `/data/ssh*` and does not start `sshd` without the binary.
## Verify (LAB after flash)
```text
adb shell ls /product/bin/sshd # expect: No such file
adb shell ls /product/bin/rsync # expect: No such file
adb shell ls /product/etc/init/init.openssh.rc # expect: present (AXP)
adb shell pidof magiskd # expect: non-empty
```
## US-S4-2 — drop Jelly + Gallery2 (AXP PRO product)
**Oracle**: same PRO zip — `product/app` has **no** Jelly / Gallery2;
`system_ext` ships Fossify + `noDefaultBrowserWebView`. Inventory:
`docs/009-axp-pro-product-apps-20260324.md`.
- `apply_009_drop_jelly_gallery2.py` — remove Lineage `PRODUCT_PACKAGES`
adds for Jelly / Gallery2
- `NoLineageJelly` uninstallable override module (AXP stub class)
- Keep `FossifyGallery` (already overrides Gallery/Gallery2/Glimpse)
LAB after flash: no `/product/app/Jelly`, no `/product/app/Gallery2`,
FossifyGallery present.
## Out of scope
- `CONFIG_SECURITY_YAMA` (kernel / US-S5 — **done** on `162840Z`)
- Switching lunch to `user` (US-S6)
- NeoLauncher / Aosmium (wont / exception)
@@ -0,0 +1,89 @@
# 009 signing / AVB (userspace Gate G)
Clone AXP/GOLDEN posture: **`user` + `release-keys`**, device **unlocked**
(`orange`). Not IOdes locked-bootloader.
Full checklist: `docs/009-hardening-signing-checklist.md`.
## Stages
| Stage | What |
|-------|------|
| US-S1 | Generate keys → `/srv/rom/secrets/android-certs/` |
| US-S2 | Wire `PRODUCT_DEFAULT_DEV_CERTIFICATE` into bacon |
| US-S3 | Project AVB key (`AVB_CUSTOM_KEY_PATH`); decide `WITH_AVB` |
| US-S4+ | Userspace harden / CVE — separate patches |
## Generate (rom-dev01 only)
```bash
# from synced repo copy or laptop scp of scripts/
sudo -u android-build bash /srv/rom/src/lineage/vendor/sunfish-a16/../../../../\
# prefer:
bash /path/to/AXP_Resurection/scripts/009-generate-release-keys.sh
```
Script writes under `/srv/rom/secrets/android-certs/` when that parent is
writable, otherwise `$HOME/.android-certs` (Lineage default). On rom-dev01
(2026-09-29) secrets/ is root-owned without group write → keys live in
`/home/android-build/.android-certs/`. Refuses overwrite. Reminds about
encrypted offline backup before first signed ship bacon.
## Wire (US-S2 — after keys exist)
In the bacon environment (not committed secrets):
```bash
export PRODUCT_DEFAULT_DEV_CERTIFICATE=$HOME/.android-certs/releasekey
# after root relocate: /srv/rom/secrets/android-certs/releasekey
# lunch lineage_sunfish-userdebug # first signed build
# later: lineage_sunfish-user
```
`009-bacon-axp-stage1-combo4.sh` auto-exports when `$HOME/.android-certs/releasekey.pk8` exists.
## AVB (US-S3)
Default tree falls back to AOSP `external/avb/test/data/testkey_rsa2048.pem`
(and LAB has been observed with `testkey_rsa4096`) plus
`BOARD_AVB_MAKE_VBMETA_IMAGE_ARGS += --flags 3` unless `WITH_AVB=true`
(`device/google/sunfish/BoardConfigLineage.mk`).
Project key generation is in `009-generate-release-keys.sh`
(`avb.pem` + `avb_pkmd.bin`). **Wire in tree** (soong-safe path), not only env:
`vendor/sunfish-a16/BoardConfig.mk` sets `BOARD_AVB_KEY_PATH` /
`BOARD_AVB_ALGORITHM=SHA256_RSA4096` when `certs/avb.pem` exists. Keep
`WITH_AVB` unset → vbmeta **flags 3** =
`HASHTREE_DISABLED|VERIFICATION_DISABLED` (libavb) → Magisk preroot on
unlocked/orange. That is **signing infrastructure**, not runtime AVB harden.
Validate after bacon (script fail-closed) on **target_files `IMAGES/`** (the
OTA pair). Do not use `out/target/product/$DEVICE/{boot,vbmeta}.img` alone —
those make images can be stock and disagree with the Magisk payload in the zip.
Gates: pubkey sha1 == `avb_pkmd.bin` (not AOSP testkey); flags 3;
`verify_image --key` + `--expected_chain_partition vbmeta_system:1:avb_pkmd.bin`;
`magiskboot cpio test` ≠ 0; LAB stays `flash.locked=0` + Magisk works.
**OEM unlocking:** leave enabled as a best-effort escape hatch (not a recovery
guarantee). Do not relock the bootloader on this Magisk track.
## Branding
`TARGET_UNOFFICIAL_BUILD_ID=AXP-Resurection-Pro` is set at the **top** of
`device/google/sunfish/lineage_sunfish.mk` (before inheriting
`vendor/lineage` → `version.mk`). Helper:
`apply_009_unofficial_build_id.py`. **Pro** = full Magisk resurrection track
(not a future slim/minimal variant). Mirrors GOLDEN’s `…-sunfish-pro` /
releasetype `PRO` intent without patching `vendor/lineage/config/version.mk`.
`branding.mk` adds `ro.axp.resurection.track=pro`. Ship scripts also publish
`AXP-Resurection-Pro-<ver>-<date>-sunfish.zip` as a hardlink/copy of the
Lineage `bacon` zip (no rebuild solely to rename).
## Never
- Commit `*.pk8`, private `*.pem`, or pass files.
- Relock LAB/GOLDEN as a project requirement (OEM unlock-always-on does not
make Magisk+relock safe).
- Flash old `kh-cve` / `kh-full` zips from 2026-09-18.