NoiseKKTest.kt

4.3 KB · raw

1package com.peachpasswords.sync23import kotlinx.coroutines.async4import kotlinx.coroutines.channels.Channel5import kotlinx.coroutines.runBlocking6import org.bouncycastle.crypto.params.X25519PrivateKeyParameters7import org.junit.Assert.assertArrayEquals8import org.junit.Assert.assertEquals9import org.junit.Assert.assertTrue10import org.junit.Test11import java.security.SecureRandom1213class NoiseKKTest {14    private fun keys(seed: Int? = null): NoiseKKKeyPair {15        val privateKey = if (seed == null) {16            X25519PrivateKeyParameters(SecureRandom())17        } else {18            X25519PrivateKeyParameters(ByteArray(32) { seed.toByte() }, 0)19        }20        return NoiseKKKeyPair(privateKey.generatePublicKey().encoded, privateKey.encoded)21    }2223    @Test24    fun `KK mutually authenticates and carries transport frames`() = runBlocking {25        val initiator = keys()26        val responder = keys()27        val aToB = Channel<ByteArray>(Channel.UNLIMITED)28        val bToA = Channel<ByteArray>(Channel.UNLIMITED)29        val initiatorHandshake = async {30            noiseKKInitiator(31                initiator.secretKey,32                initiator.publicKey,33                responder.publicKey,34                { aToB.send(it) },35                { bToA.receive() },36            )37        }38        val responderHandshake = async {39            noiseKKResponder(40                responder.secretKey,41                responder.publicKey,42                initiator.publicKey,43                { bToA.send(it) },44                { aToB.receive() },45            )46        }47        val a = initiatorHandshake.await()48        val b = responderHandshake.await()4950        assertArrayEquals(a.handshakeHash, b.handshakeHash)51        assertEquals("one", String(b.decrypt(a.encrypt("one".encodeToByteArray()))))52        assertEquals("two", String(a.decrypt(b.encrypt("two".encodeToByteArray()))))53    }5455    @Test56    fun `KK handshake hash matches browser cross-platform vector`() = runBlocking {57        val initiator = keys(0x01)58        val responder = keys(0x02)59        val initiatorEphemeral = keys(0x03)60        val responderEphemeral = keys(0x04)61        val aToB = Channel<ByteArray>(Channel.UNLIMITED)62        val bToA = Channel<ByteArray>(Channel.UNLIMITED)6364        val initiatorHandshake = async {65            noiseKKInitiator(66                initiator.secretKey,67                initiator.publicKey,68                responder.publicKey,69                { aToB.send(it) },70                { bToA.receive() },71                initiatorEphemeral,72            )73        }74        val responderHandshake = async {75            noiseKKResponder(76                responder.secretKey,77                responder.publicKey,78                initiator.publicKey,79                { bToA.send(it) },80                { aToB.receive() },81                responderEphemeral,82            )83        }8485        val a = initiatorHandshake.await()86        val b = responderHandshake.await()87        assertEquals(88            "ac3dd2e4c40290fab2a0192b0b33b37bb8c31396ffec23271ffa2a99ef3a0f75",89            pubkeyToHex(a.handshakeHash),90        )91        assertArrayEquals(a.handshakeHash, b.handshakeHash)92    }9394    @Test95    fun `KK rejects an initiator key mismatch without fallback`() = runBlocking {96        val initiator = keys()97        val attacker = keys()98        val responder = keys()99        val aToB = Channel<ByteArray>(Channel.UNLIMITED)100        val bToA = Channel<ByteArray>(Channel.UNLIMITED)101102        val initiatorHandshake = async {103            runCatching {104                noiseKKInitiator(105                    initiator.secretKey,106                    initiator.publicKey,107                    responder.publicKey,108                    { aToB.send(it) },109                    { bToA.receive() },110                )111            }112        }113        val responderHandshake = async {114            runCatching {115                noiseKKResponder(116                    responder.secretKey,117                    responder.publicKey,118                    attacker.publicKey,119                    { bToA.send(it) },120                    { aToB.receive() },121                )122            }123        }124125        val failure = responderHandshake.await().exceptionOrNull()126        assertTrue(failure?.message?.contains("decrypt", ignoreCase = true) == true)127        initiatorHandshake.cancel()128    }129}