revert(desktop): drop ProGuard keep-rule swap — binary PoW refuted the strip hypothesis

Built ./gradlew :desktopApp:proguardReleaseJars on both main and this
branch and inspected the shrunk java-keyring-1.0.4-*.jar in
desktopApp/build/compose/tmp/main-release/proguard/. Both branches
contain byte-identical macOS Keychain backend bytecode:
OsxKeychainBackend, ModernOsxKeychainBackend,
pt/davidafsilva/apple/OSXKeychain, plus all _addGenericPassword /
_findGenericPassword / _deleteGenericPassword / loadSharedObject native
methods. ProGuard is NOT stripping the macOS backend.

The compose-rules.pro comment had misled me. pt.davidafsilva.apple IS a
real transitive runtime dep of com.github.javakeyring:java-keyring —
ModernOsxKeychainBackend has a private pt.davidafsilva.apple.OSXKeychain
field. The original keep rule was correct; restore it and clarify the
comment about the transitive relationship so the next person to read
this code doesn't repeat the same mistake.

The AccountManager keychain-unavailable diagnostic + LoginScreen banner
introduced earlier in this branch are kept — they're useful for any
future failure mode in this area, not just the (refuted) ProGuard one.

See https://github.com/vitorpamplona/amethyst/pull/3260#issuecomment-4740073787
for the full PoW jar inspection. Remaining hypotheses (H2 hardened-runtime
unsigned-dylib block, H4 jpackage stripping the bundled libosxkeychain.dylib,
H5 v1.11.0 migration gap) are documented in the plan doc.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
nrobi144
2026-06-18 12:11:03 +03:00
parent 123c055828
commit a1d7889be3
2 changed files with 26 additions and 22 deletions

View File

@@ -82,14 +82,12 @@
# - androidx.sqlite (sqlite-bundled): nativeThreadSafeMode() stripped,
# so `BundledSQLiteDriver.threadingMode` threw NoSuchMethodError on the
# first relay-store query (LocalRelayStore.refreshStats).
# - com.github.javakeyring (java-keyring): Keyring.create() reflection-loads
# the OS-specific backend (OSXKeychainBackend / SecretServiceBackend /
# WinCredentialStoreBackend). Without these keeps the macOS backend was
# stripped, BackendNotSupportedException fired on every cold boot, the
# SecureKeyStorage fallback silently returned null, and any account whose
# key lived in the keychain (nsec, NIP-46 bunker ephemeral, NWC secret)
# was forced to re-log-in every launch of the release DMG. The dev/Gradle
# run path skips ProGuard, which is why it never surfaced in development.
# - pt.davidafsilva.apple (jkeychain): macOS Keychain JNI for nsec storage.
# Note: this lives in the dependency graph as a transitive runtime dep of
# `com.github.javakeyring:java-keyring` — `ModernOsxKeychainBackend` holds
# a `private pt.davidafsilva.apple.OSXKeychain` field. Keep both the
# class and its private native methods (loadSharedObject / the
# _addGenericPassword / _findGenericPassword bridge).
#
# secp256k1-kmp is already covered by the `-keep class fr.acinq.secp256k1.**`
# rule mirrored from mobile above.
@@ -98,10 +96,9 @@
native <methods>;
static <methods>;
}
-keep class com.github.javakeyring.** { *; }
-keepclassmembers class com.github.javakeyring.internal.** {
-keep class pt.davidafsilva.apple.** { *; }
-keepclassmembers class pt.davidafsilva.apple.** {
native <methods>;
<init>(...);
}
# kmp-tor — loads native Tor daemon via JNI reflection

View File

@@ -6,22 +6,29 @@ date: 2026-06-18
origin: docs/brainstorms/2026-06-18-fix-macos-bunker-relogin-brainstorm.md
---
## Implementation Status (2026-06-18)
## Implementation Status (2026-06-18, updated)
**Landed in PR 1 (Phases 1 + 2 + partial 3):**
- ✅ ProGuard keep rules fix (`desktopApp/compose-rules.pro`) — the actual bug fix
**H1 (ProGuard strip) is REFUTED.** Binary PoW: `./gradlew :desktopApp:proguardReleaseJars` on both `main` and the fix branch produces `java-keyring-1.0.4-*.jar` outputs with byte-identical macOS keychain backend bytecode (`OsxKeychainBackend`, `ModernOsxKeychainBackend`, `pt/davidafsilva/apple/OSXKeychain` including all `_addGenericPassword` / `_findGenericPassword` / `_deleteGenericPassword` / `loadSharedObject` native methods). The original `pt.davidafsilva.apple.**` keep rule was correctly targeting the transitive JNI bridge (`ModernOsxKeychainBackend` holds a `private pt.davidafsilva.apple.OSXKeychain` field). See PR #3260 comment for the full keep-rule audit.
**Landed in PR 1 (defense-in-depth only):**
-`AccountManager._keychainUnavailable: StateFlow<Boolean>` mirroring the existing `_storageCorruption` / `_forceLogoutReason` diagnostic channels
-`loadInternalAccount` + `loadBunkerAccount` raise the signal when `accounts.json.enc` points at a key the keychain cannot return
-`LoginScreen` observes the signal, renders a single-line error banner above the LoginCard; `clearKeychainUnavailable()` invoked from all successful login paths
- ✅ Four new unit tests in `AccountManagerLoadAccountTest`: Internal-no-privkey signals, Bunker-no-ephemeral signals, `clearKeychainUnavailable()` resets, happy-path does NOT signal
- ✅ Four new unit tests in `AccountManagerLoadAccountTest`
**Deferred to follow-up (verification + cross-platform sweep):**
- Phase 0 Track B: visual repro on a signed release DMG — needs a Mac. The ProGuard rule fix is what solves the reported bug; reproducing the broken state requires building + installing the unfixed DMG.
- Phase 4: Linux DEB + Windows MSI cold-boot smoke test. Same keep rule covers all `internal.**` backends so should incidentally fix them; needs CI matrix run.
- Phase 5: signed + notarized DMG verification — needs Mac + signing cert.
- Phase 0 Track A unit test that mocks `Keyring.create()` throwing — required adding a production injection seam for marginal regression value; the actual ProGuard regression is only catchable on a shrunk artifact (Phase 3 #7 smoke test in the plan below), not unit tests.
- Phase 3 #7 release-DMG smoke test extension — needs work on the existing smoke-test harness (out of scope for one-shot fix).
- ProGuard `mapping.txt` regression guard gradle task — nice-to-have, deferred.
**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.
**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.
# 🐛 fix(desktop): macOS forced re-login on cold boot — ProGuard strips java-keyring backend