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.Test10import java.security.SecureRandom1112class PairingAuthTest {13 private data class Keys(val privateKey: ByteArray, val publicKey: ByteArray)1415 private fun keys(): Keys {16 val privateKey = X25519PrivateKeyParameters(SecureRandom())17 return Keys(privateKey.encoded, privateKey.generatePublicKey().encoded)18 }1920 private fun session(hash: ByteArray) = object : NoiseNKSession {21 override fun encrypt(plaintext: ByteArray): ByteArray = plaintext22 override fun decrypt(ciphertext: ByteArray): ByteArray = ciphertext23 override val handshakeHash: ByteArray = hash24 }2526 @Test27 fun `auth proof matches browser cross-platform vector`() {28 val proof = computeInitiatorAuthProof(29 initiatorPrivateKey = ByteArray(32) { 0x01 },30 initiatorPublicKey = hexToPubkey(31 "a4e09292b651c278b9772c569f5fa9bb13d906b46ab68c9df9dc2b4409f8a209",32 ),33 responderPublicKey = hexToPubkey(34 "ce8d3ad1ccb633ec7b70c17814a5c76ecd029685050d344745ba05870e587d59",35 ),36 nonce = ByteArray(32) { 0x03 },37 handshakeHash = ByteArray(32) { 0x04 },38 )3940 assertEquals(41 "d947d11a82892a65fdde94da5bbe6ca6034cf1f7e3268fc3ce5851b55f5813ce",42 pubkeyToHex(proof),43 )44 }4546 @Test47 fun `initiator proof round trips and returns authenticated key`() = runBlocking {48 val initiator = keys()49 val responder = keys()50 val hash = ByteArray(32) { 0x07 }51 val initToResponder = Channel<ByteArray>(Channel.UNLIMITED)52 val responderToInit = Channel<ByteArray>(Channel.UNLIMITED)5354 val prove = async {55 performInitiatorPairingAuth(56 session(hash),57 initiator.privateKey,58 initiator.publicKey,59 responder.publicKey,60 { initToResponder.send(it) },61 { responderToInit.receive() },62 )63 }64 val verify = async {65 performResponderPairingAuth(66 session(hash),67 responder.privateKey,68 responder.publicKey,69 { responderToInit.send(it) },70 { initToResponder.receive() },71 )72 }7374 prove.await()75 assertArrayEquals(initiator.publicKey, verify.await())76 }7778 @Test79 fun `proof from a different handshake hash is rejected`() = runBlocking {80 val initiator = keys()81 val responder = keys()82 val initToResponder = Channel<ByteArray>(Channel.UNLIMITED)83 val responderToInit = Channel<ByteArray>(Channel.UNLIMITED)8485 val prove = async {86 performInitiatorPairingAuth(87 session(ByteArray(32) { 0x07 }),88 initiator.privateKey,89 initiator.publicKey,90 responder.publicKey,91 { initToResponder.send(it) },92 { responderToInit.receive() },93 )94 }95 val verify = async {96 runCatching {97 performResponderPairingAuth(98 session(ByteArray(32) { 0x09 }),99 responder.privateKey,100 responder.publicKey,101 { responderToInit.send(it) },102 { initToResponder.receive() },103 )104 }105 }106107 prove.await()108 val failure = verify.await().exceptionOrNull()109 assertEquals("auth proof did not verify", failure?.message)110 }111}