Files asks for your Flamenet passphrase, not a second secret

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 <noreply@anthropic.com>
This commit is contained in:
Paul Hitt
2026-09-05 10:42:56 -04:00
co-authored by Claude Opus 5
parent de968ac1aa
commit 571c18571f
3 changed files with 95 additions and 6 deletions
@@ -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,
@@ -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(
@@ -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<String> =
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",
)
}
}
}