A live test for the file round trip, and four published passwords #85
@@ -113,7 +113,7 @@ From `E2E/E2EInbox.swift`, which is the real feature list.
|
||||
| Trust state, verification, identity-change acceptance | ✅ pinned in the vault, surfaced on the thread |
|
||||
| Key-log enforcement and head gossip | ✅ opt-in enforcement, gossip detects a fork |
|
||||
| Clear thread | ✅ keeps the session, so the chat still works |
|
||||
| History at rest (encrypted, device-only key) | ⚪ |
|
||||
| History at rest (encrypted, device-only key) | ✅ **this row was stale** — sealed per thread under a Keystore key, read straight from disk; see below |
|
||||
| Presence (read and set) | ✅ shown from the relay, never inferred |
|
||||
| Buddy list, add by username | ✅ |
|
||||
| Remove, block, unblock | ✅ |
|
||||
@@ -121,7 +121,35 @@ From `E2E/E2EInbox.swift`, which is the real feature list.
|
||||
| Buddy commands | — retired; not a feature any more (owner, 2026-08-25) |
|
||||
| Voice calls (WebRTC, DTLS-SRTP, TURN) | ✅ P2P-first; ⚠️ no TURN deployed |
|
||||
| Notifications list, read state | ✅ |
|
||||
| Services (AIM/ICQ via `flamenet-oscar`) | ⚪ |
|
||||
| Services (AIM/ICQ via `flamenet-oscar`) | — retired; not a goal for this app (owner, 2026-09-04) |
|
||||
|
||||
🔴 **History at rest was marked ⚪ and it has been built the whole time — the
|
||||
third stale row in this file.** Every read goes to disk: `Session.history` calls
|
||||
`Vault.history`, which decrypts the record. There is no in-memory copy to be
|
||||
fooled by.
|
||||
|
||||
What Android does, against what iOS's `E2EHistoryStore` does:
|
||||
|
||||
| | iOS | Android |
|
||||
|---|---|---|
|
||||
| Cipher | AES-256-GCM | AES-256-GCM |
|
||||
| Key | Keychain, `ThisDeviceOnly` | Android Keystore — never leaves the process at all |
|
||||
| Bound to its slot | one file, nothing to confuse | 🟢 **the record NAME is the AAD**, so a blob cannot be moved between slots |
|
||||
| Shape | one snapshot for everything | one record per thread |
|
||||
| Off backups | excluded from iCloud | `allowBackup="false"`, and a Keystore key is unbackupable anyway |
|
||||
| At rest, locked | `.completeFileProtection` | Credential-Encrypted storage: `filesDir` is unreadable until the first unlock after boot |
|
||||
| Plain sign-out | keeps history | keeps history |
|
||||
| Erase this device | `eraseLocalData()` | `forgetEverything()` **and** the Keystore key is deleted |
|
||||
|
||||
⚠️ Two honest differences rather than a claim of equivalence. Android's
|
||||
per-record AAD is a binding iOS's single file does not need; iOS's
|
||||
`.completeFileProtection` is a per-file assertion, while Android's equivalent is
|
||||
a property of the storage volume rather than of this app's request. Neither gap
|
||||
favours one platform overall.
|
||||
|
||||
⚠️ `Vault.history` prunes expired messages on the way out, so disappearing
|
||||
messages are enforced on READ rather than by a timer nothing runs. It rewrites
|
||||
the record only when something actually expired — a read is not a write.
|
||||
|
||||
✅ Calls are built, on `io.github.webrtc-sdk:android` — the one third-party
|
||||
library in this app, and unavoidable: the alternative is hand-rolling ICE,
|
||||
@@ -341,7 +369,7 @@ the messenger rather than staying separate apps.
|
||||
| Local vault: add, organise, delete | ✅ index, folders, both storage modes, bulk delete with its report |
|
||||
| Files screen: set up, unlock, browse, add, folders, delete, promote | ✅ **verified on a device** — set up, encrypt, lock, cold-start reload, link via SAF |
|
||||
| The security lattice (labels, clearance) | ✅ encoding and clearance in Kotlin, pinned to independently-computed vectors. ⚠️ **Not Bell–LaPadula** — the ★-property is deliberately unenforced on both clients; see android#15 |
|
||||
| Share a file into a chat or group | ✅ both directions — send from the vault, save a received one back at the label it arrived with |
|
||||
| Share a file into a chat or group | ✅ both directions; `LiveFileTransferTest` proves the label survives a real relay ⚠️ **unrun** |
|
||||
| Wallet engine: BIP-39, Base58, SLIP-0010 | ✅ pinned to the published vectors |
|
||||
| Wallet engine: address derivation and signing | ✅ BIP-44 path, Base58 address, and the engine's own RFC 8032 signs — no API-33 floor |
|
||||
| Wallet: BIP-39 seed, create and restore | ✅ create, import, restore, sealed at rest, with the phrase shown once and explained |
|
||||
|
||||
@@ -37,6 +37,16 @@ class LiveConversationTest {
|
||||
private fun needFnLiveFleet() {
|
||||
LiveTarget.refuseProduction(fleet)
|
||||
assumeTrue(fleet != null, "set FN_LIVE_FLEET to run this against a relay")
|
||||
// 🔴 The passphrase comes from the environment with no fallback. This
|
||||
// file carried a hard-coded literal — a published password, used to
|
||||
// create REAL accounts — and `TestSafetyTest` could
|
||||
// not see it, because its check matched an ASSIGNMENT and this was an
|
||||
// ARGUMENT. Both are fixed together.
|
||||
assumeTrue(
|
||||
LiveTarget.passphrase().isNotEmpty(),
|
||||
"set FN_LIVE_PASSPHRASE — these tests create real accounts and this " +
|
||||
"suite will not sign up with a value written into the source",
|
||||
)
|
||||
}
|
||||
|
||||
/** A signed-up account with a registered device, ready to talk. */
|
||||
@@ -62,7 +72,8 @@ class LiveConversationTest {
|
||||
|
||||
private fun signUp(base: String, tag: String, preKeys: Int = 20): Person {
|
||||
val handle = "live$tag" + (System.nanoTime() % 10_000_000)
|
||||
val account = RelayAccounts(base).signUp(handle, "$handle@example.invalid", "a-long-enough-passphrase")
|
||||
val account = RelayAccounts(base)
|
||||
.signUp(handle, "$handle@example.invalid", LiveTarget.passphrase())
|
||||
|
||||
val identity = DeviceIdentity.create()
|
||||
val vault = Vault(MapStorage(), key())
|
||||
@@ -311,6 +322,6 @@ class LiveConversationTest {
|
||||
|
||||
private fun appPasswordOf(person: Person): String =
|
||||
appPasswords.getOrPut(person.handle) {
|
||||
RelayAccounts(fleet!!).signIn(person.handle, "a-long-enough-passphrase").appPassword
|
||||
RelayAccounts(fleet!!).signIn(person.handle, LiveTarget.passphrase()).appPassword
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
package io.flamenet.client
|
||||
|
||||
import io.flamenet.e2e.Attachment
|
||||
import io.flamenet.e2e.DeviceIdentity
|
||||
import io.flamenet.e2e.FilesIdentity
|
||||
import io.flamenet.e2e.Label
|
||||
import io.flamenet.e2e.Vault
|
||||
import java.io.File
|
||||
import javax.crypto.KeyGenerator
|
||||
import javax.crypto.SecretKey
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertContentEquals
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertTrue
|
||||
import org.junit.jupiter.api.Assumptions.assumeTrue
|
||||
|
||||
/**
|
||||
* A document leaves one person's vault and arrives in another's, through a real
|
||||
* relay.
|
||||
*
|
||||
* 🔴 **This is the seam nothing else covers.** `FileShareWireTest` proves the
|
||||
* envelope's shape, `LocalVaultTest` proves the vault at both ends, and neither
|
||||
* of them puts a byte on a wire. What is only provable here is that the label
|
||||
* survives an actual upload, an actual envelope, an actual poll and an actual
|
||||
* save — the four steps between two vaults that each look right on their own.
|
||||
*
|
||||
* ⚠️ **The classification is the only thing that survives the trip.** The
|
||||
* receiving phone files the document at whatever number arrives, because there
|
||||
* is nothing else to consult and no server to ask. A label lost in transit does
|
||||
* not error anywhere: the file simply arrives Unclassified and gets filed that
|
||||
* way, on the most permissive setting it could possibly have. That is what this
|
||||
* test is really watching for.
|
||||
*
|
||||
* ⚠️ Opt-in via `FN_LIVE_FLEET`, and pointed at a throwaway relay. It creates
|
||||
* accounts and uploads blobs.
|
||||
*
|
||||
* FN_LIVE_FLEET=http://127.0.0.1:8080 ./gradlew :client:test
|
||||
*/
|
||||
class LiveFileTransferTest {
|
||||
|
||||
private val fleet: String? = System.getenv("FN_LIVE_FLEET")
|
||||
|
||||
/**
|
||||
* ⚠️ Announced, not silent. A `return` from an unmet precondition makes a
|
||||
* test that did nothing report as PASSED — see the note in
|
||||
* `LiveConversationTest`, which is where that lesson was paid for.
|
||||
*/
|
||||
private fun needFnLiveFleet() {
|
||||
LiveTarget.refuseProduction(fleet)
|
||||
assumeTrue(fleet != null, "set FN_LIVE_FLEET to run this against a relay")
|
||||
assumeTrue(
|
||||
LiveTarget.passphrase().isNotEmpty(),
|
||||
"set FN_LIVE_PASSPHRASE — these tests create real accounts and this " +
|
||||
"suite will not sign up with a value written into the source",
|
||||
)
|
||||
}
|
||||
|
||||
private class MapStorage : Vault.Storage {
|
||||
private val records = LinkedHashMap<String, ByteArray>()
|
||||
override fun read(name: String) = records[name]
|
||||
override fun write(name: String, bytes: ByteArray) { records[name] = bytes }
|
||||
override fun delete(name: String) { records.remove(name) }
|
||||
override fun names() = records.keys.toList()
|
||||
}
|
||||
|
||||
/** Nothing is linked in these tests, so nothing should ever ask. */
|
||||
private object NoReferences : LocalVault.ReferenceResolver {
|
||||
override fun exists(bookmark: String) =
|
||||
error("a vaulted file asked for a referenced one")
|
||||
override fun read(bookmark: String): ByteArray =
|
||||
error("a vaulted file asked for a referenced one")
|
||||
override fun size(bookmark: String) =
|
||||
error("a vaulted file asked for a referenced one")
|
||||
}
|
||||
|
||||
private fun key(): SecretKey = KeyGenerator.getInstance("AES").apply { init(256) }.generateKey()
|
||||
|
||||
/** Somebody with a device, a relay session and a Files vault. */
|
||||
private class Person(
|
||||
val handle: String,
|
||||
val userId: Long,
|
||||
val identity: DeviceIdentity,
|
||||
val conversations: Conversations,
|
||||
val api: MessengerApi,
|
||||
val files: LocalVault,
|
||||
val root: File,
|
||||
)
|
||||
|
||||
private fun signUp(base: String, tag: String): Person {
|
||||
val handle = "livef$tag" + (System.nanoTime() % 10_000_000)
|
||||
val account = RelayAccounts(base)
|
||||
.signUp(handle, "$handle@example.invalid", LiveTarget.passphrase())
|
||||
|
||||
val identity = DeviceIdentity.create()
|
||||
val vault = Vault(MapStorage(), key())
|
||||
vault.saveIdentity(identity)
|
||||
|
||||
val relay = RelayClient(base, account.username, account.appPassword)
|
||||
relay.useRelayAuth(identity)
|
||||
relay.registerDevice(identity, identity.generateOneTimePreKeys(20))
|
||||
vault.saveIdentity(identity)
|
||||
|
||||
// A real Files identity, so the label keys are the ones the app derives
|
||||
// — 1,000 rounds rather than 650,000 because this asserts the format,
|
||||
// not the work factor.
|
||||
val raw = FilesIdentity.rawFromPkcs8(io.flamenet.e2e.X25519.generate().private.encoded)
|
||||
val root = File.createTempFile("livevault$tag", "").let { it.delete(); it.mkdirs(); it }
|
||||
|
||||
return Person(
|
||||
handle = handle,
|
||||
userId = account.userId,
|
||||
identity = identity,
|
||||
conversations = Conversations(identity, vault, relay).also { it.myUserId = account.userId },
|
||||
api = MessengerApi(base, account.username, account.appPassword),
|
||||
files = LocalVault(root, raw, NoReferences).also { it.load() },
|
||||
root = root,
|
||||
)
|
||||
}
|
||||
|
||||
private fun Conversations.readable(): List<Conversations.Received> =
|
||||
poll().filter { it.content.isRenderable }
|
||||
|
||||
@Test
|
||||
fun `a classified document crosses between two vaults with its label intact`() {
|
||||
needFnLiveFleet()
|
||||
val base = fleet!!
|
||||
val alice = signUp(base, "a")
|
||||
val bob = signUp(base, "b")
|
||||
|
||||
val bytes = "revenue: 41\nheadcount: 12\n".toByteArray()
|
||||
val label = Label(3, setOf(1, 2))
|
||||
val filed = alice.files.add(bytes, "Q3 Forecast.txt", label)
|
||||
|
||||
// Out of Alice's vault and onto the wire, exactly as `Session.sendFile`
|
||||
// does it: plaintext read out, the transport seals it, the label rides
|
||||
// alongside.
|
||||
val plaintext = alice.files.contents(filed)
|
||||
assertContentEquals(bytes, plaintext, "the vault handed back the wrong bytes")
|
||||
|
||||
val sent = alice.conversations.sendAttachment(
|
||||
bob.userId, plaintext, "application/octet-stream", Attachment.FILE,
|
||||
filename = "Q3 Forecast.txt",
|
||||
classification = label.classification.toInt(),
|
||||
compartments = label.compartments.toList(),
|
||||
)
|
||||
assertTrue(sent.reachedSomeone, "nothing was delivered: ${sent.failed}")
|
||||
|
||||
// In on Bob's side.
|
||||
val received = bob.conversations.readable()
|
||||
assertEquals(1, received.size, "Bob's inbox had ${received.size} items")
|
||||
val ref = assertNotNull(received[0].content.attachment,
|
||||
"the attachment did not survive the envelope")
|
||||
|
||||
assertEquals(Attachment.FILE, ref.kind)
|
||||
assertEquals("Q3 Forecast.txt", ref.filename, "the document's own name was lost")
|
||||
// 🔴 The assertion this test exists for.
|
||||
assertEquals(3, ref.classification,
|
||||
"the classification was lost in transit — the file would be filed Unclassified")
|
||||
assertEquals(listOf(1, 2), ref.compartments.sorted(),
|
||||
"the compartments were lost — the file would be filed outside its areas")
|
||||
|
||||
// And the bytes come back off the relay.
|
||||
val fetched = bob.conversations.fetchAttachment(ref)
|
||||
assertContentEquals(bytes, fetched, "the bytes did not survive the round trip")
|
||||
|
||||
// Bob saves it, at the label it arrived with — never one his phone chose.
|
||||
val saved = bob.files.add(
|
||||
fetched, ref.filename,
|
||||
Label(ref.classification.toShort(), ref.compartments.toSet()),
|
||||
)
|
||||
val entry = bob.files.list(null).single()
|
||||
assertEquals("Q3 Forecast.txt", entry.name)
|
||||
assertEquals(label, entry.label,
|
||||
"the document was filed under a different label than it was sent at")
|
||||
assertContentEquals(bytes, bob.files.contents(saved))
|
||||
|
||||
// 🔴 **Two vaults, two identities, two different keys for one label.**
|
||||
// The label key is derived from the identity, so the same
|
||||
// classification does NOT mean the same key — which is what stops a
|
||||
// stolen container from one phone opening anything on the other.
|
||||
assertTrue(
|
||||
!alice.files.labelKey(label).contentEquals(bob.files.labelKey(label)),
|
||||
"two people derived the same key for one label — a stolen vault would open both",
|
||||
)
|
||||
|
||||
alice.root.deleteRecursively()
|
||||
bob.root.deleteRecursively()
|
||||
}
|
||||
|
||||
/**
|
||||
* ⚠️ An Unclassified document must arrive as one, and be distinguishable
|
||||
* from a label that went missing. Zero is the default for an absent field,
|
||||
* so this is the case where a dropped label and a deliberate zero look
|
||||
* identical at the far end — the filename and the kind are what say the
|
||||
* envelope was understood.
|
||||
*/
|
||||
@Test
|
||||
fun `an unclassified document is still recognisably a document`() {
|
||||
needFnLiveFleet()
|
||||
val base = fleet!!
|
||||
val alice = signUp(base, "c")
|
||||
val bob = signUp(base, "d")
|
||||
|
||||
val bytes = "nothing secret here".toByteArray()
|
||||
alice.conversations.sendAttachment(
|
||||
bob.userId, bytes, "application/octet-stream", Attachment.FILE,
|
||||
filename = "notes.txt", classification = 0, compartments = emptyList(),
|
||||
)
|
||||
|
||||
val ref = assertNotNull(bob.conversations.readable().single().content.attachment)
|
||||
assertEquals(Attachment.FILE, ref.kind, "an unlabelled file stopped being a file")
|
||||
assertEquals("notes.txt", ref.filename)
|
||||
assertEquals(0, ref.classification)
|
||||
assertTrue(ref.compartments.isEmpty())
|
||||
assertContentEquals(bytes, bob.conversations.fetchAttachment(ref))
|
||||
|
||||
alice.root.deleteRecursively()
|
||||
bob.root.deleteRecursively()
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,14 @@ class LiveKeyLogTest {
|
||||
private fun needFnLiveFleet() {
|
||||
LiveTarget.refuseProduction(base)
|
||||
assumeTrue(base != null, "set FN_LIVE_FLEET to run this against a relay")
|
||||
// 🔴 No fallback. This suite carried a literal passphrase and used it
|
||||
// to create REAL accounts — a published password, since anybody who can
|
||||
// read this repository can read it.
|
||||
assumeTrue(
|
||||
LiveTarget.passphrase().isNotEmpty(),
|
||||
"set FN_LIVE_PASSPHRASE — this suite creates real accounts and will " +
|
||||
"not sign up with a value written into the source",
|
||||
)
|
||||
}
|
||||
|
||||
private class MapStorage : Vault.Storage {
|
||||
@@ -56,7 +64,7 @@ class LiveKeyLogTest {
|
||||
val account = RelayAccounts(base).signUp(
|
||||
username = handle,
|
||||
email = "$handle@example.invalid",
|
||||
password = "a-long-enough-passphrase",
|
||||
password = LiveTarget.passphrase(),
|
||||
)
|
||||
val identity: DeviceIdentity = DeviceIdentity.create()
|
||||
val vault = Vault(
|
||||
|
||||
@@ -39,6 +39,14 @@ class LiveSealedSenderTest {
|
||||
private fun needFnLiveFleet() {
|
||||
LiveTarget.refuseProduction(base)
|
||||
assumeTrue(base != null, "set FN_LIVE_FLEET to run this against a relay")
|
||||
// 🔴 No fallback. This suite carried a literal passphrase and used it
|
||||
// to create REAL accounts — a published password, since anybody who can
|
||||
// read this repository can read it.
|
||||
assumeTrue(
|
||||
LiveTarget.passphrase().isNotEmpty(),
|
||||
"set FN_LIVE_PASSPHRASE — this suite creates real accounts and will " +
|
||||
"not sign up with a value written into the source",
|
||||
)
|
||||
}
|
||||
|
||||
private class MapStorage : Vault.Storage {
|
||||
@@ -54,7 +62,7 @@ class LiveSealedSenderTest {
|
||||
val account = RelayAccounts(base).signUp(
|
||||
username = handle,
|
||||
email = "$handle@example.invalid",
|
||||
password = "a-long-enough-passphrase",
|
||||
password = LiveTarget.passphrase(),
|
||||
)
|
||||
val identity: DeviceIdentity = DeviceIdentity.create()
|
||||
val vault = Vault(
|
||||
|
||||
@@ -34,6 +34,14 @@ class LiveSignUpTest {
|
||||
private fun needFnLiveFleet() {
|
||||
LiveTarget.refuseProduction(relay)
|
||||
assumeTrue(relay != null, "set FN_LIVE_FLEET to run this against a relay")
|
||||
// 🔴 No fallback. This suite carried a literal passphrase and used it
|
||||
// to create REAL accounts — a published password, since anybody who can
|
||||
// read this repository can read it.
|
||||
assumeTrue(
|
||||
LiveTarget.passphrase().isNotEmpty(),
|
||||
"set FN_LIVE_PASSPHRASE — this suite creates real accounts and will " +
|
||||
"not sign up with a value written into the source",
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -45,7 +53,7 @@ class LiveSignUpTest {
|
||||
val account = RelayAccounts(base).signUp(
|
||||
username = handle,
|
||||
email = "$handle@example.invalid",
|
||||
password = "a-long-enough-passphrase",
|
||||
password = LiveTarget.passphrase(),
|
||||
)
|
||||
assertEquals(handle, account.username)
|
||||
assertTrue(account.appPassword.isNotEmpty())
|
||||
|
||||
@@ -97,21 +97,59 @@ class TestSafetyTest {
|
||||
fun `no live suite carries a credential literal`() {
|
||||
val offenders = mutableListOf<String>()
|
||||
|
||||
/**
|
||||
* A name that puts whatever follows it in a credential position.
|
||||
*
|
||||
* ⚠️ Deliberately NOT "secret". Test prose is full of it — a marker
|
||||
* string called `secret` that the test then greps for in a container is
|
||||
* a fixture, not a credential — and a check that cries wolf is a check
|
||||
* somebody deletes.
|
||||
*/
|
||||
fun isCredentialName(name: String) = listOf("password", "passphrase", "credential")
|
||||
.any { name.contains(it, ignoreCase = true) }
|
||||
|
||||
/** A literal that is plainly something else. */
|
||||
fun isNotACredential(value: String) =
|
||||
value.startsWith("http") || value.contains("@") || value.contains("/")
|
||||
|
||||
for (file in testSources()) {
|
||||
val code = codeLines(file.readText())
|
||||
if (!reachesALiveRelay(code)) continue
|
||||
for (line in code) {
|
||||
val looksLikeCredential = line.contains("PASSPHRASE") ||
|
||||
line.contains("passphrase", ignoreCase = true) ||
|
||||
line.contains("password", ignoreCase = true)
|
||||
if (!looksLikeCredential) continue
|
||||
// `val X = "…"` or `const val X = "…"` — an assignment to a
|
||||
// non-empty literal. A getter that reads the environment has no
|
||||
// `= "` on it and is the shape this wants people to use.
|
||||
val assigned = Regex("""\bva[lr]\s+\w+\s*(:\s*String\s*)?=\s*"([^"]+)"""")
|
||||
.find(line)?.groupValues?.getOrNull(2)
|
||||
if (assigned != null) {
|
||||
offenders += "${file.name}: $assigned"
|
||||
|
||||
for ((index, line) in code.withIndex()) {
|
||||
for (literal in Regex(""""([^"\\$]{6,})"""").findAll(line)) {
|
||||
val value = literal.groupValues[1]
|
||||
if (isNotACredential(value)) continue
|
||||
val before = line.substring(0, literal.range.first)
|
||||
|
||||
// 1. Anything assigned to a credential-shaped name, with or
|
||||
// without `val` — `const val PASSPHRASE = "…"`,
|
||||
// `password = "…"`, `passphrase = "…"`.
|
||||
//
|
||||
// 🔴 The `val` was once required, and `password = "…"` on
|
||||
// its own line inside a multi-line argument list — the most
|
||||
// ordinary Kotlin formatting there is — walked past it in
|
||||
// three suites.
|
||||
val assignedTo = Regex("""(\w+)\s*=\s*$""").find(before)?.groupValues?.get(1)
|
||||
if (assignedTo != null && isCredentialName(assignedTo)) {
|
||||
offenders += "${file.name}: $value (assigned to $assignedTo)"
|
||||
continue
|
||||
}
|
||||
|
||||
// 2. A positional literal handed to sign-up on the SAME
|
||||
// line. Nothing else it could be.
|
||||
//
|
||||
// ⚠️ Same line only, and that is a deliberate limit rather
|
||||
// than an oversight. Widening it to a few lines around the
|
||||
// call flagged test tags, assertion messages and message
|
||||
// bodies as credentials — forty of them — which is how a
|
||||
// check stops being read. The multi-line case is covered by
|
||||
// rule 1 instead, which sees `password = "…"` wherever it
|
||||
// sits, and that is the formatting that actually occurs.
|
||||
if (assignedTo != null) continue
|
||||
if (Regex("""\bsign(Up|In)\s*\(""").containsMatchIn(before)) {
|
||||
offenders += "${file.name}: $value (passed to sign-up)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user