mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-09 08:04:45 +00:00
build(desktop): add osxkeychain.so regression guard task — fail release if stripped
The original "ProGuard strips the keychain backend" hypothesis turned out to be wrong twice (PR 3260 comments document the binary PoW that refuted both H1 strip-of-classes and H1b strip-of-native-resource). The full 117 KB osxkeychain.so resource ships intact in the proguarded jkeychain-1.1.0-*.jar today, and Keyring.create() round-trips fine against the proguarded classpath on macOS. But the user-reported bug pattern (every cold boot, keychain key missing → forced re-login) maps so cleanly onto a hypothetical future strip-of-native-resource that the guard is worth keeping. Cheap to run (one unzip scan after proguardReleaseJars), wired onto every release packaging task (DMG, MSI, DEB, RPM, current-OS distributable, runRelease) so a regression can't slip past. Fails the build with a self-contained explanation pointing at the next person who has to debug it. The actual root cause of the reported bug remains unidentified after three refuted hypotheses (see plan doc PoW table); needs the affected user's Console.app logs + ~/.amethyst state to make further progress. The LoginScreen "keychain-unavailable" diagnostic banner from the earlier commit is unchanged and still earns its keep regardless of which failure mode eventually turns out to be the cause. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
a1d7889be3
commit
49929a4afa
@@ -1,6 +1,7 @@
|
||||
import de.undercouch.gradle.tasks.download.Download
|
||||
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
|
||||
import java.nio.file.Files
|
||||
import java.util.zip.ZipFile
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.jetbrainsKotlinJvm)
|
||||
@@ -273,3 +274,76 @@ val createReleaseAppImage by tasks.registering(Exec::class) {
|
||||
// AppImage standard env var: extracts + runs without mounting.
|
||||
environment("APPIMAGE_EXTRACT_AND_RUN", "1")
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Native lib regression guard
|
||||
// ============================================================================
|
||||
// `pt.davidafsilva.apple:jkeychain` ships its `osxkeychain.so` native library
|
||||
// as a resource at the JAR root, and `OSXKeychain.loadSharedObject()` reaches
|
||||
// it with `getResourceAsStream("/osxkeychain.so")`. If a future ProGuard
|
||||
// upgrade, dep swap, or build-config change strips this resource from the
|
||||
// release distributable, every macOS user is forced back to the login screen
|
||||
// on every cold boot (the Keychain backend can't load, `SecureKeyStorage`
|
||||
// falls back to its password-prompted encrypted file, which never prompts
|
||||
// in a GUI cold boot → keys silently null → bunker / nsec / NWC unrecoverable).
|
||||
//
|
||||
// As of the current build the resource DOES survive ProGuard (it ships in the
|
||||
// proguarded jkeychain-1.1.0-*.jar with all 117 KB intact), so this task is
|
||||
// a regression guard, not a workaround. It's wired onto every release task so
|
||||
// it fails the build immediately if the .so disappears.
|
||||
val verifyJkeychainNativeSurvivesProguard by tasks.registering {
|
||||
description = "Fail the release build if osxkeychain.so is stripped from proguarded output (would break macOS Keychain at runtime)"
|
||||
group = "verification"
|
||||
dependsOn("proguardReleaseJars")
|
||||
val proguardDir = layout.buildDirectory.dir("compose/tmp/main-release/proguard")
|
||||
inputs.dir(proguardDir).withPropertyName("proguardOutput").withPathSensitivity(PathSensitivity.RELATIVE)
|
||||
val marker = layout.buildDirectory.file("verify-jkeychain-native.ok")
|
||||
outputs.file(marker)
|
||||
doLast {
|
||||
val dir = proguardDir.get().asFile
|
||||
require(dir.isDirectory) { "ProGuard output dir missing: $dir" }
|
||||
val jars = dir.listFiles { f -> f.extension == "jar" } ?: emptyArray()
|
||||
require(jars.isNotEmpty()) { "No proguarded jars under $dir" }
|
||||
val foundIn = jars.firstOrNull { jar ->
|
||||
val zip = ZipFile(jar)
|
||||
try {
|
||||
val entries = zip.entries()
|
||||
var found = false
|
||||
while (entries.hasMoreElements()) {
|
||||
if (entries.nextElement().name == "osxkeychain.so") {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
found
|
||||
} finally {
|
||||
zip.close()
|
||||
}
|
||||
}
|
||||
require(foundIn != null) {
|
||||
"REGRESSION: osxkeychain.so missing from every proguarded jar in $dir.\n" +
|
||||
"OSXKeychain.loadSharedObject() calls getResourceAsStream(\"/osxkeychain.so\") — if this\n" +
|
||||
"resource is not at classpath root in the release distributable, every macOS user is\n" +
|
||||
"forced to re-log-in on every cold boot of the release DMG."
|
||||
}
|
||||
marker.get().asFile.writeText("verified osxkeychain.so survived ProGuard in ${foundIn.name}\n")
|
||||
println("verifyJkeychainNativeSurvivesProguard: osxkeychain.so present in ${foundIn.name}")
|
||||
}
|
||||
}
|
||||
|
||||
// Gate every release-packaging task on the verification so a stripped .so
|
||||
// never reaches a release artifact.
|
||||
listOf(
|
||||
"packageReleaseDmg",
|
||||
"packageReleaseMsi",
|
||||
"packageReleaseDeb",
|
||||
"packageReleaseRpm",
|
||||
"packageReleaseDistributionForCurrentOS",
|
||||
"packageReleaseUberJarForCurrentOS",
|
||||
"createReleaseDistributable",
|
||||
"runReleaseDistributable",
|
||||
).forEach { name ->
|
||||
tasks.matching { it.name == name }.configureEach {
|
||||
dependsOn(verifyJkeychainNativeSurvivesProguard)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,16 +19,28 @@ origin: docs/brainstorms/2026-06-18-fix-macos-bunker-relogin-brainstorm.md
|
||||
**Reverted in PR 1:**
|
||||
- ❌ ProGuard keep-rule change — the hypothesis it was based on is refuted by binary PoW; the original `pt.davidafsilva.apple.**` keep is correct and stays in place.
|
||||
|
||||
**Open — actual root cause still unidentified.** Working hypotheses:
|
||||
- **H2 — Hardened runtime blocks unsigned dylib load.** `OSXKeychain.loadSharedObject()` extracts `libosxkeychain.dylib` to `/tmp` and `System.load()`s it. Hardened-runtime + Gatekeeper on a notarized DMG **rejects loading an unsigned dylib at runtime** unless the app entitlements include `com.apple.security.cs.disable-library-validation`. Dev `./gradlew :desktopApp:run` doesn't apply hardened runtime — explains the dev-works / release-fails split.
|
||||
- **H4 — jpackage strips the dylib resource.** Compose-Multiplatform's jpackage path could re-pack jars without preserving `META-INF/native/*` or resource-style `.dylib` files. Worth examining the DMG bundle structure.
|
||||
- **H5 — pre-hardening accounts.json.enc migration gap.** If user installed `v1.11.0` then upgraded, the migration from `bunker_uri.txt` → `accounts.json.enc` (commit `46caa4d79`) might leave the bunker entry missing. But this would not happen "every restart, always" — it would happen ONCE post-upgrade. Lower likelihood.
|
||||
**Open — actual root cause still unidentified after three refuted hypotheses.**
|
||||
|
||||
**Verification plan (still needs a Mac + ideally the affected user):**
|
||||
- `./gradlew :desktopApp:packageReleaseDmg` locally, install, log in with bunker, quit, relaunch — DOES the bug reproduce on a locally-built (unsigned) DMG?
|
||||
- If reproduces on unsigned DMG → not hardened runtime, look at jpackage stripping.
|
||||
- If only reproduces on signed/notarized DMG → H2 is the answer; fix is `--mac-entitlements` or `--mac-hardened-runtime` flag config.
|
||||
- `codesign --display --entitlements - /Applications/Amethyst.app` on the affected user's installed DMG to see what entitlements were actually applied.
|
||||
### PoW logs from 2026-06-18 / 06-19 investigation
|
||||
|
||||
| # | Hypothesis | Test | Result |
|
||||
|---|------------|------|--------|
|
||||
| H1 | ProGuard strips macOS keychain backend classes | `javap` on `OsxKeychainBackend.class` + `ModernOsxKeychainBackend.class` in proguarded vs unshrunk jar | **REFUTED** — bytecode identical, all native methods preserved |
|
||||
| H1b | ProGuard strips `osxkeychain.so` native resource | `unzip -l desktopApp/build/compose/tmp/main-release/proguard/jkeychain-1.1.0-*.jar` | **REFUTED** — file present, 117016 bytes |
|
||||
| H2 | Hardened runtime + Library Validation blocks unsigned dylib load | `codesign -d --entitlements -` on locally-built `Amethyst.app` | **REFUTED** — `com.apple.security.cs.disable-library-validation = true` is present in default entitlements |
|
||||
| — | `Keyring.create()` + `setPassword` + `getPassword` round-trip on macOS Keychain | Java program run with `java -cp <proguarded jars> KeychainTest` | **PASS** — round-trip succeeds against proguarded classpath on this host |
|
||||
| — | Locally-built release distributable launches | `./Amethyst.app/Contents/MacOS/Amethyst` foreground for 10s | **OK** — only unrelated VLC plugin-cache warnings on stderr |
|
||||
|
||||
**What this means:** the bug cannot be reproduced on a locally-built (unsigned, adhoc-signed) release distributable on this Apple Silicon Mac. Either:
|
||||
- The bug requires the actual CI-built release DMG (something in CI's environment / jpackage runtime differs from local)
|
||||
- Or the bug is environment-specific to the affected user (macOS version, prior Keychain state, quarantine xattrs, an upgrade path we haven't replicated)
|
||||
- Or there's a different code path (not keychain) that breaks on cold boot for bunker accounts specifically
|
||||
|
||||
**Next investigation steps need a Mac + the affected user:**
|
||||
- Affected user: `Console.app` filter "Amethyst", attempt bunker login, force-quit, relaunch — share log lines.
|
||||
- Affected user: `security find-generic-password -s amethyst-desktop` immediately after first bunker login — does the entry exist?
|
||||
- Affected user: `ls -la ~/.amethyst/` — does `accounts.json.enc` contain a bunker entry after first login?
|
||||
- Mac reviewer: download the *signed/notarized* GitHub release DMG, install, repro on a clean macOS user account.
|
||||
|
||||
# 🐛 fix(desktop): macOS forced re-login on cold boot — ProGuard strips java-keyring backend
|
||||
|
||||
|
||||
Reference in New Issue
Block a user