Files
flamenet-messenger-android/engine/src/test/kotlin/io/flamenet/e2e/X3DHTest.kt
T
Paul HittandClaude Fable 5.1 2fa430f21c API-style comments and ktlint formatting
Every comment is reduced to its first sentence plus @param, @return
and @throws tags. Comment lines: 20040 to 8526. ktlint 1.8 applied
with its default (official Kotlin) style across all modules; 262 files
reformatted. The 264 remaining ktlint findings are naming and filename
rules, which are code changes, and are left alone.

Comments and formatting only. 1208 tests pass, counted from the XML.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-08 20:02:07 -04:00

114 lines
4.0 KiB
Kotlin

package io.flamenet.e2e
import kotlin.test.Test
import kotlin.test.assertContentEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
/**
* X3DH, both directions.
*/
class X3DHTest {
private fun raw(kp: java.security.KeyPair) = X25519.rawFromPublicKey(kp.public)
@Test
fun `both sides derive the same secret, with a one-time prekey`() {
val aliceIdentity = X25519.generate()
val bobIdentity = X25519.generate()
val bobSigned = X25519.generate()
val bobOneTime = X25519.generate()
val alice =
X3DH.initiate(
identityPrivateDH = aliceIdentity.private,
bundle =
X3DH.Bundle(
identityKeyDH = raw(bobIdentity),
signedPreKey = raw(bobSigned),
oneTimePreKey = raw(bobOneTime),
),
)
val bob =
X3DH.respond(
identityPrivateDH = bobIdentity.private,
signedPreKeyPrivate = bobSigned.private,
oneTimePrivate = bobOneTime.private,
initiatorIdentityDH = raw(aliceIdentity),
initiatorEphemeral = alice.ephemeralPublic,
)
assertContentEquals(alice.sharedSecret, bob, "the two sides disagree")
assertTrue(alice.sharedSecret.size == 32)
}
@Test
fun `both sides agree when the one-time prekey pool is drained`() {
// A degraded but valid state: initial forward secrecy is lost, and the
// session must still work.
val aliceIdentity = X25519.generate()
val bobIdentity = X25519.generate()
val bobSigned = X25519.generate()
val alice =
X3DH.initiate(
identityPrivateDH = aliceIdentity.private,
bundle = X3DH.Bundle(raw(bobIdentity), raw(bobSigned), null),
)
val bob =
X3DH.respond(
identityPrivateDH = bobIdentity.private,
signedPreKeyPrivate = bobSigned.private,
oneTimePrivate = null,
initiatorIdentityDH = raw(aliceIdentity),
initiatorEphemeral = alice.ephemeralPublic,
)
assertContentEquals(alice.sharedSecret, bob)
}
@Test
fun `using a one-time prekey changes the result`() {
// Otherwise DH4 is contributing nothing and its absence would be
// undetectable, which would make a drained pool silently equivalent to
// a healthy one.
val aliceIdentity = X25519.generate()
val bobIdentity = X25519.generate()
val bobSigned = X25519.generate()
val bobOneTime = X25519.generate()
val ephemeral = X25519.generate()
val withOpk =
X3DH.initiate(
aliceIdentity.private,
X3DH.Bundle(raw(bobIdentity), raw(bobSigned), raw(bobOneTime)),
ephemeral,
)
val withoutOpk =
X3DH.initiate(
aliceIdentity.private,
X3DH.Bundle(raw(bobIdentity), raw(bobSigned), null),
ephemeral,
)
assertFalse(withOpk.sharedSecret.contentEquals(withoutOpk.sharedSecret))
}
@Test
fun `a different responder derives a different secret`() {
val aliceIdentity = X25519.generate()
val bobIdentity = X25519.generate()
val bobSigned = X25519.generate()
val malloryIdentity = X25519.generate()
val mallorySigned = X25519.generate()
val toBob =
X3DH.initiate(aliceIdentity.private, X3DH.Bundle(raw(bobIdentity), raw(bobSigned), null))
val malloryTries =
X3DH.respond(
identityPrivateDH = malloryIdentity.private,
signedPreKeyPrivate = mallorySigned.private,
oneTimePrivate = null,
initiatorIdentityDH = raw(aliceIdentity),
initiatorEphemeral = toBob.ephemeralPublic,
)
assertFalse(toBob.sharedSecret.contentEquals(malloryTries))
}
}