Repository navigation
Conversation
New KMP module :apps:flipcash:shared:device-link. Everything is in commonMain except the OS keyring vault. - SeedVault: interface for the stored account seed. KeyringSeedVault (jvmMain) backs it with Keychain, Credential Manager or Secret Service through java-keyring; those are native APIs with no common surface. Android keeps AuthManager for now. - LinkInvite: the QR payload: host, port, the desktop's one-time X25519 key, and a 16-byte one-time secret. - LinkCrypto: libsodium kx (phone is client, desktop server). The transcript binds both public keys and the secret; the six-digit code is a keyed hash of the transcript under the phone-to-desktop key, and the seed is sealed with XChaCha20-Poly1305 with the transcript as AD. - DesktopLinkHost / PhoneLinker: one connection over ktor-network sockets. The phone sends the sealed seed only from Pending.confirm(), after the user says the codes match. ktor-network is a second networking stack next to OkHttp/gRPC; it keeps the sockets in common code instead of an expect/actual per platform. Tests are jvmTest, not commonTest: the Android libsodium binding loads its .so from an APK, so the protocol can't run as a host test. LinkProtocolTest runs both sides over a loopback socket (7 cases, including a swapped phone key, a wrong secret, tampered ciphertext and cancel). KeyringSeedVaultTest is opt-in (-Pflipcash.keyringTest) and writes a random value under a test-only service name, then deletes it.
At launch the desktop signs in with the dev phrase if one is set, else with the seed a phone linked into the Keychain. With neither, it shows DesktopLinkScreen; when the phone sends the seed, the desktop writes it to the vault and signs in. The dev phrase is never written to the vault. The screens (DesktopLinkScreen, PhoneLinkScreen) and the phone's state holder (PhoneLinkController) are in device-link's commonMain. The phone screen takes the camera as a composable slot. PhoneLinkController reads the seed only after the user confirms the codes match; a new loopback test checks that. Two pieces stay in the desktop app (JVM-only): - DesktopSession: FlipcashDatabase.init takes a java.io.File. - lanAddress(): picks the address the phone dials from java.net.NetworkInterface. Only the desktop needs it. `snapshot -Pscreen=link` renders the link screen without signing in.
Settings > Advanced shows Link a Desktop when FeatureFlag.LinkDesktop is on (default off, Internal track). The screen is device-link's common PhoneLinkScreen and PhoneLinkController; LinkDesktopScreen only adds the Android pieces: a Hilt view model that reads the entropy from UserManager on confirm, the camera permission, and the ML Kit CodeScanner from ui:scanner. It lives in features/myaccount rather than device-link's androidMain because the scanner, the permission helpers and the settings navigation are all Android-only modules that myaccount already sits on.
features/login imported two internal symbols from features/purchase (PurchaseAccountScreenContent and PurchaseAccountViewModel) to show the registration purchase inside onboarding. OnboardingFlowScreen now takes an OnboardingPurchaseStep composable, the app passes purchase's new OnboardingPurchaseStep, and login no longer depends on the purchase module. Both modules stay src/main: neither is KMP yet, and converting them is outside this cut.
encodeBase64 now goes through android.util.Base64.encode (the shared base64Encode actual from the desktop spike's jvm target). The login view model tests only mocked encodeToString, so the Android stub returned null and the wrong-entropy-size test never saw its error. It passes on code/cash.
This was referenced Oct 24, 2026
Collaborator
Author
|
Landed in #1894 as one squash against code/cash. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #1847. Third slice of the desktop spike: the desktop gets its seed from a phone over the LAN and keeps it in the OS keyring, and features/login stops depending on features/purchase.
Seed vault and link protocol
New KMP module
:apps:flipcash:shared:device-link. Everything is in commonMain except the keyring vault.SeedVaultis the interface.KeyringSeedVault(jvmMain) backs it with Keychain, Credential Manager or Secret Service through java-keyring, since those are native APIs with no common surface. Android keepsAuthManagerfor now.LinkInvite): host, port, a one-time X25519 key and a 16-byte one-time secret. QR rendering uses qrose, which is multiplatform.LinkCryptouses libsodiumcrypto_kx. The transcript binds both public keys and the secret. Both screens show a six-digit code keyed from the transcript, and the seed is sealed with XChaCha20-Poly1305 with the transcript as associated data.Pending.confirm(), after the user says the codes match. That confirmation is what stops someone on the LAN from swapping in their own key.LinkProtocolTestruns both sides over a loopback socket: matching codes, a swapped phone key, a wrong secret, tampered ciphertext, cancel, and the seed arriving only after confirm. These are jvmTest rather than commonTest because the Android libsodium binding loads its.sofrom an APK.KeyringSeedVaultTestis opt-in (-Pflipcash.keyringTest) and writes a random value under a test-only service name, then deletes it.Desktop
At launch the desktop signs in with the dev phrase if one is set, else with the seed in the keyring. With neither, it shows
DesktopLinkScreen, writes the seed it receives to the vault and signs in. The dev phrase is never written to the vault. Two pieces stay JVM-only in:apps:desktop:DesktopSession(FlipcashDatabase.inittakes ajava.io.File) andlanAddress()(java.net.NetworkInterface).Android
Settings > Advanced shows Link a Desktop when
FeatureFlag.LinkDesktopis on (default off, Internal track). The screen and its state holder are device-link's commonPhoneLinkScreenandPhoneLinkController.LinkDesktopScreenin features/myaccount adds only the camera permission, the ML Kit scanner and a view model that reads the entropy on confirm.Login → purchase
OnboardingFlowScreenused twointernalpurchase symbols. It now takes anOnboardingPurchaseStepcomposable from the app, and login dropsproject(":apps:flipcash:features:purchase").Also
LoginViewModelErrorTestandLoginViewModelCreateAccountTestnow mockandroid.util.Base64.encodeas well asencodeToString.encodeBase64goes throughencodesince #1838, so the wrong-entropy-size test failed on this stack; it passes oncode/cash.