Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,4 @@ ios/Local.xcconfig
cli/where_cli_state.json
*.swp
*.txt
*.log
TODO_SPEC_UPDATES.md
.log
Original file line number Diff line number Diff line change
Expand Up @@ -409,7 +409,7 @@ internal class E2eeStore(
database.invitesQueries.insertPendingInvite(
ekPub = invite.qrPayload.ekPub,
suggestedName = invite.qrPayload.suggestedName,
fingerprint = invite.qrPayload.fingerprint,
fingerprint = invite.qrPayload.fingerprint ?: "",
discoverySecret = invite.qrPayload.discoverySecret,
privKeyBlob = invite.aliceEkPriv,
createdAt = invite.createdAt,
Expand Down
13 changes: 3 additions & 10 deletions shared/src/commonMain/kotlin/net/af0/where/e2ee/KeyExchange.kt
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,10 @@ object KeyExchange {
}

/**
* Bob: verify the QR fingerprint, perform single-term DH, derive SK, compute
* key_confirmation HMAC, and return the KeyExchangeInit message plus the initial
* session state.
* Bob: perform single-term DH, derive SK, compute key_confirmation HMAC, and
* return the KeyExchangeInit message plus the initial session state.
*
* Throws AuthenticationException if the fingerprint or key_confirmation fails.
* Throws AuthenticationException if key_confirmation fails.
*/
fun bobProcessQr(
qr: QrPayload,
Expand All @@ -48,12 +47,6 @@ object KeyExchange {
throw ProtocolVersionException("Unsupported protocol version ${qr.protocolVersion}")
}

// VERIFY FINGERPRINT (#157)
val expectedFp = qrFingerprint(qr.ekPub)
if (expectedFp != qr.fingerprint) {
throw AuthenticationException("QR code fingerprint mismatch — possible tampering or mis-scan")
}

val ekB = generateX25519KeyPair()
val sk = x25519(ekB.priv, qr.ekPub)

Expand Down
4 changes: 2 additions & 2 deletions shared/src/commonMain/kotlin/net/af0/where/e2ee/Types.kt
Original file line number Diff line number Diff line change
Expand Up @@ -216,9 +216,9 @@ data class QrPayload(
@Serializable(with = ByteArrayBase64Serializer::class) val ekPub: ByteArray,
@SerialName("suggested_name")
val suggestedName: String,
// hex(SHA-256(ekPub)[0:20])
// hex(SHA-256(ekPub)[0:20]); optional — ignored by receiver (Stage 1 of §4.2 removal)
@SerialName("fingerprint")
val fingerprint: String,
val fingerprint: String? = null,
// Fresh random 32-byte secret; HKDF IKM for the discovery token (§4.2).
@SerialName("discovery_secret")
@Serializable(with = ByteArrayBase64Serializer::class) val discoverySecret: ByteArray,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class KeyExchangeTest {
val (qr, ekPriv) = KeyExchange.aliceCreateQrPayload("Alice")

assertEquals(32, qr.ekPub.size)
assertEquals(40, qr.fingerprint.length) // hex(SHA-256(ekPub)[0:20])
assertEquals(40, qr.fingerprint?.length) // hex(SHA-256(ekPub)[0:20]); still sent by Alice

// ekPriv is 32 bytes and non-zero.
assertEquals(32, ekPriv.size)
Expand Down Expand Up @@ -214,21 +214,6 @@ class KeyExchangeTest {
sk.zeroize()
}

@Test
fun `bobProcessQr rejects tampered fingerprint`() {
val (qr, _) = KeyExchange.aliceCreateQrPayload("Alice")
val badQr = qr.copy(fingerprint = "0".repeat(40)) // Tampered fingerprint

val threw =
try {
KeyExchange.bobProcessQr(badQr, "Bob")
false
} catch (_: AuthenticationException) {
true
}
assertTrue(threw, "Expected AuthenticationException for tampered fingerprint")
}

@Test
fun `name encryption and decryption works correctly`() {
val (qr, aliceEkPriv) = KeyExchange.aliceCreateQrPayload("Alice")
Expand Down
Loading