Match the persisted peer state on its fingerprint field

findAuthenticatedFingerprintByPeerID took the first stored record that
began with the peer ID. If that record's first field was not a
fingerprint, the search stopped there with nothing found. The lookup
now reads the fingerprint field of each record and takes the one that
is 64 hex characters starting with the peer ID, skipping any record
whose field is not a fingerprint. Records this app writes behave the
same as before.

One test puts a record whose first field is not a fingerprint ahead of
the real one and checks that the real one is still found.
This commit is contained in:
heyaim 2026-09-04 17:27:22 -05:00
parent e1da7f8960
commit 605958b405
2 changed files with 26 additions and 4 deletions

View File

@ -393,16 +393,27 @@ class SecureIdentityStateManager {
/**
* Fingerprint of the persisted authenticated peer state for a peer ID. A peer ID is the
* first sixteen hex characters of the fingerprint of its Noise static key, so a record
* whose fingerprint starts with the peer ID is that peer's; the record must still parse.
* first sixteen hex characters of the fingerprint of its Noise static key. Each record is
* matched on its fingerprint field, 64 hex characters starting with the peer ID; a record
* whose field does not parse is skipped, and the matched record must still load.
*/
fun findAuthenticatedFingerprintByPeerID(peerID: String): String? {
val prefix = peerID.lowercase()
if (prefix.length != 16 || !prefix.all { it in '0'..'9' || it in 'a'..'f' }) return null
val records = prefs.getStringSet(KEY_AUTHENTICATED_PEER_STATES, emptySet()) ?: return null
val fingerprint = records.firstOrNull { it.startsWith(prefix) }?.substringBefore(':') ?: return null
return fingerprint.takeIf { isValidFingerprint(it) && getAuthenticatedPeerState(it) != null }
val fingerprint = fingerprintFieldFor(prefix, records) ?: return null
return fingerprint.takeIf { getAuthenticatedPeerState(it) != null }
}
/**
* The fingerprint field of the first record, in the order given, that parses to 64 hex
* characters starting with [prefix]. A record whose field does not parse is skipped, not
* taken for a match on the raw string.
*/
internal fun fingerprintFieldFor(prefix: String, records: Iterable<String>): String? =
records.asSequence()
.map { it.substringBefore(':').lowercase() }
.firstOrNull { isValidFingerprint(it) && it.startsWith(prefix) }
// MARK: - Peer ID Rotation Management (removed)
// Android now derives peer ID from the persisted Noise identity fingerprint.

View File

@ -52,4 +52,15 @@ class SecureAuthenticatedPeerStateStoreTest {
assertNull("a cached nickname is not a persisted identity", store.persistedFingerprintFor(peerID))
assertNull(store.load(fingerprint))
}
@Test
fun `a record whose fingerprint field is not 64 hex is ignored`() {
val (_, identity) = freshStore()
// The raw string starts with the peer ID but the fingerprint field is only 16 characters.
// Examined first, it must be skipped and the real record still found.
val malformed = "$peerID:0:${"00".repeat(32)}"
val real = "$fingerprint:0:${"5a".repeat(32)}"
assertEquals(fingerprint, identity.fingerprintFieldFor(peerID, listOf(malformed, real)))
}
}