1package com.peachpasswords.sync23import kotlinx.coroutines.Dispatchers4import kotlinx.coroutines.async5import kotlinx.coroutines.channels.Channel6import kotlinx.coroutines.runBlocking7import org.bouncycastle.crypto.params.X25519PrivateKeyParameters8import org.junit.Assert.assertEquals9import org.junit.Assert.assertFalse10import org.junit.Assert.assertNotEquals11import org.junit.Assert.assertTrue12import org.junit.Test13import java.security.SecureRandom1415161718192021class CloudRelayDialerTest {22 private data class KeyPair(val publicKey: ByteArray, val privateKey: ByteArray)2324 private fun keys(): KeyPair {25 val privateKey = X25519PrivateKeyParameters(SecureRandom())26 return KeyPair(privateKey.generatePublicKey().encoded, privateKey.encoded)27 }2829 private fun socketPair(): Pair<RelayBinarySocket, RelayBinarySocket> {30 val leftInbox = Channel<ByteArray>(Channel.UNLIMITED)31 val rightInbox = Channel<ByteArray>(Channel.UNLIMITED)32 fun socket(inbox: Channel<ByteArray>, peer: Channel<ByteArray>) =33 object : RelayBinarySocket {34 override fun send(frame: ByteArray) {35 check(peer.trySend(frame.copyOf()).isSuccess)36 }3738 override suspend fun receive(): ByteArray = inbox.receive()3940 override fun close(code: Int, reason: String) {41 inbox.close()42 }43 }44 return socket(leftInbox, rightInbox) to socket(rightInbox, leftInbox)45 }4647 private fun identity(id: String, name: String, publicKeyHex: String) = LocalDeviceIdentity(48 deviceId = id,49 deviceName = name,50 deviceType = DeviceType.ANDROID,51 longTermPublicKeyHex = publicKeyHex,52 signingPublicKeyHex = "33".repeat(32),53 )5455 @Test56 fun `two phones complete the reconnect wire, one dialling and one waiting`() = runBlocking {57 val dialerKeys = keys()58 val waiterKeys = keys()59 val secretHex = "71".repeat(32)60 val secret = Rendezvous.secretFromHex(secretHex)61 val now = System.currentTimeMillis() / 1000L62 63 val rendezvousId = Rendezvous.waitingIds(secret, now).first()64 assertTrue(rendezvousId in Rendezvous.dialingIds(secret, now))6566 val dialerIdentity = identity("dialer", "Fold", pubkeyToHex(dialerKeys.publicKey))67 val waiterIdentity = identity("waiter", "Tablet", pubkeyToHex(waiterKeys.publicKey))68 val dialerAsSeenByWaiter = PairedDevice(69 deviceId = "dialer",70 deviceName = "Fold",71 deviceType = "android",72 longTermPublicKeyHex = pubkeyToHex(dialerKeys.publicKey),73 rendezvousSecretHex = secretHex,74 pairedAt = 1L,75 )76 val waiterAsSeenByDialer = PairedDevice(77 deviceId = "waiter",78 deviceName = "Tablet",79 deviceType = "android",80 longTermPublicKeyHex = pubkeyToHex(waiterKeys.publicKey),81 rendezvousSecretHex = secretHex,82 pairedAt = 1L,83 )8485 val (dialerSocket, waiterSocket) = socketPair()86 var dialerSynced = false87 var waiterSynced = false8889 val waiter = async(Dispatchers.Default) {90 runCloudRelayResponderSession(91 socket = waiterSocket,92 known = dialerAsSeenByWaiter,93 registeredRendezvousId = rendezvousId,94 ourStaticPrivateKey = waiterKeys.privateKey,95 ourStaticPublicKey = waiterKeys.publicKey,96 localIdentity = waiterIdentity,97 vaultFactory = { null },98 onSyncComplete = { waiterSynced = true },99 )100 }101 val dialer = async(Dispatchers.Default) {102 runCloudRelayInitiatorSession(103 socket = dialerSocket,104 known = waiterAsSeenByDialer,105 rendezvousId = rendezvousId,106 ourStaticPrivateKey = dialerKeys.privateKey,107 ourStaticPublicKey = dialerKeys.publicKey,108 localIdentity = dialerIdentity,109 vaultFactory = { null },110 onSyncComplete = { dialerSynced = true },111 )112 }113114 waiter.await()115 dialer.await()116 117 118 assertFalse(dialerSynced)119 assertFalse(waiterSynced)120 }121122 @Test123 fun `a peer presenting a different key than KK authenticated is refused`() = runBlocking {124 val dialerKeys = keys()125 val waiterKeys = keys()126 val impostorKeys = keys()127 val secretHex = "72".repeat(32)128 val rendezvousId = Rendezvous.waitingIds(129 Rendezvous.secretFromHex(secretHex),130 System.currentTimeMillis() / 1000L,131 ).first()132 val (dialerSocket, waiterSocket) = socketPair()133134 135 136 137 val waiter = async(Dispatchers.Default) {138 val selector = String(waiterSocket.receive(), Charsets.UTF_8)139 assertTrue(selector.contains("PEACH_RECONNECT"))140 val session = noiseKKResponder(141 waiterKeys.privateKey,142 waiterKeys.publicKey,143 dialerKeys.publicKey,144 waiterSocket::send,145 waiterSocket::receive,146 )147 receiveSyncFrame(waiterSocket::receive, session::decrypt)148 sendSyncFrame(149 encodePairIdentityFrame(150 identity("waiter", "Tablet", pubkeyToHex(impostorKeys.publicKey)),151 ),152 session::encrypt,153 waiterSocket::send,154 )155 }156157 val failure = runCatching {158 runCloudRelayInitiatorSession(159 socket = dialerSocket,160 known = PairedDevice(161 deviceId = "waiter",162 deviceType = "android",163 longTermPublicKeyHex = pubkeyToHex(waiterKeys.publicKey),164 rendezvousSecretHex = secretHex,165 pairedAt = 1L,166 ),167 rendezvousId = rendezvousId,168 ourStaticPrivateKey = dialerKeys.privateKey,169 ourStaticPublicKey = dialerKeys.publicKey,170 localIdentity = identity("dialer", "Fold", pubkeyToHex(dialerKeys.publicKey)),171 vaultFactory = { null },172 onSyncComplete = {},173 )174 }.exceptionOrNull()175 waiter.await()176177 assertTrue(178 "expected identity mismatch, got $failure",179 failure?.message?.contains("does not match authenticated KK key") == true,180 )181 }182}183184class SyncPeerRoleTest {185 private fun peer(type: String?, keyHex: String?) = PairedDevice(186 deviceId = "peer",187 deviceType = type,188 longTermPublicKeyHex = keyHex,189 pairedAt = 1L,190 )191192 @Test193 fun `we never dial a browser extension, whichever key is lower`() {194 val low = "aa".repeat(32)195 val high = "bb".repeat(32)196 assertFalse(weDialPeer(low, peer("extension", high)))197 assertFalse(weDialPeer(high, peer("extension", low)))198 }199200 @Test201 fun `phone to phone assigns exactly one dialler`() {202 val ours = "aa".repeat(32)203 val theirs = "bb".repeat(32)204 205 206 207 assertTrue(weDialPeer(ours, peer("android", theirs)))208 assertFalse(weDialPeer(theirs, peer("android", ours)))209 assertNotEquals(210 weDialPeer(ours, peer("android", theirs)),211 weDialPeer(theirs, peer("android", ours)),212 )213 }214215 @Test216 fun `a tablet is a phone for this purpose, and an unknown type is too`() {217 val low = "11".repeat(32)218 val high = "ff".repeat(32)219 assertTrue(weDialPeer(low, peer("ios", high)))220 assertTrue(weDialPeer(low, peer(null, high)))221 assertFalse(weDialPeer(high, peer(null, low)))222 }223224 @Test225 fun `a peer with no static key is never dialled`() {226 assertFalse(weDialPeer("00".repeat(32), peer("android", null)))227 }228229 @Test230 fun `roles hold across many random pairs`() {231 val random = SecureRandom()232 repeat(200) {233 val a = ByteArray(32).also(random::nextBytes).let(::pubkeyToHex)234 val b = ByteArray(32).also(random::nextBytes).let(::pubkeyToHex)235 if (a == b) return@repeat236 assertNotEquals(weDialPeer(a, peer("android", b)), weDialPeer(b, peer("android", a)))237 }238 }239}