From 571c18571f8d99ff8f27187b12add306c2be7bbe Mon Sep 17 00:00:00 2001 From: Paul Hitt Date: Sat, 5 Sep 2026 10:42:56 -0400 Subject: [PATCH] Files asks for your Flamenet passphrase, not a second secret MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reported: "on android, the files passcode should just reuse the account like ios does." 🔴 **The difference was entirely in the words, and iOS has been right all along.** iOS says "your passphrase" and takes whatever is typed, so a person naturally uses their Flamenet one and it simply works. Android said the files were "encrypted with a passphrase of their own", labelled the field "Choose a passphrase", and later asked for "your Files passphrase" — three separate places telling somebody to invent, and then remember, a second secret. ⚠️ **Forgetting it is a one-way door.** The label keys are derived from the identity that passphrase protects and exist nowhere else, so a forgotten Files passphrase makes every vaulted file permanently unreadable. Sending people off to mint an extra secret for no reason put them one lapse of memory from that, and the app already has a paragraph explaining the consequence. ⚠️ **Nothing in the derivation changed, deliberately.** It is still a free-choice string protecting the identity record, so anyone who already set a different one keeps opening their files with it exactly as before. Making the account passphrase the ACTUAL key would be a migration with permanent data loss on the far side of any mistake, and it is not what was asked for. That also means the copy IS the feature here, so a copy guard is the only guard there can be. Proven by reverting the wording: both tests go red naming the file and the phrase. 1194 tests pass. Co-Authored-By: Claude Opus 5 --- .../io/flamenet/messenger/app/ChatScreens.kt | 2 +- .../io/flamenet/messenger/app/FilesScreen.kt | 25 +++++-- .../app/FilesUsesTheAccountPassphraseTest.kt | 74 +++++++++++++++++++ 3 files changed, 95 insertions(+), 6 deletions(-) create mode 100644 app/src/test/kotlin/io/flamenet/messenger/app/FilesUsesTheAccountPassphraseTest.kt diff --git a/app/src/main/kotlin/io/flamenet/messenger/app/ChatScreens.kt b/app/src/main/kotlin/io/flamenet/messenger/app/ChatScreens.kt index 214548f..ea1e3eb 100644 --- a/app/src/main/kotlin/io/flamenet/messenger/app/ChatScreens.kt +++ b/app/src/main/kotlin/io/flamenet/messenger/app/ChatScreens.kt @@ -773,7 +773,7 @@ fun ColumnScope.AccountScreen( Text("Files", fontWeight = FontWeight.Medium, fontSize = 16.sp) Spacer(Modifier.height(Space.xs)) Text( - "Files encrypted on this phone, with a passphrase of their own. " + "Files encrypted on this phone, opened with your Flamenet passphrase. " + "Nothing is stored on a server.", fontSize = 13.sp, color = Palette.SecondaryText, diff --git a/app/src/main/kotlin/io/flamenet/messenger/app/FilesScreen.kt b/app/src/main/kotlin/io/flamenet/messenger/app/FilesScreen.kt index 321833f..5fb7186 100644 --- a/app/src/main/kotlin/io/flamenet/messenger/app/FilesScreen.kt +++ b/app/src/main/kotlin/io/flamenet/messenger/app/FilesScreen.kt @@ -118,10 +118,22 @@ private fun ColumnScope.FilesSetUp( var passphrase by remember { mutableStateOf("") } var again by remember { mutableStateOf("") } + // 🔴 **"A passphrase of their own" told people to invent a second secret, + // and iOS never did.** iOS says "your passphrase" and takes whatever is + // typed, so a person naturally uses their Flamenet one and it simply + // works. Android's copy sent them off to make up something else, then + // asked for it again weeks later — and forgetting it is the one-way door + // described above this function. + // + // ⚠️ **Nothing about the derivation changes.** This is still a free-choice + // string protecting the identity record, so anyone who already set a + // different one keeps opening their files with it. Making the account + // passphrase the ACTUAL key would be a migration with permanent data loss + // on the far side of any mistake, and it is not what this fixes. Text( - "Files on this phone are encrypted with a passphrase of their own. " - + "Flamenet doesn't keep it, so nobody can open them by picking up " - + "your unlocked phone.", + "Use your Flamenet passphrase — the one you sign in with. Files on " + + "this phone are encrypted with it, and Flamenet keeps no copy, so " + + "nobody can open them by picking up your unlocked phone.", fontSize = 14.sp, color = Palette.SecondaryText, ) Spacer(Modifier.height(Space.sm)) @@ -134,7 +146,7 @@ private fun ColumnScope.FilesSetUp( OutlinedTextField( value = passphrase, onValueChange = { passphrase = it }, - label = { Text("Choose a passphrase") }, + label = { Text("Your Flamenet passphrase") }, visualTransformation = PasswordVisualTransformation(), singleLine = true, modifier = Modifier.fillMaxWidth().testTag("files-new-passphrase"), @@ -168,7 +180,10 @@ private fun ColumnScope.FilesUnlock( ) { var passphrase by remember { mutableStateOf("") } - Text("Enter your Files passphrase to open them.", + // ⚠️ Not "your Files passphrase". That name implied a second secret even + // for somebody who had used their Flamenet one, and left them hunting for + // a passphrase they never set. + Text("Enter your Flamenet passphrase to open them.", fontSize = 14.sp, color = Palette.SecondaryText) Spacer(Modifier.height(Space.md)) OutlinedTextField( diff --git a/app/src/test/kotlin/io/flamenet/messenger/app/FilesUsesTheAccountPassphraseTest.kt b/app/src/test/kotlin/io/flamenet/messenger/app/FilesUsesTheAccountPassphraseTest.kt new file mode 100644 index 0000000..30e666e --- /dev/null +++ b/app/src/test/kotlin/io/flamenet/messenger/app/FilesUsesTheAccountPassphraseTest.kt @@ -0,0 +1,74 @@ +package io.flamenet.messenger.app + +import java.io.File +import kotlin.test.Test +import kotlin.test.assertTrue + +/** + * Files asks for the Flamenet passphrase, not a second secret. + * + * 🔴 **The screen used to say the files were encrypted "with a passphrase of + * their own", and iOS never said anything like it.** iOS says "your + * passphrase" and takes whatever is typed, so people use their Flamenet one + * and it works. Android's copy sent them off to invent something else — and + * forgetting it is a one-way door: the label keys are derived from the + * identity it protects and exist nowhere else, so every vaulted file becomes + * permanently unreadable. + * + * ⚠️ **This is about the words, and the words are the whole feature here.** + * Nothing in the derivation changed: it is still a free-choice string + * protecting the identity record, so anyone who already set a different one + * keeps opening their files with it. A guard on the copy is therefore the only + * guard there can be. + */ +class FilesUsesTheAccountPassphraseTest { + + private fun source(name: String): String { + var dir = File(System.getProperty("user.dir")) + while (!File(dir, "app/src/main/kotlin").isDirectory && dir.parentFile != null) { + dir = dir.parentFile + } + val f = File(dir, "app/src/main/kotlin/io/flamenet/messenger/app/$name") + assertTrue(f.isFile, "could not find $name — this guard is reading nothing") + return f.readText() + } + + /** Lines that are shown, not lines that explain. */ + private fun copyLines(body: String): List = + body.lines().filterNot { + val t = it.trimStart() + t.startsWith("//") || t.startsWith("*") || t.startsWith("/*") + } + + @Test + fun `the setup screen names the Flamenet passphrase`() { + val shown = copyLines(source("FilesScreen.kt")).joinToString("\n") + assertTrue( + shown.contains("Use your Flamenet passphrase"), + "the Files setup screen no longer tells people to use the passphrase " + + "they already have, which is the whole of android's parity gap with iOS", + ) + } + + /** + * ⚠️ The exact phrase that caused it. Kept as a literal because a person + * rewording this screen should have to read the comment above the string + * and decide deliberately, not discover the consequence from a user. + */ + @Test + fun `no screen tells somebody to invent a second secret`() { + for (name in listOf("FilesScreen.kt", "ChatScreens.kt")) { + val shown = copyLines(source(name)).joinToString("\n") + assertTrue( + !shown.contains("passphrase of their own"), + "$name tells people the files have a passphrase of their own — " + + "iOS asks for the one they already have, and forgetting a " + + "second one destroys every vaulted file", + ) + assertTrue( + !shown.contains("Choose a passphrase"), + "$name still asks somebody to choose a new passphrase for Files", + ) + } + } +} -- 2.54.0