From 4504c6c934a123b384fc7469d63e8ca9f4710833 Mon Sep 17 00:00:00 2001 From: Gus Narea Date: Fri, 4 Aug 2023 14:40:15 +0100 Subject: [PATCH 01/14] chore(deps): Upgrade ktlint to v0.50 (#324) --- .editorconfig | 4 ++ lib/build.gradle | 2 +- .../awaladroid/AndroidPrivateKeyStore.kt | 4 +- .../java/tech/relaycorp/awaladroid/Awala.kt | 6 +- .../tech/relaycorp/awaladroid/AwalaContext.kt | 2 +- .../relaycorp/awaladroid/GatewayClientImpl.kt | 31 ++++----- ...tewayCertificateChangeBroadcastReceiver.kt | 2 +- .../IncomingParcelBroadcastReceiver.kt | 2 +- .../background/ServiceInteractor.kt | 8 +-- .../tech/relaycorp/awaladroid/common/Keys.kt | 2 +- .../awaladroid/endpoint/ChannelManager.kt | 18 ++--- .../awaladroid/endpoint/FirstPartyEndpoint.kt | 42 ++++++------ .../HandleGatewayCertificateChange.kt | 2 +- .../endpoint/PrivateThirdPartyEndpointData.kt | 8 +-- .../endpoint/PublicThirdPartyEndpointData.kt | 10 +-- .../endpoint/RenewExpiringCertificates.kt | 6 +- .../awaladroid/endpoint/ThirdPartyEndpoint.kt | 30 ++++---- .../awaladroid/messaging/IncomingMessage.kt | 16 ++--- .../awaladroid/messaging/OutgoingMessage.kt | 12 ++-- .../awaladroid/messaging/ParcelId.kt | 2 +- .../awaladroid/messaging/ReceiveMessages.kt | 12 ++-- .../awaladroid/messaging/SendMessage.kt | 10 +-- .../awaladroid/storage/StorageImpl.kt | 16 ++--- .../storage/persistence/DiskPersistence.kt | 6 +- .../awaladroid/AndroidPrivateKeyStoreTest.kt | 2 +- .../tech/relaycorp/awaladroid/AwalaTest.kt | 10 +-- .../awaladroid/GatewayClientImplTest.kt | 17 +++-- .../awaladroid/endpoint/ChannelManagerTest.kt | 30 ++++---- .../endpoint/FirstPartyEndpointTest.kt | 68 +++++++++---------- .../endpoint/PrivateThirdPartyEndpointTest.kt | 42 ++++++------ .../endpoint/PublicThirdPartyEndpointTest.kt | 16 ++--- .../endpoint/RenewExpiringCertificatesTest.kt | 4 +- .../messaging/IncomingMessageTest.kt | 34 +++++----- .../messaging/OutgoingMessageTest.kt | 20 +++--- .../messaging/ReceiveMessagesTest.kt | 32 ++++----- .../awaladroid/messaging/SendMessageTest.kt | 2 +- .../awaladroid/storage/StorageImplTest.kt | 22 +++--- .../persistence/DiskPersistenceTest.kt | 9 +-- .../relaycorp/awaladroid/test/AssertUtils.kt | 2 +- .../awaladroid/test/FakeAndroidKeyStore.kt | 4 +- .../awaladroid/test/MessageFactory.kt | 2 +- .../awaladroid/test/MockContextTestCase.kt | 30 ++++---- .../test/ThirdPartyEndpointFactory.kt | 2 +- 43 files changed, 304 insertions(+), 297 deletions(-) diff --git a/.editorconfig b/.editorconfig index 0f5cbac9..aec9a137 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,6 +1,10 @@ # http://editorconfig.org root = true +[*.{kt,kts}] +ij_kotlin_allow_trailing_comma = true +ij_kotlin_allow_trailing_comma_on_call_site = true + [*] charset = utf-8 end_of_line = lf diff --git a/lib/build.gradle b/lib/build.gradle index f365c5a5..d1b0da87 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -121,7 +121,7 @@ dokkaHtml.configure { ktlint { verbose = true android = true - version = "0.43.2" + version = "0.50.0" } afterEvaluate { diff --git a/lib/src/main/java/tech/relaycorp/awaladroid/AndroidPrivateKeyStore.kt b/lib/src/main/java/tech/relaycorp/awaladroid/AndroidPrivateKeyStore.kt index 4f2394ff..bcaed7f9 100644 --- a/lib/src/main/java/tech/relaycorp/awaladroid/AndroidPrivateKeyStore.kt +++ b/lib/src/main/java/tech/relaycorp/awaladroid/AndroidPrivateKeyStore.kt @@ -3,9 +3,9 @@ package tech.relaycorp.awaladroid import android.content.Context import androidx.security.crypto.EncryptedFile import androidx.security.crypto.MasterKey -import java.io.File import tech.relaycorp.awala.keystores.file.FileKeystoreRoot import tech.relaycorp.awala.keystores.file.FilePrivateKeyStore +import java.io.File internal class AndroidPrivateKeyStore( root: FileKeystoreRoot, @@ -20,7 +20,7 @@ internal class AndroidPrivateKeyStore( context, file, masterKey, - EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB + EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB, ).build() private val masterKey by lazy { diff --git a/lib/src/main/java/tech/relaycorp/awaladroid/Awala.kt b/lib/src/main/java/tech/relaycorp/awaladroid/Awala.kt index 5c4ce251..e463e8e7 100644 --- a/lib/src/main/java/tech/relaycorp/awaladroid/Awala.kt +++ b/lib/src/main/java/tech/relaycorp/awaladroid/Awala.kt @@ -1,7 +1,6 @@ package tech.relaycorp.awaladroid import android.content.Context -import java.io.File import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch import tech.relaycorp.awala.keystores.file.FileCertificateStore @@ -15,6 +14,7 @@ import tech.relaycorp.awaladroid.endpoint.RenewExpiringCertificates import tech.relaycorp.awaladroid.storage.StorageImpl import tech.relaycorp.awaladroid.storage.persistence.DiskPersistence import tech.relaycorp.relaynet.nodes.EndpointManager +import java.io.File public object Awala { internal const val POWEB_PORT = 13276 @@ -42,7 +42,7 @@ public object Awala { this.context = AwalaContext( StorageImpl(DiskPersistence(context.filesDir.path.toString())), GatewayClientImpl( - serviceInteractorBuilder = { ServiceInteractor(context) } + serviceInteractorBuilder = { ServiceInteractor(context) }, ), EndpointManager(androidPrivateKeyStore, fileSessionPublicKeystore), ChannelManager { @@ -51,7 +51,7 @@ public object Awala { androidPrivateKeyStore, fileSessionPublicKeystore, fileCertificateStore, - HandleGatewayCertificateChange(androidPrivateKeyStore) + HandleGatewayCertificateChange(androidPrivateKeyStore), ) coroutineScope { diff --git a/lib/src/main/java/tech/relaycorp/awaladroid/AwalaContext.kt b/lib/src/main/java/tech/relaycorp/awaladroid/AwalaContext.kt index 7a7fcd63..20cc9a8c 100644 --- a/lib/src/main/java/tech/relaycorp/awaladroid/AwalaContext.kt +++ b/lib/src/main/java/tech/relaycorp/awaladroid/AwalaContext.kt @@ -16,5 +16,5 @@ internal data class AwalaContext( val privateKeyStore: PrivateKeyStore, val sessionPublicKeyStore: SessionPublicKeyStore, val certificateStore: CertificateStore, - val handleGatewayCertificateChange: HandleGatewayCertificateChange + val handleGatewayCertificateChange: HandleGatewayCertificateChange, ) diff --git a/lib/src/main/java/tech/relaycorp/awaladroid/GatewayClientImpl.kt b/lib/src/main/java/tech/relaycorp/awaladroid/GatewayClientImpl.kt index d044ead1..7e97367e 100644 --- a/lib/src/main/java/tech/relaycorp/awaladroid/GatewayClientImpl.kt +++ b/lib/src/main/java/tech/relaycorp/awaladroid/GatewayClientImpl.kt @@ -1,11 +1,5 @@ package tech.relaycorp.awaladroid -import java.security.KeyPair -import java.util.logging.Level -import kotlin.coroutines.CoroutineContext -import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException -import kotlin.coroutines.suspendCoroutine import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow @@ -28,6 +22,12 @@ import tech.relaycorp.relaynet.bindings.pdc.PDCClient import tech.relaycorp.relaynet.bindings.pdc.ServerException import tech.relaycorp.relaynet.messages.control.PrivateNodeRegistration import tech.relaycorp.relaynet.messages.control.PrivateNodeRegistrationRequest +import java.security.KeyPair +import java.util.logging.Level +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine /** * Private gateway client. @@ -39,7 +39,7 @@ internal constructor( private val pdcClientBuilder: () -> PDCClient = { PoWebClient.initLocal(port = Awala.POWEB_PORT) }, private val sendMessage: SendMessage = SendMessage(), - private val receiveMessages: ReceiveMessages = ReceiveMessages() + private val receiveMessages: ReceiveMessages = ReceiveMessages(), ) { // Gateway @@ -59,12 +59,12 @@ internal constructor( bind( Awala.GATEWAY_SYNC_ACTION, Awala.GATEWAY_PACKAGE, - Awala.GATEWAY_SYNC_COMPONENT + Awala.GATEWAY_SYNC_COMPONENT, ) } catch (exp: ServiceInteractor.BindFailedException) { throw GatewayBindingException( "Failed binding to Awala Gateway for registration", - exp + exp, ) } } @@ -86,12 +86,11 @@ internal constructor( @Throws( RegistrationFailedException::class, - GatewayProtocolException::class + GatewayProtocolException::class, ) internal suspend fun registerEndpoint(keyPair: KeyPair): PrivateNodeRegistration = withContext(coroutineContext) { try { - val preAuthSerialized = preRegister() val request = PrivateNodeRegistrationRequest(keyPair.public, preAuthSerialized) val requestSerialized = request.serialize(keyPair.private) @@ -117,14 +116,14 @@ internal constructor( @Throws( ServiceInteractor.BindFailedException::class, ServiceInteractor.SendFailedException::class, - GatewayProtocolException::class + GatewayProtocolException::class, ) private suspend fun preRegister(): ByteArray { val interactor = serviceInteractorBuilder().apply { bind( Awala.GATEWAY_PRE_REGISTER_ACTION, Awala.GATEWAY_PACKAGE, - Awala.GATEWAY_PRE_REGISTER_COMPONENT + Awala.GATEWAY_PRE_REGISTER_COMPONENT, ) } @@ -134,7 +133,7 @@ internal constructor( if (replyMessage.what != REGISTRATION_AUTHORIZATION) { interactor.unbind() cont.resumeWithException( - GatewayProtocolException("Pre-registration failed, received wrong reply") + GatewayProtocolException("Pre-registration failed, received wrong reply"), ) return@sendMessage } @@ -150,7 +149,7 @@ internal constructor( GatewayBindingException::class, GatewayProtocolException::class, SendMessageException::class, - RejectedMessageException::class + RejectedMessageException::class, ) public suspend fun sendMessage(message: OutgoingMessage) { if (gwServiceInteractor == null) { @@ -178,7 +177,7 @@ internal constructor( logger.log( Level.SEVERE, "Could not bind to gateway to receive new messages", - exp + exp, ) return@withContext } diff --git a/lib/src/main/java/tech/relaycorp/awaladroid/background/GatewayCertificateChangeBroadcastReceiver.kt b/lib/src/main/java/tech/relaycorp/awaladroid/background/GatewayCertificateChangeBroadcastReceiver.kt index 442eedbb..c22e3367 100644 --- a/lib/src/main/java/tech/relaycorp/awaladroid/background/GatewayCertificateChangeBroadcastReceiver.kt +++ b/lib/src/main/java/tech/relaycorp/awaladroid/background/GatewayCertificateChangeBroadcastReceiver.kt @@ -3,11 +3,11 @@ package tech.relaycorp.awaladroid.background import android.content.BroadcastReceiver import android.content.Context import android.content.Intent -import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import tech.relaycorp.awaladroid.Awala +import kotlin.coroutines.CoroutineContext internal class GatewayCertificateChangeBroadcastReceiver : BroadcastReceiver() { diff --git a/lib/src/main/java/tech/relaycorp/awaladroid/background/IncomingParcelBroadcastReceiver.kt b/lib/src/main/java/tech/relaycorp/awaladroid/background/IncomingParcelBroadcastReceiver.kt index 67903421..c56d7560 100644 --- a/lib/src/main/java/tech/relaycorp/awaladroid/background/IncomingParcelBroadcastReceiver.kt +++ b/lib/src/main/java/tech/relaycorp/awaladroid/background/IncomingParcelBroadcastReceiver.kt @@ -3,11 +3,11 @@ package tech.relaycorp.awaladroid.background import android.content.BroadcastReceiver import android.content.Context import android.content.Intent -import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import tech.relaycorp.awaladroid.Awala +import kotlin.coroutines.CoroutineContext internal class IncomingParcelBroadcastReceiver : BroadcastReceiver() { diff --git a/lib/src/main/java/tech/relaycorp/awaladroid/background/ServiceInteractor.kt b/lib/src/main/java/tech/relaycorp/awaladroid/background/ServiceInteractor.kt index 06ee8a9e..6d29508c 100644 --- a/lib/src/main/java/tech/relaycorp/awaladroid/background/ServiceInteractor.kt +++ b/lib/src/main/java/tech/relaycorp/awaladroid/background/ServiceInteractor.kt @@ -10,13 +10,13 @@ import android.os.Looper import android.os.Message import android.os.Messenger import android.os.RemoteException +import tech.relaycorp.awaladroid.common.Logging.logger import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException import kotlin.coroutines.suspendCoroutine -import tech.relaycorp.awaladroid.common.Logging.logger internal class ServiceInteractor( - private val context: Context + private val context: Context, ) { private var serviceConnection: ServiceConnection? = null @@ -66,14 +66,14 @@ internal class ServiceInteractor( val intent = Intent(action).apply { component = ComponentName( packageName, - componentName + componentName, ) } val bindWasSuccessful = context.bindService( intent, serviceConnection, - Context.BIND_AUTO_CREATE + Context.BIND_AUTO_CREATE, ) if (!bindWasSuccessful) cont.resumeWithException(BindFailedException("Binding failed")) } diff --git a/lib/src/main/java/tech/relaycorp/awaladroid/common/Keys.kt b/lib/src/main/java/tech/relaycorp/awaladroid/common/Keys.kt index 13af152f..6878e965 100644 --- a/lib/src/main/java/tech/relaycorp/awaladroid/common/Keys.kt +++ b/lib/src/main/java/tech/relaycorp/awaladroid/common/Keys.kt @@ -1,12 +1,12 @@ package tech.relaycorp.awaladroid.common +import org.bouncycastle.jce.provider.BouncyCastleProvider import java.security.KeyFactory import java.security.KeyPair import java.security.PrivateKey import java.security.PublicKey import java.security.interfaces.RSAPrivateCrtKey import java.security.spec.RSAPublicKeySpec -import org.bouncycastle.jce.provider.BouncyCastleProvider internal fun PrivateKey.toKeyPair(): KeyPair = KeyPair(toPublicKey(), this) diff --git a/lib/src/main/java/tech/relaycorp/awaladroid/endpoint/ChannelManager.kt b/lib/src/main/java/tech/relaycorp/awaladroid/endpoint/ChannelManager.kt index e1566b9e..d7122998 100644 --- a/lib/src/main/java/tech/relaycorp/awaladroid/endpoint/ChannelManager.kt +++ b/lib/src/main/java/tech/relaycorp/awaladroid/endpoint/ChannelManager.kt @@ -1,35 +1,35 @@ package tech.relaycorp.awaladroid.endpoint import android.content.SharedPreferences -import java.security.PublicKey -import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import tech.relaycorp.relaynet.wrappers.nodeId +import java.security.PublicKey +import kotlin.coroutines.CoroutineContext internal class ChannelManager( internal val coroutineContext: CoroutineContext = Dispatchers.IO, - sharedPreferencesGetter: () -> SharedPreferences + sharedPreferencesGetter: () -> SharedPreferences, ) { internal val sharedPreferences by lazy(sharedPreferencesGetter) suspend fun create( firstPartyEndpoint: FirstPartyEndpoint, - thirdPartyEndpoint: ThirdPartyEndpoint + thirdPartyEndpoint: ThirdPartyEndpoint, ) { create(firstPartyEndpoint, thirdPartyEndpoint.nodeId) } suspend fun create( firstPartyEndpoint: FirstPartyEndpoint, - thirdPartyEndpointPublicKey: PublicKey + thirdPartyEndpointPublicKey: PublicKey, ) { create(firstPartyEndpoint, thirdPartyEndpointPublicKey.nodeId) } private suspend fun create( firstPartyEndpoint: FirstPartyEndpoint, - thirdPartyEndpointNodeId: String + thirdPartyEndpointNodeId: String, ) { withContext(coroutineContext) { val originalValue = @@ -38,7 +38,7 @@ internal class ChannelManager( with(sharedPreferences.edit()) { putStringSet( firstPartyEndpoint.nodeId, - originalValue + mutableListOf(thirdPartyEndpointNodeId) + originalValue + mutableListOf(thirdPartyEndpointNodeId), ) commit() } @@ -57,7 +57,7 @@ internal class ChannelManager( } suspend fun delete( - thirdPartyEndpoint: ThirdPartyEndpoint + thirdPartyEndpoint: ThirdPartyEndpoint, ) { withContext(coroutineContext) { sharedPreferences.all.forEach { (key, value) -> @@ -85,7 +85,7 @@ internal class ChannelManager( withContext(coroutineContext) { return@withContext sharedPreferences.getStringSet( firstPartyEndpoint.nodeId, - emptySet() + emptySet(), ) ?: emptySet() } } diff --git a/lib/src/main/java/tech/relaycorp/awaladroid/endpoint/FirstPartyEndpoint.kt b/lib/src/main/java/tech/relaycorp/awaladroid/endpoint/FirstPartyEndpoint.kt index d58d27f7..840dc776 100644 --- a/lib/src/main/java/tech/relaycorp/awaladroid/endpoint/FirstPartyEndpoint.kt +++ b/lib/src/main/java/tech/relaycorp/awaladroid/endpoint/FirstPartyEndpoint.kt @@ -1,9 +1,5 @@ package tech.relaycorp.awaladroid.endpoint -import java.security.PrivateKey -import java.security.PublicKey -import java.time.ZonedDateTime -import java.util.logging.Level import tech.relaycorp.awaladroid.Awala import tech.relaycorp.awaladroid.AwaladroidException import tech.relaycorp.awaladroid.GatewayProtocolException @@ -24,6 +20,10 @@ import tech.relaycorp.relaynet.wrappers.generateRSAKeyPair import tech.relaycorp.relaynet.wrappers.nodeId import tech.relaycorp.relaynet.wrappers.x509.Certificate import tech.relaycorp.relaynet.wrappers.x509.CertificateException +import java.security.PrivateKey +import java.security.PublicKey +import java.time.ZonedDateTime +import java.util.logging.Level /** * An endpoint owned by the current instance of the app. @@ -51,11 +51,11 @@ internal constructor( @Throws(CertificateException::class) public suspend fun issueAuthorization( thirdPartyEndpoint: ThirdPartyEndpoint, - expiryDate: ZonedDateTime + expiryDate: ZonedDateTime, ): ByteArray = issueAuthorization( thirdPartyEndpoint.identityKey, - expiryDate + expiryDate, ) /** @@ -64,7 +64,7 @@ internal constructor( @Throws(CertificateException::class) public suspend fun issueAuthorization( thirdPartyEndpointPublicKeySerialized: ByteArray, - expiryDate: ZonedDateTime + expiryDate: ZonedDateTime, ): ByteArray { val thirdPartyEndpointPublicKey = deserializePDAGranteePublicKey(thirdPartyEndpointPublicKeySerialized) @@ -74,20 +74,20 @@ internal constructor( @Throws(CertificateException::class) private suspend fun issueAuthorization( thirdPartyEndpointPublicKey: PublicKey, - expiryDate: ZonedDateTime + expiryDate: ZonedDateTime, ): ByteArray { val pda = issueDeliveryAuthorization( subjectPublicKey = thirdPartyEndpointPublicKey, issuerPrivateKey = identityPrivateKey, validityEndDate = expiryDate, - issuerCertificate = identityCertificate + issuerCertificate = identityCertificate, ) val deliveryAuth = CertificationPath(pda, pdaChain) val context = Awala.getContextOrThrow() val sessionKeyPair = context.endpointManager.generateSessionKeyPair( nodeId, - thirdPartyEndpointPublicKey.nodeId + thirdPartyEndpointPublicKey.nodeId, ) val connParams = PrivateEndpointConnParams( @@ -104,7 +104,7 @@ internal constructor( */ @Throws(CertificateException::class) public suspend fun authorizeIndefinitely( - thirdPartyEndpoint: ThirdPartyEndpoint + thirdPartyEndpoint: ThirdPartyEndpoint, ): ByteArray = authorizeIndefinitely(thirdPartyEndpoint.identityKey) @@ -113,7 +113,7 @@ internal constructor( */ @Throws(CertificateException::class) public suspend fun authorizeIndefinitely( - thirdPartyEndpointPublicKeySerialized: ByteArray + thirdPartyEndpointPublicKeySerialized: ByteArray, ): ByteArray { val thirdPartyEndpointPublicKey = deserializePDAGranteePublicKey(thirdPartyEndpointPublicKeySerialized) @@ -134,14 +134,14 @@ internal constructor( } private fun deserializePDAGranteePublicKey( - thirdPartyEndpointPublicKeySerialized: ByteArray + thirdPartyEndpointPublicKeySerialized: ByteArray, ): PublicKey { val thirdPartyEndpointPublicKey = try { thirdPartyEndpointPublicKeySerialized.deserializeRSAPublicKey() } catch (exc: KeyException) { throw AuthorizationIssuanceException( "PDA grantee public key is not a valid RSA public key", - exc + exc, ) } return thirdPartyEndpointPublicKey @@ -164,7 +164,7 @@ internal constructor( identityPrivateKey, registration.privateNodeCertificate, listOf(registration.gatewayCertificate), - registration.gatewayInternetAddress + registration.gatewayInternetAddress, ) val gatewayId = registration.gatewayCertificate.subjectId @@ -189,12 +189,12 @@ internal constructor( for (thirdPartyEndpointAddress in thirdPartyEndpointAddresses) { val thirdPartyEndpoint = ThirdPartyEndpoint.load( this@FirstPartyEndpoint.nodeId, - thirdPartyEndpointAddress + thirdPartyEndpointAddress, ) if (thirdPartyEndpoint == null) { logger.log( Level.INFO, - "Ignoring missing third-party endpoint $thirdPartyEndpointAddress" + "Ignoring missing third-party endpoint $thirdPartyEndpointAddress", ) break } @@ -240,7 +240,7 @@ internal constructor( keyPair.private, registration.privateNodeCertificate, listOf(registration.gatewayCertificate), - registration.gatewayInternetAddress + registration.gatewayInternetAddress, ) try { @@ -258,7 +258,7 @@ internal constructor( registration.privateNodeCertificate, listOf(registration.gatewayCertificate), ), - gatewayId + gatewayId, ) } catch (exc: KeyStoreBackendException) { throw PersistenceException("Failed to save certificate", exc) @@ -291,7 +291,7 @@ internal constructor( ?: throw PersistenceException("Failed to load gateway address for endpoint") val certificatePath = try { context.certificateStore.retrieveLatest( - nodeId, gatewayNodeId + nodeId, gatewayNodeId, ) ?: return null } catch (exc: KeyStoreBackendException) { @@ -300,7 +300,7 @@ internal constructor( val internetAddress: String = context.storage.internetAddress.get() ?: throw PersistenceException( - "Failed to load gateway internet address for endpoint" + "Failed to load gateway internet address for endpoint", ) return FirstPartyEndpoint( diff --git a/lib/src/main/java/tech/relaycorp/awaladroid/endpoint/HandleGatewayCertificateChange.kt b/lib/src/main/java/tech/relaycorp/awaladroid/endpoint/HandleGatewayCertificateChange.kt index c786c01f..9b210e9c 100644 --- a/lib/src/main/java/tech/relaycorp/awaladroid/endpoint/HandleGatewayCertificateChange.kt +++ b/lib/src/main/java/tech/relaycorp/awaladroid/endpoint/HandleGatewayCertificateChange.kt @@ -4,7 +4,7 @@ import tech.relaycorp.relaynet.keystores.PrivateKeyStore import tech.relaycorp.relaynet.wrappers.nodeId internal class HandleGatewayCertificateChange( - private val privateKeyStore: PrivateKeyStore + private val privateKeyStore: PrivateKeyStore, ) { suspend operator fun invoke() { diff --git a/lib/src/main/java/tech/relaycorp/awaladroid/endpoint/PrivateThirdPartyEndpointData.kt b/lib/src/main/java/tech/relaycorp/awaladroid/endpoint/PrivateThirdPartyEndpointData.kt index 9be918f7..43e0ed95 100644 --- a/lib/src/main/java/tech/relaycorp/awaladroid/endpoint/PrivateThirdPartyEndpointData.kt +++ b/lib/src/main/java/tech/relaycorp/awaladroid/endpoint/PrivateThirdPartyEndpointData.kt @@ -1,7 +1,5 @@ package tech.relaycorp.awaladroid.endpoint -import java.nio.ByteBuffer -import java.security.PublicKey import org.bson.BSONException import org.bson.BsonBinary import org.bson.BsonBinaryReader @@ -10,6 +8,8 @@ import org.bson.io.BasicOutputBuffer import tech.relaycorp.awaladroid.storage.persistence.PersistenceException import tech.relaycorp.relaynet.pki.CertificationPath import tech.relaycorp.relaynet.wrappers.deserializeRSAPublicKey +import java.nio.ByteBuffer +import java.security.PublicKey internal data class PrivateThirdPartyEndpointData( val identityKey: PublicKey, @@ -24,7 +24,7 @@ internal data class PrivateThirdPartyEndpointData( w.writeStartDocument() w.writeBinaryData( "identity_key", - BsonBinary(identityKey.encoded) + BsonBinary(identityKey.encoded), ) w.writeBinaryData("pda_path", BsonBinary(pdaPath.serialize())) w.writeString("internet_address", internetGatewayAddress) @@ -58,7 +58,7 @@ internal data class PrivateThirdPartyEndpointData( } catch (exp: BSONException) { throw PersistenceException( "Could not deserialize PrivateThirdPartyEndpoint", - exp + exp, ) } } diff --git a/lib/src/main/java/tech/relaycorp/awaladroid/endpoint/PublicThirdPartyEndpointData.kt b/lib/src/main/java/tech/relaycorp/awaladroid/endpoint/PublicThirdPartyEndpointData.kt index 6850cbb8..48b6f265 100644 --- a/lib/src/main/java/tech/relaycorp/awaladroid/endpoint/PublicThirdPartyEndpointData.kt +++ b/lib/src/main/java/tech/relaycorp/awaladroid/endpoint/PublicThirdPartyEndpointData.kt @@ -1,7 +1,5 @@ package tech.relaycorp.awaladroid.endpoint -import java.nio.ByteBuffer -import java.security.PublicKey import org.bson.BSONException import org.bson.BsonBinary import org.bson.BsonBinaryReader @@ -9,6 +7,8 @@ import org.bson.BsonBinaryWriter import org.bson.io.BasicOutputBuffer import tech.relaycorp.awaladroid.storage.persistence.PersistenceException import tech.relaycorp.relaynet.wrappers.deserializeRSAPublicKey +import java.nio.ByteBuffer +import java.security.PublicKey internal data class PublicThirdPartyEndpointData( val internetAddress: String, @@ -23,7 +23,7 @@ internal data class PublicThirdPartyEndpointData( w.writeString("internet_address", internetAddress) w.writeBinaryData( "identity_key", - BsonBinary(identityKey.encoded) + BsonBinary(identityKey.encoded), ) w.writeEndDocument() } @@ -41,7 +41,7 @@ internal data class PublicThirdPartyEndpointData( r.readStartDocument() PublicThirdPartyEndpointData( r.readString("internet_address"), - r.readBinaryData("identity_key").data.deserializeRSAPublicKey() + r.readBinaryData("identity_key").data.deserializeRSAPublicKey(), ).also { r.readEndDocument() } @@ -49,7 +49,7 @@ internal data class PublicThirdPartyEndpointData( } catch (exp: BSONException) { throw PersistenceException( "Could not deserialize PublicThirdPartyEndpoint", - exp + exp, ) } } diff --git a/lib/src/main/java/tech/relaycorp/awaladroid/endpoint/RenewExpiringCertificates.kt b/lib/src/main/java/tech/relaycorp/awaladroid/endpoint/RenewExpiringCertificates.kt index 51c8c1e2..2e35f620 100644 --- a/lib/src/main/java/tech/relaycorp/awaladroid/endpoint/RenewExpiringCertificates.kt +++ b/lib/src/main/java/tech/relaycorp/awaladroid/endpoint/RenewExpiringCertificates.kt @@ -1,14 +1,14 @@ package tech.relaycorp.awaladroid.endpoint -import java.time.ZonedDateTime -import kotlin.time.Duration.Companion.days import tech.relaycorp.relaynet.keystores.PrivateKeyStore import tech.relaycorp.relaynet.wrappers.nodeId import tech.relaycorp.relaynet.wrappers.x509.Certificate +import java.time.ZonedDateTime +import kotlin.time.Duration.Companion.days internal class RenewExpiringCertificates( private val privateKeyStore: PrivateKeyStore, - private val firstPartyEndpointLoader: suspend (String) -> FirstPartyEndpoint? + private val firstPartyEndpointLoader: suspend (String) -> FirstPartyEndpoint?, ) { suspend operator fun invoke() { diff --git a/lib/src/main/java/tech/relaycorp/awaladroid/endpoint/ThirdPartyEndpoint.kt b/lib/src/main/java/tech/relaycorp/awaladroid/endpoint/ThirdPartyEndpoint.kt index b6f84899..877c665c 100644 --- a/lib/src/main/java/tech/relaycorp/awaladroid/endpoint/ThirdPartyEndpoint.kt +++ b/lib/src/main/java/tech/relaycorp/awaladroid/endpoint/ThirdPartyEndpoint.kt @@ -1,6 +1,5 @@ package tech.relaycorp.awaladroid.endpoint -import java.security.PublicKey import tech.relaycorp.awaladroid.Awala import tech.relaycorp.awaladroid.AwaladroidException import tech.relaycorp.awaladroid.SetupPendingException @@ -13,13 +12,14 @@ import tech.relaycorp.relaynet.messages.Recipient import tech.relaycorp.relaynet.pki.CertificationPathException import tech.relaycorp.relaynet.wrappers.nodeId import tech.relaycorp.relaynet.wrappers.x509.Certificate +import java.security.PublicKey /** * An endpoint owned by a different instance of this app, or a different app in the same service. */ public sealed class ThirdPartyEndpoint( internal val identityKey: PublicKey, - public val internetAddress: String + public val internetAddress: String, ) : Endpoint(identityKey.nodeId) { internal val recipient: Recipient @@ -40,7 +40,7 @@ public sealed class ThirdPartyEndpoint( @Throws(PersistenceException::class) internal suspend fun load( firstPartyAddress: String, - thirdPartyId: String + thirdPartyId: String, ): ThirdPartyEndpoint? = PublicThirdPartyEndpoint.load(thirdPartyId) ?: PrivateThirdPartyEndpoint.load(thirdPartyId, firstPartyAddress) @@ -58,7 +58,7 @@ public class PrivateThirdPartyEndpoint internal constructor( identityKey: PublicKey, internal val pda: Certificate, internal val pdaChain: List, - internetAddress: String + internetAddress: String, ) : ThirdPartyEndpoint(identityKey, internetAddress) { private val storageKey = "${firstPartyEndpointAddress}_$nodeId" @@ -82,14 +82,14 @@ public class PrivateThirdPartyEndpoint internal constructor( val pdaSubjectAddress = deliveryAuth.leafCertificate.subjectId if (pdaSubjectAddress != firstPartyEndpointAddress) { throw InvalidAuthorizationException( - "PDA subject ($pdaSubjectAddress) is not first-party endpoint" + "PDA subject ($pdaSubjectAddress) is not first-party endpoint", ) } val pdaIssuerAddress = deliveryAuth.certificateAuthorities.first().subjectId if (pdaIssuerAddress != nodeId) { throw InvalidAuthorizationException( - "PDA issuer ($pdaIssuerAddress) is not third-party endpoint" + "PDA issuer ($pdaIssuerAddress) is not third-party endpoint", ) } @@ -97,7 +97,7 @@ public class PrivateThirdPartyEndpoint internal constructor( val data = PrivateThirdPartyEndpointData( identityKey, deliveryAuth, - connectionParams.internetGatewayAddress + connectionParams.internetGatewayAddress, ) context.storage.privateThirdParty.set(storageKey, data) } @@ -109,7 +109,7 @@ public class PrivateThirdPartyEndpoint internal constructor( @Throws(PersistenceException::class, SetupPendingException::class) public suspend fun load( thirdPartyAddress: String, - firstPartyAddress: String + firstPartyAddress: String, ): PrivateThirdPartyEndpoint? { val key = "${firstPartyAddress}_$thirdPartyAddress" val storage = Awala.getContextOrThrow().storage @@ -135,7 +135,7 @@ public class PrivateThirdPartyEndpoint internal constructor( SetupPendingException::class, ) public suspend fun import( - connectionParamsSerialized: ByteArray + connectionParamsSerialized: ByteArray, ): PrivateThirdPartyEndpoint { val context = Awala.getContextOrThrow() @@ -153,7 +153,7 @@ public class PrivateThirdPartyEndpoint internal constructor( context.privateKeyStore.retrieveIdentityKey(firstPartyAddress) } catch (exc: MissingKeyException) { throw UnknownFirstPartyEndpointException( - "First-party endpoint $firstPartyAddress is not registered" + "First-party endpoint $firstPartyAddress is not registered", ) } @@ -174,7 +174,7 @@ public class PrivateThirdPartyEndpoint internal constructor( val data = PrivateThirdPartyEndpointData( params.identityKey, pdaPath, - params.internetGatewayAddress + params.internetGatewayAddress, ) context.storage.privateThirdParty.set(endpoint.storageKey, data) @@ -192,7 +192,7 @@ public class PrivateThirdPartyEndpoint internal constructor( */ public class PublicThirdPartyEndpoint internal constructor( internetAddress: String, - identityKey: PublicKey + identityKey: PublicKey, ) : ThirdPartyEndpoint(identityKey, internetAddress) { @Throws(PersistenceException::class, SetupPendingException::class) @@ -225,7 +225,7 @@ public class PublicThirdPartyEndpoint internal constructor( SetupPendingException::class, ) public suspend fun import( - connectionParamsSerialized: ByteArray + connectionParamsSerialized: ByteArray, ): PublicThirdPartyEndpoint { val context = Awala.getContextOrThrow() val connectionParams = try { @@ -241,8 +241,8 @@ public class PublicThirdPartyEndpoint internal constructor( peerNodeId, PublicThirdPartyEndpointData( connectionParams.internetAddress, - connectionParams.identityKey - ) + connectionParams.identityKey, + ), ) context.sessionPublicKeyStore.save( connectionParams.sessionKey, diff --git a/lib/src/main/java/tech/relaycorp/awaladroid/messaging/IncomingMessage.kt b/lib/src/main/java/tech/relaycorp/awaladroid/messaging/IncomingMessage.kt index f09854bc..aed33586 100644 --- a/lib/src/main/java/tech/relaycorp/awaladroid/messaging/IncomingMessage.kt +++ b/lib/src/main/java/tech/relaycorp/awaladroid/messaging/IncomingMessage.kt @@ -1,6 +1,5 @@ package tech.relaycorp.awaladroid.messaging -import java.util.logging.Level import tech.relaycorp.awaladroid.Awala import tech.relaycorp.awaladroid.SetupPendingException import tech.relaycorp.awaladroid.common.Logging.logger @@ -18,6 +17,7 @@ import tech.relaycorp.relaynet.keystores.MissingKeyException import tech.relaycorp.relaynet.messages.InvalidMessageException import tech.relaycorp.relaynet.messages.Parcel import tech.relaycorp.relaynet.wrappers.cms.EnvelopedDataException +import java.util.logging.Level /** * An incoming service message. @@ -33,7 +33,7 @@ public class IncomingMessage internal constructor( public val content: ByteArray, public val senderEndpoint: ThirdPartyEndpoint, public val recipientEndpoint: FirstPartyEndpoint, - public val ack: suspend () -> Unit + public val ack: suspend () -> Unit, ) : Message() { internal companion object { @@ -50,7 +50,7 @@ public class IncomingMessage internal constructor( internal suspend fun build(parcel: Parcel, ack: suspend () -> Unit): IncomingMessage? { val recipientEndpoint = FirstPartyEndpoint.load(parcel.recipient.id) ?: throw UnknownFirstPartyEndpointException( - "Unknown first-party endpoint ${parcel.recipient.id}" + "Unknown first-party endpoint ${parcel.recipient.id}", ) val sender = ThirdPartyEndpoint.load( @@ -59,7 +59,7 @@ public class IncomingMessage internal constructor( ) ?: throw UnknownThirdPartyEndpointException( "Unknown third-party endpoint " + "${parcel.senderCertificate.subjectId} " + - "for first-party endpoint ${parcel.recipient.id}" + "for first-party endpoint ${parcel.recipient.id}", ) val context = Awala.getContextOrThrow() @@ -68,7 +68,7 @@ public class IncomingMessage internal constructor( context.endpointManager.unwrapMessagePayload(parcel) } catch (e: MissingKeyException) { throw UnknownThirdPartyEndpointException( - "Missing third-party endpoint session keys" + "Missing third-party endpoint session keys", ) } if (serviceMessage.type == PDA_PATH_TYPE) { @@ -81,7 +81,7 @@ public class IncomingMessage internal constructor( content = serviceMessage.content, senderEndpoint = sender, recipientEndpoint = recipientEndpoint, - ack = ack + ack = ack, ) } @@ -93,7 +93,7 @@ public class IncomingMessage internal constructor( if (senderEndpoint is PublicThirdPartyEndpoint) { logger.info( "Ignoring connection params from public endpoint ${senderEndpoint.nodeId} " + - "(${senderEndpoint.internetAddress})" + "(${senderEndpoint.internetAddress})", ) return } @@ -122,7 +122,7 @@ public class IncomingMessage internal constructor( } logger.info( "Updated connection params from ${senderEndpoint.nodeId} for " + - recipientEndpoint.nodeId + recipientEndpoint.nodeId, ) } } diff --git a/lib/src/main/java/tech/relaycorp/awaladroid/messaging/OutgoingMessage.kt b/lib/src/main/java/tech/relaycorp/awaladroid/messaging/OutgoingMessage.kt index 097026d4..5dff00ef 100644 --- a/lib/src/main/java/tech/relaycorp/awaladroid/messaging/OutgoingMessage.kt +++ b/lib/src/main/java/tech/relaycorp/awaladroid/messaging/OutgoingMessage.kt @@ -1,7 +1,5 @@ package tech.relaycorp.awaladroid.messaging -import java.time.Duration -import java.time.ZonedDateTime import tech.relaycorp.awaladroid.Awala import tech.relaycorp.awaladroid.endpoint.FirstPartyEndpoint import tech.relaycorp.awaladroid.endpoint.PrivateThirdPartyEndpoint @@ -11,6 +9,8 @@ import tech.relaycorp.relaynet.issueEndpointCertificate import tech.relaycorp.relaynet.messages.Parcel import tech.relaycorp.relaynet.messages.payloads.ServiceMessage import tech.relaycorp.relaynet.wrappers.x509.Certificate +import java.time.Duration +import java.time.ZonedDateTime /** * An outgoing service message. @@ -56,7 +56,7 @@ private constructor( senderEndpoint: FirstPartyEndpoint, recipientEndpoint: ThirdPartyEndpoint, parcelExpiryDate: ZonedDateTime = maxExpiryDate(), - parcelId: ParcelId = ParcelId.generate() + parcelId: ParcelId = ParcelId.generate(), ): OutgoingMessage { val message = OutgoingMessage( senderEndpoint, @@ -72,7 +72,7 @@ private constructor( private suspend fun buildParcel( serviceMessageType: String, - serviceMessageContent: ByteArray + serviceMessageContent: ByteArray, ): Parcel { val serviceMessage = ServiceMessage(serviceMessageType, serviceMessageContent) val endpointManager = Awala.getContextOrThrow().endpointManager @@ -88,7 +88,7 @@ private constructor( messageId = parcelId.value, creationDate = parcelCreationDate, ttl = ttl, - senderCertificateChain = getSenderCertificateChain() + senderCertificateChain = getSenderCertificateChain(), ) } @@ -103,7 +103,7 @@ private constructor( senderEndpoint.identityCertificate.subjectPublicKey, senderEndpoint.identityPrivateKey, validityStartDate = parcelCreationDate, - validityEndDate = parcelExpiryDate + validityEndDate = parcelExpiryDate, ) private fun getSenderCertificateChain(): Set = diff --git a/lib/src/main/java/tech/relaycorp/awaladroid/messaging/ParcelId.kt b/lib/src/main/java/tech/relaycorp/awaladroid/messaging/ParcelId.kt index bb3a6da3..5b122040 100644 --- a/lib/src/main/java/tech/relaycorp/awaladroid/messaging/ParcelId.kt +++ b/lib/src/main/java/tech/relaycorp/awaladroid/messaging/ParcelId.kt @@ -15,7 +15,7 @@ import java.util.UUID */ public class ParcelId internal constructor( - public val value: String + public val value: String, ) { public companion object { /** diff --git a/lib/src/main/java/tech/relaycorp/awaladroid/messaging/ReceiveMessages.kt b/lib/src/main/java/tech/relaycorp/awaladroid/messaging/ReceiveMessages.kt index fff75465..55df5a82 100644 --- a/lib/src/main/java/tech/relaycorp/awaladroid/messaging/ReceiveMessages.kt +++ b/lib/src/main/java/tech/relaycorp/awaladroid/messaging/ReceiveMessages.kt @@ -1,6 +1,5 @@ package tech.relaycorp.awaladroid.messaging -import java.util.logging.Level import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.flatMapLatest @@ -26,15 +25,16 @@ import tech.relaycorp.relaynet.ramf.InvalidPayloadException import tech.relaycorp.relaynet.ramf.RAMFException import tech.relaycorp.relaynet.wrappers.cms.EnvelopedDataException import tech.relaycorp.relaynet.wrappers.nodeId +import java.util.logging.Level internal class ReceiveMessages( - private val pdcClientBuilder: () -> PDCClient = { PoWebClient.initLocal(Awala.POWEB_PORT) } + private val pdcClientBuilder: () -> PDCClient = { PoWebClient.initLocal(Awala.POWEB_PORT) }, ) { @Throws( ReceiveMessageException::class, GatewayProtocolException::class, - PersistenceException::class + PersistenceException::class, ) fun receive(): Flow = getNonceSigners() @@ -66,7 +66,7 @@ internal class ReceiveMessages( ?: return@flatMap emptyList() context.certificateStore.retrieveAll( nodeId, - privateGatewayId + privateGatewayId, ).map { Signer( it.leafCertificate, @@ -102,13 +102,13 @@ internal class ReceiveMessages( } catch (exp: EnvelopedDataException) { parcelCollection.disregard( "Failed to decrypt parcel; sender might have used wrong key", - exp + exp, ) return@mapNotNull null } catch (exp: InvalidPayloadException) { parcelCollection.disregard( "Incoming parcel did not encapsulate a valid service message", - exp + exp, ) return@mapNotNull null } diff --git a/lib/src/main/java/tech/relaycorp/awaladroid/messaging/SendMessage.kt b/lib/src/main/java/tech/relaycorp/awaladroid/messaging/SendMessage.kt index 8e353eb7..4acc09a3 100644 --- a/lib/src/main/java/tech/relaycorp/awaladroid/messaging/SendMessage.kt +++ b/lib/src/main/java/tech/relaycorp/awaladroid/messaging/SendMessage.kt @@ -1,6 +1,5 @@ package tech.relaycorp.awaladroid.messaging -import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import tech.relaycorp.awaladroid.Awala @@ -12,16 +11,17 @@ import tech.relaycorp.relaynet.bindings.pdc.PDCClient import tech.relaycorp.relaynet.bindings.pdc.RejectedParcelException import tech.relaycorp.relaynet.bindings.pdc.ServerException import tech.relaycorp.relaynet.bindings.pdc.Signer +import kotlin.coroutines.CoroutineContext internal class SendMessage( private val pdcClientBuilder: () -> PDCClient = { PoWebClient.initLocal(Awala.POWEB_PORT) }, - private val coroutineContext: CoroutineContext = Dispatchers.IO + private val coroutineContext: CoroutineContext = Dispatchers.IO, ) { @Throws( SendMessageException::class, RejectedMessageException::class, - GatewayProtocolException::class + GatewayProtocolException::class, ) suspend fun send(message: OutgoingMessage) { withContext(coroutineContext) { @@ -33,8 +33,8 @@ internal class SendMessage( message.parcel.serialize(senderPrivateKey), Signer( message.senderEndpoint.identityCertificate, - senderPrivateKey - ) + senderPrivateKey, + ), ) } } catch (e: ServerException) { diff --git a/lib/src/main/java/tech/relaycorp/awaladroid/storage/StorageImpl.kt b/lib/src/main/java/tech/relaycorp/awaladroid/storage/StorageImpl.kt index d37e1f06..c8b2f78a 100644 --- a/lib/src/main/java/tech/relaycorp/awaladroid/storage/StorageImpl.kt +++ b/lib/src/main/java/tech/relaycorp/awaladroid/storage/StorageImpl.kt @@ -1,16 +1,16 @@ package tech.relaycorp.awaladroid.storage import androidx.annotation.VisibleForTesting -import java.nio.charset.Charset import tech.relaycorp.awaladroid.endpoint.PrivateThirdPartyEndpointData import tech.relaycorp.awaladroid.endpoint.PublicThirdPartyEndpointData import tech.relaycorp.awaladroid.storage.persistence.Persistence import tech.relaycorp.awaladroid.storage.persistence.PersistenceException +import java.nio.charset.Charset // TODO: Test internal class StorageImpl constructor( - persistence: Persistence + persistence: Persistence, ) { private val ascii = Charset.forName("ASCII") @@ -18,7 +18,7 @@ constructor( persistence = persistence, prefix = "gateway_id_", serializer = { address: String -> address.toByteArray(ascii) }, - deserializer = { addressSerialized: ByteArray -> addressSerialized.toString(ascii) } + deserializer = { addressSerialized: ByteArray -> addressSerialized.toString(ascii) }, ) internal val internetAddress: SingleModule = SingleModule( @@ -27,21 +27,21 @@ constructor( serializer = { internetAddress: String -> internetAddress.toByteArray(ascii) }, deserializer = { internetAddressSerialized: ByteArray -> internetAddressSerialized.toString(ascii) - } + }, ) internal val publicThirdParty: Module = Module( persistence = persistence, prefix = "public_third_party_", serializer = PublicThirdPartyEndpointData::serialize, - deserializer = PublicThirdPartyEndpointData::deserialize + deserializer = PublicThirdPartyEndpointData::deserialize, ) internal val privateThirdParty: Module = Module( persistence = persistence, prefix = "private_third_party_", serializer = PrivateThirdPartyEndpointData::serialize, - deserializer = PrivateThirdPartyEndpointData::deserialize + deserializer = PrivateThirdPartyEndpointData::deserialize, ) internal open class Module( @@ -49,7 +49,7 @@ constructor( @get:VisibleForTesting internal val prefix: String, private val serializer: (T) -> ByteArray, - private val deserializer: (ByteArray) -> T + private val deserializer: (ByteArray) -> T, ) { @Throws(PersistenceException::class) @@ -79,7 +79,7 @@ constructor( persistence: Persistence, prefix: String, serializer: (T) -> ByteArray, - deserializer: (ByteArray) -> T + deserializer: (ByteArray) -> T, ) : Module(persistence, prefix, serializer, deserializer) { @Throws(PersistenceException::class) diff --git a/lib/src/main/java/tech/relaycorp/awaladroid/storage/persistence/DiskPersistence.kt b/lib/src/main/java/tech/relaycorp/awaladroid/storage/persistence/DiskPersistence.kt index ed094ecf..06236d76 100644 --- a/lib/src/main/java/tech/relaycorp/awaladroid/storage/persistence/DiskPersistence.kt +++ b/lib/src/main/java/tech/relaycorp/awaladroid/storage/persistence/DiskPersistence.kt @@ -1,15 +1,15 @@ package tech.relaycorp.awaladroid.storage.persistence +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import java.io.File import java.io.IOException import kotlin.coroutines.CoroutineContext -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext internal class DiskPersistence( private val fileDir: String, private val coroutineContext: CoroutineContext = Dispatchers.IO, - private val rootFolder: String = "awaladroid" + private val rootFolder: String = "awaladroid", ) : Persistence { @Suppress("BlockingMethodInNonBlockingContext") diff --git a/lib/src/test/java/tech/relaycorp/awaladroid/AndroidPrivateKeyStoreTest.kt b/lib/src/test/java/tech/relaycorp/awaladroid/AndroidPrivateKeyStoreTest.kt index 4d5552d3..6fe6d06c 100644 --- a/lib/src/test/java/tech/relaycorp/awaladroid/AndroidPrivateKeyStoreTest.kt +++ b/lib/src/test/java/tech/relaycorp/awaladroid/AndroidPrivateKeyStoreTest.kt @@ -1,6 +1,5 @@ package tech.relaycorp.awaladroid -import java.io.File import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Before @@ -12,6 +11,7 @@ import tech.relaycorp.awala.keystores.file.FileKeystoreRoot import tech.relaycorp.awaladroid.test.FakeAndroidKeyStore import tech.relaycorp.relaynet.testing.pki.KeyPairSet import tech.relaycorp.relaynet.testing.pki.PDACertPath +import java.io.File @RunWith(RobolectricTestRunner::class) public class AndroidPrivateKeyStoreTest { diff --git a/lib/src/test/java/tech/relaycorp/awaladroid/AwalaTest.kt b/lib/src/test/java/tech/relaycorp/awaladroid/AwalaTest.kt index 8aa2c8c4..70c931b2 100644 --- a/lib/src/test/java/tech/relaycorp/awaladroid/AwalaTest.kt +++ b/lib/src/test/java/tech/relaycorp/awaladroid/AwalaTest.kt @@ -3,9 +3,6 @@ package tech.relaycorp.awaladroid import android.content.Context import com.nhaarman.mockitokotlin2.spy import com.nhaarman.mockitokotlin2.verify -import java.io.File -import java.time.Duration -import java.time.ZonedDateTime import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest @@ -26,6 +23,9 @@ import tech.relaycorp.relaynet.issueEndpointCertificate import tech.relaycorp.relaynet.pki.CertificationPath import tech.relaycorp.relaynet.testing.pki.KeyPairSet import tech.relaycorp.relaynet.wrappers.nodeId +import java.io.File +import java.time.Duration +import java.time.ZonedDateTime @RunWith(RobolectricTestRunner::class) public class AwalaTest { @@ -106,7 +106,7 @@ public class AwalaTest { certificateStore.retrieveLatest( expiringCertificate.subjectId, expiringCertificate.issuerCommonName, - ) + ), ) // Retry until expiration @@ -116,7 +116,7 @@ public class AwalaTest { advanceUntilIdle() certificateStore.retrieveLatest( KeyPairSet.PRIVATE_ENDPOINT.public.nodeId, - KeyPairSet.PRIVATE_GW.private.nodeId + KeyPairSet.PRIVATE_GW.private.nodeId, ) ?: return@runTest } throw AssertionError("Expired certificate not deleted") diff --git a/lib/src/test/java/tech/relaycorp/awaladroid/GatewayClientImplTest.kt b/lib/src/test/java/tech/relaycorp/awaladroid/GatewayClientImplTest.kt index c983b012..c93e9fd0 100644 --- a/lib/src/test/java/tech/relaycorp/awaladroid/GatewayClientImplTest.kt +++ b/lib/src/test/java/tech/relaycorp/awaladroid/GatewayClientImplTest.kt @@ -8,7 +8,6 @@ import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.times import com.nhaarman.mockitokotlin2.verify import com.nhaarman.mockitokotlin2.whenever -import java.time.ZonedDateTime import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.flow @@ -40,6 +39,7 @@ import tech.relaycorp.relaynet.testing.pdc.MockPDCClient import tech.relaycorp.relaynet.testing.pdc.RegisterNodeCall import tech.relaycorp.relaynet.testing.pki.KeyPairSet import tech.relaycorp.relaynet.testing.pki.PDACertPath +import java.time.ZonedDateTime @RunWith(RobolectricTestRunner::class) internal class GatewayClientImplTest : MockContextTestCase() { @@ -51,8 +51,11 @@ internal class GatewayClientImplTest : MockContextTestCase() { private val receiveMessages = mock() override val gatewayClient = GatewayClientImpl( - coroutineScope.coroutineContext, { serviceInteractor }, { pdcClient }, sendMessage, - receiveMessages + coroutineScope.coroutineContext, + { serviceInteractor }, + { pdcClient }, + sendMessage, + receiveMessages, ) // Binding @@ -64,7 +67,7 @@ internal class GatewayClientImplTest : MockContextTestCase() { verify(serviceInteractor).bind( Awala.GATEWAY_SYNC_ACTION, Awala.GATEWAY_PACKAGE, - Awala.GATEWAY_SYNC_COMPONENT + Awala.GATEWAY_SYNC_COMPONENT, ) } @@ -113,7 +116,7 @@ internal class GatewayClientImplTest : MockContextTestCase() { .bind( Awala.GATEWAY_PRE_REGISTER_ACTION, Awala.GATEWAY_PACKAGE, - Awala.GATEWAY_PRE_REGISTER_COMPONENT + Awala.GATEWAY_PRE_REGISTER_COMPONENT, ) verify(serviceInteractor) .bind(Awala.GATEWAY_SYNC_ACTION, Awala.GATEWAY_PACKAGE, Awala.GATEWAY_SYNC_COMPONENT) @@ -167,7 +170,7 @@ internal class GatewayClientImplTest : MockContextTestCase() { private fun buildPnra() = PrivateNodeRegistrationAuthorization( ZonedDateTime.now().plusDays(1), - PDACertPath.PRIVATE_GW.serialize() + PDACertPath.PRIVATE_GW.serialize(), ) private fun buildAuthorizationReplyMessage(): Message { @@ -237,7 +240,7 @@ internal class GatewayClientImplTest : MockContextTestCase() { .bind( eq(Awala.GATEWAY_SYNC_ACTION), eq(Awala.GATEWAY_PACKAGE), - eq(Awala.GATEWAY_SYNC_COMPONENT) + eq(Awala.GATEWAY_SYNC_COMPONENT), ) verify(serviceInteractor) .unbind() diff --git a/lib/src/test/java/tech/relaycorp/awaladroid/endpoint/ChannelManagerTest.kt b/lib/src/test/java/tech/relaycorp/awaladroid/endpoint/ChannelManagerTest.kt index 5cefed01..e1db3154 100644 --- a/lib/src/test/java/tech/relaycorp/awaladroid/endpoint/ChannelManagerTest.kt +++ b/lib/src/test/java/tech/relaycorp/awaladroid/endpoint/ChannelManagerTest.kt @@ -17,7 +17,7 @@ internal class ChannelManagerTest { private val androidContext = RuntimeEnvironment.getApplication() private val sharedPreferences = androidContext.getSharedPreferences( "channel-test", - Context.MODE_PRIVATE + Context.MODE_PRIVATE, ) private val firstPartyEndpoint = FirstPartyEndpointFactory.build() @@ -42,7 +42,7 @@ internal class ChannelManagerTest { fun create_non_existing() = runTest { assertEquals( null, - sharedPreferences.getStringSet(firstPartyEndpoint.nodeId, null) + sharedPreferences.getStringSet(firstPartyEndpoint.nodeId, null), ) val manager = ChannelManager(coroutineContext) { sharedPreferences } @@ -50,7 +50,7 @@ internal class ChannelManagerTest { assertEquals( setOf(thirdPartyEndpoint.nodeId), - sharedPreferences.getStringSet(firstPartyEndpoint.nodeId, null) + sharedPreferences.getStringSet(firstPartyEndpoint.nodeId, null), ) } @@ -63,7 +63,7 @@ internal class ChannelManagerTest { assertEquals( setOf(thirdPartyEndpoint.nodeId), - sharedPreferences.getStringSet(firstPartyEndpoint.nodeId, null) + sharedPreferences.getStringSet(firstPartyEndpoint.nodeId, null), ) } @@ -76,7 +76,7 @@ internal class ChannelManagerTest { assertEquals( setOf(thirdPartyEndpoint.nodeId), - sharedPreferences.getStringSet(firstPartyEndpoint.nodeId, null) + sharedPreferences.getStringSet(firstPartyEndpoint.nodeId, null), ) } @@ -88,7 +88,7 @@ internal class ChannelManagerTest { assertEquals( null, - sharedPreferences.getStringSet(firstPartyEndpoint.nodeId, null) + sharedPreferences.getStringSet(firstPartyEndpoint.nodeId, null), ) } @@ -101,7 +101,7 @@ internal class ChannelManagerTest { assertEquals( null, - sharedPreferences.getStringSet(firstPartyEndpoint.nodeId, null) + sharedPreferences.getStringSet(firstPartyEndpoint.nodeId, null), ) } @@ -112,7 +112,7 @@ internal class ChannelManagerTest { with(sharedPreferences.edit()) { putStringSet( firstPartyEndpoint.nodeId, - mutableSetOf(unrelatedThirdPartyEndpointAddress) + mutableSetOf(unrelatedThirdPartyEndpointAddress), ) apply() } @@ -121,7 +121,7 @@ internal class ChannelManagerTest { assertEquals( mutableSetOf(unrelatedThirdPartyEndpointAddress), - sharedPreferences.getStringSet(firstPartyEndpoint.nodeId, null) + sharedPreferences.getStringSet(firstPartyEndpoint.nodeId, null), ) } @@ -132,7 +132,7 @@ internal class ChannelManagerTest { with(sharedPreferences.edit()) { putStringSet( firstPartyEndpoint.nodeId, - mutableSetOf(unrelatedThirdPartyEndpointAddress, thirdPartyEndpoint.nodeId) + mutableSetOf(unrelatedThirdPartyEndpointAddress, thirdPartyEndpoint.nodeId), ) apply() } @@ -141,7 +141,7 @@ internal class ChannelManagerTest { assertEquals( setOf(unrelatedThirdPartyEndpointAddress), - sharedPreferences.getStringSet(firstPartyEndpoint.nodeId, null) + sharedPreferences.getStringSet(firstPartyEndpoint.nodeId, null), ) } @@ -152,7 +152,7 @@ internal class ChannelManagerTest { with(sharedPreferences.edit()) { putString( firstPartyEndpoint.nodeId, - malformedValue + malformedValue, ) apply() } @@ -161,7 +161,7 @@ internal class ChannelManagerTest { assertEquals( malformedValue, - sharedPreferences.getString(firstPartyEndpoint.nodeId, null) + sharedPreferences.getString(firstPartyEndpoint.nodeId, null), ) } @@ -172,7 +172,7 @@ internal class ChannelManagerTest { with(sharedPreferences.edit()) { putInt( firstPartyEndpoint.nodeId, - malformedValue + malformedValue, ) apply() } @@ -181,7 +181,7 @@ internal class ChannelManagerTest { assertEquals( malformedValue, - sharedPreferences.getInt(firstPartyEndpoint.nodeId, 0) + sharedPreferences.getInt(firstPartyEndpoint.nodeId, 0), ) } diff --git a/lib/src/test/java/tech/relaycorp/awaladroid/endpoint/FirstPartyEndpointTest.kt b/lib/src/test/java/tech/relaycorp/awaladroid/endpoint/FirstPartyEndpointTest.kt index ad367e7f..f83ca4b3 100644 --- a/lib/src/test/java/tech/relaycorp/awaladroid/endpoint/FirstPartyEndpointTest.kt +++ b/lib/src/test/java/tech/relaycorp/awaladroid/endpoint/FirstPartyEndpointTest.kt @@ -9,8 +9,6 @@ import com.nhaarman.mockitokotlin2.times import com.nhaarman.mockitokotlin2.verify import com.nhaarman.mockitokotlin2.verifyZeroInteractions import com.nhaarman.mockitokotlin2.whenever -import java.security.PublicKey -import java.time.ZonedDateTime import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest import nl.altindag.log.LogCaptor @@ -41,6 +39,8 @@ import tech.relaycorp.relaynet.testing.keystores.MockPrivateKeyStore import tech.relaycorp.relaynet.testing.pki.KeyPairSet import tech.relaycorp.relaynet.testing.pki.PDACertPath import tech.relaycorp.relaynet.wrappers.nodeId +import java.security.PublicKey +import java.time.ZonedDateTime internal class FirstPartyEndpointTest : MockContextTestCase() { @Test @@ -70,8 +70,8 @@ internal class FirstPartyEndpointTest : MockContextTestCase() { PrivateNodeRegistration( PDACertPath.PRIVATE_ENDPOINT, PDACertPath.PRIVATE_GW, - internetGatewayAddress - ) + internetGatewayAddress, + ), ) val endpoint = FirstPartyEndpoint.register() @@ -81,12 +81,12 @@ internal class FirstPartyEndpointTest : MockContextTestCase() { assertEquals(endpoint.identityPrivateKey, identityPrivateKey) val identityCertificatePath = certificateStore.retrieveLatest( endpoint.identityCertificate.subjectId, - PDACertPath.PRIVATE_GW.subjectId + PDACertPath.PRIVATE_GW.subjectId, ) assertEquals(PDACertPath.PRIVATE_ENDPOINT, identityCertificatePath!!.leafCertificate) verify(storage.gatewayId).set( endpoint.nodeId, - PDACertPath.PRIVATE_GW.subjectId + PDACertPath.PRIVATE_GW.subjectId, ) verify(storage.internetAddress).set(internetGatewayAddress) } @@ -103,15 +103,15 @@ internal class FirstPartyEndpointTest : MockContextTestCase() { PrivateNodeRegistration( newCertificate, PDACertPath.PRIVATE_GW, - "" - ) + "", + ), ) endpoint.reRegister() val identityCertificatePath = certificateStore.retrieveLatest( endpoint.identityPrivateKey.nodeId, - PDACertPath.PRIVATE_GW.subjectId + PDACertPath.PRIVATE_GW.subjectId, ) assertEquals(newCertificate, identityCertificatePath!!.leafCertificate) } @@ -142,14 +142,14 @@ internal class FirstPartyEndpointTest : MockContextTestCase() { PrivateNodeRegistration( PDACertPath.PRIVATE_ENDPOINT, PDACertPath.PRIVATE_GW, - "" - ) + "", + ), ) val savingException = Exception("Oh noes") setAwalaContext( Awala.getContextOrThrow().copy( - privateKeyStore = MockPrivateKeyStore(savingException = savingException) - ) + privateKeyStore = MockPrivateKeyStore(savingException = savingException), + ), ) val exception = assertThrows(PersistenceException::class.java) { @@ -169,14 +169,14 @@ internal class FirstPartyEndpointTest : MockContextTestCase() { PrivateNodeRegistration( PDACertPath.PRIVATE_ENDPOINT, PDACertPath.PRIVATE_GW, - "" - ) + "", + ), ) val savingException = Exception("Oh noes") setAwalaContext( Awala.getContextOrThrow().copy( - certificateStore = MockCertificateStore(savingException = savingException) - ) + certificateStore = MockCertificateStore(savingException = savingException), + ), ) val exception = assertThrows(PersistenceException::class.java) { @@ -216,8 +216,8 @@ internal class FirstPartyEndpointTest : MockContextTestCase() { fun load_withKeystoreError(): Unit = runTest { setAwalaContext( Awala.getContextOrThrow().copy( - privateKeyStore = MockPrivateKeyStore(retrievalException = Exception("Oh noes")) - ) + privateKeyStore = MockPrivateKeyStore(retrievalException = Exception("Oh noes")), + ), ) whenever(storage.gatewayId.get()) .thenReturn(PDACertPath.PRIVATE_GW.subjectId) @@ -268,8 +268,8 @@ internal class FirstPartyEndpointTest : MockContextTestCase() { val retrievalException = Exception("Oh noes") setAwalaContext( Awala.getContextOrThrow().copy( - certificateStore = MockCertificateStore(retrievalException = retrievalException) - ) + certificateStore = MockCertificateStore(retrievalException = retrievalException), + ), ) val exception = assertThrows(PersistenceException::class.java) { @@ -300,7 +300,7 @@ internal class FirstPartyEndpointTest : MockContextTestCase() { val authorization = firstPartyEndpoint.issueAuthorization( KeyPairSet.PDA_GRANTEE.public.encoded, - expiryDate + expiryDate, ) validateAuthorization(authorization, firstPartyEndpoint, expiryDate) @@ -315,7 +315,7 @@ internal class FirstPartyEndpointTest : MockContextTestCase() { runBlocking { firstPartyEndpoint.issueAuthorization( "This is not a key".toByteArray(), - expiryDate + expiryDate, ) } } @@ -349,7 +349,7 @@ internal class FirstPartyEndpointTest : MockContextTestCase() { eq(firstPartyEndpoint), argThat { encoded.asList() == KeyPairSet.PDA_GRANTEE.public.encoded.asList() - } + }, ) } @@ -360,7 +360,7 @@ internal class FirstPartyEndpointTest : MockContextTestCase() { val exception = assertThrows(AuthorizationIssuanceException::class.java) { runBlocking { firstPartyEndpoint.authorizeIndefinitely( - "This is not a key".toByteArray() + "This is not a key".toByteArray(), ) } } @@ -392,7 +392,7 @@ internal class FirstPartyEndpointTest : MockContextTestCase() { verify(gatewayClient, never()).sendMessage(any()) assertTrue( - logCaptor.infoLogs.contains("Ignoring missing third-party endpoint $missingAddress") + logCaptor.infoLogs.contains("Ignoring missing third-party endpoint $missingAddress"), ) } @@ -411,7 +411,7 @@ internal class FirstPartyEndpointTest : MockContextTestCase() { assertEquals(firstPartyEndpoint, outgoingMessage.senderEndpoint) assertEquals( channel.thirdPartyEndpoint.nodeId, - outgoingMessage.recipientEndpoint.nodeId + outgoingMessage.recipientEndpoint.nodeId, ) // Verify the PDA val (serviceMessage) = @@ -422,7 +422,7 @@ internal class FirstPartyEndpointTest : MockContextTestCase() { pdaPath.validate() assertEquals( channel.thirdPartyEndpoint.identityKey, - pdaPath.leafCertificate.subjectPublicKey + pdaPath.leafCertificate.subjectPublicKey, ) assertEquals(firstPartyEndpoint.pdaChain, pdaPath.certificateAuthorities) assertEquals(pdaPath.leafCertificate.expiryDate, outgoingMessage.parcelExpiryDate) @@ -445,18 +445,18 @@ internal class FirstPartyEndpointTest : MockContextTestCase() { private fun validateAuthorization( paramsSerialized: ByteArray, firstPartyEndpoint: FirstPartyEndpoint, - expiryDate: ZonedDateTime + expiryDate: ZonedDateTime, ) { val params = PrivateEndpointConnParams.deserialize(paramsSerialized) assertEquals( firstPartyEndpoint.publicKey, - params.identityKey + params.identityKey, ) assertEquals( firstPartyEndpoint.internetAddress, - params.internetGatewayAddress + params.internetGatewayAddress, ) val authorization = params.deliveryAuth @@ -464,15 +464,15 @@ private fun validateAuthorization( val pda = authorization.leafCertificate assertEquals( KeyPairSet.PDA_GRANTEE.public.encoded.asList(), - pda.subjectPublicKey.encoded.asList() + pda.subjectPublicKey.encoded.asList(), ) assertEquals( 2, - pda.getCertificationPath(emptyList(), listOf(PDACertPath.PRIVATE_ENDPOINT)).size + pda.getCertificationPath(emptyList(), listOf(PDACertPath.PRIVATE_ENDPOINT)).size, ) assertSameDateTime( expiryDate, - pda.expiryDate + pda.expiryDate, ) // PDA chain diff --git a/lib/src/test/java/tech/relaycorp/awaladroid/endpoint/PrivateThirdPartyEndpointTest.kt b/lib/src/test/java/tech/relaycorp/awaladroid/endpoint/PrivateThirdPartyEndpointTest.kt index fb8b3b9b..e065d49d 100644 --- a/lib/src/test/java/tech/relaycorp/awaladroid/endpoint/PrivateThirdPartyEndpointTest.kt +++ b/lib/src/test/java/tech/relaycorp/awaladroid/endpoint/PrivateThirdPartyEndpointTest.kt @@ -5,8 +5,6 @@ import com.nhaarman.mockitokotlin2.argThat import com.nhaarman.mockitokotlin2.eq import com.nhaarman.mockitokotlin2.verify import com.nhaarman.mockitokotlin2.whenever -import java.time.ZonedDateTime -import java.util.UUID import kotlinx.coroutines.test.runTest import org.junit.Assert.assertArrayEquals import org.junit.Assert.assertEquals @@ -27,6 +25,8 @@ import tech.relaycorp.relaynet.testing.pki.PDACertPath import tech.relaycorp.relaynet.wrappers.generateRSAKeyPair import tech.relaycorp.relaynet.wrappers.nodeId import tech.relaycorp.relaynet.wrappers.x509.CertificateException +import java.time.ZonedDateTime +import java.util.UUID internal class PrivateThirdPartyEndpointTest : MockContextTestCase() { private val thirdPartyEndpointCertificate = issueEndpointCertificate( @@ -68,10 +68,10 @@ internal class PrivateThirdPartyEndpointTest : MockContextTestCase() { KeyPairSet.PRIVATE_ENDPOINT.public, CertificationPath( PDACertPath.PDA, - listOf(PDACertPath.PRIVATE_ENDPOINT, PDACertPath.PRIVATE_GW) + listOf(PDACertPath.PRIVATE_ENDPOINT, PDACertPath.PRIVATE_GW), ), internetGatewayAddress, - ) + ), ) val firstAddress = UUID.randomUUID().toString() val thirdAddress = UUID.randomUUID().toString() @@ -94,8 +94,8 @@ internal class PrivateThirdPartyEndpointTest : MockContextTestCase() { assertNull( PrivateThirdPartyEndpoint.load( UUID.randomUUID().toString(), - UUID.randomUUID().toString() - ) + UUID.randomUUID().toString(), + ), ) } @@ -105,27 +105,27 @@ internal class PrivateThirdPartyEndpointTest : MockContextTestCase() { val deliveryAuth = CertificationPath( pda, - listOf(thirdPartyEndpointCertificate) + listOf(thirdPartyEndpointCertificate), ) val paramsSerialized = serializeConnectionParams(deliveryAuth) val endpoint = PrivateThirdPartyEndpoint.import(paramsSerialized) assertEquals( firstPartyEndpoint.nodeId, - endpoint.firstPartyEndpointAddress + endpoint.firstPartyEndpointAddress, ) assertEquals( KeyPairSet.PDA_GRANTEE.public.nodeId, - endpoint.nodeId + endpoint.nodeId, ) assertEquals( KeyPairSet.PDA_GRANTEE.public, - endpoint.identityKey + endpoint.identityKey, ) assertEquals(pda, endpoint.pda) assertArrayEquals( arrayOf(thirdPartyEndpointCertificate), - endpoint.pdaChain.toTypedArray() + endpoint.pdaChain.toTypedArray(), ) verify(storage.privateThirdParty).set( @@ -135,7 +135,7 @@ internal class PrivateThirdPartyEndpointTest : MockContextTestCase() { this.pdaPath.leafCertificate == pda && this.pdaPath.certificateAuthorities == deliveryAuth.certificateAuthorities && this.internetGatewayAddress == internetGatewayAddress - } + }, ) assertEquals(sessionKey, sessionPublicKeystore.retrieve(endpoint.nodeId)) @@ -151,7 +151,7 @@ internal class PrivateThirdPartyEndpointTest : MockContextTestCase() { } catch (exception: UnknownFirstPartyEndpointException) { assertEquals( "First-party endpoint ${firstPartyCert.subjectId} is not registered", - exception.message + exception.message, ) return@runTest } @@ -167,19 +167,19 @@ internal class PrivateThirdPartyEndpointTest : MockContextTestCase() { val unrelatedCertificate = issueEndpointCertificate( unrelatedKeyPair.public, unrelatedKeyPair.private, - ZonedDateTime.now().plusDays(1) + ZonedDateTime.now().plusDays(1), ) val invalidPDA = issueDeliveryAuthorization( subjectPublicKey = firstPartyEndpoint.identityCertificate.subjectPublicKey, issuerPrivateKey = unrelatedKeyPair.private, validityEndDate = ZonedDateTime.now().plusDays(1), - issuerCertificate = unrelatedCertificate + issuerCertificate = unrelatedCertificate, ) val pdaPath = CertificationPath( invalidPDA, - listOf(thirdPartyEndpointCertificate) + listOf(thirdPartyEndpointCertificate), ) val paramsSerialized = serializeConnectionParams(pdaPath) try { @@ -235,7 +235,7 @@ internal class PrivateThirdPartyEndpointTest : MockContextTestCase() { KeyPairSet.PDA_GRANTEE.private, now.minusSeconds(1), thirdPartyEndpointCertificate, - now.minusSeconds(2) + now.minusSeconds(2), ) val pdaPath = CertificationPath(expiredPDA, listOf(thirdPartyEndpointCertificate)) @@ -257,7 +257,7 @@ internal class PrivateThirdPartyEndpointTest : MockContextTestCase() { val identityKey = KeyPairSet.PRIVATE_ENDPOINT.public val pdaPath = CertificationPath( pda, - listOf(PDACertPath.PRIVATE_GW, PDACertPath.INTERNET_GW) + listOf(PDACertPath.PRIVATE_GW, PDACertPath.INTERNET_GW), ) val dataSerialized = PrivateThirdPartyEndpointData( identityKey, @@ -270,7 +270,7 @@ internal class PrivateThirdPartyEndpointTest : MockContextTestCase() { assertEquals(pda, data.pdaPath.leafCertificate) assertEquals( listOf(PDACertPath.PRIVATE_GW, PDACertPath.INTERNET_GW), - data.pdaPath.certificateAuthorities + data.pdaPath.certificateAuthorities, ) assertEquals(internetGatewayAddress, data.internetGatewayAddress) } @@ -363,7 +363,7 @@ internal class PrivateThirdPartyEndpointTest : MockContextTestCase() { KeyPairSet.PDA_GRANTEE.public, deliveryAuth, thirdPartyEndpoint.internetAddress, - ) + ), ) } @@ -392,7 +392,7 @@ internal class PrivateThirdPartyEndpointTest : MockContextTestCase() { private fun makeConnectionParams( thirdPartyEndpoint: PrivateThirdPartyEndpoint, - deliveryAuth: CertificationPath + deliveryAuth: CertificationPath, ) = PrivateEndpointConnParams( thirdPartyEndpoint.identityKey, thirdPartyEndpoint.internetAddress, diff --git a/lib/src/test/java/tech/relaycorp/awaladroid/endpoint/PublicThirdPartyEndpointTest.kt b/lib/src/test/java/tech/relaycorp/awaladroid/endpoint/PublicThirdPartyEndpointTest.kt index c8a20864..47c6b300 100644 --- a/lib/src/test/java/tech/relaycorp/awaladroid/endpoint/PublicThirdPartyEndpointTest.kt +++ b/lib/src/test/java/tech/relaycorp/awaladroid/endpoint/PublicThirdPartyEndpointTest.kt @@ -3,7 +3,6 @@ package tech.relaycorp.awaladroid.endpoint import com.nhaarman.mockitokotlin2.any import com.nhaarman.mockitokotlin2.verify import com.nhaarman.mockitokotlin2.whenever -import java.util.UUID import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Assert.assertNull @@ -16,6 +15,7 @@ import tech.relaycorp.relaynet.SessionKeyPair import tech.relaycorp.relaynet.testing.pki.KeyPairSet import tech.relaycorp.relaynet.testing.pki.PDACertPath import tech.relaycorp.relaynet.wrappers.nodeId +import java.util.UUID internal class PublicThirdPartyEndpointTest : MockContextTestCase() { private val internetAddress = "example.org" @@ -50,8 +50,8 @@ internal class PublicThirdPartyEndpointTest : MockContextTestCase() { .thenReturn( PublicThirdPartyEndpointData( internetAddress, - KeyPairSet.PDA_GRANTEE.public - ) + KeyPairSet.PDA_GRANTEE.public, + ), ) val endpoint = PublicThirdPartyEndpoint.load(id)!! @@ -71,7 +71,7 @@ internal class PublicThirdPartyEndpointTest : MockContextTestCase() { val connectionParams = NodeConnectionParams( internetAddress, KeyPairSet.PDA_GRANTEE.public, - SessionKeyPair.generate().sessionKey + SessionKeyPair.generate().sessionKey, ) val thirdPartyEndpoint = PublicThirdPartyEndpoint.import(connectionParams.serialize()) @@ -82,8 +82,8 @@ internal class PublicThirdPartyEndpointTest : MockContextTestCase() { PDACertPath.PDA.subjectId, PublicThirdPartyEndpointData( connectionParams.internetAddress, - connectionParams.identityKey - ) + connectionParams.identityKey, + ), ) sessionPublicKeystore.retrieve(thirdPartyEndpoint.nodeId) } @@ -92,7 +92,7 @@ internal class PublicThirdPartyEndpointTest : MockContextTestCase() { fun import_invalidConnectionParams() = runTest { try { PublicThirdPartyEndpoint.import( - "malformed".toByteArray() + "malformed".toByteArray(), ) } catch (exception: InvalidThirdPartyEndpoint) { assertEquals("Connection params serialization is malformed", exception.message) @@ -123,7 +123,7 @@ internal class PublicThirdPartyEndpointTest : MockContextTestCase() { ownSessionKeyPair.privateKey, ownSessionKeyPair.sessionKey.keyId, firstPartyEndpoint.nodeId, - thirdPartyEndpoint.nodeId + thirdPartyEndpoint.nodeId, ) val peerSessionKey = SessionKeyPair.generate().sessionKey sessionPublicKeystore.save(peerSessionKey, thirdPartyEndpoint.nodeId) diff --git a/lib/src/test/java/tech/relaycorp/awaladroid/endpoint/RenewExpiringCertificatesTest.kt b/lib/src/test/java/tech/relaycorp/awaladroid/endpoint/RenewExpiringCertificatesTest.kt index 936be4f4..3e3d4afb 100644 --- a/lib/src/test/java/tech/relaycorp/awaladroid/endpoint/RenewExpiringCertificatesTest.kt +++ b/lib/src/test/java/tech/relaycorp/awaladroid/endpoint/RenewExpiringCertificatesTest.kt @@ -4,13 +4,13 @@ import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.never import com.nhaarman.mockitokotlin2.verify import com.nhaarman.mockitokotlin2.whenever -import java.time.ZonedDateTime import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import tech.relaycorp.relaynet.issueEndpointCertificate import tech.relaycorp.relaynet.keystores.PrivateKeyStore import tech.relaycorp.relaynet.testing.pki.KeyPairSet +import java.time.ZonedDateTime internal class RenewExpiringCertificatesTest() { @@ -47,7 +47,7 @@ internal class RenewExpiringCertificatesTest() { val expiringCert = issueEndpointCertificate( KeyPairSet.PRIVATE_ENDPOINT.public, KeyPairSet.PRIVATE_GW.private, - certExpiryDate + certExpiryDate, ) whenever(firstPartyEndpoint.identityCertificate).thenReturn(expiringCert) return firstPartyEndpoint diff --git a/lib/src/test/java/tech/relaycorp/awaladroid/messaging/IncomingMessageTest.kt b/lib/src/test/java/tech/relaycorp/awaladroid/messaging/IncomingMessageTest.kt index 30a54fca..134610a0 100644 --- a/lib/src/test/java/tech/relaycorp/awaladroid/messaging/IncomingMessageTest.kt +++ b/lib/src/test/java/tech/relaycorp/awaladroid/messaging/IncomingMessageTest.kt @@ -5,7 +5,6 @@ import com.nhaarman.mockitokotlin2.argThat import com.nhaarman.mockitokotlin2.eq import com.nhaarman.mockitokotlin2.never import com.nhaarman.mockitokotlin2.verify -import java.time.ZonedDateTime import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest import nl.altindag.log.LogCaptor @@ -36,6 +35,7 @@ import tech.relaycorp.relaynet.testing.keystores.MockPrivateKeyStore import tech.relaycorp.relaynet.testing.keystores.MockSessionPublicKeyStore import tech.relaycorp.relaynet.testing.pki.KeyPairSet import tech.relaycorp.relaynet.testing.pki.PDACertPath +import java.time.ZonedDateTime internal class IncomingMessageTest : MockContextTestCase() { private val thirdPartyEndpointCertificate = issueEndpointCertificate( @@ -56,14 +56,14 @@ internal class IncomingMessageTest : MockContextTestCase() { val parcel = Parcel( recipient = Recipient( channel.firstPartyEndpoint.nodeId, - channel.firstPartyEndpoint.nodeId + channel.firstPartyEndpoint.nodeId, ), payload = thirdPartyEndpointManager.wrapMessagePayload( serviceMessage, channel.firstPartyEndpoint.nodeId, - channel.thirdPartyEndpoint.nodeId + channel.thirdPartyEndpoint.nodeId, ), - senderCertificate = PDACertPath.PDA + senderCertificate = PDACertPath.PDA, ) val message = IncomingMessage.build(parcel) {} @@ -130,8 +130,8 @@ internal class IncomingMessageTest : MockContextTestCase() { assertTrue( logCaptor.infoLogs.contains( "Ignoring connection params from public endpoint ${thirdPartyEndpoint.nodeId} " + - "(${thirdPartyEndpoint.internetAddress})" - ) + "(${thirdPartyEndpoint.internetAddress})", + ), ) } @@ -153,8 +153,8 @@ internal class IncomingMessageTest : MockContextTestCase() { assertTrue( logCaptor.infoLogs.contains( "Ignoring malformed connection params for ${channel.firstPartyEndpoint.nodeId} " + - "from ${channel.thirdPartyEndpoint.nodeId}" - ) + "from ${channel.thirdPartyEndpoint.nodeId}", + ), ) } @@ -186,8 +186,8 @@ internal class IncomingMessageTest : MockContextTestCase() { assertTrue( logCaptor.infoLogs.contains( "Ignoring invalid connection params for ${channel.firstPartyEndpoint.nodeId} " + - "from ${channel.thirdPartyEndpoint.nodeId}" - ) + "from ${channel.thirdPartyEndpoint.nodeId}", + ), ) } @@ -217,8 +217,8 @@ internal class IncomingMessageTest : MockContextTestCase() { assertTrue( logCaptor.infoLogs.contains( "Updated connection params from ${thirdPartyEndpoint.nodeId} for " + - channel.firstPartyEndpoint.nodeId - ) + channel.firstPartyEndpoint.nodeId, + ), ) verify(storage.privateThirdParty).set( eq("${channel.firstPartyEndpoint.nodeId}_${thirdPartyEndpoint.nodeId}"), @@ -233,7 +233,7 @@ internal class IncomingMessageTest : MockContextTestCase() { private fun makeConnParams( channel: EndpointChannel, - deliveryAuth: CertificationPath + deliveryAuth: CertificationPath, ) = PrivateEndpointConnParams( channel.thirdPartyEndpoint.identityKey, channel.thirdPartyEndpoint.internetAddress, @@ -256,25 +256,25 @@ internal class IncomingMessageTest : MockContextTestCase() { ) return EndpointManager( thirdPartyPrivateKeyStore, - thirdPartySessionPublicKeyStore + thirdPartySessionPublicKeyStore, ) } private suspend fun encryptConnectionParams( channel: EndpointChannel, - params: PrivateEndpointConnParams + params: PrivateEndpointConnParams, ): ByteArray = encryptParcelPayload(channel, params.serialize()) private suspend fun encryptParcelPayload( channel: EndpointChannel, - plaintext: ByteArray + plaintext: ByteArray, ): ByteArray { val thirdPartyEndpointManager = makeThirdPartyEndpointManager(channel) val pdaPathServiceMessage = makePDAPathMessage(plaintext) return thirdPartyEndpointManager.wrapMessagePayload( pdaPathServiceMessage, channel.firstPartyEndpoint.nodeId, - channel.thirdPartyEndpoint.nodeId + channel.thirdPartyEndpoint.nodeId, ) } diff --git a/lib/src/test/java/tech/relaycorp/awaladroid/messaging/OutgoingMessageTest.kt b/lib/src/test/java/tech/relaycorp/awaladroid/messaging/OutgoingMessageTest.kt index 5dbb2b0b..881738b5 100644 --- a/lib/src/test/java/tech/relaycorp/awaladroid/messaging/OutgoingMessageTest.kt +++ b/lib/src/test/java/tech/relaycorp/awaladroid/messaging/OutgoingMessageTest.kt @@ -1,9 +1,5 @@ package tech.relaycorp.awaladroid.messaging -import java.time.Duration -import java.time.ZonedDateTime -import kotlin.math.abs -import kotlin.random.Random import kotlinx.coroutines.test.runTest import org.junit.Assert.assertArrayEquals import org.junit.Assert.assertEquals @@ -15,6 +11,10 @@ import tech.relaycorp.awaladroid.test.MessageFactory import tech.relaycorp.awaladroid.test.MockContextTestCase import tech.relaycorp.awaladroid.test.RecipientAddressType import tech.relaycorp.awaladroid.test.assertSameDateTime +import java.time.Duration +import java.time.ZonedDateTime +import kotlin.math.abs +import kotlin.random.Random internal class OutgoingMessageTest : MockContextTestCase() { @@ -37,7 +37,7 @@ internal class OutgoingMessageTest : MockContextTestCase() { val difference = Duration.between( message.parcel.expiryDate, - message.parcel.creationDate.plusDays(180) + message.parcel.creationDate.plusDays(180), ) assertTrue(abs(difference.toDays()) == 0L) } @@ -52,7 +52,7 @@ internal class OutgoingMessageTest : MockContextTestCase() { Random.Default.nextBytes(10), senderEndpoint, recipientEndpoint, - parcelExpiryDate + parcelExpiryDate, ) val differenceSeconds = @@ -72,7 +72,7 @@ internal class OutgoingMessageTest : MockContextTestCase() { assertEquals(message.recipientEndpoint.nodeId, message.parcel.recipient.id) assertEquals( recipientPublicEndpoint.internetAddress, - message.parcel.recipient.internetAddress + message.parcel.recipient.internetAddress, ) assertEquals(message.parcelId.value, message.parcel.id) assertSameDateTime(message.parcelCreationDate, message.parcel.creationDate) @@ -127,7 +127,7 @@ internal class OutgoingMessageTest : MockContextTestCase() { assertEquals(message.recipientEndpoint.nodeId, message.parcel.recipient.id) assertEquals( message.recipientEndpoint.internetAddress, - message.parcel.recipient.internetAddress + message.parcel.recipient.internetAddress, ) assertEquals(message.parcelId.value, message.parcel.id) assertSameDateTime(message.parcelCreationDate, message.parcel.creationDate) @@ -142,7 +142,7 @@ internal class OutgoingMessageTest : MockContextTestCase() { assertEquals( (message.recipientEndpoint as PrivateThirdPartyEndpoint).pda, - message.parcel.senderCertificate + message.parcel.senderCertificate, ) } @@ -154,7 +154,7 @@ internal class OutgoingMessageTest : MockContextTestCase() { assertArrayEquals( (message.recipientEndpoint as PrivateThirdPartyEndpoint).pdaChain.toTypedArray(), - message.parcel.senderCertificateChain.toTypedArray() + message.parcel.senderCertificateChain.toTypedArray(), ) } } diff --git a/lib/src/test/java/tech/relaycorp/awaladroid/messaging/ReceiveMessagesTest.kt b/lib/src/test/java/tech/relaycorp/awaladroid/messaging/ReceiveMessagesTest.kt index 19bb9e1e..c064619e 100644 --- a/lib/src/test/java/tech/relaycorp/awaladroid/messaging/ReceiveMessagesTest.kt +++ b/lib/src/test/java/tech/relaycorp/awaladroid/messaging/ReceiveMessagesTest.kt @@ -1,6 +1,5 @@ package tech.relaycorp.awaladroid.messaging -import java.time.ZonedDateTime import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.toCollection @@ -29,6 +28,7 @@ import tech.relaycorp.relaynet.testing.pki.KeyPairSet import tech.relaycorp.relaynet.testing.pki.PDACertPath import tech.relaycorp.relaynet.wrappers.generateECDHKeyPair import tech.relaycorp.relaynet.wrappers.nodeId +import java.time.ZonedDateTime internal class ReceiveMessagesTest : MockContextTestCase() { @@ -99,13 +99,13 @@ internal class ReceiveMessagesTest : MockContextTestCase() { val invalidParcel = Parcel( recipient = Recipient(KeyPairSet.PRIVATE_ENDPOINT.public.nodeId), payload = "".toByteArray(), - senderCertificate = PDACertPath.PRIVATE_ENDPOINT + senderCertificate = PDACertPath.PRIVATE_ENDPOINT, ) var ackWasCalled = false val parcelCollection = ParcelCollection( parcelSerialized = invalidParcel.serialize(KeyPairSet.PRIVATE_ENDPOINT.private), trustedCertificates = emptyList(), // sender won't be trusted - ack = { ackWasCalled = true } + ack = { ackWasCalled = true }, ) val collectParcelsCall = CollectParcelsCall(Result.success(flowOf(parcelCollection))) pdcClient = MockPDCClient(collectParcelsCall) @@ -125,7 +125,7 @@ internal class ReceiveMessagesTest : MockContextTestCase() { val parcelCollection = ParcelCollection( parcelSerialized = "1234".toByteArray(), trustedCertificates = emptyList(), - ack = { ackWasCalled = true } + ack = { ackWasCalled = true }, ) val collectParcelsCall = CollectParcelsCall(Result.success(flowOf(parcelCollection))) pdcClient = MockPDCClient(collectParcelsCall) @@ -187,11 +187,11 @@ internal class ReceiveMessagesTest : MockContextTestCase() { PublicThirdPartyEndpointData( channel.thirdPartyEndpoint.nodeId, channel.thirdPartyEndpoint.identityKey, - ) + ), ) val parcelPayload = serviceMessage.encrypt( channel.firstPartySessionKeyPair.sessionKey.copy( - publicKey = generateECDHKeyPair().public // Invalid encryption key + publicKey = generateECDHKeyPair().public, // Invalid encryption key ), channel.thirdPartySessionKeyPair, ) @@ -199,7 +199,7 @@ internal class ReceiveMessagesTest : MockContextTestCase() { recipient = Recipient(PDACertPath.PRIVATE_ENDPOINT.subjectId), payload = parcelPayload, senderCertificate = PDACertPath.PDA, - senderCertificateChain = setOf(PDACertPath.PRIVATE_ENDPOINT, PDACertPath.PRIVATE_GW) + senderCertificateChain = setOf(PDACertPath.PRIVATE_ENDPOINT, PDACertPath.PRIVATE_GW), ) var ackWasCalled = false val parcelCollection = parcel.toParcelCollection { ackWasCalled = true } @@ -213,8 +213,8 @@ internal class ReceiveMessagesTest : MockContextTestCase() { assertTrue(ackWasCalled) assertTrue( logCaptor.warnLogs.contains( - "Failed to decrypt parcel; sender might have used wrong key" - ) + "Failed to decrypt parcel; sender might have used wrong key", + ), ) } @@ -227,7 +227,7 @@ internal class ReceiveMessagesTest : MockContextTestCase() { PublicThirdPartyEndpointData( channel.thirdPartyEndpoint.nodeId, channel.thirdPartyEndpoint.identityKey, - ) + ), ) val parcel = Parcel( recipient = Recipient(PDACertPath.PRIVATE_ENDPOINT.subjectId), @@ -236,7 +236,7 @@ internal class ReceiveMessagesTest : MockContextTestCase() { channel.thirdPartySessionKeyPair, ), senderCertificate = PDACertPath.PDA, - senderCertificateChain = setOf(PDACertPath.PRIVATE_ENDPOINT, PDACertPath.PRIVATE_GW) + senderCertificateChain = setOf(PDACertPath.PRIVATE_ENDPOINT, PDACertPath.PRIVATE_GW), ) var ackWasCalled = false val parcelCollection = parcel.toParcelCollection { ackWasCalled = true } @@ -250,8 +250,8 @@ internal class ReceiveMessagesTest : MockContextTestCase() { assertTrue(ackWasCalled) assertTrue( logCaptor.warnLogs.contains( - "Incoming parcel did not encapsulate a valid service message" - ) + "Incoming parcel did not encapsulate a valid service message", + ), ) } @@ -266,14 +266,14 @@ internal class ReceiveMessagesTest : MockContextTestCase() { issuerPrivateKey = KeyPairSet.PRIVATE_ENDPOINT.private, issuerCertificate = PDACertPath.PRIVATE_ENDPOINT, validityStartDate = ZonedDateTime.now().minusDays(1), - validityEndDate = ZonedDateTime.now().plusDays(1) + validityEndDate = ZonedDateTime.now().plusDays(1), ), - senderCertificateChain = setOf(PDACertPath.PRIVATE_ENDPOINT, PDACertPath.PRIVATE_GW) + senderCertificateChain = setOf(PDACertPath.PRIVATE_ENDPOINT, PDACertPath.PRIVATE_GW), ) private fun Parcel.toParcelCollection(ack: suspend () -> Unit = {}) = ParcelCollection( parcelSerialized = serialize(KeyPairSet.PDA_GRANTEE.private), trustedCertificates = listOf(PDACertPath.PRIVATE_ENDPOINT, PDACertPath.PRIVATE_GW), - ack = ack + ack = ack, ) } diff --git a/lib/src/test/java/tech/relaycorp/awaladroid/messaging/SendMessageTest.kt b/lib/src/test/java/tech/relaycorp/awaladroid/messaging/SendMessageTest.kt index bd634513..7db90a83 100644 --- a/lib/src/test/java/tech/relaycorp/awaladroid/messaging/SendMessageTest.kt +++ b/lib/src/test/java/tech/relaycorp/awaladroid/messaging/SendMessageTest.kt @@ -49,7 +49,7 @@ internal class SendMessageTest : MockContextTestCase() { val signer = deliverParcelCall.arguments!!.deliverySigner assertEquals( message.senderEndpoint.identityCertificate.subjectId, - signer.certificate.subjectId + signer.certificate.subjectId, ) } diff --git a/lib/src/test/java/tech/relaycorp/awaladroid/storage/StorageImplTest.kt b/lib/src/test/java/tech/relaycorp/awaladroid/storage/StorageImplTest.kt index 357786cc..dff7c501 100644 --- a/lib/src/test/java/tech/relaycorp/awaladroid/storage/StorageImplTest.kt +++ b/lib/src/test/java/tech/relaycorp/awaladroid/storage/StorageImplTest.kt @@ -5,8 +5,6 @@ import com.nhaarman.mockitokotlin2.eq import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.verify import com.nhaarman.mockitokotlin2.whenever -import java.nio.charset.Charset -import java.util.UUID import kotlinx.coroutines.test.runTest import org.junit.Assert.assertArrayEquals import org.junit.Assert.assertTrue @@ -17,6 +15,8 @@ import tech.relaycorp.awaladroid.storage.persistence.Persistence import tech.relaycorp.relaynet.pki.CertificationPath import tech.relaycorp.relaynet.testing.pki.KeyPairSet import tech.relaycorp.relaynet.testing.pki.PDACertPath +import java.nio.charset.Charset +import java.util.UUID internal class StorageImplTest { @@ -28,7 +28,7 @@ internal class StorageImplTest { val charset = Charset.forName("ASCII") storage.gatewayId.testGet( PDACertPath.PRIVATE_GW.subjectId.toByteArray(charset), - PDACertPath.PRIVATE_GW.subjectId + PDACertPath.PRIVATE_GW.subjectId, ) storage.gatewayId.testSet( PDACertPath.PRIVATE_GW.subjectId, @@ -43,7 +43,7 @@ internal class StorageImplTest { KeyPairSet.PRIVATE_ENDPOINT.public, CertificationPath( PDACertPath.PDA, - listOf(PDACertPath.PRIVATE_GW) + listOf(PDACertPath.PRIVATE_GW), ), "gateway.com", ) @@ -65,7 +65,7 @@ internal class StorageImplTest { fun publicThirdParty() = runTest { val data = PublicThirdPartyEndpointData( "example.org", - KeyPairSet.INTERNET_GW.public + KeyPairSet.INTERNET_GW.public, ) val rawData = data.serialize() @@ -81,7 +81,7 @@ internal class StorageImplTest { private suspend fun StorageImpl.Module.testGet( rawData: ByteArray, expectedOutput: T, - equalityCheck: ((T, T) -> Boolean) = Any::equals + equalityCheck: ((T, T) -> Boolean) = Any::equals, ) { val key = UUID.randomUUID().toString() whenever(persistence.get(any())).thenReturn(rawData) @@ -89,13 +89,13 @@ internal class StorageImplTest { verify(persistence).get(eq("$prefix$key")) assertTrue( "expected $expectedOutput, got $output", - equalityCheck(expectedOutput, output) + equalityCheck(expectedOutput, output), ) } private suspend fun StorageImpl.Module.testSet( data: T, - expectedRawData: ByteArray + expectedRawData: ByteArray, ) { val key = UUID.randomUUID().toString() set(key, data) @@ -125,20 +125,20 @@ internal class StorageImplTest { private suspend fun StorageImpl.SingleModule.testGet( rawData: ByteArray, expectedOutput: T, - equalityCheck: ((T, T) -> Boolean) = Any::equals + equalityCheck: ((T, T) -> Boolean) = Any::equals, ) { whenever(persistence.get(any())).thenReturn(rawData) val output = get()!! verify(persistence).get(eq("${prefix}base")) assertTrue( "expected $expectedOutput, got $output", - equalityCheck(expectedOutput, output) + equalityCheck(expectedOutput, output), ) } private suspend fun StorageImpl.SingleModule.testSet( data: T, - expectedRawData: ByteArray + expectedRawData: ByteArray, ) { set(data) verify(persistence).set(eq("${prefix}base"), eq(expectedRawData)) diff --git a/lib/src/test/java/tech/relaycorp/awaladroid/storage/persistence/DiskPersistenceTest.kt b/lib/src/test/java/tech/relaycorp/awaladroid/storage/persistence/DiskPersistenceTest.kt index 483c5cfc..deab9606 100644 --- a/lib/src/test/java/tech/relaycorp/awaladroid/storage/persistence/DiskPersistenceTest.kt +++ b/lib/src/test/java/tech/relaycorp/awaladroid/storage/persistence/DiskPersistenceTest.kt @@ -1,8 +1,5 @@ package tech.relaycorp.awaladroid.storage.persistence -import java.io.File -import java.nio.charset.Charset -import kotlin.io.path.createTempDirectory import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest @@ -14,6 +11,9 @@ import org.junit.Assert.assertThrows import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test +import java.io.File +import java.nio.charset.Charset +import kotlin.io.path.createTempDirectory internal class DiskPersistenceTest { private val coroutineScope = TestScope() @@ -21,13 +21,14 @@ internal class DiskPersistenceTest { private lateinit var filesDir: String private lateinit var subject: DiskPersistence + @Before fun initDiskPersistence(): Unit = runBlocking { filesDir = createTempDirectory("rootDir").toString() subject = DiskPersistence( filesDir, coroutineScope.coroutineContext, - rootFolder + rootFolder, ) } diff --git a/lib/src/test/java/tech/relaycorp/awaladroid/test/AssertUtils.kt b/lib/src/test/java/tech/relaycorp/awaladroid/test/AssertUtils.kt index 29ce8df2..f9bbc3d9 100644 --- a/lib/src/test/java/tech/relaycorp/awaladroid/test/AssertUtils.kt +++ b/lib/src/test/java/tech/relaycorp/awaladroid/test/AssertUtils.kt @@ -1,8 +1,8 @@ package tech.relaycorp.awaladroid.test +import org.junit.Assert import java.time.Duration import java.time.ZonedDateTime -import org.junit.Assert internal fun assertSameDateTime(date1: ZonedDateTime, date2: ZonedDateTime) = Assert.assertTrue(Duration.between(date1, date2).seconds < 2) diff --git a/lib/src/test/java/tech/relaycorp/awaladroid/test/FakeAndroidKeyStore.kt b/lib/src/test/java/tech/relaycorp/awaladroid/test/FakeAndroidKeyStore.kt index 0c1ed70d..6510315d 100644 --- a/lib/src/test/java/tech/relaycorp/awaladroid/test/FakeAndroidKeyStore.kt +++ b/lib/src/test/java/tech/relaycorp/awaladroid/test/FakeAndroidKeyStore.kt @@ -62,14 +62,14 @@ public object FakeAndroidKeyStore { alias: String?, key: Key?, password: CharArray?, - chain: Array? + chain: Array?, ): Unit = wrapped.setKeyEntry(alias, key, password, chain) override fun engineSetKeyEntry( alias: String?, key: ByteArray?, - chain: Array? + chain: Array?, ): Unit = wrapped.setKeyEntry(alias, key, chain) override fun engineStore(stream: OutputStream?, password: CharArray?): Unit = diff --git a/lib/src/test/java/tech/relaycorp/awaladroid/test/MessageFactory.kt b/lib/src/test/java/tech/relaycorp/awaladroid/test/MessageFactory.kt index e5c9b398..853670a2 100644 --- a/lib/src/test/java/tech/relaycorp/awaladroid/test/MessageFactory.kt +++ b/lib/src/test/java/tech/relaycorp/awaladroid/test/MessageFactory.kt @@ -18,6 +18,6 @@ internal object MessageFactory { type = serviceMessage.type, content = serviceMessage.content, senderEndpoint = ThirdPartyEndpointFactory.buildPublic(), - recipientEndpoint = FirstPartyEndpointFactory.build() + recipientEndpoint = FirstPartyEndpointFactory.build(), ) {} } diff --git a/lib/src/test/java/tech/relaycorp/awaladroid/test/MockContextTestCase.kt b/lib/src/test/java/tech/relaycorp/awaladroid/test/MockContextTestCase.kt index 5a07a57e..ba24a4eb 100644 --- a/lib/src/test/java/tech/relaycorp/awaladroid/test/MockContextTestCase.kt +++ b/lib/src/test/java/tech/relaycorp/awaladroid/test/MockContextTestCase.kt @@ -47,8 +47,8 @@ internal abstract class MockContextTestCase { privateKeyStore, sessionPublicKeystore, certificateStore, - handleGatewayCertificateChange - ) + handleGatewayCertificateChange, + ), ) } @@ -62,7 +62,7 @@ internal abstract class MockContextTestCase { fun unsetContext(): Unit = unsetAwalaContext() protected suspend fun createEndpointChannel( - thirdPartyEndpointType: RecipientAddressType + thirdPartyEndpointType: RecipientAddressType, ): EndpointChannel { val firstPartyEndpoint = createFirstPartyEndpoint() @@ -103,7 +103,7 @@ internal abstract class MockContextTestCase { certificateStore.save( CertificationPath( certificate, - firstPartyEndpoint.identityCertificateChain + firstPartyEndpoint.identityCertificateChain, ), certificate.issuerCommonName, ) @@ -117,11 +117,11 @@ internal abstract class MockContextTestCase { } else { storage.gatewayId.set( firstPartyEndpoint.nodeId, - certificate.issuerCommonName + certificate.issuerCommonName, ) storage.internetAddress.set( - gatewayAddress + gatewayAddress, ) } @@ -131,7 +131,7 @@ internal abstract class MockContextTestCase { private suspend fun createThirdPartyEndpoint( thirdPartyEndpointType: RecipientAddressType, sessionKey: SessionKey, - firstPartyEndpoint: FirstPartyEndpoint + firstPartyEndpoint: FirstPartyEndpoint, ): ThirdPartyEndpoint { val thirdPartyEndpoint: ThirdPartyEndpoint when (thirdPartyEndpointType) { @@ -139,36 +139,36 @@ internal abstract class MockContextTestCase { thirdPartyEndpoint = ThirdPartyEndpointFactory.buildPrivate() val authBundle = CertificationPath( PDACertPath.PDA, - listOf(PDACertPath.PRIVATE_ENDPOINT, PDACertPath.PRIVATE_GW) + listOf(PDACertPath.PRIVATE_ENDPOINT, PDACertPath.PRIVATE_GW), ) whenever( storage.privateThirdParty.get( - "${firstPartyEndpoint.nodeId}_${thirdPartyEndpoint.nodeId}" - ) + "${firstPartyEndpoint.nodeId}_${thirdPartyEndpoint.nodeId}", + ), ).thenReturn( PrivateThirdPartyEndpointData( KeyPairSet.PDA_GRANTEE.public, authBundle, thirdPartyEndpoint.internetAddress, - ) + ), ) } else -> { thirdPartyEndpoint = ThirdPartyEndpointFactory.buildPublic() whenever( - storage.publicThirdParty.get(thirdPartyEndpoint.nodeId) + storage.publicThirdParty.get(thirdPartyEndpoint.nodeId), ).thenReturn( PublicThirdPartyEndpointData( thirdPartyEndpoint.internetAddress, - thirdPartyEndpoint.identityKey - ) + thirdPartyEndpoint.identityKey, + ), ) } } sessionPublicKeystore.save( sessionKey, - thirdPartyEndpoint.nodeId + thirdPartyEndpoint.nodeId, ) return thirdPartyEndpoint } diff --git a/lib/src/test/java/tech/relaycorp/awaladroid/test/ThirdPartyEndpointFactory.kt b/lib/src/test/java/tech/relaycorp/awaladroid/test/ThirdPartyEndpointFactory.kt index 5346c0f3..01030422 100644 --- a/lib/src/test/java/tech/relaycorp/awaladroid/test/ThirdPartyEndpointFactory.kt +++ b/lib/src/test/java/tech/relaycorp/awaladroid/test/ThirdPartyEndpointFactory.kt @@ -11,7 +11,7 @@ internal object ThirdPartyEndpointFactory { fun buildPublic(): PublicThirdPartyEndpoint { return PublicThirdPartyEndpoint( internetAddress, - KeyPairSet.PDA_GRANTEE.public + KeyPairSet.PDA_GRANTEE.public, ) } From 3dfe72d7037e12012b53511630cca9221ff7f69d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 4 Aug 2023 14:15:28 +0000 Subject: [PATCH 02/14] fix(deps): bump kotlinCoroutinesVersion from 1.6.4 to 1.7.3 (#323) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps `kotlinCoroutinesVersion` from 1.6.4 to 1.7.3. Updates `org.jetbrains.kotlinx:kotlinx-coroutines-core` from 1.6.4 to 1.7.3
Release notes

Sourced from org.jetbrains.kotlinx:kotlinx-coroutines-core's releases.

1.7.3

  • Disabled the publication of the multiplatform library metadata for the old (1.6 and earlier) KMP Gradle plugin (#3809).
  • Fixed a bug introduced in 1.7.2 that disabled the coroutine debugger in IDEA (#3822).

1.7.2

Bug fixes and improvements

  • Coroutines debugger no longer keeps track of coroutines with empty coroutine context (#3782).
  • CopyableThreadContextElement now properly copies an element when crossing the coroutine boundary in flowOn (#3787). Thanks @​wanyingd1996!
  • Coroutine timeouts no longer prevent K/N newSingleThreadContext from closing (#3768).
  • A non-linearizability in Mutex during tryLock/unlock sequence with owners is fixed (#3745).
  • Atomicfu version is updated to 0.21.0.

1.7.1

Bug fixes and improvements

  • Special characters in coroutine names in JSON dumps are supported (#3747)
  • The binary compatibility of the experimental overload of runTest is restored (#3673)
  • Channels that don't use onUndeliveredElement now allocate less memory (#3646)

1.7.0

Core API significant improvements

  • New Channel implementation with significant performance improvements across the API (#3621).
  • New select operator implementation: faster, more lightweight, and more robust (#3020).
  • Mutex and Semaphore now share the same underlying data structure (#3020).
  • Dispatchers.IO is added to K/N (#3205)
    • newFixedThreadPool and Dispatchers.Default implementations on K/N were wholly rewritten to support graceful growth under load (#3595).
  • kotlinx-coroutines-test rework:
    • Add the timeout parameter to runTest for the whole-test timeout, 10 seconds by default (#3270). This replaces the configuration of quiescence timeouts, which is now deprecated (#3603).
    • The withTimeout exception messages indicate if the timeout used the virtual time (#3588).
    • TestCoroutineScheduler, runTest, and TestScope API are promoted to stable (#3622).
    • runTest now also fails if there were uncaught exceptions in coroutines not inherited from the test coroutine (#1205).

Breaking changes

  • Old K/N memory model is no longer supported (#3375).
  • New generic upper bounds were added to reactive integration API where the language since 1.8.0 dictates (#3393).
  • kotlinx-coroutines-core and kotlinx-coroutines-jdk8 artifacts were merged into a single artifact (#3268).
  • Artificial stackframes in stacktrace recovery no longer contain the \b symbol and are now navigable in IDE and supplied with proper documentation (#2291).
  • CoroutineContext.isActive returns true for contexts without any job in them (#3300).

Bug fixes and improvements

  • Kotlin version is updated to 1.8.20
  • Atomicfu version is updated to 0.20.2.
  • JavaFx version is updated to 17.0.2 in kotlinx-coroutines-javafx (#3671)..
  • JPMS is supported (#2237). Thanks @​lion7!
  • BroadcastChannel and all the corresponding API are deprecated (#2680).
  • Added all supported K/N targets (#3601, #812, #855).

... (truncated)

Changelog

Sourced from org.jetbrains.kotlinx:kotlinx-coroutines-core's changelog.

Version 1.7.3

  • Disabled the publication of the multiplatform library metadata for the old (1.6 and earlier) KMP Gradle plugin (#3809).
  • Fixed a bug introduced in 1.7.2 that disabled the coroutine debugger in IDEA (#3822).

Version 1.7.2

Bug fixes and improvements

  • Coroutines debugger no longer keeps track of coroutines with empty coroutine context (#3782).
  • CopyableThreadContextElement now properly copies an element when crossing the coroutine boundary in flowOn (#3787). Thanks @​wanyingd1996!
  • Coroutine timeouts no longer prevent K/N newSingleThreadContext from closing (#3768).
  • A non-linearizability in Mutex during tryLock/unlock sequence with owners is fixed (#3745).
  • Atomicfu version is updated to 0.21.0.

Version 1.7.1

Bug fixes and improvements

  • Special characters in coroutine names in JSON dumps are supported (#3747)
  • The binary compatibility of the experimental overload of runTest is restored (#3673)
  • Channels that don't use onUndeliveredElement now allocate less memory (#3646)

Version 1.7.0

Core API significant improvements

  • New Channel implementation with significant performance improvements across the API (#3621).
  • New select operator implementation: faster, more lightweight, and more robust (#3020).
  • Mutex and Semaphore now share the same underlying data structure (#3020).
  • Dispatchers.IO is added to K/N (#3205)
    • newFixedThreadPool and Dispatchers.Default implementations on K/N were wholly rewritten to support graceful growth under load (#3595).
  • kotlinx-coroutines-test rework:
    • Add the timeout parameter to runTest for the whole-test timeout, 10 seconds by default (#3270). This replaces the configuration of quiescence timeouts, which is now deprecated (#3603).
    • The withTimeout exception messages indicate if the timeout used the virtual time (#3588).
    • TestCoroutineScheduler, runTest, and TestScope API are promoted to stable (#3622).
    • runTest now also fails if there were uncaught exceptions in coroutines not inherited from the test coroutine (#1205).

Breaking changes

  • Old K/N memory model is no longer supported (#3375).
  • New generic upper bounds were added to reactive integration API where the language since 1.8.0 dictates (#3393).
  • kotlinx-coroutines-core and kotlinx-coroutines-jdk8 artifacts were merged into a single artifact (#3268).
  • Artificial stackframes in stacktrace recovery no longer contain the \b symbol and are now navigable in IDE and supplied with proper documentation (#2291).
  • CoroutineContext.isActive returns true for contexts without any job in them (#3300).

Bug fixes and improvements

  • Kotlin version is updated to 1.8.20
  • Atomicfu version is updated to 0.20.2.

... (truncated)

Commits
  • 35d88f1 Version 1.7.3
  • 74d2aeb Merge remote-tracking branch 'origin/master' into develop
  • 47f0a46 Fix the IDEA debugger (#3822)
  • 387628b Merge pull request #3815 from Kotlin/roman.efremov/MR/expect-annotations
  • 99f0804 fix: errors in runnable snippet in doc (#3818)
  • 3c9e856 Make annotations on expect declarations comply with new compiler restriction
  • c675e3f 3789: Update flow.timeout example to re-throw (#3801)
  • 9b06a69 Clarify documentation of Mutex.lock() behavior (#3816)
  • ef623b8 Revert "Remove @PublishedApi from unwrap to comply with new compiler rest...
  • 5c4a252 Stop building and publishing compatibility MPP metadata variant (#3809)
  • Additional commits viewable in compare view

Updates `org.jetbrains.kotlinx:kotlinx-coroutines-test` from 1.6.4 to 1.7.3
Release notes

Sourced from org.jetbrains.kotlinx:kotlinx-coroutines-test's releases.

1.7.3

  • Disabled the publication of the multiplatform library metadata for the old (1.6 and earlier) KMP Gradle plugin (#3809).
  • Fixed a bug introduced in 1.7.2 that disabled the coroutine debugger in IDEA (#3822).

1.7.2

Bug fixes and improvements

  • Coroutines debugger no longer keeps track of coroutines with empty coroutine context (#3782).
  • CopyableThreadContextElement now properly copies an element when crossing the coroutine boundary in flowOn (#3787). Thanks @​wanyingd1996!
  • Coroutine timeouts no longer prevent K/N newSingleThreadContext from closing (#3768).
  • A non-linearizability in Mutex during tryLock/unlock sequence with owners is fixed (#3745).
  • Atomicfu version is updated to 0.21.0.

1.7.1

Bug fixes and improvements

  • Special characters in coroutine names in JSON dumps are supported (#3747)
  • The binary compatibility of the experimental overload of runTest is restored (#3673)
  • Channels that don't use onUndeliveredElement now allocate less memory (#3646)

1.7.0

Core API significant improvements

  • New Channel implementation with significant performance improvements across the API (#3621).
  • New select operator implementation: faster, more lightweight, and more robust (#3020).
  • Mutex and Semaphore now share the same underlying data structure (#3020).
  • Dispatchers.IO is added to K/N (#3205)
    • newFixedThreadPool and Dispatchers.Default implementations on K/N were wholly rewritten to support graceful growth under load (#3595).
  • kotlinx-coroutines-test rework:
    • Add the timeout parameter to runTest for the whole-test timeout, 10 seconds by default (#3270). This replaces the configuration of quiescence timeouts, which is now deprecated (#3603).
    • The withTimeout exception messages indicate if the timeout used the virtual time (#3588).
    • TestCoroutineScheduler, runTest, and TestScope API are promoted to stable (#3622).
    • runTest now also fails if there were uncaught exceptions in coroutines not inherited from the test coroutine (#1205).

Breaking changes

  • Old K/N memory model is no longer supported (#3375).
  • New generic upper bounds were added to reactive integration API where the language since 1.8.0 dictates (#3393).
  • kotlinx-coroutines-core and kotlinx-coroutines-jdk8 artifacts were merged into a single artifact (#3268).
  • Artificial stackframes in stacktrace recovery no longer contain the \b symbol and are now navigable in IDE and supplied with proper documentation (#2291).
  • CoroutineContext.isActive returns true for contexts without any job in them (#3300).

Bug fixes and improvements

  • Kotlin version is updated to 1.8.20
  • Atomicfu version is updated to 0.20.2.
  • JavaFx version is updated to 17.0.2 in kotlinx-coroutines-javafx (#3671)..
  • JPMS is supported (#2237). Thanks @​lion7!
  • BroadcastChannel and all the corresponding API are deprecated (#2680).
  • Added all supported K/N targets (#3601, #812, #855).

... (truncated)

Changelog

Sourced from org.jetbrains.kotlinx:kotlinx-coroutines-test's changelog.

Version 1.7.3

  • Disabled the publication of the multiplatform library metadata for the old (1.6 and earlier) KMP Gradle plugin (#3809).
  • Fixed a bug introduced in 1.7.2 that disabled the coroutine debugger in IDEA (#3822).

Version 1.7.2

Bug fixes and improvements

  • Coroutines debugger no longer keeps track of coroutines with empty coroutine context (#3782).
  • CopyableThreadContextElement now properly copies an element when crossing the coroutine boundary in flowOn (#3787). Thanks @​wanyingd1996!
  • Coroutine timeouts no longer prevent K/N newSingleThreadContext from closing (#3768).
  • A non-linearizability in Mutex during tryLock/unlock sequence with owners is fixed (#3745).
  • Atomicfu version is updated to 0.21.0.

Version 1.7.1

Bug fixes and improvements

  • Special characters in coroutine names in JSON dumps are supported (#3747)
  • The binary compatibility of the experimental overload of runTest is restored (#3673)
  • Channels that don't use onUndeliveredElement now allocate less memory (#3646)

Version 1.7.0

Core API significant improvements

  • New Channel implementation with significant performance improvements across the API (#3621).
  • New select operator implementation: faster, more lightweight, and more robust (#3020).
  • Mutex and Semaphore now share the same underlying data structure (#3020).
  • Dispatchers.IO is added to K/N (#3205)
    • newFixedThreadPool and Dispatchers.Default implementations on K/N were wholly rewritten to support graceful growth under load (#3595).
  • kotlinx-coroutines-test rework:
    • Add the timeout parameter to runTest for the whole-test timeout, 10 seconds by default (#3270). This replaces the configuration of quiescence timeouts, which is now deprecated (#3603).
    • The withTimeout exception messages indicate if the timeout used the virtual time (#3588).
    • TestCoroutineScheduler, runTest, and TestScope API are promoted to stable (#3622).
    • runTest now also fails if there were uncaught exceptions in coroutines not inherited from the test coroutine (#1205).

Breaking changes

  • Old K/N memory model is no longer supported (#3375).
  • New generic upper bounds were added to reactive integration API where the language since 1.8.0 dictates (#3393).
  • kotlinx-coroutines-core and kotlinx-coroutines-jdk8 artifacts were merged into a single artifact (#3268).
  • Artificial stackframes in stacktrace recovery no longer contain the \b symbol and are now navigable in IDE and supplied with proper documentation (#2291).
  • CoroutineContext.isActive returns true for contexts without any job in them (#3300).

Bug fixes and improvements

  • Kotlin version is updated to 1.8.20
  • Atomicfu version is updated to 0.20.2.

... (truncated)

Commits
  • 35d88f1 Version 1.7.3
  • 74d2aeb Merge remote-tracking branch 'origin/master' into develop
  • 47f0a46 Fix the IDEA debugger (#3822)
  • 387628b Merge pull request #3815 from Kotlin/roman.efremov/MR/expect-annotations
  • 99f0804 fix: errors in runnable snippet in doc (#3818)
  • 3c9e856 Make annotations on expect declarations comply with new compiler restriction
  • c675e3f 3789: Update flow.timeout example to re-throw (#3801)
  • 9b06a69 Clarify documentation of Mutex.lock() behavior (#3816)
  • ef623b8 Revert "Remove @PublishedApi from unwrap to comply with new compiler rest...
  • 5c4a252 Stop building and publishing compatibility MPP metadata variant (#3809)
  • Additional commits viewable in compare view

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
--- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index eec23c5e..c06301d1 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ buildscript { ext { kotlinVersion = '1.8.21' - kotlinCoroutinesVersion = '1.6.4' + kotlinCoroutinesVersion = '1.7.3' } repositories { google() From ba90108c79f5c0ea8d3487d98dc506fd5b14d81b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Aug 2023 21:47:13 +0000 Subject: [PATCH 03/14] fix(deps): bump tech.relaycorp:awala-testing from 1.5.13 to 1.5.22 (#327) Bumps [tech.relaycorp:awala-testing](https://github.com/relaycorp/awala-testing-jvm) from 1.5.13 to 1.5.22.
Release notes

Sourced from tech.relaycorp:awala-testing's releases.

v1.5.22

1.5.22 (2023-08-14)

Bug Fixes

  • deps: Bump org.jlleitschuh.gradle.ktlint from 11.5.0 to 11.5.1 (#126) (c3a8a9a)

v1.5.21

1.5.21 (2023-07-31)

Bug Fixes

v1.5.20

1.5.20 (2023-07-24)

Bug Fixes

  • deps: Bump junit5Version from 5.9.3 to 5.10.0 (#124) (e084b49)

v1.5.19

1.5.19 (2023-07-10)

Bug Fixes

  • deps: Bump org.jlleitschuh.gradle.ktlint from 11.4.2 to 11.5.0 (#123) (6e4e9d4)

v1.5.18

1.5.18 (2023-07-03)

Bug Fixes

v1.5.17

1.5.17 (2023-06-12)

Bug Fixes

  • deps: Bump org.jlleitschuh.gradle.ktlint from 11.3.2 to 11.4.0 (#120) (65bc037)

v1.5.16

... (truncated)

Commits
  • c3a8a9a fix(deps): Bump org.jlleitschuh.gradle.ktlint from 11.5.0 to 11.5.1 (#126)
  • 35c2dcf fix(deps): Bump kotlinCoroutinesVersion from 1.7.2 to 1.7.3 (#125)
  • e084b49 fix(deps): Bump junit5Version from 5.9.3 to 5.10.0 (#124)
  • 6e4e9d4 fix(deps): Bump org.jlleitschuh.gradle.ktlint from 11.4.2 to 11.5.0 (#123)
  • 0be9268 fix(deps): Bump kotlinCoroutinesVersion from 1.7.1 to 1.7.2 (#122)
  • 6efd977 fix(deps): Bump org.jlleitschuh.gradle.ktlint from 11.4.0 to 11.4.2 (#121)
  • 65bc037 fix(deps): Bump org.jlleitschuh.gradle.ktlint from 11.3.2 to 11.4.0 (#120)
  • 976cbe7 fix(deps): Bump org.jetbrains.dokka from 1.7.10 to 1.8.20 (#119)
  • 2b492cf fix(deps): Bump kotlinCoroutinesVersion from 1.7.0 to 1.7.1 (#118)
  • 49ca3dd fix(deps): Bump kotlinCoroutinesVersion from 1.6.4 to 1.7.0 (#117)
  • See full diff in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=tech.relaycorp:awala-testing&package-manager=gradle&previous-version=1.5.13&new-version=1.5.22)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
--- lib/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/build.gradle b/lib/build.gradle index d1b0da87..6c73e5a6 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -67,7 +67,7 @@ dependencies { implementation 'tech.relaycorp:awala:1.67.3' implementation 'tech.relaycorp:awala-keystore-file:1.6.13' implementation 'tech.relaycorp:poweb:1.5.35' - testImplementation 'tech.relaycorp:awala-testing:1.5.13' + testImplementation 'tech.relaycorp:awala-testing:1.5.22' // Security implementation 'androidx.security:security-crypto:1.1.0-alpha06' From 6dc0c804319b7163d813a55bd456476e7e1692c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Santos?= Date: Thu, 7 Sep 2023 18:37:07 +0100 Subject: [PATCH 04/14] fix: Concurrent messages received (#334) Fixes #329 --- .../relaycorp/awaladroid/GatewayClientImpl.kt | 7 +++++++ .../awaladroid/GatewayClientImplTest.kt | 17 +++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/lib/src/main/java/tech/relaycorp/awaladroid/GatewayClientImpl.kt b/lib/src/main/java/tech/relaycorp/awaladroid/GatewayClientImpl.kt index 7e97367e..aaa1dbff 100644 --- a/lib/src/main/java/tech/relaycorp/awaladroid/GatewayClientImpl.kt +++ b/lib/src/main/java/tech/relaycorp/awaladroid/GatewayClientImpl.kt @@ -23,6 +23,7 @@ import tech.relaycorp.relaynet.bindings.pdc.ServerException import tech.relaycorp.relaynet.messages.control.PrivateNodeRegistration import tech.relaycorp.relaynet.messages.control.PrivateNodeRegistrationRequest import java.security.KeyPair +import java.util.concurrent.atomic.AtomicBoolean import java.util.logging.Level import kotlin.coroutines.CoroutineContext import kotlin.coroutines.resume @@ -45,6 +46,7 @@ internal constructor( // Gateway private var gwServiceInteractor: ServiceInteractor? = null + private val isReceivingMessages = AtomicBoolean(false) /** * Bind to the gateway to be able to communicate with it. @@ -183,6 +185,9 @@ internal constructor( } } + if (isReceivingMessages.get()) return@withContext + isReceivingMessages.set(true) + try { receiveMessages .receive() @@ -195,6 +200,8 @@ internal constructor( logger.log(Level.SEVERE, "Could not receive new messages", exp) } + isReceivingMessages.set(false) + if (!wasAlreadyBound) unbind() } } diff --git a/lib/src/test/java/tech/relaycorp/awaladroid/GatewayClientImplTest.kt b/lib/src/test/java/tech/relaycorp/awaladroid/GatewayClientImplTest.kt index c93e9fd0..c1db7f70 100644 --- a/lib/src/test/java/tech/relaycorp/awaladroid/GatewayClientImplTest.kt +++ b/lib/src/test/java/tech/relaycorp/awaladroid/GatewayClientImplTest.kt @@ -9,6 +9,7 @@ import com.nhaarman.mockitokotlin2.times import com.nhaarman.mockitokotlin2.verify import com.nhaarman.mockitokotlin2.whenever import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOf @@ -40,6 +41,7 @@ import tech.relaycorp.relaynet.testing.pdc.RegisterNodeCall import tech.relaycorp.relaynet.testing.pki.KeyPairSet import tech.relaycorp.relaynet.testing.pki.PDACertPath import java.time.ZonedDateTime +import kotlin.time.Duration.Companion.seconds @RunWith(RobolectricTestRunner::class) internal class GatewayClientImplTest : MockContextTestCase() { @@ -284,4 +286,19 @@ internal class GatewayClientImplTest : MockContextTestCase() { gatewayClient.checkForNewMessages() } + + @Test + fun checkForNewMessages_doesStartSimultaneousReceiveMessages() = coroutineScope.runTest { + whenever(receiveMessages.receive()).thenReturn(flow { delay(1.seconds) }) + + repeat(10) { + coroutineScope.launch { + gatewayClient.checkForNewMessages() + } + } + + delay(1.seconds) + + verify(receiveMessages, times(1)).receive() + } } From 2152e288b1b305926014337bc170f588d033a515 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Santos?= Date: Wed, 20 Sep 2023 14:48:16 +0200 Subject: [PATCH 05/14] fix: NonceSignerException crash when ReceiveMessages.receive is called without first-part endpoints (#339) Also skips parcel collection without 1st party endpoints. Closes #338 --- .../awaladroid/messaging/ReceiveMessages.kt | 56 ++++++++++++------ .../messaging/ReceiveMessagesTest.kt | 58 ++++++++++++++++++- .../awaladroid/test/MockContextTestCase.kt | 5 +- 3 files changed, 97 insertions(+), 22 deletions(-) diff --git a/lib/src/main/java/tech/relaycorp/awaladroid/messaging/ReceiveMessages.kt b/lib/src/main/java/tech/relaycorp/awaladroid/messaging/ReceiveMessages.kt index 55df5a82..02a9662b 100644 --- a/lib/src/main/java/tech/relaycorp/awaladroid/messaging/ReceiveMessages.kt +++ b/lib/src/main/java/tech/relaycorp/awaladroid/messaging/ReceiveMessages.kt @@ -2,6 +2,8 @@ package tech.relaycorp.awaladroid.messaging import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.onCompletion @@ -31,28 +33,43 @@ internal class ReceiveMessages( private val pdcClientBuilder: () -> PDCClient = { PoWebClient.initLocal(Awala.POWEB_PORT) }, ) { - @Throws( - ReceiveMessageException::class, - GatewayProtocolException::class, - PersistenceException::class, - ) + /** + * Flow may throw: + * - ReceiveMessageException + * - GatewayProtocolException + */ + @Throws(PersistenceException::class) fun receive(): Flow = getNonceSigners() .flatMapLatest { nonceSigners -> + if (nonceSigners.isEmpty()) { + logger.log( + Level.WARNING, + "Skipping parcel collection because there are no first-party endpoints", + ) + return@flatMapLatest emptyFlow() + } + val pdcClient = pdcClientBuilder() - try { - collectParcels(pdcClient, nonceSigners) - .onCompletion { - @Suppress("BlockingMethodInNonBlockingContext") - pdcClient.close() + collectParcels(pdcClient, nonceSigners) + .catch { + throw when (it) { + is ServerException -> + ReceiveMessageException("Server error", it) + + is ClientBindingException -> + GatewayProtocolException("Client error", it) + + is NonceSignerException -> + GatewayProtocolException("Client signing error", it) + + else -> it } - } catch (exp: ServerException) { - throw ReceiveMessageException("Server error", exp) - } catch (exp: ClientBindingException) { - throw GatewayProtocolException("Client error", exp) - } catch (exp: NonceSignerException) { - throw GatewayProtocolException("Client signing error", exp) - } + } + .onCompletion { + @Suppress("BlockingMethodInNonBlockingContext") + pdcClient.close() + } } @Throws(PersistenceException::class) @@ -77,6 +94,11 @@ internal class ReceiveMessages( .toTypedArray() }.asFlow() + /** + * Flow may throw: + * - ReceiveMessageException + * - GatewayProtocolException + */ @Throws(PersistenceException::class) private suspend fun collectParcels(pdcClient: PDCClient, nonceSigners: Array) = pdcClient diff --git a/lib/src/test/java/tech/relaycorp/awaladroid/messaging/ReceiveMessagesTest.kt b/lib/src/test/java/tech/relaycorp/awaladroid/messaging/ReceiveMessagesTest.kt index c064619e..55070735 100644 --- a/lib/src/test/java/tech/relaycorp/awaladroid/messaging/ReceiveMessagesTest.kt +++ b/lib/src/test/java/tech/relaycorp/awaladroid/messaging/ReceiveMessagesTest.kt @@ -1,14 +1,18 @@ package tech.relaycorp.awaladroid.messaging import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.toCollection import kotlinx.coroutines.test.runTest import nl.altindag.log.LogCaptor import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Test import tech.relaycorp.awaladroid.GatewayProtocolException +import tech.relaycorp.awaladroid.endpoint.FirstPartyEndpoint import tech.relaycorp.awaladroid.endpoint.PublicThirdPartyEndpointData import tech.relaycorp.awaladroid.test.EndpointChannel import tech.relaycorp.awaladroid.test.MockContextTestCase @@ -18,6 +22,7 @@ import tech.relaycorp.relaynet.bindings.pdc.NonceSignerException import tech.relaycorp.relaynet.bindings.pdc.ParcelCollection import tech.relaycorp.relaynet.bindings.pdc.ServerBindingException import tech.relaycorp.relaynet.issueDeliveryAuthorization +import tech.relaycorp.relaynet.issueEndpointCertificate import tech.relaycorp.relaynet.messages.Parcel import tech.relaycorp.relaynet.messages.Recipient import tech.relaycorp.relaynet.messages.payloads.CargoMessageSet @@ -27,6 +32,7 @@ import tech.relaycorp.relaynet.testing.pdc.MockPDCClient import tech.relaycorp.relaynet.testing.pki.KeyPairSet import tech.relaycorp.relaynet.testing.pki.PDACertPath import tech.relaycorp.relaynet.wrappers.generateECDHKeyPair +import tech.relaycorp.relaynet.wrappers.generateRSAKeyPair import tech.relaycorp.relaynet.wrappers.nodeId import java.time.ZonedDateTime @@ -72,7 +78,10 @@ internal class ReceiveMessagesTest : MockContextTestCase() { @Test(expected = ReceiveMessageException::class) fun collectParcelsGetsServerError() = runTest { - val collectParcelsCall = CollectParcelsCall(Result.failure(ServerBindingException(""))) + createFirstPartyEndpoint() + val collectParcelsCall = CollectParcelsCall( + Result.success(flow { throw ServerBindingException("") }), + ) pdcClient = MockPDCClient(collectParcelsCall) subject.receive().collect() @@ -80,7 +89,10 @@ internal class ReceiveMessagesTest : MockContextTestCase() { @Test(expected = GatewayProtocolException::class) fun collectParcelsGetsClientError() = runTest { - val collectParcelsCall = CollectParcelsCall(Result.failure(ClientBindingException(""))) + createFirstPartyEndpoint() + val collectParcelsCall = CollectParcelsCall( + Result.success(flow { throw ClientBindingException("") }), + ) pdcClient = MockPDCClient(collectParcelsCall) subject.receive().collect() @@ -88,14 +100,34 @@ internal class ReceiveMessagesTest : MockContextTestCase() { @Test(expected = GatewayProtocolException::class) fun collectParcelsGetsSigningError() = runTest { - val collectParcelsCall = CollectParcelsCall(Result.failure(NonceSignerException(""))) + createFirstPartyEndpoint() + val collectParcelsCall = CollectParcelsCall( + Result.success(flow { throw NonceSignerException("") }), + ) + pdcClient = MockPDCClient(collectParcelsCall) + + subject.receive().collect() + } + + @Test + fun collectParcelsWithoutFirstPartyEndpoints() = runTest { + val logCaptor = LogCaptor.forClass(ReceiveMessages::class.java) + val collectParcelsCall = CollectParcelsCall(Result.success(emptyFlow())) pdcClient = MockPDCClient(collectParcelsCall) subject.receive().collect() + + assertFalse(collectParcelsCall.wasCalled) + assertTrue( + logCaptor.warnLogs.contains( + "Skipping parcel collection because there are no first-party endpoints", + ), + ) } @Test fun receiveInvalidParcel_ackedButNotDeliveredToApp() = runTest { + createFirstPartyEndpoint() val invalidParcel = Parcel( recipient = Recipient(KeyPairSet.PRIVATE_ENDPOINT.public.nodeId), payload = "".toByteArray(), @@ -121,6 +153,7 @@ internal class ReceiveMessagesTest : MockContextTestCase() { @Test fun receiveMalformedParcel_ackedButNotDeliveredToApp() = runTest { + createFirstPartyEndpoint() var ackWasCalled = false val parcelCollection = ParcelCollection( parcelSerialized = "1234".toByteArray(), @@ -149,6 +182,7 @@ internal class ReceiveMessagesTest : MockContextTestCase() { pdcClient = MockPDCClient(collectParcelsCall) channel.firstPartyEndpoint.delete() + createAnotherFirstPartyEndpoint() val messages = subject.receive().toCollection(mutableListOf()) @@ -276,4 +310,22 @@ internal class ReceiveMessagesTest : MockContextTestCase() { trustedCertificates = listOf(PDACertPath.PRIVATE_ENDPOINT, PDACertPath.PRIVATE_GW), ack = ack, ) + + private suspend fun createAnotherFirstPartyEndpoint() { + val anotherKey = generateRSAKeyPair() + createFirstPartyEndpoint( + FirstPartyEndpoint( + anotherKey.private, // Different key + issueEndpointCertificate( + anotherKey.public, + KeyPairSet.PRIVATE_GW.private, + ZonedDateTime.now().plusHours(1), + PDACertPath.PRIVATE_GW, + validityStartDate = ZonedDateTime.now().minusMinutes(1), + ), + listOf(PDACertPath.PRIVATE_GW), + "frankfurt.relaycorp.cloud", + ), + ) + } } diff --git a/lib/src/test/java/tech/relaycorp/awaladroid/test/MockContextTestCase.kt b/lib/src/test/java/tech/relaycorp/awaladroid/test/MockContextTestCase.kt index ba24a4eb..788e2223 100644 --- a/lib/src/test/java/tech/relaycorp/awaladroid/test/MockContextTestCase.kt +++ b/lib/src/test/java/tech/relaycorp/awaladroid/test/MockContextTestCase.kt @@ -92,8 +92,9 @@ internal abstract class MockContextTestCase { ) } - protected suspend fun createFirstPartyEndpoint(): FirstPartyEndpoint { - val firstPartyEndpoint = FirstPartyEndpointFactory.build() + protected suspend fun createFirstPartyEndpoint( + firstPartyEndpoint: FirstPartyEndpoint = FirstPartyEndpointFactory.build(), + ): FirstPartyEndpoint { val gatewayAddress = "example.org" privateKeyStore.saveIdentityKey( firstPartyEndpoint.identityPrivateKey, From 9a66b957d1c0679c6f83c38c53c8ce312175ab24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Santos?= Date: Wed, 27 Sep 2023 11:29:43 +0200 Subject: [PATCH 06/14] fix: Receivers should await setup (#342) Closes #341 --- .../java/tech/relaycorp/awaladroid/Awala.kt | 48 +++++++++++++------ ...tewayCertificateChangeBroadcastReceiver.kt | 2 +- .../IncomingParcelBroadcastReceiver.kt | 2 +- .../tech/relaycorp/awaladroid/AwalaTest.kt | 33 +++++++++++-- .../awaladroid/test/AwalaContextUnits.kt | 5 +- 5 files changed, 69 insertions(+), 21 deletions(-) diff --git a/lib/src/main/java/tech/relaycorp/awaladroid/Awala.kt b/lib/src/main/java/tech/relaycorp/awaladroid/Awala.kt index e463e8e7..324b7566 100644 --- a/lib/src/main/java/tech/relaycorp/awaladroid/Awala.kt +++ b/lib/src/main/java/tech/relaycorp/awaladroid/Awala.kt @@ -1,8 +1,11 @@ package tech.relaycorp.awaladroid import android.content.Context +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch +import kotlinx.coroutines.withTimeout import tech.relaycorp.awala.keystores.file.FileCertificateStore import tech.relaycorp.awala.keystores.file.FileKeystoreRoot import tech.relaycorp.awala.keystores.file.FileSessionPublicKeystore @@ -15,6 +18,8 @@ import tech.relaycorp.awaladroid.storage.StorageImpl import tech.relaycorp.awaladroid.storage.persistence.DiskPersistence import tech.relaycorp.relaynet.nodes.EndpointManager import java.io.File +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds public object Awala { internal const val POWEB_PORT = 13276 @@ -39,19 +44,21 @@ public object Awala { val fileSessionPublicKeystore = FileSessionPublicKeystore(keystoreRoot) val fileCertificateStore = FileCertificateStore(keystoreRoot) - this.context = AwalaContext( - StorageImpl(DiskPersistence(context.filesDir.path.toString())), - GatewayClientImpl( - serviceInteractorBuilder = { ServiceInteractor(context) }, + contextDeferred.complete( + AwalaContext( + StorageImpl(DiskPersistence(context.filesDir.path.toString())), + GatewayClientImpl( + serviceInteractorBuilder = { ServiceInteractor(context) }, + ), + EndpointManager(androidPrivateKeyStore, fileSessionPublicKeystore), + ChannelManager { + context.getSharedPreferences("awaladroid-channels", Context.MODE_PRIVATE) + }, + androidPrivateKeyStore, + fileSessionPublicKeystore, + fileCertificateStore, + HandleGatewayCertificateChange(androidPrivateKeyStore), ), - EndpointManager(androidPrivateKeyStore, fileSessionPublicKeystore), - ChannelManager { - context.getSharedPreferences("awaladroid-channels", Context.MODE_PRIVATE) - }, - androidPrivateKeyStore, - fileSessionPublicKeystore, - fileCertificateStore, - HandleGatewayCertificateChange(androidPrivateKeyStore), ) coroutineScope { @@ -62,8 +69,21 @@ public object Awala { } } - internal var context: AwalaContext? = null - internal fun getContextOrThrow(): AwalaContext = context ?: throw SetupPendingException() + internal var contextDeferred: CompletableDeferred = CompletableDeferred() + + internal fun getContextOrThrow(): AwalaContext = try { + contextDeferred.getCompleted() + } catch (e: IllegalStateException) { + throw SetupPendingException() + } + + internal suspend fun awaitContextOrThrow(timeout: Duration = 3.seconds): AwalaContext = try { + withTimeout(timeout) { + contextDeferred.await() + } + } catch (e: TimeoutCancellationException) { + throw SetupPendingException() + } } /** diff --git a/lib/src/main/java/tech/relaycorp/awaladroid/background/GatewayCertificateChangeBroadcastReceiver.kt b/lib/src/main/java/tech/relaycorp/awaladroid/background/GatewayCertificateChangeBroadcastReceiver.kt index c22e3367..c1919135 100644 --- a/lib/src/main/java/tech/relaycorp/awaladroid/background/GatewayCertificateChangeBroadcastReceiver.kt +++ b/lib/src/main/java/tech/relaycorp/awaladroid/background/GatewayCertificateChangeBroadcastReceiver.kt @@ -15,7 +15,7 @@ internal class GatewayCertificateChangeBroadcastReceiver : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { CoroutineScope(coroutineContext).launch { - Awala.getContextOrThrow().handleGatewayCertificateChange() + Awala.awaitContextOrThrow().handleGatewayCertificateChange() } } } diff --git a/lib/src/main/java/tech/relaycorp/awaladroid/background/IncomingParcelBroadcastReceiver.kt b/lib/src/main/java/tech/relaycorp/awaladroid/background/IncomingParcelBroadcastReceiver.kt index c56d7560..d12ddcf2 100644 --- a/lib/src/main/java/tech/relaycorp/awaladroid/background/IncomingParcelBroadcastReceiver.kt +++ b/lib/src/main/java/tech/relaycorp/awaladroid/background/IncomingParcelBroadcastReceiver.kt @@ -15,7 +15,7 @@ internal class IncomingParcelBroadcastReceiver : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { CoroutineScope(coroutineContext).launch { - Awala.getContextOrThrow().gatewayClient.checkForNewMessages() + Awala.awaitContextOrThrow().gatewayClient.checkForNewMessages() } } } diff --git a/lib/src/test/java/tech/relaycorp/awaladroid/AwalaTest.kt b/lib/src/test/java/tech/relaycorp/awaladroid/AwalaTest.kt index 70c931b2..017dc116 100644 --- a/lib/src/test/java/tech/relaycorp/awaladroid/AwalaTest.kt +++ b/lib/src/test/java/tech/relaycorp/awaladroid/AwalaTest.kt @@ -3,13 +3,16 @@ package tech.relaycorp.awaladroid import android.content.Context import com.nhaarman.mockitokotlin2.spy import com.nhaarman.mockitokotlin2.verify +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull -import org.junit.Assert.assertThrows import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test @@ -26,6 +29,7 @@ import tech.relaycorp.relaynet.wrappers.nodeId import java.io.File import java.time.Duration import java.time.ZonedDateTime +import kotlin.time.Duration.Companion.milliseconds @RunWith(RobolectricTestRunner::class) public class AwalaTest { @@ -33,9 +37,9 @@ public class AwalaTest { @After public fun tearDownAwala(): Unit = unsetAwalaContext() - @Test + @Test(expected = SetupPendingException::class) public fun useBeforeSetup() { - assertThrows(SetupPendingException::class.java) { Awala.getContextOrThrow() } + Awala.getContextOrThrow() } @Test @@ -45,6 +49,29 @@ public class AwalaTest { Awala.getContextOrThrow() } + @Test(expected = SetupPendingException::class) + public fun awaitWithoutSetup(): Unit = runTest { + Awala.awaitContextOrThrow(100.milliseconds) + } + + @Test(expected = SetupPendingException::class) + public fun awaitWithLateSetup(): Unit = runTest { + CoroutineScope(UnconfinedTestDispatcher()).launch { + delay(200.milliseconds) + Awala.setUp(RuntimeEnvironment.getApplication()) + } + Awala.awaitContextOrThrow(100.milliseconds) + } + + @Test(expected = SetupPendingException::class) + public fun awaitAfterSetup(): Unit = runTest { + CoroutineScope(UnconfinedTestDispatcher()).launch { + delay(500.milliseconds) + Awala.setUp(RuntimeEnvironment.getApplication()) + } + Awala.awaitContextOrThrow(1000.milliseconds) + } + @Test public fun keystores(): Unit = runTest { val androidContext = RuntimeEnvironment.getApplication() diff --git a/lib/src/test/java/tech/relaycorp/awaladroid/test/AwalaContextUnits.kt b/lib/src/test/java/tech/relaycorp/awaladroid/test/AwalaContextUnits.kt index 87297c2c..2aa35239 100644 --- a/lib/src/test/java/tech/relaycorp/awaladroid/test/AwalaContextUnits.kt +++ b/lib/src/test/java/tech/relaycorp/awaladroid/test/AwalaContextUnits.kt @@ -1,12 +1,13 @@ package tech.relaycorp.awaladroid.test +import kotlinx.coroutines.CompletableDeferred import tech.relaycorp.awaladroid.Awala import tech.relaycorp.awaladroid.AwalaContext internal fun setAwalaContext(context: AwalaContext) { - Awala.context = context + Awala.contextDeferred = CompletableDeferred(context) } internal fun unsetAwalaContext() { - Awala.context = null + Awala.contextDeferred = CompletableDeferred() } From 8b84e97089ee45b5a22753ff63e9e8b703a6d86f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Santos?= Date: Wed, 27 Sep 2023 13:14:08 +0200 Subject: [PATCH 07/14] fix: Wrap internal Android encryption exception (#343) Closes #340 --- .../awaladroid/AndroidPrivateKeyStore.kt | 31 +++++++++++++++---- .../awaladroid/AndroidPrivateKeyStoreTest.kt | 11 +++++++ 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/lib/src/main/java/tech/relaycorp/awaladroid/AndroidPrivateKeyStore.kt b/lib/src/main/java/tech/relaycorp/awaladroid/AndroidPrivateKeyStore.kt index bcaed7f9..dd39611e 100644 --- a/lib/src/main/java/tech/relaycorp/awaladroid/AndroidPrivateKeyStore.kt +++ b/lib/src/main/java/tech/relaycorp/awaladroid/AndroidPrivateKeyStore.kt @@ -6,22 +6,38 @@ import androidx.security.crypto.MasterKey import tech.relaycorp.awala.keystores.file.FileKeystoreRoot import tech.relaycorp.awala.keystores.file.FilePrivateKeyStore import java.io.File +import javax.crypto.AEADBadTagException internal class AndroidPrivateKeyStore( root: FileKeystoreRoot, private val context: Context, -) : FilePrivateKeyStore(root) { - override fun makeEncryptedInputStream(file: File) = buildEncryptedFile(file).openFileInput() - - override fun makeEncryptedOutputStream(file: File) = buildEncryptedFile(file).openFileOutput() - - private fun buildEncryptedFile(file: File) = + private val encryptedFileBuilder: (File, MasterKey) -> EncryptedFile = { file, masterKey -> EncryptedFile.Builder( context, file, masterKey, EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB, ).build() + }, +) : FilePrivateKeyStore(root) { + + @Throws(EncryptionInitializationException::class) + override fun makeEncryptedInputStream(file: File) = buildEncryptedFile(file).openFileInput() + + @Throws(EncryptionInitializationException::class) + override fun makeEncryptedOutputStream(file: File) = buildEncryptedFile(file).openFileOutput() + + @Throws(EncryptionInitializationException::class) + private fun buildEncryptedFile(file: File): EncryptedFile = + try { + encryptedFileBuilder(file, masterKey) + } catch (exception: AEADBadTagException) { + // Known issue: https://issuetracker.google.com/issues/164901843 + throw EncryptionInitializationException( + "Could not build encrypted file due to internal issue", + exception, + ) + } private val masterKey by lazy { MasterKey.Builder(context, MASTER_KEY_ALIAS) @@ -33,3 +49,6 @@ internal class AndroidPrivateKeyStore( private const val MASTER_KEY_ALIAS = "_awaladroid_master_key_" } } + +public class EncryptionInitializationException(message: String, cause: Throwable) : + AwaladroidException(message, cause) diff --git a/lib/src/test/java/tech/relaycorp/awaladroid/AndroidPrivateKeyStoreTest.kt b/lib/src/test/java/tech/relaycorp/awaladroid/AndroidPrivateKeyStoreTest.kt index 6fe6d06c..1b730134 100644 --- a/lib/src/test/java/tech/relaycorp/awaladroid/AndroidPrivateKeyStoreTest.kt +++ b/lib/src/test/java/tech/relaycorp/awaladroid/AndroidPrivateKeyStoreTest.kt @@ -12,6 +12,7 @@ import tech.relaycorp.awaladroid.test.FakeAndroidKeyStore import tech.relaycorp.relaynet.testing.pki.KeyPairSet import tech.relaycorp.relaynet.testing.pki.PDACertPath import java.io.File +import javax.crypto.AEADBadTagException @RunWith(RobolectricTestRunner::class) public class AndroidPrivateKeyStoreTest { @@ -33,4 +34,14 @@ public class AndroidPrivateKeyStoreTest { val retrievedId = store.retrieveIdentityKey(certificate.subjectId) assertEquals(id, retrievedId) } + + @Test(expected = EncryptionInitializationException::class) + public fun failWithAEADBadTagException(): Unit = runTest { + val androidContext = RuntimeEnvironment.getApplication() + val root = FileKeystoreRoot(File(androidContext.filesDir, "tmp-keystore")) + val store = AndroidPrivateKeyStore(root, androidContext) { _, _ -> + throw AEADBadTagException("") + } + store.saveIdentityKey(KeyPairSet.PRIVATE_ENDPOINT.private) + } } From b3635a1ae5e4ec854bae32f0d649e2cc807cd0e0 Mon Sep 17 00:00:00 2001 From: Gus Narea Date: Wed, 4 Oct 2023 16:55:23 +0100 Subject: [PATCH 08/14] feat: Throw `GatewayUnregisteredException` when gateway is not registered (#347) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit … with its Internet gateway --- .../java/tech/relaycorp/awaladroid/Awala.kt | 1 + .../relaycorp/awaladroid/GatewayClientImpl.kt | 34 ++++++++++++++----- .../awaladroid/endpoint/FirstPartyEndpoint.kt | 3 ++ .../HandleGatewayCertificateChange.kt | 2 ++ .../endpoint/RenewExpiringCertificates.kt | 2 ++ .../awaladroid/GatewayClientImplTest.kt | 13 +++++++ 6 files changed, 47 insertions(+), 8 deletions(-) diff --git a/lib/src/main/java/tech/relaycorp/awaladroid/Awala.kt b/lib/src/main/java/tech/relaycorp/awaladroid/Awala.kt index 324b7566..9abbe073 100644 --- a/lib/src/main/java/tech/relaycorp/awaladroid/Awala.kt +++ b/lib/src/main/java/tech/relaycorp/awaladroid/Awala.kt @@ -37,6 +37,7 @@ public object Awala { /** * Set up the endpoint library. */ + @Throws(GatewayUnregisteredException::class) public suspend fun setUp(context: Context) { val keystoreRoot = FileKeystoreRoot(File(context.filesDir, "awaladroid${File.separator}keystores")) diff --git a/lib/src/main/java/tech/relaycorp/awaladroid/GatewayClientImpl.kt b/lib/src/main/java/tech/relaycorp/awaladroid/GatewayClientImpl.kt index aaa1dbff..3758e790 100644 --- a/lib/src/main/java/tech/relaycorp/awaladroid/GatewayClientImpl.kt +++ b/lib/src/main/java/tech/relaycorp/awaladroid/GatewayClientImpl.kt @@ -89,6 +89,7 @@ internal constructor( @Throws( RegistrationFailedException::class, GatewayProtocolException::class, + GatewayUnregisteredException::class, ) internal suspend fun registerEndpoint(keyPair: KeyPair): PrivateNodeRegistration = withContext(coroutineContext) { @@ -119,6 +120,7 @@ internal constructor( ServiceInteractor.BindFailedException::class, ServiceInteractor.SendFailedException::class, GatewayProtocolException::class, + GatewayUnregisteredException::class, ) private suspend fun preRegister(): ByteArray { val interactor = serviceInteractorBuilder().apply { @@ -132,15 +134,24 @@ internal constructor( return suspendCoroutine { cont -> val request = android.os.Message.obtain(null, PREREGISTRATION_REQUEST) interactor.sendMessage(request) { replyMessage -> - if (replyMessage.what != REGISTRATION_AUTHORIZATION) { - interactor.unbind() - cont.resumeWithException( - GatewayProtocolException("Pre-registration failed, received wrong reply"), - ) - return@sendMessage - } interactor.unbind() - cont.resume(replyMessage.data.getByteArray("auth")!!) + when (replyMessage.what) { + REGISTRATION_AUTHORIZATION -> { + cont.resume(replyMessage.data.getByteArray("auth")!!) + } + GATEWAY_NOT_REGISTERED -> { + cont.resumeWithException( + GatewayUnregisteredException("Gateway not registered"), + ) + } + else -> { + cont.resumeWithException( + GatewayProtocolException( + "Pre-registration failed, received wrong reply", + ), + ) + } + } } } } @@ -209,6 +220,7 @@ internal constructor( internal companion object { internal const val PREREGISTRATION_REQUEST = 1 internal const val REGISTRATION_AUTHORIZATION = 2 + internal const val GATEWAY_NOT_REGISTERED = 4 } } @@ -230,6 +242,12 @@ public open class GatewayProtocolException(message: String, cause: Throwable? = public class GatewayBindingException(message: String, cause: Throwable? = null) : GatewayException(message, cause) +/** + * The gateway isn't yet registered with its Internet peer. + */ +public class GatewayUnregisteredException(message: String, cause: Throwable? = null) : + GatewayException(message, cause) + /** * Failure to register a first-party endpoint. */ diff --git a/lib/src/main/java/tech/relaycorp/awaladroid/endpoint/FirstPartyEndpoint.kt b/lib/src/main/java/tech/relaycorp/awaladroid/endpoint/FirstPartyEndpoint.kt index 840dc776..57001eb7 100644 --- a/lib/src/main/java/tech/relaycorp/awaladroid/endpoint/FirstPartyEndpoint.kt +++ b/lib/src/main/java/tech/relaycorp/awaladroid/endpoint/FirstPartyEndpoint.kt @@ -3,6 +3,7 @@ package tech.relaycorp.awaladroid.endpoint import tech.relaycorp.awaladroid.Awala import tech.relaycorp.awaladroid.AwaladroidException import tech.relaycorp.awaladroid.GatewayProtocolException +import tech.relaycorp.awaladroid.GatewayUnregisteredException import tech.relaycorp.awaladroid.RegistrationFailedException import tech.relaycorp.awaladroid.SetupPendingException import tech.relaycorp.awaladroid.common.Logging.logger @@ -153,6 +154,7 @@ internal constructor( @Throws( RegistrationFailedException::class, GatewayProtocolException::class, + GatewayUnregisteredException::class, PersistenceException::class, SetupPendingException::class, ) @@ -228,6 +230,7 @@ internal constructor( @Throws( RegistrationFailedException::class, GatewayProtocolException::class, + GatewayUnregisteredException::class, PersistenceException::class, SetupPendingException::class, ) diff --git a/lib/src/main/java/tech/relaycorp/awaladroid/endpoint/HandleGatewayCertificateChange.kt b/lib/src/main/java/tech/relaycorp/awaladroid/endpoint/HandleGatewayCertificateChange.kt index 9b210e9c..d9fd6fa0 100644 --- a/lib/src/main/java/tech/relaycorp/awaladroid/endpoint/HandleGatewayCertificateChange.kt +++ b/lib/src/main/java/tech/relaycorp/awaladroid/endpoint/HandleGatewayCertificateChange.kt @@ -1,5 +1,6 @@ package tech.relaycorp.awaladroid.endpoint +import tech.relaycorp.awaladroid.GatewayUnregisteredException import tech.relaycorp.relaynet.keystores.PrivateKeyStore import tech.relaycorp.relaynet.wrappers.nodeId @@ -7,6 +8,7 @@ internal class HandleGatewayCertificateChange( private val privateKeyStore: PrivateKeyStore, ) { + @Throws(GatewayUnregisteredException::class) suspend operator fun invoke() { privateKeyStore.retrieveAllIdentityKeys() .mapNotNull { FirstPartyEndpoint.load(it.nodeId) } diff --git a/lib/src/main/java/tech/relaycorp/awaladroid/endpoint/RenewExpiringCertificates.kt b/lib/src/main/java/tech/relaycorp/awaladroid/endpoint/RenewExpiringCertificates.kt index 2e35f620..b654c328 100644 --- a/lib/src/main/java/tech/relaycorp/awaladroid/endpoint/RenewExpiringCertificates.kt +++ b/lib/src/main/java/tech/relaycorp/awaladroid/endpoint/RenewExpiringCertificates.kt @@ -1,5 +1,6 @@ package tech.relaycorp.awaladroid.endpoint +import tech.relaycorp.awaladroid.GatewayUnregisteredException import tech.relaycorp.relaynet.keystores.PrivateKeyStore import tech.relaycorp.relaynet.wrappers.nodeId import tech.relaycorp.relaynet.wrappers.x509.Certificate @@ -11,6 +12,7 @@ internal class RenewExpiringCertificates( private val firstPartyEndpointLoader: suspend (String) -> FirstPartyEndpoint?, ) { + @Throws(GatewayUnregisteredException::class) suspend operator fun invoke() { privateKeyStore.retrieveAllIdentityKeys() .mapNotNull { firstPartyEndpointLoader(it.nodeId) } diff --git a/lib/src/test/java/tech/relaycorp/awaladroid/GatewayClientImplTest.kt b/lib/src/test/java/tech/relaycorp/awaladroid/GatewayClientImplTest.kt index c1db7f70..e3bf41c1 100644 --- a/lib/src/test/java/tech/relaycorp/awaladroid/GatewayClientImplTest.kt +++ b/lib/src/test/java/tech/relaycorp/awaladroid/GatewayClientImplTest.kt @@ -170,6 +170,19 @@ internal class GatewayClientImplTest : MockContextTestCase() { gatewayClient.registerEndpoint(KeyPairSet.PRIVATE_ENDPOINT) } + @Test(expected = GatewayUnregisteredException::class) + internal fun registerEndpoint_withFailedRegistrationDueToGatewayUnregistered() = + coroutineScope.runTest { + val replyMessage = Message.obtain(null, GatewayClientImpl.GATEWAY_NOT_REGISTERED) + whenever(serviceInteractor.sendMessage(any(), any())).thenAnswer { + it.getArgument<((Message) -> Unit)?>(1)(replyMessage) + } + + pdcClient = MockPDCClient() + + gatewayClient.registerEndpoint(KeyPairSet.PRIVATE_ENDPOINT) + } + private fun buildPnra() = PrivateNodeRegistrationAuthorization( ZonedDateTime.now().plusDays(1), PDACertPath.PRIVATE_GW.serialize(), From 377109077c5d7057c378e7eea794887159b5530f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Santos?= Date: Tue, 10 Oct 2023 19:06:25 +0200 Subject: [PATCH 09/14] fix(deps): Update awala dependencies (#348) --- lib/build.gradle | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/build.gradle b/lib/build.gradle index 6c73e5a6..9e41dcf0 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -64,10 +64,10 @@ dependencies { implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinCoroutinesVersion" // Awala - implementation 'tech.relaycorp:awala:1.67.3' - implementation 'tech.relaycorp:awala-keystore-file:1.6.13' - implementation 'tech.relaycorp:poweb:1.5.35' - testImplementation 'tech.relaycorp:awala-testing:1.5.22' + implementation 'tech.relaycorp:awala:1.67.10' + implementation 'tech.relaycorp:awala-keystore-file:1.6.31' + implementation 'tech.relaycorp:poweb:1.5.68' + testImplementation 'tech.relaycorp:awala-testing:1.5.24' // Security implementation 'androidx.security:security-crypto:1.1.0-alpha06' From f7643d0df5f2846154b5c26b13bc2f999fbb97c1 Mon Sep 17 00:00:00 2001 From: Gus Narea Date: Tue, 24 Oct 2023 15:57:02 +0100 Subject: [PATCH 10/14] fix(PrivateKeyStore): Set a explicit keyset preference name (#352) To avoid cryptic `AEADBadTagException`s when multiple `MasterKey`s are used by the app. --- .../tech/relaycorp/awaladroid/AndroidPrivateKeyStore.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/src/main/java/tech/relaycorp/awaladroid/AndroidPrivateKeyStore.kt b/lib/src/main/java/tech/relaycorp/awaladroid/AndroidPrivateKeyStore.kt index dd39611e..b3c4d6e0 100644 --- a/lib/src/main/java/tech/relaycorp/awaladroid/AndroidPrivateKeyStore.kt +++ b/lib/src/main/java/tech/relaycorp/awaladroid/AndroidPrivateKeyStore.kt @@ -17,7 +17,11 @@ internal class AndroidPrivateKeyStore( file, masterKey, EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB, - ).build() + ) + // Set a explicit preference name to avoid cryptic `AEADBadTagException`s when multiple + // `MasterKey`s are used by the app. + .setKeysetPrefName(ENCRYPTED_FILE_PREFERENCE_NAME) + .build() }, ) : FilePrivateKeyStore(root) { @@ -47,6 +51,7 @@ internal class AndroidPrivateKeyStore( companion object { private const val MASTER_KEY_ALIAS = "_awaladroid_master_key_" + private const val ENCRYPTED_FILE_PREFERENCE_NAME = "awala-private-key-store" } } From 0c8033e09f5455e4a64f341719b02915e66c2ee4 Mon Sep 17 00:00:00 2001 From: Gus Narea Date: Sat, 28 Oct 2023 10:41:24 +0100 Subject: [PATCH 11/14] fix(OutgoingMessage): Throw `InvalidMessageException` when parcel is too big (#353) - [x] Upgrade Awala lib to use newly-exported constant. - [x] Export max size of parcel payload. --- lib/build.gradle | 2 +- .../messaging/InvalidMessageException.kt | 11 ++++++++ .../relaycorp/awaladroid/messaging/Message.kt | 14 +++++++++- .../awaladroid/messaging/OutgoingMessage.kt | 28 ++++++++++++------- .../awaladroid/messaging/MessageTest.kt | 14 ++++++++++ .../messaging/OutgoingMessageTest.kt | 24 +++++++++++++++- 6 files changed, 80 insertions(+), 13 deletions(-) create mode 100644 lib/src/main/java/tech/relaycorp/awaladroid/messaging/InvalidMessageException.kt create mode 100644 lib/src/test/java/tech/relaycorp/awaladroid/messaging/MessageTest.kt diff --git a/lib/build.gradle b/lib/build.gradle index 9e41dcf0..141940c8 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -64,7 +64,7 @@ dependencies { implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinCoroutinesVersion" // Awala - implementation 'tech.relaycorp:awala:1.67.10' + implementation 'tech.relaycorp:awala:1.68.0' implementation 'tech.relaycorp:awala-keystore-file:1.6.31' implementation 'tech.relaycorp:poweb:1.5.68' testImplementation 'tech.relaycorp:awala-testing:1.5.24' diff --git a/lib/src/main/java/tech/relaycorp/awaladroid/messaging/InvalidMessageException.kt b/lib/src/main/java/tech/relaycorp/awaladroid/messaging/InvalidMessageException.kt new file mode 100644 index 00000000..82178a5d --- /dev/null +++ b/lib/src/main/java/tech/relaycorp/awaladroid/messaging/InvalidMessageException.kt @@ -0,0 +1,11 @@ +package tech.relaycorp.awaladroid.messaging + +import tech.relaycorp.awaladroid.AwaladroidException + +/** + * Exception thrown when an incoming or outgoing service message is invalid. + */ +public class InvalidMessageException( + message: String, + cause: Throwable, +) : AwaladroidException(message, cause) diff --git a/lib/src/main/java/tech/relaycorp/awaladroid/messaging/Message.kt b/lib/src/main/java/tech/relaycorp/awaladroid/messaging/Message.kt index 21d52678..14edf999 100644 --- a/lib/src/main/java/tech/relaycorp/awaladroid/messaging/Message.kt +++ b/lib/src/main/java/tech/relaycorp/awaladroid/messaging/Message.kt @@ -1,6 +1,18 @@ package tech.relaycorp.awaladroid.messaging +import tech.relaycorp.relaynet.ramf.RAMFMessage + /** * A service message. */ -public abstract class Message +public abstract class Message { + public companion object { + private const val PESSIMISTIC_CMS_ENVELOPEDDATA_OVERHEAD_OCTETS = 1024 + + /** + * The maximum size of the content of a message. + */ + public const val MAX_CONTENT_SIZE: Int = + RAMFMessage.MAX_PAYLOAD_LENGTH - PESSIMISTIC_CMS_ENVELOPEDDATA_OVERHEAD_OCTETS + } +} diff --git a/lib/src/main/java/tech/relaycorp/awaladroid/messaging/OutgoingMessage.kt b/lib/src/main/java/tech/relaycorp/awaladroid/messaging/OutgoingMessage.kt index 5dff00ef..798c69eb 100644 --- a/lib/src/main/java/tech/relaycorp/awaladroid/messaging/OutgoingMessage.kt +++ b/lib/src/main/java/tech/relaycorp/awaladroid/messaging/OutgoingMessage.kt @@ -8,6 +8,7 @@ import tech.relaycorp.awaladroid.endpoint.ThirdPartyEndpoint import tech.relaycorp.relaynet.issueEndpointCertificate import tech.relaycorp.relaynet.messages.Parcel import tech.relaycorp.relaynet.messages.payloads.ServiceMessage +import tech.relaycorp.relaynet.ramf.RAMFException import tech.relaycorp.relaynet.wrappers.x509.Certificate import java.time.Duration import java.time.ZonedDateTime @@ -43,13 +44,14 @@ private constructor( /** * Create an outgoing service message (but don't send it). * - * @param type The type of the message (e.g., "application/vnd.relaynet.ping-v1.ping"). + * @param type The type of the message (e.g., "application/vnd.awala.ping-v1.ping"). * @param content The contents of the service message. * @param senderEndpoint The endpoint used to send the message. * @param recipientEndpoint The endpoint that will receive the message. * @param parcelExpiryDate The date when the parcel should expire. * @param parcelId The id of the parcel. */ + @Throws(InvalidMessageException::class) public suspend fun build( type: String, content: ByteArray, @@ -70,6 +72,7 @@ private constructor( } } + @Throws(InvalidMessageException::class) private suspend fun buildParcel( serviceMessageType: String, serviceMessageContent: ByteArray, @@ -81,15 +84,20 @@ private constructor( recipientEndpoint.nodeId, senderEndpoint.nodeId, ) - return Parcel( - recipient = recipientEndpoint.recipient, - payload = payload, - senderCertificate = getSenderCertificate(), - messageId = parcelId.value, - creationDate = parcelCreationDate, - ttl = ttl, - senderCertificateChain = getSenderCertificateChain(), - ) + val parcel = try { + Parcel( + recipient = recipientEndpoint.recipient, + payload = payload, + senderCertificate = getSenderCertificate(), + messageId = parcelId.value, + creationDate = parcelCreationDate, + ttl = ttl, + senderCertificateChain = getSenderCertificateChain(), + ) + } catch (exc: RAMFException) { + throw InvalidMessageException("Failed to create parcel", exc) + } + return parcel } private fun getSenderCertificate(): Certificate = diff --git a/lib/src/test/java/tech/relaycorp/awaladroid/messaging/MessageTest.kt b/lib/src/test/java/tech/relaycorp/awaladroid/messaging/MessageTest.kt new file mode 100644 index 00000000..59bf397c --- /dev/null +++ b/lib/src/test/java/tech/relaycorp/awaladroid/messaging/MessageTest.kt @@ -0,0 +1,14 @@ +package tech.relaycorp.awaladroid.messaging + +import org.junit.Assert +import org.junit.Test +import tech.relaycorp.relaynet.ramf.RAMFMessage + +public class MessageTest { + @Test + public fun maxContentSize() { + val expectedMax = RAMFMessage.MAX_PAYLOAD_LENGTH - 1024 + + Assert.assertEquals(Message.MAX_CONTENT_SIZE, expectedMax) + } +} diff --git a/lib/src/test/java/tech/relaycorp/awaladroid/messaging/OutgoingMessageTest.kt b/lib/src/test/java/tech/relaycorp/awaladroid/messaging/OutgoingMessageTest.kt index 881738b5..fc0a0348 100644 --- a/lib/src/test/java/tech/relaycorp/awaladroid/messaging/OutgoingMessageTest.kt +++ b/lib/src/test/java/tech/relaycorp/awaladroid/messaging/OutgoingMessageTest.kt @@ -1,8 +1,10 @@ package tech.relaycorp.awaladroid.messaging +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest import org.junit.Assert.assertArrayEquals import org.junit.Assert.assertEquals +import org.junit.Assert.assertThrows import org.junit.Assert.assertTrue import org.junit.Test import tech.relaycorp.awaladroid.endpoint.PrivateThirdPartyEndpoint @@ -11,13 +13,14 @@ import tech.relaycorp.awaladroid.test.MessageFactory import tech.relaycorp.awaladroid.test.MockContextTestCase import tech.relaycorp.awaladroid.test.RecipientAddressType import tech.relaycorp.awaladroid.test.assertSameDateTime +import tech.relaycorp.relaynet.ramf.RAMFException +import tech.relaycorp.relaynet.ramf.RAMFMessage import java.time.Duration import java.time.ZonedDateTime import kotlin.math.abs import kotlin.random.Random internal class OutgoingMessageTest : MockContextTestCase() { - @Test fun build_creationDate() = runTest { val channel = createEndpointChannel(RecipientAddressType.PRIVATE) @@ -60,6 +63,25 @@ internal class OutgoingMessageTest : MockContextTestCase() { assertTrue(abs(differenceSeconds) < 3) } + @Test + fun build_bigServiceMessage() = runTest { + val (senderEndpoint, recipientEndpoint) = createEndpointChannel(RecipientAddressType.PUBLIC) + + val exception = assertThrows(InvalidMessageException::class.java) { + runBlocking { + OutgoingMessage.build( + "the type", + ByteArray(RAMFMessage.MAX_PAYLOAD_LENGTH + 1), + senderEndpoint, + recipientEndpoint, + ) + } + } + + assertEquals("Failed to create parcel", exception.message) + assertTrue(exception.cause is RAMFException) + } + // Public Recipient @Test From e965f63e06f607acf32cf6067ed4b7c5750d2fdc Mon Sep 17 00:00:00 2001 From: Gus Narea Date: Sun, 29 Oct 2023 22:26:48 +0000 Subject: [PATCH 12/14] feat(FirstPartyEndpoint): Expose third-party node id when authorising (#354) Needed in https://github.com/relaycorp/letro-android/pull/162 --- .../awaladroid/endpoint/FirstPartyEndpoint.kt | 15 ++++++++------- .../endpoint/ThirdPartyEndpointAuth.kt | 16 ++++++++++++++++ .../endpoint/FirstPartyEndpointTest.kt | 13 +++++++++++++ 3 files changed, 37 insertions(+), 7 deletions(-) create mode 100644 lib/src/main/java/tech/relaycorp/awaladroid/endpoint/ThirdPartyEndpointAuth.kt diff --git a/lib/src/main/java/tech/relaycorp/awaladroid/endpoint/FirstPartyEndpoint.kt b/lib/src/main/java/tech/relaycorp/awaladroid/endpoint/FirstPartyEndpoint.kt index 57001eb7..ed0c5b2b 100644 --- a/lib/src/main/java/tech/relaycorp/awaladroid/endpoint/FirstPartyEndpoint.kt +++ b/lib/src/main/java/tech/relaycorp/awaladroid/endpoint/FirstPartyEndpoint.kt @@ -57,7 +57,7 @@ internal constructor( issueAuthorization( thirdPartyEndpoint.identityKey, expiryDate, - ) + ).auth /** * Issue a PDA for a third-party endpoint using its public key. @@ -66,7 +66,7 @@ internal constructor( public suspend fun issueAuthorization( thirdPartyEndpointPublicKeySerialized: ByteArray, expiryDate: ZonedDateTime, - ): ByteArray { + ): ThirdPartyEndpointAuth { val thirdPartyEndpointPublicKey = deserializePDAGranteePublicKey(thirdPartyEndpointPublicKeySerialized) return issueAuthorization(thirdPartyEndpointPublicKey, expiryDate) @@ -76,7 +76,7 @@ internal constructor( private suspend fun issueAuthorization( thirdPartyEndpointPublicKey: PublicKey, expiryDate: ZonedDateTime, - ): ByteArray { + ): ThirdPartyEndpointAuth { val pda = issueDeliveryAuthorization( subjectPublicKey = thirdPartyEndpointPublicKey, issuerPrivateKey = identityPrivateKey, @@ -97,7 +97,8 @@ internal constructor( deliveryAuth, sessionKeyPair.sessionKey, ) - return connParams.serialize() + val authSerialized = connParams.serialize() + return ThirdPartyEndpointAuth(thirdPartyEndpointPublicKey.nodeId, authSerialized) } /** @@ -107,7 +108,7 @@ internal constructor( public suspend fun authorizeIndefinitely( thirdPartyEndpoint: ThirdPartyEndpoint, ): ByteArray = - authorizeIndefinitely(thirdPartyEndpoint.identityKey) + authorizeIndefinitely(thirdPartyEndpoint.identityKey).auth /** * Issue a PDA for a third-party endpoint (using its public key) and renew it indefinitely. @@ -115,7 +116,7 @@ internal constructor( @Throws(CertificateException::class) public suspend fun authorizeIndefinitely( thirdPartyEndpointPublicKeySerialized: ByteArray, - ): ByteArray { + ): ThirdPartyEndpointAuth { val thirdPartyEndpointPublicKey = deserializePDAGranteePublicKey(thirdPartyEndpointPublicKeySerialized) return authorizeIndefinitely(thirdPartyEndpointPublicKey) @@ -124,7 +125,7 @@ internal constructor( @Throws(CertificateException::class) private suspend fun authorizeIndefinitely( thirdPartyEndpointPublicKey: PublicKey, - ): ByteArray { + ): ThirdPartyEndpointAuth { val authorization = issueAuthorization(thirdPartyEndpointPublicKey, identityCertificate.expiryDate) diff --git a/lib/src/main/java/tech/relaycorp/awaladroid/endpoint/ThirdPartyEndpointAuth.kt b/lib/src/main/java/tech/relaycorp/awaladroid/endpoint/ThirdPartyEndpointAuth.kt new file mode 100644 index 00000000..a3951446 --- /dev/null +++ b/lib/src/main/java/tech/relaycorp/awaladroid/endpoint/ThirdPartyEndpointAuth.kt @@ -0,0 +1,16 @@ +package tech.relaycorp.awaladroid.endpoint + +/** + * Parcel delivery authorization for a third-party endpoint. + */ +public class ThirdPartyEndpointAuth( + /** + * Id of the third-party endpoint. + */ + public val endpointId: String, + + /** + * The authorization serialized. + */ + public val auth: ByteArray, +) diff --git a/lib/src/test/java/tech/relaycorp/awaladroid/endpoint/FirstPartyEndpointTest.kt b/lib/src/test/java/tech/relaycorp/awaladroid/endpoint/FirstPartyEndpointTest.kt index f83ca4b3..a9a618db 100644 --- a/lib/src/test/java/tech/relaycorp/awaladroid/endpoint/FirstPartyEndpointTest.kt +++ b/lib/src/test/java/tech/relaycorp/awaladroid/endpoint/FirstPartyEndpointTest.kt @@ -442,6 +442,19 @@ internal class FirstPartyEndpointTest : MockContextTestCase() { } } +private fun validateAuthorization( + auth: ThirdPartyEndpointAuth, + firstPartyEndpoint: FirstPartyEndpoint, + expiryDate: ZonedDateTime, +) { + assertEquals( + auth.endpointId, + KeyPairSet.PDA_GRANTEE.public.nodeId, + ) + + validateAuthorization(auth.auth, firstPartyEndpoint, expiryDate) +} + private fun validateAuthorization( paramsSerialized: ByteArray, firstPartyEndpoint: FirstPartyEndpoint, From d718ec6c9c6b8a664e504efaa66485ec22d6c6dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Santos?= Date: Tue, 28 Nov 2023 16:37:24 +0000 Subject: [PATCH 13/14] fix(deps): Update Gradle and AGP to 8 (#357) Addresses https://github.com/relaycorp/relayverse/issues/48 --------- Co-authored-by: Gus Narea --- .github/workflows/ci.yml | 2 +- build.gradle | 4 +- gradle.properties | 2 +- gradle/wrapper/gradle-wrapper.jar | Bin 59536 -> 62076 bytes gradle/wrapper/gradle-wrapper.properties | 6 +- gradlew | 276 ++++++++++++++--------- gradlew.bat | 15 +- jitpack.yml | 2 +- lib/build.gradle | 30 +-- lib/src/main/AndroidManifest.xml | 3 +- 10 files changed, 202 insertions(+), 138 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6c25eff3..8e3577c3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,7 +6,7 @@ jobs: ci: uses: relaycorp/shared-workflows/.github/workflows/jvm-lib-ci.yml@main with: - java_versions: "[11]" + java_versions: "[17]" release: needs: ci diff --git a/build.gradle b/build.gradle index c06301d1..4e3726e3 100644 --- a/build.gradle +++ b/build.gradle @@ -7,9 +7,9 @@ buildscript { google() } dependencies { - classpath 'com.android.tools.build:gradle:7.2.1' + classpath 'com.android.tools.build:gradle:8.1.4' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" - classpath("org.jetbrains.dokka:dokka-core:1.7.10") + classpath("org.jetbrains.dokka:dokka-core:1.9.10") } } diff --git a/gradle.properties b/gradle.properties index fc193d9a..d78fbdbc 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ ## For more details on how to configure your build environment visit # http://www.gradle.org/docs/current/userguide/build_environment.html -org.gradle.jvmargs=-Xmx4096M -XX:MaxPermSize=512m +org.gradle.jvmargs=-Xmx2048m # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 7454180f2ae8848c63b8b4dea2cb829da983f2fa..c1962a79e29d3e0ab67b14947c167a862655af9b 100644 GIT binary patch delta 40133 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 \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MSYS* | MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar @@ -87,9 +118,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -98,7 +129,7 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" + JAVACMD=java which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the @@ -106,80 +137,109 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=`expr $i + 1` + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg done - case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=`save "$@"` -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index 107acd32..93e3f59f 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -14,7 +14,7 @@ @rem limitations under the License. @rem -@if "%DEBUG%" == "" @echo off +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -25,7 +25,8 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -40,7 +41,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute +if %ERRORLEVEL% equ 0 goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -75,13 +76,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal diff --git a/jitpack.yml b/jitpack.yml index 1ad2f6be..4d28ed38 100644 --- a/jitpack.yml +++ b/jitpack.yml @@ -1,3 +1,3 @@ # configuration file for building snapshots and releases with jitpack.io jdk: - - openjdk11 + - openjdk17 diff --git a/lib/build.gradle b/lib/build.gradle index 141940c8..cbf0b889 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -4,20 +4,20 @@ plugins { id 'kotlin-kapt' id 'maven-publish' id 'org.jlleitschuh.gradle.ktlint' version "11.5.0" - id 'org.jetbrains.dokka' version "1.7.10" + id 'org.jetbrains.dokka' version "1.9.10" } apply from: 'jacoco.gradle' android { - compileSdkVersion 33 - buildToolsVersion "30.0.3" + compileSdk 33 defaultConfig { - minSdkVersion 23 - targetSdkVersion 33 + minSdk 23 + targetSdk 33 versionCode 1 versionName "1.0.0" + namespace 'tech.relaycorp.awaladroid' testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles "consumer-rules.pro" @@ -34,11 +34,11 @@ android { } compileOptions { coreLibraryDesugaringEnabled true - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = JavaVersion.VERSION_1_8 + jvmTarget = JavaVersion.VERSION_17 freeCompilerArgs += "-Xexplicit-api=strict" } testOptions { @@ -48,11 +48,13 @@ android { } } packagingOptions { - exclude 'META-INF/AL2.0' - exclude 'META-INF/LGPL2' - exclude 'META-INF/LGPL2.1' - exclude 'META-INF/licenses/*' - exclude '**/attach_hotspot_windows.dll' + jniLibs { + useLegacyPackaging false + excludes += ['META-INF/AL2.0', 'META-INF/LGPL2', 'META-INF/LGPL2.1', 'META-INF/licenses/*'] + } + resources { + excludes += ['META-INF/AL2.0', 'META-INF/LGPL2', 'META-INF/LGPL2.1', 'META-INF/licenses/*', '**/attach_hotspot_windows.dll'] + } } } @@ -100,7 +102,7 @@ dependencies { tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { kotlinOptions { - jvmTarget = JavaVersion.VERSION_1_8 + jvmTarget = JavaVersion.VERSION_17 freeCompilerArgs += [ '-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi', '-opt-in=kotlinx.coroutines.FlowPreview', diff --git a/lib/src/main/AndroidManifest.xml b/lib/src/main/AndroidManifest.xml index c960be75..badd0bae 100644 --- a/lib/src/main/AndroidManifest.xml +++ b/lib/src/main/AndroidManifest.xml @@ -1,7 +1,6 @@ + xmlns:tools="http://schemas.android.com/tools"> From 9f6c1bbb105b9306da78231a810d4770a6d32020 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Santos?= Date: Tue, 12 Dec 2023 15:19:56 +0000 Subject: [PATCH 14/14] fix(deps): Update Kotlin to 1.9 (#358) Addresses https://github.com/relaycorp/relayverse/issues/47 --- .editorconfig | 8 +- build.gradle | 4 +- lib/build.gradle | 10 +- .../awaladroid/AndroidPrivateKeyStore.kt | 1 - .../java/tech/relaycorp/awaladroid/Awala.kt | 24 +- .../relaycorp/awaladroid/GatewayClientImpl.kt | 323 ++++----- ...tewayCertificateChangeBroadcastReceiver.kt | 6 +- .../IncomingParcelBroadcastReceiver.kt | 6 +- .../background/ServiceInteractor.kt | 56 +- .../tech/relaycorp/awaladroid/common/Keys.kt | 3 +- .../relaycorp/awaladroid/common/Logging.kt | 1 - .../awaladroid/endpoint/ChannelManager.kt | 8 +- .../awaladroid/endpoint/FirstPartyEndpoint.kt | 478 +++++++------ .../HandleGatewayCertificateChange.kt | 1 - .../endpoint/RenewExpiringCertificates.kt | 1 - .../awaladroid/endpoint/ThirdPartyEndpoint.kt | 70 +- .../endpoint/ThirdPartyEndpointAuth.kt | 1 - .../awaladroid/messaging/IncomingMessage.kt | 70 +- .../awaladroid/messaging/OutgoingMessage.kt | 185 ++--- .../awaladroid/messaging/ParcelId.kt | 34 +- .../awaladroid/messaging/ReceiveMessages.kt | 100 +-- .../awaladroid/messaging/SendMessage.kt | 1 - .../awaladroid/storage/StorageImpl.kt | 143 ++-- .../storage/persistence/DiskPersistence.kt | 46 +- .../storage/persistence/Persistance.kt | 6 +- .../awaladroid/AndroidPrivateKeyStoreTest.kt | 38 +- .../tech/relaycorp/awaladroid/AwalaTest.kt | 183 ++--- .../awaladroid/GatewayClientImplTest.kt | 338 +++++---- .../IncomingParcelBroadcastReceiverTest.kt | 15 +- .../awaladroid/endpoint/ChannelManagerTest.kt | 258 +++---- .../endpoint/FirstPartyEndpointTest.kt | 657 ++++++++++-------- .../endpoint/PrivateThirdPartyEndpointTest.kt | 594 ++++++++-------- .../endpoint/PublicThirdPartyEndpointTest.kt | 162 +++-- .../endpoint/RenewExpiringCertificatesTest.kt | 47 +- .../messaging/IncomingMessageTest.kt | 348 +++++----- .../messaging/OutgoingMessageTest.kt | 249 ++++--- .../messaging/ReceiveMessagesTest.kt | 522 +++++++------- .../awaladroid/messaging/SendMessageTest.kt | 94 +-- .../awaladroid/storage/MockStorage.kt | 13 +- .../awaladroid/storage/StorageImplTest.kt | 98 +-- .../persistence/DiskPersistenceTest.kt | 148 ++-- .../relaycorp/awaladroid/test/AssertUtils.kt | 6 +- .../awaladroid/test/FakeAndroidKeyStore.kt | 61 +- .../test/FirstPartyEndpointFactory.kt | 13 +- .../awaladroid/test/MessageFactory.kt | 26 +- .../awaladroid/test/MockContextTestCase.kt | 20 +- .../awaladroid/test/MockPersistence.kt | 5 +- .../awaladroid/test/RecipientAddressType.kt | 3 +- .../test/ThirdPartyEndpointFactory.kt | 19 +- 49 files changed, 2932 insertions(+), 2571 deletions(-) diff --git a/.editorconfig b/.editorconfig index aec9a137..e663a3b6 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,10 +1,6 @@ # http://editorconfig.org root = true -[*.{kt,kts}] -ij_kotlin_allow_trailing_comma = true -ij_kotlin_allow_trailing_comma_on_call_site = true - [*] charset = utf-8 end_of_line = lf @@ -12,3 +8,7 @@ indent_size = 4 indent_style = space insert_final_newline = true max_line_length = 100 + +[*.{kt,kts}] +ij_kotlin_allow_trailing_comma = true +ij_kotlin_allow_trailing_comma_on_call_site = true diff --git a/build.gradle b/build.gradle index 4e3726e3..a1f1286f 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ buildscript { ext { - kotlinVersion = '1.8.21' + kotlinVersion = '1.9.21' kotlinCoroutinesVersion = '1.7.3' } repositories { @@ -9,7 +9,7 @@ buildscript { dependencies { classpath 'com.android.tools.build:gradle:8.1.4' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" - classpath("org.jetbrains.dokka:dokka-core:1.9.10") + classpath 'org.jetbrains.dokka:dokka-core:1.9.10' } } diff --git a/lib/build.gradle b/lib/build.gradle index cbf0b889..66b9f249 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -3,18 +3,18 @@ plugins { id 'kotlin-android' id 'kotlin-kapt' id 'maven-publish' - id 'org.jlleitschuh.gradle.ktlint' version "11.5.0" + id 'org.jlleitschuh.gradle.ktlint' version "11.6.1" id 'org.jetbrains.dokka' version "1.9.10" } apply from: 'jacoco.gradle' android { - compileSdk 33 + compileSdk 34 defaultConfig { minSdk 23 - targetSdk 33 + targetSdk 34 versionCode 1 versionName "1.0.0" namespace 'tech.relaycorp.awaladroid' @@ -60,7 +60,7 @@ android { dependencies { // Java 8 - coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.2.0' + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4' // Kotlin implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinCoroutinesVersion" @@ -123,7 +123,7 @@ dokkaHtml.configure { ktlint { verbose = true android = true - version = "0.50.0" + version = "1.0.1" } afterEvaluate { diff --git a/lib/src/main/java/tech/relaycorp/awaladroid/AndroidPrivateKeyStore.kt b/lib/src/main/java/tech/relaycorp/awaladroid/AndroidPrivateKeyStore.kt index b3c4d6e0..2e74bb31 100644 --- a/lib/src/main/java/tech/relaycorp/awaladroid/AndroidPrivateKeyStore.kt +++ b/lib/src/main/java/tech/relaycorp/awaladroid/AndroidPrivateKeyStore.kt @@ -24,7 +24,6 @@ internal class AndroidPrivateKeyStore( .build() }, ) : FilePrivateKeyStore(root) { - @Throws(EncryptionInitializationException::class) override fun makeEncryptedInputStream(file: File) = buildEncryptedFile(file).openFileInput() diff --git a/lib/src/main/java/tech/relaycorp/awaladroid/Awala.kt b/lib/src/main/java/tech/relaycorp/awaladroid/Awala.kt index 9abbe073..0debfca9 100644 --- a/lib/src/main/java/tech/relaycorp/awaladroid/Awala.kt +++ b/lib/src/main/java/tech/relaycorp/awaladroid/Awala.kt @@ -72,19 +72,21 @@ public object Awala { internal var contextDeferred: CompletableDeferred = CompletableDeferred() - internal fun getContextOrThrow(): AwalaContext = try { - contextDeferred.getCompleted() - } catch (e: IllegalStateException) { - throw SetupPendingException() - } + internal fun getContextOrThrow(): AwalaContext = + try { + contextDeferred.getCompleted() + } catch (e: IllegalStateException) { + throw SetupPendingException() + } - internal suspend fun awaitContextOrThrow(timeout: Duration = 3.seconds): AwalaContext = try { - withTimeout(timeout) { - contextDeferred.await() + internal suspend fun awaitContextOrThrow(timeout: Duration = 3.seconds): AwalaContext = + try { + withTimeout(timeout) { + contextDeferred.await() + } + } catch (e: TimeoutCancellationException) { + throw SetupPendingException() } - } catch (e: TimeoutCancellationException) { - throw SetupPendingException() - } } /** diff --git a/lib/src/main/java/tech/relaycorp/awaladroid/GatewayClientImpl.kt b/lib/src/main/java/tech/relaycorp/awaladroid/GatewayClientImpl.kt index 3758e790..9c232e31 100644 --- a/lib/src/main/java/tech/relaycorp/awaladroid/GatewayClientImpl.kt +++ b/lib/src/main/java/tech/relaycorp/awaladroid/GatewayClientImpl.kt @@ -34,195 +34,196 @@ import kotlin.coroutines.suspendCoroutine * Private gateway client. */ public class GatewayClientImpl -internal constructor( - private val coroutineContext: CoroutineContext = Dispatchers.IO, - private val serviceInteractorBuilder: () -> ServiceInteractor, - private val pdcClientBuilder: () -> PDCClient = - { PoWebClient.initLocal(port = Awala.POWEB_PORT) }, - private val sendMessage: SendMessage = SendMessage(), - private val receiveMessages: ReceiveMessages = ReceiveMessages(), -) { - - // Gateway - - private var gwServiceInteractor: ServiceInteractor? = null - private val isReceivingMessages = AtomicBoolean(false) - - /** - * Bind to the gateway to be able to communicate with it. - */ - @Throws(GatewayBindingException::class) - public suspend fun bind() { - withContext(coroutineContext) { - if (gwServiceInteractor != null) return@withContext // Already connected - - gwServiceInteractor = serviceInteractorBuilder().apply { - try { - bind( - Awala.GATEWAY_SYNC_ACTION, - Awala.GATEWAY_PACKAGE, - Awala.GATEWAY_SYNC_COMPONENT, - ) - } catch (exp: ServiceInteractor.BindFailedException) { - throw GatewayBindingException( - "Failed binding to Awala Gateway for registration", - exp, - ) - } + internal constructor( + private val coroutineContext: CoroutineContext = Dispatchers.IO, + private val serviceInteractorBuilder: () -> ServiceInteractor, + private val pdcClientBuilder: () -> PDCClient = + { PoWebClient.initLocal(port = Awala.POWEB_PORT) }, + private val sendMessage: SendMessage = SendMessage(), + private val receiveMessages: ReceiveMessages = ReceiveMessages(), + ) { + // Gateway + + private var gwServiceInteractor: ServiceInteractor? = null + private val isReceivingMessages = AtomicBoolean(false) + + /** + * Bind to the gateway to be able to communicate with it. + */ + @Throws(GatewayBindingException::class) + public suspend fun bind() { + withContext(coroutineContext) { + if (gwServiceInteractor != null) return@withContext // Already connected + + gwServiceInteractor = + serviceInteractorBuilder().apply { + try { + bind( + Awala.GATEWAY_SYNC_ACTION, + Awala.GATEWAY_PACKAGE, + Awala.GATEWAY_SYNC_COMPONENT, + ) + } catch (exp: ServiceInteractor.BindFailedException) { + throw GatewayBindingException( + "Failed binding to Awala Gateway for registration", + exp, + ) + } + } + delay(1_000) // Wait for server to start } - delay(1_000) // Wait for server to start } - } - /** - * Unbind from the gateway. - * - * Make sure to call this when you no longer need to communicate with the gateway. - */ - public fun unbind() { - gwServiceInteractor?.unbind() - gwServiceInteractor = null - } + /** + * Unbind from the gateway. + * + * Make sure to call this when you no longer need to communicate with the gateway. + */ + public fun unbind() { + gwServiceInteractor?.unbind() + gwServiceInteractor = null + } - // First-Party Endpoints + // First-Party Endpoints - @Throws( - RegistrationFailedException::class, - GatewayProtocolException::class, - GatewayUnregisteredException::class, - ) - internal suspend fun registerEndpoint(keyPair: KeyPair): PrivateNodeRegistration = - withContext(coroutineContext) { - try { - val preAuthSerialized = preRegister() - val request = PrivateNodeRegistrationRequest(keyPair.public, preAuthSerialized) - val requestSerialized = request.serialize(keyPair.private) + @Throws( + RegistrationFailedException::class, + GatewayProtocolException::class, + GatewayUnregisteredException::class, + ) + internal suspend fun registerEndpoint(keyPair: KeyPair): PrivateNodeRegistration = + withContext(coroutineContext) { + try { + val preAuthSerialized = preRegister() + val request = PrivateNodeRegistrationRequest(keyPair.public, preAuthSerialized) + val requestSerialized = request.serialize(keyPair.private) - bind() + bind() - return@withContext pdcClientBuilder().use { - it.registerNode(requestSerialized) + return@withContext pdcClientBuilder().use { + it.registerNode(requestSerialized) + } + } catch (exp: ServiceInteractor.BindFailedException) { + throw RegistrationFailedException("Failed binding to gateway", exp) + } catch (exp: ServiceInteractor.SendFailedException) { + throw RegistrationFailedException("Failed communicating with gateway", exp) + } catch (exp: ServerException) { + throw RegistrationFailedException("Registration failed due to server", exp) + } catch (exp: ClientBindingException) { + throw GatewayProtocolException("Registration failed due to client", exp) + } catch (exp: GatewayBindingException) { + throw RegistrationFailedException("Failed binding to gateway", exp) } - } catch (exp: ServiceInteractor.BindFailedException) { - throw RegistrationFailedException("Failed binding to gateway", exp) - } catch (exp: ServiceInteractor.SendFailedException) { - throw RegistrationFailedException("Failed communicating with gateway", exp) - } catch (exp: ServerException) { - throw RegistrationFailedException("Registration failed due to server", exp) - } catch (exp: ClientBindingException) { - throw GatewayProtocolException("Registration failed due to client", exp) - } catch (exp: GatewayBindingException) { - throw RegistrationFailedException("Failed binding to gateway", exp) } - } - @Throws( - ServiceInteractor.BindFailedException::class, - ServiceInteractor.SendFailedException::class, - GatewayProtocolException::class, - GatewayUnregisteredException::class, - ) - private suspend fun preRegister(): ByteArray { - val interactor = serviceInteractorBuilder().apply { - bind( - Awala.GATEWAY_PRE_REGISTER_ACTION, - Awala.GATEWAY_PACKAGE, - Awala.GATEWAY_PRE_REGISTER_COMPONENT, - ) - } + @Throws( + ServiceInteractor.BindFailedException::class, + ServiceInteractor.SendFailedException::class, + GatewayProtocolException::class, + GatewayUnregisteredException::class, + ) + private suspend fun preRegister(): ByteArray { + val interactor = + serviceInteractorBuilder().apply { + bind( + Awala.GATEWAY_PRE_REGISTER_ACTION, + Awala.GATEWAY_PACKAGE, + Awala.GATEWAY_PRE_REGISTER_COMPONENT, + ) + } - return suspendCoroutine { cont -> - val request = android.os.Message.obtain(null, PREREGISTRATION_REQUEST) - interactor.sendMessage(request) { replyMessage -> - interactor.unbind() - when (replyMessage.what) { - REGISTRATION_AUTHORIZATION -> { - cont.resume(replyMessage.data.getByteArray("auth")!!) - } - GATEWAY_NOT_REGISTERED -> { - cont.resumeWithException( - GatewayUnregisteredException("Gateway not registered"), - ) - } - else -> { - cont.resumeWithException( - GatewayProtocolException( - "Pre-registration failed, received wrong reply", - ), - ) + return suspendCoroutine { cont -> + val request = android.os.Message.obtain(null, PREREGISTRATION_REQUEST) + interactor.sendMessage(request) { replyMessage -> + interactor.unbind() + when (replyMessage.what) { + REGISTRATION_AUTHORIZATION -> { + cont.resume(replyMessage.data.getByteArray("auth")!!) + } + GATEWAY_NOT_REGISTERED -> { + cont.resumeWithException( + GatewayUnregisteredException("Gateway not registered"), + ) + } + else -> { + cont.resumeWithException( + GatewayProtocolException( + "Pre-registration failed, received wrong reply", + ), + ) + } } } } } - } - // Messaging - - @Throws( - GatewayBindingException::class, - GatewayProtocolException::class, - SendMessageException::class, - RejectedMessageException::class, - ) - public suspend fun sendMessage(message: OutgoingMessage) { - if (gwServiceInteractor == null) { - throw GatewayBindingException("Gateway not bound") + // Messaging + + @Throws( + GatewayBindingException::class, + GatewayProtocolException::class, + SendMessageException::class, + RejectedMessageException::class, + ) + public suspend fun sendMessage(message: OutgoingMessage) { + if (gwServiceInteractor == null) { + throw GatewayBindingException("Gateway not bound") + } + sendMessage.send(message) } - sendMessage.send(message) - } - private val incomingMessageChannel = MutableSharedFlow(1) + private val incomingMessageChannel = MutableSharedFlow(1) - /** - * Receive messages from the gateway. - */ - public fun receiveMessages(): Flow = incomingMessageChannel.asSharedFlow() + /** + * Receive messages from the gateway. + */ + public fun receiveMessages(): Flow = incomingMessageChannel.asSharedFlow() - // Internal + // Internal - internal suspend fun checkForNewMessages() { - withContext(coroutineContext) { - val wasAlreadyBound = gwServiceInteractor != null - if (!wasAlreadyBound) { - try { - bind() - } catch (exp: GatewayBindingException) { - logger.log( - Level.SEVERE, - "Could not bind to gateway to receive new messages", - exp, - ) - return@withContext + internal suspend fun checkForNewMessages() { + withContext(coroutineContext) { + val wasAlreadyBound = gwServiceInteractor != null + if (!wasAlreadyBound) { + try { + bind() + } catch (exp: GatewayBindingException) { + logger.log( + Level.SEVERE, + "Could not bind to gateway to receive new messages", + exp, + ) + return@withContext + } } - } - if (isReceivingMessages.get()) return@withContext - isReceivingMessages.set(true) - - try { - receiveMessages - .receive() - .collect(incomingMessageChannel::emit) - } catch (exp: ReceiveMessageException) { - logger.log(Level.SEVERE, "Could not receive new messages", exp) - } catch (exp: GatewayProtocolException) { - logger.log(Level.SEVERE, "Could not receive new messages", exp) - } catch (exp: PersistenceException) { - logger.log(Level.SEVERE, "Could not receive new messages", exp) - } + if (isReceivingMessages.get()) return@withContext + isReceivingMessages.set(true) - isReceivingMessages.set(false) + try { + receiveMessages + .receive() + .collect(incomingMessageChannel::emit) + } catch (exp: ReceiveMessageException) { + logger.log(Level.SEVERE, "Could not receive new messages", exp) + } catch (exp: GatewayProtocolException) { + logger.log(Level.SEVERE, "Could not receive new messages", exp) + } catch (exp: PersistenceException) { + logger.log(Level.SEVERE, "Could not receive new messages", exp) + } + + isReceivingMessages.set(false) - if (!wasAlreadyBound) unbind() + if (!wasAlreadyBound) unbind() + } } - } - internal companion object { - internal const val PREREGISTRATION_REQUEST = 1 - internal const val REGISTRATION_AUTHORIZATION = 2 - internal const val GATEWAY_NOT_REGISTERED = 4 + internal companion object { + internal const val PREREGISTRATION_REQUEST = 1 + internal const val REGISTRATION_AUTHORIZATION = 2 + internal const val GATEWAY_NOT_REGISTERED = 4 + } } -} /** * General class for all exceptions deriving from interactions with the gateway. diff --git a/lib/src/main/java/tech/relaycorp/awaladroid/background/GatewayCertificateChangeBroadcastReceiver.kt b/lib/src/main/java/tech/relaycorp/awaladroid/background/GatewayCertificateChangeBroadcastReceiver.kt index c1919135..a562dd13 100644 --- a/lib/src/main/java/tech/relaycorp/awaladroid/background/GatewayCertificateChangeBroadcastReceiver.kt +++ b/lib/src/main/java/tech/relaycorp/awaladroid/background/GatewayCertificateChangeBroadcastReceiver.kt @@ -10,10 +10,12 @@ import tech.relaycorp.awaladroid.Awala import kotlin.coroutines.CoroutineContext internal class GatewayCertificateChangeBroadcastReceiver : BroadcastReceiver() { - internal var coroutineContext: CoroutineContext = Dispatchers.IO - override fun onReceive(context: Context?, intent: Intent?) { + override fun onReceive( + context: Context?, + intent: Intent?, + ) { CoroutineScope(coroutineContext).launch { Awala.awaitContextOrThrow().handleGatewayCertificateChange() } diff --git a/lib/src/main/java/tech/relaycorp/awaladroid/background/IncomingParcelBroadcastReceiver.kt b/lib/src/main/java/tech/relaycorp/awaladroid/background/IncomingParcelBroadcastReceiver.kt index d12ddcf2..d9684f08 100644 --- a/lib/src/main/java/tech/relaycorp/awaladroid/background/IncomingParcelBroadcastReceiver.kt +++ b/lib/src/main/java/tech/relaycorp/awaladroid/background/IncomingParcelBroadcastReceiver.kt @@ -10,10 +10,12 @@ import tech.relaycorp.awaladroid.Awala import kotlin.coroutines.CoroutineContext internal class IncomingParcelBroadcastReceiver : BroadcastReceiver() { - internal var coroutineContext: CoroutineContext = Dispatchers.IO - override fun onReceive(context: Context?, intent: Intent?) { + override fun onReceive( + context: Context?, + intent: Intent?, + ) { CoroutineScope(coroutineContext).launch { Awala.awaitContextOrThrow().gatewayClient.checkForNewMessages() } diff --git a/lib/src/main/java/tech/relaycorp/awaladroid/background/ServiceInteractor.kt b/lib/src/main/java/tech/relaycorp/awaladroid/background/ServiceInteractor.kt index 6d29508c..e025c3f0 100644 --- a/lib/src/main/java/tech/relaycorp/awaladroid/background/ServiceInteractor.kt +++ b/lib/src/main/java/tech/relaycorp/awaladroid/background/ServiceInteractor.kt @@ -18,17 +18,23 @@ import kotlin.coroutines.suspendCoroutine internal class ServiceInteractor( private val context: Context, ) { - private var serviceConnection: ServiceConnection? = null private var binder: IBinder? = null @Throws(BindFailedException::class) - suspend fun bind(action: String, packageName: String, componentName: String) = - suspendCoroutine { cont -> - var isResumed = false + suspend fun bind( + action: String, + packageName: String, + componentName: String, + ) = suspendCoroutine { cont -> + var isResumed = false - val serviceConnection = object : ServiceConnection { - override fun onServiceConnected(p0: ComponentName?, binder: IBinder) { + val serviceConnection = + object : ServiceConnection { + override fun onServiceConnected( + p0: ComponentName?, + binder: IBinder, + ) { logger.info("Connected to service $packageName - $componentName") serviceConnection = this this@ServiceInteractor.binder = binder @@ -63,20 +69,23 @@ internal class ServiceInteractor( } } - val intent = Intent(action).apply { - component = ComponentName( - packageName, - componentName, - ) + val intent = + Intent(action).apply { + component = + ComponentName( + packageName, + componentName, + ) } - val bindWasSuccessful = context.bindService( + val bindWasSuccessful = + context.bindService( intent, serviceConnection, Context.BIND_AUTO_CREATE, ) - if (!bindWasSuccessful) cont.resumeWithException(BindFailedException("Binding failed")) - } + if (!bindWasSuccessful) cont.resumeWithException(BindFailedException("Binding failed")) + } fun unbind() { serviceConnection?.let { context.unbindService(it) } @@ -84,16 +93,22 @@ internal class ServiceInteractor( } @Throws(BindFailedException::class, SendFailedException::class) - fun sendMessage(message: Message, reply: ((Message) -> Unit)? = null) { + fun sendMessage( + message: Message, + reply: ((Message) -> Unit)? = null, + ) { val binder = binder ?: throw BindFailedException("Service not bound") val looper = Looper.myLooper() ?: Looper.getMainLooper() reply?.let { - message.replyTo = Messenger(object : Handler(looper) { - override fun handleMessage(msg: Message) { - reply(msg) - } - }) + message.replyTo = + Messenger( + object : Handler(looper) { + override fun handleMessage(msg: Message) { + reply(msg) + } + }, + ) } try { Messenger(binder).send(message) @@ -103,5 +118,6 @@ internal class ServiceInteractor( } class BindFailedException(message: String) : Exception(message) + class SendFailedException(throwable: Throwable) : Exception(throwable) } diff --git a/lib/src/main/java/tech/relaycorp/awaladroid/common/Keys.kt b/lib/src/main/java/tech/relaycorp/awaladroid/common/Keys.kt index 6878e965..ae44fd2b 100644 --- a/lib/src/main/java/tech/relaycorp/awaladroid/common/Keys.kt +++ b/lib/src/main/java/tech/relaycorp/awaladroid/common/Keys.kt @@ -8,8 +8,7 @@ import java.security.PublicKey import java.security.interfaces.RSAPrivateCrtKey import java.security.spec.RSAPublicKeySpec -internal fun PrivateKey.toKeyPair(): KeyPair = - KeyPair(toPublicKey(), this) +internal fun PrivateKey.toKeyPair(): KeyPair = KeyPair(toPublicKey(), this) internal fun PrivateKey.toPublicKey(): PublicKey { val rsaPrivateKey = this as RSAPrivateCrtKey diff --git a/lib/src/main/java/tech/relaycorp/awaladroid/common/Logging.kt b/lib/src/main/java/tech/relaycorp/awaladroid/common/Logging.kt index 660b6741..327f0005 100644 --- a/lib/src/main/java/tech/relaycorp/awaladroid/common/Logging.kt +++ b/lib/src/main/java/tech/relaycorp/awaladroid/common/Logging.kt @@ -4,7 +4,6 @@ import java.util.logging.Level import java.util.logging.Logger internal object Logging { - private val rootLogger by lazy { Logger.getLogger("") } val Any.logger: Logger get() = Logger.getLogger(javaClass.name) diff --git a/lib/src/main/java/tech/relaycorp/awaladroid/endpoint/ChannelManager.kt b/lib/src/main/java/tech/relaycorp/awaladroid/endpoint/ChannelManager.kt index d7122998..ba7db77f 100644 --- a/lib/src/main/java/tech/relaycorp/awaladroid/endpoint/ChannelManager.kt +++ b/lib/src/main/java/tech/relaycorp/awaladroid/endpoint/ChannelManager.kt @@ -45,9 +45,7 @@ internal class ChannelManager( } } - suspend fun delete( - firstPartyEndpoint: FirstPartyEndpoint, - ) { + suspend fun delete(firstPartyEndpoint: FirstPartyEndpoint) { withContext(coroutineContext) { with(sharedPreferences.edit()) { remove(firstPartyEndpoint.nodeId) @@ -56,9 +54,7 @@ internal class ChannelManager( } } - suspend fun delete( - thirdPartyEndpoint: ThirdPartyEndpoint, - ) { + suspend fun delete(thirdPartyEndpoint: ThirdPartyEndpoint) { withContext(coroutineContext) { sharedPreferences.all.forEach { (key, value) -> // Skip malformed values diff --git a/lib/src/main/java/tech/relaycorp/awaladroid/endpoint/FirstPartyEndpoint.kt b/lib/src/main/java/tech/relaycorp/awaladroid/endpoint/FirstPartyEndpoint.kt index ed0c5b2b..3f178ebd 100644 --- a/lib/src/main/java/tech/relaycorp/awaladroid/endpoint/FirstPartyEndpoint.kt +++ b/lib/src/main/java/tech/relaycorp/awaladroid/endpoint/FirstPartyEndpoint.kt @@ -30,203 +30,129 @@ import java.util.logging.Level * An endpoint owned by the current instance of the app. */ public class FirstPartyEndpoint -internal constructor( - internal val identityPrivateKey: PrivateKey, - internal val identityCertificate: Certificate, - internal val identityCertificateChain: List, - public val internetAddress: String, -) : Endpoint(identityPrivateKey.nodeId) { - - /** - * The RSA public key of the endpoint. - */ - public val publicKey: PublicKey get() = identityCertificate.subjectPublicKey - - internal val pdaChain: List - get() = - listOf(identityCertificate) + identityCertificateChain - - /** - * Issue a PDA for a third-party endpoint. - */ - @Throws(CertificateException::class) - public suspend fun issueAuthorization( - thirdPartyEndpoint: ThirdPartyEndpoint, - expiryDate: ZonedDateTime, - ): ByteArray = - issueAuthorization( - thirdPartyEndpoint.identityKey, - expiryDate, - ).auth - - /** - * Issue a PDA for a third-party endpoint using its public key. - */ - @Throws(CertificateException::class) - public suspend fun issueAuthorization( - thirdPartyEndpointPublicKeySerialized: ByteArray, - expiryDate: ZonedDateTime, - ): ThirdPartyEndpointAuth { - val thirdPartyEndpointPublicKey = - deserializePDAGranteePublicKey(thirdPartyEndpointPublicKeySerialized) - return issueAuthorization(thirdPartyEndpointPublicKey, expiryDate) - } - - @Throws(CertificateException::class) - private suspend fun issueAuthorization( - thirdPartyEndpointPublicKey: PublicKey, - expiryDate: ZonedDateTime, - ): ThirdPartyEndpointAuth { - val pda = issueDeliveryAuthorization( - subjectPublicKey = thirdPartyEndpointPublicKey, - issuerPrivateKey = identityPrivateKey, - validityEndDate = expiryDate, - issuerCertificate = identityCertificate, - ) - val deliveryAuth = CertificationPath(pda, pdaChain) - - val context = Awala.getContextOrThrow() - val sessionKeyPair = context.endpointManager.generateSessionKeyPair( - nodeId, - thirdPartyEndpointPublicKey.nodeId, - ) + internal constructor( + internal val identityPrivateKey: PrivateKey, + internal val identityCertificate: Certificate, + internal val identityCertificateChain: List, + public val internetAddress: String, + ) : Endpoint(identityPrivateKey.nodeId) { + /** + * The RSA public key of the endpoint. + */ + public val publicKey: PublicKey get() = identityCertificate.subjectPublicKey - val connParams = PrivateEndpointConnParams( - this.publicKey, - this.internetAddress, - deliveryAuth, - sessionKeyPair.sessionKey, - ) - val authSerialized = connParams.serialize() - return ThirdPartyEndpointAuth(thirdPartyEndpointPublicKey.nodeId, authSerialized) - } + internal val pdaChain: List + get() = + listOf(identityCertificate) + identityCertificateChain - /** - * Issue a PDA for a third-party endpoint and renew it indefinitely. - */ - @Throws(CertificateException::class) - public suspend fun authorizeIndefinitely( - thirdPartyEndpoint: ThirdPartyEndpoint, - ): ByteArray = - authorizeIndefinitely(thirdPartyEndpoint.identityKey).auth - - /** - * Issue a PDA for a third-party endpoint (using its public key) and renew it indefinitely. - */ - @Throws(CertificateException::class) - public suspend fun authorizeIndefinitely( - thirdPartyEndpointPublicKeySerialized: ByteArray, - ): ThirdPartyEndpointAuth { - val thirdPartyEndpointPublicKey = - deserializePDAGranteePublicKey(thirdPartyEndpointPublicKeySerialized) - return authorizeIndefinitely(thirdPartyEndpointPublicKey) - } + /** + * Issue a PDA for a third-party endpoint. + */ + @Throws(CertificateException::class) + public suspend fun issueAuthorization( + thirdPartyEndpoint: ThirdPartyEndpoint, + expiryDate: ZonedDateTime, + ): ByteArray = + issueAuthorization( + thirdPartyEndpoint.identityKey, + expiryDate, + ).auth - @Throws(CertificateException::class) - private suspend fun authorizeIndefinitely( - thirdPartyEndpointPublicKey: PublicKey, - ): ThirdPartyEndpointAuth { - val authorization = - issueAuthorization(thirdPartyEndpointPublicKey, identityCertificate.expiryDate) + /** + * Issue a PDA for a third-party endpoint using its public key. + */ + @Throws(CertificateException::class) + public suspend fun issueAuthorization( + thirdPartyEndpointPublicKeySerialized: ByteArray, + expiryDate: ZonedDateTime, + ): ThirdPartyEndpointAuth { + val thirdPartyEndpointPublicKey = + deserializePDAGranteePublicKey(thirdPartyEndpointPublicKeySerialized) + return issueAuthorization(thirdPartyEndpointPublicKey, expiryDate) + } - val context = Awala.getContextOrThrow() - context.channelManager.create(this, thirdPartyEndpointPublicKey) + @Throws(CertificateException::class) + private suspend fun issueAuthorization( + thirdPartyEndpointPublicKey: PublicKey, + expiryDate: ZonedDateTime, + ): ThirdPartyEndpointAuth { + val pda = + issueDeliveryAuthorization( + subjectPublicKey = thirdPartyEndpointPublicKey, + issuerPrivateKey = identityPrivateKey, + validityEndDate = expiryDate, + issuerCertificate = identityCertificate, + ) + val deliveryAuth = CertificationPath(pda, pdaChain) - return authorization - } + val context = Awala.getContextOrThrow() + val sessionKeyPair = + context.endpointManager.generateSessionKeyPair( + nodeId, + thirdPartyEndpointPublicKey.nodeId, + ) - private fun deserializePDAGranteePublicKey( - thirdPartyEndpointPublicKeySerialized: ByteArray, - ): PublicKey { - val thirdPartyEndpointPublicKey = try { - thirdPartyEndpointPublicKeySerialized.deserializeRSAPublicKey() - } catch (exc: KeyException) { - throw AuthorizationIssuanceException( - "PDA grantee public key is not a valid RSA public key", - exc, - ) + val connParams = + PrivateEndpointConnParams( + this.publicKey, + this.internetAddress, + deliveryAuth, + sessionKeyPair.sessionKey, + ) + val authSerialized = connParams.serialize() + return ThirdPartyEndpointAuth(thirdPartyEndpointPublicKey.nodeId, authSerialized) } - return thirdPartyEndpointPublicKey - } - /** - * Re-register endpoints after gateway certificate change - */ - @Throws( - RegistrationFailedException::class, - GatewayProtocolException::class, - GatewayUnregisteredException::class, - PersistenceException::class, - SetupPendingException::class, - ) - internal suspend fun reRegister(): FirstPartyEndpoint { - val context = Awala.getContextOrThrow() - - val registration = context.gatewayClient.registerEndpoint(identityPrivateKey.toKeyPair()) - val newEndpoint = FirstPartyEndpoint( - identityPrivateKey, - registration.privateNodeCertificate, - listOf(registration.gatewayCertificate), - registration.gatewayInternetAddress, - ) + /** + * Issue a PDA for a third-party endpoint and renew it indefinitely. + */ + @Throws(CertificateException::class) + public suspend fun authorizeIndefinitely( + thirdPartyEndpoint: ThirdPartyEndpoint, + ): ByteArray = authorizeIndefinitely(thirdPartyEndpoint.identityKey).auth - val gatewayId = registration.gatewayCertificate.subjectId - try { - context.certificateStore.save( - CertificationPath( - registration.privateNodeCertificate, - listOf(registration.gatewayCertificate), - ), - gatewayId, - ) - } catch (exc: KeyStoreBackendException) { - throw PersistenceException("Failed to save certificate", exc) + /** + * Issue a PDA for a third-party endpoint (using its public key) and renew it indefinitely. + */ + @Throws(CertificateException::class) + public suspend fun authorizeIndefinitely( + thirdPartyEndpointPublicKeySerialized: ByteArray, + ): ThirdPartyEndpointAuth { + val thirdPartyEndpointPublicKey = + deserializePDAGranteePublicKey(thirdPartyEndpointPublicKeySerialized) + return authorizeIndefinitely(thirdPartyEndpointPublicKey) } - return newEndpoint - } + @Throws(CertificateException::class) + private suspend fun authorizeIndefinitely( + thirdPartyEndpointPublicKey: PublicKey, + ): ThirdPartyEndpointAuth { + val authorization = + issueAuthorization(thirdPartyEndpointPublicKey, identityCertificate.expiryDate) - internal suspend fun reissuePDAs() { - val context = Awala.getContextOrThrow() - val thirdPartyEndpointAddresses = context.channelManager.getLinkedEndpointAddresses(this) - for (thirdPartyEndpointAddress in thirdPartyEndpointAddresses) { - val thirdPartyEndpoint = ThirdPartyEndpoint.load( - this@FirstPartyEndpoint.nodeId, - thirdPartyEndpointAddress, - ) - if (thirdPartyEndpoint == null) { - logger.log( - Level.INFO, - "Ignoring missing third-party endpoint $thirdPartyEndpointAddress", - ) - break - } + val context = Awala.getContextOrThrow() + context.channelManager.create(this, thirdPartyEndpointPublicKey) - val message = OutgoingMessage.build( - "application/vnd+relaycorp.awala.pda-path", - issueAuthorization(thirdPartyEndpoint, identityCertificate.expiryDate), - this, - thirdPartyEndpoint, - identityCertificate.expiryDate, - ) - context.gatewayClient.sendMessage(message) + return authorization } - } - /** - * Delete the endpoint. - */ - @Throws(PersistenceException::class, SetupPendingException::class) - public suspend fun delete() { - val context = Awala.getContextOrThrow() - context.privateKeyStore.deleteKeys(nodeId) - context.certificateStore.delete(nodeId, identityCertificate.issuerCommonName) - context.channelManager.delete(this) - } + private fun deserializePDAGranteePublicKey( + thirdPartyEndpointPublicKeySerialized: ByteArray, + ): PublicKey { + val thirdPartyEndpointPublicKey = + try { + thirdPartyEndpointPublicKeySerialized.deserializeRSAPublicKey() + } catch (exc: KeyException) { + throw AuthorizationIssuanceException( + "PDA grantee public key is not a valid RSA public key", + exc, + ) + } + return thirdPartyEndpointPublicKey + } - public companion object { /** - * Generate endpoint and register it with the private gateway. + * Re-register endpoints after gateway certificate change */ @Throws( RegistrationFailedException::class, @@ -235,25 +161,20 @@ internal constructor( PersistenceException::class, SetupPendingException::class, ) - public suspend fun register(): FirstPartyEndpoint { + internal suspend fun reRegister(): FirstPartyEndpoint { val context = Awala.getContextOrThrow() - val keyPair = generateRSAKeyPair() - - val registration = context.gatewayClient.registerEndpoint(keyPair) - val endpoint = FirstPartyEndpoint( - keyPair.private, - registration.privateNodeCertificate, - listOf(registration.gatewayCertificate), - registration.gatewayInternetAddress, - ) - try { - context.privateKeyStore.saveIdentityKey( - keyPair.private, + val registration = + context.gatewayClient.registerEndpoint( + identityPrivateKey.toKeyPair(), + ) + val newEndpoint = + FirstPartyEndpoint( + identityPrivateKey, + registration.privateNodeCertificate, + listOf(registration.gatewayCertificate), + registration.gatewayInternetAddress, ) - } catch (exc: KeyStoreBackendException) { - throw PersistenceException("Failed to save identity key", exc) - } val gatewayId = registration.gatewayCertificate.subjectId try { @@ -268,54 +189,149 @@ internal constructor( throw PersistenceException("Failed to save certificate", exc) } - context.storage.gatewayId.set( - endpoint.nodeId, - gatewayId, - ) - - context.storage.internetAddress.set(registration.gatewayInternetAddress) + return newEndpoint + } - return endpoint + internal suspend fun reissuePDAs() { + val context = Awala.getContextOrThrow() + val thirdPartyEndpointAddresses = + context.channelManager.getLinkedEndpointAddresses( + this, + ) + for (thirdPartyEndpointAddress in thirdPartyEndpointAddresses) { + val thirdPartyEndpoint = + ThirdPartyEndpoint.load( + this@FirstPartyEndpoint.nodeId, + thirdPartyEndpointAddress, + ) + if (thirdPartyEndpoint == null) { + logger.log( + Level.INFO, + "Ignoring missing third-party endpoint $thirdPartyEndpointAddress", + ) + break + } + + val message = + OutgoingMessage.build( + "application/vnd+relaycorp.awala.pda-path", + issueAuthorization(thirdPartyEndpoint, identityCertificate.expiryDate), + this, + thirdPartyEndpoint, + identityCertificate.expiryDate, + ) + context.gatewayClient.sendMessage(message) + } } /** - * Load an endpoint by its address. + * Delete the endpoint. */ @Throws(PersistenceException::class, SetupPendingException::class) - public suspend fun load(nodeId: String): FirstPartyEndpoint? { + public suspend fun delete() { val context = Awala.getContextOrThrow() - val identityPrivateKey = try { - context.privateKeyStore.retrieveIdentityKey(nodeId) - } catch (exc: MissingKeyException) { - return null - } catch (exc: KeyStoreBackendException) { - throw PersistenceException("Failed to load private key of endpoint", exc) - } - val gatewayNodeId = context.storage.gatewayId.get(nodeId) - ?: throw PersistenceException("Failed to load gateway address for endpoint") - val certificatePath = try { - context.certificateStore.retrieveLatest( - nodeId, gatewayNodeId, + context.privateKeyStore.deleteKeys(nodeId) + context.certificateStore.delete(nodeId, identityCertificate.issuerCommonName) + context.channelManager.delete(this) + } + + public companion object { + /** + * Generate endpoint and register it with the private gateway. + */ + @Throws( + RegistrationFailedException::class, + GatewayProtocolException::class, + GatewayUnregisteredException::class, + PersistenceException::class, + SetupPendingException::class, + ) + public suspend fun register(): FirstPartyEndpoint { + val context = Awala.getContextOrThrow() + val keyPair = generateRSAKeyPair() + + val registration = context.gatewayClient.registerEndpoint(keyPair) + val endpoint = + FirstPartyEndpoint( + keyPair.private, + registration.privateNodeCertificate, + listOf(registration.gatewayCertificate), + registration.gatewayInternetAddress, + ) + + try { + context.privateKeyStore.saveIdentityKey( + keyPair.private, + ) + } catch (exc: KeyStoreBackendException) { + throw PersistenceException("Failed to save identity key", exc) + } + + val gatewayId = registration.gatewayCertificate.subjectId + try { + context.certificateStore.save( + CertificationPath( + registration.privateNodeCertificate, + listOf(registration.gatewayCertificate), + ), + gatewayId, + ) + } catch (exc: KeyStoreBackendException) { + throw PersistenceException("Failed to save certificate", exc) + } + + context.storage.gatewayId.set( + endpoint.nodeId, + gatewayId, ) - ?: return null - } catch (exc: KeyStoreBackendException) { - throw PersistenceException("Failed to load certificate for endpoint", exc) + + context.storage.internetAddress.set(registration.gatewayInternetAddress) + + return endpoint } - val internetAddress: String = context.storage.internetAddress.get() - ?: throw PersistenceException( - "Failed to load gateway internet address for endpoint", + /** + * Load an endpoint by its address. + */ + @Throws(PersistenceException::class, SetupPendingException::class) + public suspend fun load(nodeId: String): FirstPartyEndpoint? { + val context = Awala.getContextOrThrow() + val identityPrivateKey = + try { + context.privateKeyStore.retrieveIdentityKey(nodeId) + } catch (exc: MissingKeyException) { + return null + } catch (exc: KeyStoreBackendException) { + throw PersistenceException("Failed to load private key of endpoint", exc) + } + val gatewayNodeId = + context.storage.gatewayId.get(nodeId) + ?: throw PersistenceException("Failed to load gateway address for endpoint") + val certificatePath = + try { + context.certificateStore.retrieveLatest( + nodeId, gatewayNodeId, + ) + ?: return null + } catch (exc: KeyStoreBackendException) { + throw PersistenceException("Failed to load certificate for endpoint", exc) + } + + val internetAddress: String = + context.storage.internetAddress.get() + ?: throw PersistenceException( + "Failed to load gateway internet address for endpoint", + ) + + return FirstPartyEndpoint( + identityPrivateKey, + certificatePath.leafCertificate, + certificatePath.certificateAuthorities, + internetAddress, ) - - return FirstPartyEndpoint( - identityPrivateKey, - certificatePath.leafCertificate, - certificatePath.certificateAuthorities, - internetAddress, - ) + } } } -} /** * Failure to issue a PDA. diff --git a/lib/src/main/java/tech/relaycorp/awaladroid/endpoint/HandleGatewayCertificateChange.kt b/lib/src/main/java/tech/relaycorp/awaladroid/endpoint/HandleGatewayCertificateChange.kt index d9fd6fa0..4ed529c1 100644 --- a/lib/src/main/java/tech/relaycorp/awaladroid/endpoint/HandleGatewayCertificateChange.kt +++ b/lib/src/main/java/tech/relaycorp/awaladroid/endpoint/HandleGatewayCertificateChange.kt @@ -7,7 +7,6 @@ import tech.relaycorp.relaynet.wrappers.nodeId internal class HandleGatewayCertificateChange( private val privateKeyStore: PrivateKeyStore, ) { - @Throws(GatewayUnregisteredException::class) suspend operator fun invoke() { privateKeyStore.retrieveAllIdentityKeys() diff --git a/lib/src/main/java/tech/relaycorp/awaladroid/endpoint/RenewExpiringCertificates.kt b/lib/src/main/java/tech/relaycorp/awaladroid/endpoint/RenewExpiringCertificates.kt index b654c328..4fb11111 100644 --- a/lib/src/main/java/tech/relaycorp/awaladroid/endpoint/RenewExpiringCertificates.kt +++ b/lib/src/main/java/tech/relaycorp/awaladroid/endpoint/RenewExpiringCertificates.kt @@ -11,7 +11,6 @@ internal class RenewExpiringCertificates( private val privateKeyStore: PrivateKeyStore, private val firstPartyEndpointLoader: suspend (String) -> FirstPartyEndpoint?, ) { - @Throws(GatewayUnregisteredException::class) suspend operator fun invoke() { privateKeyStore.retrieveAllIdentityKeys() diff --git a/lib/src/main/java/tech/relaycorp/awaladroid/endpoint/ThirdPartyEndpoint.kt b/lib/src/main/java/tech/relaycorp/awaladroid/endpoint/ThirdPartyEndpoint.kt index 877c665c..928ead87 100644 --- a/lib/src/main/java/tech/relaycorp/awaladroid/endpoint/ThirdPartyEndpoint.kt +++ b/lib/src/main/java/tech/relaycorp/awaladroid/endpoint/ThirdPartyEndpoint.kt @@ -21,7 +21,6 @@ public sealed class ThirdPartyEndpoint( internal val identityKey: PublicKey, public val internetAddress: String, ) : Endpoint(identityKey.nodeId) { - internal val recipient: Recipient get() = Recipient(nodeId, internetAddress) @@ -60,7 +59,6 @@ public class PrivateThirdPartyEndpoint internal constructor( internal val pdaChain: List, internetAddress: String, ) : ThirdPartyEndpoint(identityKey, internetAddress) { - private val storageKey = "${firstPartyEndpointAddress}_$nodeId" @Throws(PersistenceException::class, SetupPendingException::class) @@ -94,11 +92,12 @@ public class PrivateThirdPartyEndpoint internal constructor( } val context = Awala.getContextOrThrow() - val data = PrivateThirdPartyEndpointData( - identityKey, - deliveryAuth, - connectionParams.internetGatewayAddress, - ) + val data = + PrivateThirdPartyEndpointData( + identityKey, + deliveryAuth, + connectionParams.internetGatewayAddress, + ) context.storage.privateThirdParty.set(storageKey, data) } @@ -139,11 +138,12 @@ public class PrivateThirdPartyEndpoint internal constructor( ): PrivateThirdPartyEndpoint { val context = Awala.getContextOrThrow() - val params = try { - PrivateEndpointConnParams.deserialize(connectionParamsSerialized) - } catch (exc: InvalidNodeConnectionParams) { - throw InvalidThirdPartyEndpoint("Malformed connection params", exc) - } + val params = + try { + PrivateEndpointConnParams.deserialize(connectionParamsSerialized) + } catch (exc: InvalidNodeConnectionParams) { + throw InvalidThirdPartyEndpoint("Malformed connection params", exc) + } val pdaPath = params.deliveryAuth val pda = pdaPath.leafCertificate val pdaChain = pdaPath.certificateAuthorities @@ -163,19 +163,21 @@ public class PrivateThirdPartyEndpoint internal constructor( throw InvalidAuthorizationException("PDA path is invalid", exc) } - val endpoint = PrivateThirdPartyEndpoint( - firstPartyAddress, - params.identityKey, - pda, - pdaChain, - params.internetGatewayAddress, - ) + val endpoint = + PrivateThirdPartyEndpoint( + firstPartyAddress, + params.identityKey, + pda, + pdaChain, + params.internetGatewayAddress, + ) - val data = PrivateThirdPartyEndpointData( - params.identityKey, - pdaPath, - params.internetGatewayAddress, - ) + val data = + PrivateThirdPartyEndpointData( + params.identityKey, + pdaPath, + params.internetGatewayAddress, + ) context.storage.privateThirdParty.set(endpoint.storageKey, data) context.sessionPublicKeyStore.save(params.sessionKey, endpoint.nodeId) @@ -194,7 +196,6 @@ public class PublicThirdPartyEndpoint internal constructor( internetAddress: String, identityKey: PublicKey, ) : ThirdPartyEndpoint(identityKey, internetAddress) { - @Throws(PersistenceException::class, SetupPendingException::class) override suspend fun delete() { val context = Awala.getContextOrThrow() @@ -228,14 +229,15 @@ public class PublicThirdPartyEndpoint internal constructor( connectionParamsSerialized: ByteArray, ): PublicThirdPartyEndpoint { val context = Awala.getContextOrThrow() - val connectionParams = try { - NodeConnectionParams.deserialize(connectionParamsSerialized) - } catch (exc: InvalidNodeConnectionParams) { - throw InvalidThirdPartyEndpoint( - "Connection params serialization is malformed", - exc, - ) - } + val connectionParams = + try { + NodeConnectionParams.deserialize(connectionParamsSerialized) + } catch (exc: InvalidNodeConnectionParams) { + throw InvalidThirdPartyEndpoint( + "Connection params serialization is malformed", + exc, + ) + } val peerNodeId = connectionParams.identityKey.nodeId context.storage.publicThirdParty.set( peerNodeId, @@ -257,7 +259,9 @@ public class PublicThirdPartyEndpoint internal constructor( } public class UnknownThirdPartyEndpointException(message: String) : AwaladroidException(message) + public class UnknownFirstPartyEndpointException(message: String) : AwaladroidException(message) + public class InvalidThirdPartyEndpoint(message: String, cause: Throwable? = null) : AwaladroidException(message, cause) diff --git a/lib/src/main/java/tech/relaycorp/awaladroid/endpoint/ThirdPartyEndpointAuth.kt b/lib/src/main/java/tech/relaycorp/awaladroid/endpoint/ThirdPartyEndpointAuth.kt index a3951446..8534a2c2 100644 --- a/lib/src/main/java/tech/relaycorp/awaladroid/endpoint/ThirdPartyEndpointAuth.kt +++ b/lib/src/main/java/tech/relaycorp/awaladroid/endpoint/ThirdPartyEndpointAuth.kt @@ -8,7 +8,6 @@ public class ThirdPartyEndpointAuth( * Id of the third-party endpoint. */ public val endpointId: String, - /** * The authorization serialized. */ diff --git a/lib/src/main/java/tech/relaycorp/awaladroid/messaging/IncomingMessage.kt b/lib/src/main/java/tech/relaycorp/awaladroid/messaging/IncomingMessage.kt index aed33586..0183daad 100644 --- a/lib/src/main/java/tech/relaycorp/awaladroid/messaging/IncomingMessage.kt +++ b/lib/src/main/java/tech/relaycorp/awaladroid/messaging/IncomingMessage.kt @@ -35,7 +35,6 @@ public class IncomingMessage internal constructor( public val recipientEndpoint: FirstPartyEndpoint, public val ack: suspend () -> Unit, ) : Message() { - internal companion object { private const val PDA_PATH_TYPE = "application/vnd+relaycorp.awala.pda-path" @@ -47,30 +46,36 @@ public class IncomingMessage internal constructor( InvalidMessageException::class, SetupPendingException::class, ) - internal suspend fun build(parcel: Parcel, ack: suspend () -> Unit): IncomingMessage? { - val recipientEndpoint = FirstPartyEndpoint.load(parcel.recipient.id) - ?: throw UnknownFirstPartyEndpointException( - "Unknown first-party endpoint ${parcel.recipient.id}", - ) + internal suspend fun build( + parcel: Parcel, + ack: suspend () -> Unit, + ): IncomingMessage? { + val recipientEndpoint = + FirstPartyEndpoint.load(parcel.recipient.id) + ?: throw UnknownFirstPartyEndpointException( + "Unknown first-party endpoint ${parcel.recipient.id}", + ) - val sender = ThirdPartyEndpoint.load( - parcel.recipient.id, - parcel.senderCertificate.subjectId, - ) ?: throw UnknownThirdPartyEndpointException( - "Unknown third-party endpoint " + - "${parcel.senderCertificate.subjectId} " + - "for first-party endpoint ${parcel.recipient.id}", - ) + val sender = + ThirdPartyEndpoint.load( + parcel.recipient.id, + parcel.senderCertificate.subjectId, + ) ?: throw UnknownThirdPartyEndpointException( + "Unknown third-party endpoint " + + "${parcel.senderCertificate.subjectId} " + + "for first-party endpoint ${parcel.recipient.id}", + ) val context = Awala.getContextOrThrow() - val serviceMessage = try { - context.endpointManager.unwrapMessagePayload(parcel) - } catch (e: MissingKeyException) { - throw UnknownThirdPartyEndpointException( - "Missing third-party endpoint session keys", - ) - } + val serviceMessage = + try { + context.endpointManager.unwrapMessagePayload(parcel) + } catch (e: MissingKeyException) { + throw UnknownThirdPartyEndpointException( + "Missing third-party endpoint session keys", + ) + } if (serviceMessage.type == PDA_PATH_TYPE) { processConnectionParams(serviceMessage.content, sender, recipientEndpoint) ack() @@ -97,17 +102,18 @@ public class IncomingMessage internal constructor( ) return } - val params = try { - PrivateEndpointConnParams.deserialize(paramsSerialized) - } catch (exc: InvalidNodeConnectionParams) { - logger.log( - Level.INFO, - "Ignoring malformed connection params for ${recipientEndpoint.nodeId} " + - "from ${senderEndpoint.nodeId}", - exc, - ) - return - } + val params = + try { + PrivateEndpointConnParams.deserialize(paramsSerialized) + } catch (exc: InvalidNodeConnectionParams) { + logger.log( + Level.INFO, + "Ignoring malformed connection params for ${recipientEndpoint.nodeId} " + + "from ${senderEndpoint.nodeId}", + exc, + ) + return + } try { (senderEndpoint as PrivateThirdPartyEndpoint).updateParams(params) diff --git a/lib/src/main/java/tech/relaycorp/awaladroid/messaging/OutgoingMessage.kt b/lib/src/main/java/tech/relaycorp/awaladroid/messaging/OutgoingMessage.kt index 798c69eb..b7b714e0 100644 --- a/lib/src/main/java/tech/relaycorp/awaladroid/messaging/OutgoingMessage.kt +++ b/lib/src/main/java/tech/relaycorp/awaladroid/messaging/OutgoingMessage.kt @@ -22,101 +22,110 @@ import java.time.ZonedDateTime * @property parcelId The parcel id. */ public class OutgoingMessage -private constructor( - public val senderEndpoint: FirstPartyEndpoint, - public val recipientEndpoint: ThirdPartyEndpoint, - public val parcelExpiryDate: ZonedDateTime, - public val parcelId: ParcelId, - internal val parcelCreationDate: ZonedDateTime, -) : Message() { + private constructor( + public val senderEndpoint: FirstPartyEndpoint, + public val recipientEndpoint: ThirdPartyEndpoint, + public val parcelExpiryDate: ZonedDateTime, + public val parcelId: ParcelId, + internal val parcelCreationDate: ZonedDateTime, + ) : Message() { + internal lateinit var parcel: Parcel + private set - internal lateinit var parcel: Parcel - private set - - internal val ttl get() = Duration.between(parcelCreationDate, parcelExpiryDate).seconds.toInt() + internal val ttl get() = + Duration.between( + parcelCreationDate, + parcelExpiryDate, + ).seconds.toInt() - public companion object { - private val CLOCK_DRIFT_OFFSET = Duration.ofMinutes(5) - private val MAX_TTL = Duration.ofDays(180) + public companion object { + private val CLOCK_DRIFT_OFFSET = Duration.ofMinutes(5) + private val MAX_TTL = Duration.ofDays(180) - private fun maxExpiryDate() = ZonedDateTime.now().plus(MAX_TTL).minus(CLOCK_DRIFT_OFFSET) + private fun maxExpiryDate() = + ZonedDateTime.now().plus( + MAX_TTL, + ).minus(CLOCK_DRIFT_OFFSET) - /** - * Create an outgoing service message (but don't send it). - * - * @param type The type of the message (e.g., "application/vnd.awala.ping-v1.ping"). - * @param content The contents of the service message. - * @param senderEndpoint The endpoint used to send the message. - * @param recipientEndpoint The endpoint that will receive the message. - * @param parcelExpiryDate The date when the parcel should expire. - * @param parcelId The id of the parcel. - */ - @Throws(InvalidMessageException::class) - public suspend fun build( - type: String, - content: ByteArray, - senderEndpoint: FirstPartyEndpoint, - recipientEndpoint: ThirdPartyEndpoint, - parcelExpiryDate: ZonedDateTime = maxExpiryDate(), - parcelId: ParcelId = ParcelId.generate(), - ): OutgoingMessage { - val message = OutgoingMessage( - senderEndpoint, - recipientEndpoint, - parcelExpiryDate, - parcelId, - ZonedDateTime.now().minus(CLOCK_DRIFT_OFFSET), - ) - message.parcel = message.buildParcel(type, content) - return message + /** + * Create an outgoing service message (but don't send it). + * + * @param type The type of the message (e.g., "application/vnd.awala.ping-v1.ping"). + * @param content The contents of the service message. + * @param senderEndpoint The endpoint used to send the message. + * @param recipientEndpoint The endpoint that will receive the message. + * @param parcelExpiryDate The date when the parcel should expire. + * @param parcelId The id of the parcel. + */ + @Throws(InvalidMessageException::class) + public suspend fun build( + type: String, + content: ByteArray, + senderEndpoint: FirstPartyEndpoint, + recipientEndpoint: ThirdPartyEndpoint, + parcelExpiryDate: ZonedDateTime = maxExpiryDate(), + parcelId: ParcelId = ParcelId.generate(), + ): OutgoingMessage { + val message = + OutgoingMessage( + senderEndpoint, + recipientEndpoint, + parcelExpiryDate, + parcelId, + ZonedDateTime.now().minus(CLOCK_DRIFT_OFFSET), + ) + message.parcel = message.buildParcel(type, content) + return message + } } - } - @Throws(InvalidMessageException::class) - private suspend fun buildParcel( - serviceMessageType: String, - serviceMessageContent: ByteArray, - ): Parcel { - val serviceMessage = ServiceMessage(serviceMessageType, serviceMessageContent) - val endpointManager = Awala.getContextOrThrow().endpointManager - val payload = endpointManager.wrapMessagePayload( - serviceMessage, - recipientEndpoint.nodeId, - senderEndpoint.nodeId, - ) - val parcel = try { - Parcel( - recipient = recipientEndpoint.recipient, - payload = payload, - senderCertificate = getSenderCertificate(), - messageId = parcelId.value, - creationDate = parcelCreationDate, - ttl = ttl, - senderCertificateChain = getSenderCertificateChain(), - ) - } catch (exc: RAMFException) { - throw InvalidMessageException("Failed to create parcel", exc) + @Throws(InvalidMessageException::class) + private suspend fun buildParcel( + serviceMessageType: String, + serviceMessageContent: ByteArray, + ): Parcel { + val serviceMessage = ServiceMessage(serviceMessageType, serviceMessageContent) + val endpointManager = Awala.getContextOrThrow().endpointManager + val payload = + endpointManager.wrapMessagePayload( + serviceMessage, + recipientEndpoint.nodeId, + senderEndpoint.nodeId, + ) + val parcel = + try { + Parcel( + recipient = recipientEndpoint.recipient, + payload = payload, + senderCertificate = getSenderCertificate(), + messageId = parcelId.value, + creationDate = parcelCreationDate, + ttl = ttl, + senderCertificateChain = getSenderCertificateChain(), + ) + } catch (exc: RAMFException) { + throw InvalidMessageException("Failed to create parcel", exc) + } + return parcel } - return parcel - } - private fun getSenderCertificate(): Certificate = - when (recipientEndpoint) { - is PublicThirdPartyEndpoint -> getSelfSignedSenderCertificate() - is PrivateThirdPartyEndpoint -> recipientEndpoint.pda - } + private fun getSenderCertificate(): Certificate = + when (recipientEndpoint) { + is PublicThirdPartyEndpoint -> getSelfSignedSenderCertificate() + is PrivateThirdPartyEndpoint -> recipientEndpoint.pda + } - private fun getSelfSignedSenderCertificate(): Certificate = - issueEndpointCertificate( - senderEndpoint.identityCertificate.subjectPublicKey, - senderEndpoint.identityPrivateKey, - validityStartDate = parcelCreationDate, - validityEndDate = parcelExpiryDate, - ) + private fun getSelfSignedSenderCertificate(): Certificate = + issueEndpointCertificate( + senderEndpoint.identityCertificate.subjectPublicKey, + senderEndpoint.identityPrivateKey, + validityStartDate = parcelCreationDate, + validityEndDate = parcelExpiryDate, + ) - private fun getSenderCertificateChain(): Set = - when (recipientEndpoint) { - is PublicThirdPartyEndpoint -> emptySet() - is PrivateThirdPartyEndpoint -> recipientEndpoint.pdaChain.toSet() - } -} + private fun getSenderCertificateChain(): Set = + when (recipientEndpoint) { + is PublicThirdPartyEndpoint -> emptySet() + is PrivateThirdPartyEndpoint -> recipientEndpoint.pdaChain.toSet() + } + } diff --git a/lib/src/main/java/tech/relaycorp/awaladroid/messaging/ParcelId.kt b/lib/src/main/java/tech/relaycorp/awaladroid/messaging/ParcelId.kt index 5b122040..f3a26487 100644 --- a/lib/src/main/java/tech/relaycorp/awaladroid/messaging/ParcelId.kt +++ b/lib/src/main/java/tech/relaycorp/awaladroid/messaging/ParcelId.kt @@ -14,22 +14,22 @@ import java.util.UUID * Note that the behavior above is scoped to the same sender/recipient pair. */ public class ParcelId -internal constructor( - public val value: String, -) { - public companion object { - /** - * Generate a new parcel id. - */ - public fun generate(): ParcelId = ParcelId(UUID.randomUUID().toString()) - } + internal constructor( + public val value: String, + ) { + public companion object { + /** + * Generate a new parcel id. + */ + public fun generate(): ParcelId = ParcelId(UUID.randomUUID().toString()) + } - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other !is ParcelId) return false - if (value != other.value) return false - return true - } + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is ParcelId) return false + if (value != other.value) return false + return true + } - override fun hashCode(): Int = value.hashCode() -} + override fun hashCode(): Int = value.hashCode() + } diff --git a/lib/src/main/java/tech/relaycorp/awaladroid/messaging/ReceiveMessages.kt b/lib/src/main/java/tech/relaycorp/awaladroid/messaging/ReceiveMessages.kt index 02a9662b..1714f96a 100644 --- a/lib/src/main/java/tech/relaycorp/awaladroid/messaging/ReceiveMessages.kt +++ b/lib/src/main/java/tech/relaycorp/awaladroid/messaging/ReceiveMessages.kt @@ -32,7 +32,6 @@ import java.util.logging.Level internal class ReceiveMessages( private val pdcClientBuilder: () -> PDCClient = { PoWebClient.initLocal(Awala.POWEB_PORT) }, ) { - /** * Flow may throw: * - ReceiveMessageException @@ -73,26 +72,27 @@ internal class ReceiveMessages( } @Throws(PersistenceException::class) - private fun getNonceSigners() = suspend { - val context = Awala.getContextOrThrow() - context.privateKeyStore.retrieveAllIdentityKeys() - .flatMap { identityPrivateKey -> - val nodeId = identityPrivateKey.nodeId - val privateGatewayId = - context.storage.gatewayId.get(nodeId) - ?: return@flatMap emptyList() - context.certificateStore.retrieveAll( - nodeId, - privateGatewayId, - ).map { - Signer( - it.leafCertificate, - identityPrivateKey, - ) + private fun getNonceSigners() = + suspend { + val context = Awala.getContextOrThrow() + context.privateKeyStore.retrieveAllIdentityKeys() + .flatMap { identityPrivateKey -> + val nodeId = identityPrivateKey.nodeId + val privateGatewayId = + context.storage.gatewayId.get(nodeId) + ?: return@flatMap emptyList() + context.certificateStore.retrieveAll( + nodeId, + privateGatewayId, + ).map { + Signer( + it.leafCertificate, + identityPrivateKey, + ) + } } - } - .toTypedArray() - }.asFlow() + .toTypedArray() + }.asFlow() /** * Flow may throw: @@ -100,11 +100,14 @@ internal class ReceiveMessages( * - GatewayProtocolException */ @Throws(PersistenceException::class) - private suspend fun collectParcels(pdcClient: PDCClient, nonceSigners: Array) = - pdcClient - .collectParcels(nonceSigners, StreamingMode.CloseUponCompletion) - .mapNotNull { parcelCollection -> - val parcel = try { + private suspend fun collectParcels( + pdcClient: PDCClient, + nonceSigners: Array, + ) = pdcClient + .collectParcels(nonceSigners, StreamingMode.CloseUponCompletion) + .mapNotNull { parcelCollection -> + val parcel = + try { parcelCollection.deserializeAndValidateParcel() } catch (exp: RAMFException) { parcelCollection.disregard("Malformed incoming parcel", exp) @@ -113,31 +116,34 @@ internal class ReceiveMessages( parcelCollection.disregard("Invalid incoming parcel", exp) return@mapNotNull null } - try { - IncomingMessage.build(parcel) { parcelCollection.ack() } - } catch (exp: UnknownFirstPartyEndpointException) { - parcelCollection.disregard("Incoming parcel with invalid recipient", exp) - return@mapNotNull null - } catch (exp: UnknownThirdPartyEndpointException) { - parcelCollection.disregard("Incoming parcel issues with invalid sender", exp) - return@mapNotNull null - } catch (exp: EnvelopedDataException) { - parcelCollection.disregard( - "Failed to decrypt parcel; sender might have used wrong key", - exp, - ) - return@mapNotNull null - } catch (exp: InvalidPayloadException) { - parcelCollection.disregard( - "Incoming parcel did not encapsulate a valid service message", - exp, - ) - return@mapNotNull null - } + try { + IncomingMessage.build(parcel) { parcelCollection.ack() } + } catch (exp: UnknownFirstPartyEndpointException) { + parcelCollection.disregard("Incoming parcel with invalid recipient", exp) + return@mapNotNull null + } catch (exp: UnknownThirdPartyEndpointException) { + parcelCollection.disregard("Incoming parcel issues with invalid sender", exp) + return@mapNotNull null + } catch (exp: EnvelopedDataException) { + parcelCollection.disregard( + "Failed to decrypt parcel; sender might have used wrong key", + exp, + ) + return@mapNotNull null + } catch (exp: InvalidPayloadException) { + parcelCollection.disregard( + "Incoming parcel did not encapsulate a valid service message", + exp, + ) + return@mapNotNull null } + } } -private suspend fun ParcelCollection.disregard(reason: String, exc: Throwable) { +private suspend fun ParcelCollection.disregard( + reason: String, + exc: Throwable, +) { logger.log(Level.WARNING, reason, exc) ack() } diff --git a/lib/src/main/java/tech/relaycorp/awaladroid/messaging/SendMessage.kt b/lib/src/main/java/tech/relaycorp/awaladroid/messaging/SendMessage.kt index 4acc09a3..f412077a 100644 --- a/lib/src/main/java/tech/relaycorp/awaladroid/messaging/SendMessage.kt +++ b/lib/src/main/java/tech/relaycorp/awaladroid/messaging/SendMessage.kt @@ -17,7 +17,6 @@ internal class SendMessage( private val pdcClientBuilder: () -> PDCClient = { PoWebClient.initLocal(Awala.POWEB_PORT) }, private val coroutineContext: CoroutineContext = Dispatchers.IO, ) { - @Throws( SendMessageException::class, RejectedMessageException::class, diff --git a/lib/src/main/java/tech/relaycorp/awaladroid/storage/StorageImpl.kt b/lib/src/main/java/tech/relaycorp/awaladroid/storage/StorageImpl.kt index c8b2f78a..fe9664bf 100644 --- a/lib/src/main/java/tech/relaycorp/awaladroid/storage/StorageImpl.kt +++ b/lib/src/main/java/tech/relaycorp/awaladroid/storage/StorageImpl.kt @@ -9,83 +9,90 @@ import java.nio.charset.Charset // TODO: Test internal class StorageImpl -constructor( - persistence: Persistence, -) { + constructor( + persistence: Persistence, + ) { + private val ascii = Charset.forName("ASCII") + internal val gatewayId: SingleModule = + SingleModule( + persistence = persistence, + prefix = "gateway_id_", + serializer = { address: String -> address.toByteArray(ascii) }, + deserializer = { + addressSerialized: ByteArray -> + addressSerialized.toString(ascii) + }, + ) - private val ascii = Charset.forName("ASCII") - internal val gatewayId: SingleModule = SingleModule( - persistence = persistence, - prefix = "gateway_id_", - serializer = { address: String -> address.toByteArray(ascii) }, - deserializer = { addressSerialized: ByteArray -> addressSerialized.toString(ascii) }, - ) + internal val internetAddress: SingleModule = + SingleModule( + persistence = persistence, + prefix = "internet_address_", + serializer = { internetAddress: String -> internetAddress.toByteArray(ascii) }, + deserializer = { internetAddressSerialized: ByteArray -> + internetAddressSerialized.toString(ascii) + }, + ) - internal val internetAddress: SingleModule = SingleModule( - persistence = persistence, - prefix = "internet_address_", - serializer = { internetAddress: String -> internetAddress.toByteArray(ascii) }, - deserializer = { internetAddressSerialized: ByteArray -> - internetAddressSerialized.toString(ascii) - }, - ) + internal val publicThirdParty: Module = + Module( + persistence = persistence, + prefix = "public_third_party_", + serializer = PublicThirdPartyEndpointData::serialize, + deserializer = PublicThirdPartyEndpointData::deserialize, + ) - internal val publicThirdParty: Module = Module( - persistence = persistence, - prefix = "public_third_party_", - serializer = PublicThirdPartyEndpointData::serialize, - deserializer = PublicThirdPartyEndpointData::deserialize, - ) + internal val privateThirdParty: Module = + Module( + persistence = persistence, + prefix = "private_third_party_", + serializer = PrivateThirdPartyEndpointData::serialize, + deserializer = PrivateThirdPartyEndpointData::deserialize, + ) - internal val privateThirdParty: Module = Module( - persistence = persistence, - prefix = "private_third_party_", - serializer = PrivateThirdPartyEndpointData::serialize, - deserializer = PrivateThirdPartyEndpointData::deserialize, - ) + internal open class Module( + private val persistence: Persistence, + @get:VisibleForTesting + internal val prefix: String, + private val serializer: (T) -> ByteArray, + private val deserializer: (ByteArray) -> T, + ) { + @Throws(PersistenceException::class) + suspend fun set( + key: String, + data: T, + ) { + persistence.set("$prefix$key", serializer(data)) + } - internal open class Module( - private val persistence: Persistence, - @get:VisibleForTesting - internal val prefix: String, - private val serializer: (T) -> ByteArray, - private val deserializer: (ByteArray) -> T, - ) { + @Throws(PersistenceException::class) + suspend fun get(key: String): T? = + persistence.get("$prefix$key")?.let { deserializer(it) } - @Throws(PersistenceException::class) - suspend fun set(key: String, data: T) { - persistence.set("$prefix$key", serializer(data)) - } + @Throws(PersistenceException::class) + suspend fun delete(key: String) { + persistence.delete("$prefix$key") + } - @Throws(PersistenceException::class) - suspend fun get(key: String): T? = - persistence.get("$prefix$key")?.let { deserializer(it) } + suspend fun deleteAll() { + persistence.deleteAll(prefix) + } - @Throws(PersistenceException::class) - suspend fun delete(key: String) { - persistence.delete("$prefix$key") + suspend fun list(): List = + persistence.list(prefix) + .map { it.substring(prefix.length) } } - suspend fun deleteAll() { - persistence.deleteAll(prefix) - } + internal class SingleModule( + persistence: Persistence, + prefix: String, + serializer: (T) -> ByteArray, + deserializer: (ByteArray) -> T, + ) : Module(persistence, prefix, serializer, deserializer) { + @Throws(PersistenceException::class) + suspend fun get() = get("base") - suspend fun list(): List = - persistence.list(prefix) - .map { it.substring(prefix.length) } - } - - internal class SingleModule( - persistence: Persistence, - prefix: String, - serializer: (T) -> ByteArray, - deserializer: (ByteArray) -> T, - ) : Module(persistence, prefix, serializer, deserializer) { - - @Throws(PersistenceException::class) - suspend fun get() = get("base") - - @Throws(PersistenceException::class) - suspend fun set(data: T) = set("base", data) + @Throws(PersistenceException::class) + suspend fun set(data: T) = set("base", data) + } } -} diff --git a/lib/src/main/java/tech/relaycorp/awaladroid/storage/persistence/DiskPersistence.kt b/lib/src/main/java/tech/relaycorp/awaladroid/storage/persistence/DiskPersistence.kt index 06236d76..a5efca86 100644 --- a/lib/src/main/java/tech/relaycorp/awaladroid/storage/persistence/DiskPersistence.kt +++ b/lib/src/main/java/tech/relaycorp/awaladroid/storage/persistence/DiskPersistence.kt @@ -11,10 +11,12 @@ internal class DiskPersistence( private val coroutineContext: CoroutineContext = Dispatchers.IO, private val rootFolder: String = "awaladroid", ) : Persistence { - @Suppress("BlockingMethodInNonBlockingContext") @Throws(PersistenceException::class) - override suspend fun set(location: String, data: ByteArray) { + override suspend fun set( + location: String, + data: ByteArray, + ) { withContext(coroutineContext) { deleteIfExists(location) try { @@ -29,18 +31,19 @@ internal class DiskPersistence( @Suppress("BlockingMethodInNonBlockingContext") @Throws(PersistenceException::class) - override suspend fun get(location: String): ByteArray? = withContext(coroutineContext) { - try { - buildFile(location) - .inputStream() - .use { it.readBytes() } - } catch (exception: IOException) { - if (buildFile(location).exists()) { - throw PersistenceException("Failed to read file at $location", exception) + override suspend fun get(location: String): ByteArray? = + withContext(coroutineContext) { + try { + buildFile(location) + .inputStream() + .use { it.readBytes() } + } catch (exception: IOException) { + if (buildFile(location).exists()) { + throw PersistenceException("Failed to read file at $location", exception) + } + null } - null } - } @Throws(PersistenceException::class) override suspend fun delete(location: String) { @@ -63,15 +66,16 @@ internal class DiskPersistence( } } - override suspend fun list(locationPrefix: String) = withContext(coroutineContext) { - val rootFolder = buildFile("") - rootFolder - .walkTopDown() - .toList() - .let { it.subList(1, it.size) } // skip first, the root - .map { it.absolutePath.replace(rootFolder.absolutePath + File.separator, "") } - .filter { it.startsWith(locationPrefix) } - } + override suspend fun list(locationPrefix: String) = + withContext(coroutineContext) { + val rootFolder = buildFile("") + rootFolder + .walkTopDown() + .toList() + .let { it.subList(1, it.size) } // skip first, the root + .map { it.absolutePath.replace(rootFolder.absolutePath + File.separator, "") } + .filter { it.startsWith(locationPrefix) } + } // Helpers diff --git a/lib/src/main/java/tech/relaycorp/awaladroid/storage/persistence/Persistance.kt b/lib/src/main/java/tech/relaycorp/awaladroid/storage/persistence/Persistance.kt index a8c773ad..97dbd8a1 100644 --- a/lib/src/main/java/tech/relaycorp/awaladroid/storage/persistence/Persistance.kt +++ b/lib/src/main/java/tech/relaycorp/awaladroid/storage/persistence/Persistance.kt @@ -3,9 +3,11 @@ package tech.relaycorp.awaladroid.storage.persistence import tech.relaycorp.awaladroid.AwaladroidException internal interface Persistence { - @Throws(PersistenceException::class) - suspend fun set(location: String, data: ByteArray) + suspend fun set( + location: String, + data: ByteArray, + ) @Throws(PersistenceException::class) suspend fun get(location: String): ByteArray? diff --git a/lib/src/test/java/tech/relaycorp/awaladroid/AndroidPrivateKeyStoreTest.kt b/lib/src/test/java/tech/relaycorp/awaladroid/AndroidPrivateKeyStoreTest.kt index 1b730134..4446ed49 100644 --- a/lib/src/test/java/tech/relaycorp/awaladroid/AndroidPrivateKeyStoreTest.kt +++ b/lib/src/test/java/tech/relaycorp/awaladroid/AndroidPrivateKeyStoreTest.kt @@ -16,32 +16,34 @@ import javax.crypto.AEADBadTagException @RunWith(RobolectricTestRunner::class) public class AndroidPrivateKeyStoreTest { - @Before public fun setUp() { FakeAndroidKeyStore.setup } @Test - public fun saveAndRetrieve(): Unit = runTest { - val androidContext = RuntimeEnvironment.getApplication() - val root = FileKeystoreRoot(File(androidContext.filesDir, "tmp-keystore")) - val store = AndroidPrivateKeyStore(root, androidContext) - val id = KeyPairSet.PRIVATE_ENDPOINT.private - val certificate = PDACertPath.PRIVATE_ENDPOINT + public fun saveAndRetrieve(): Unit = + runTest { + val androidContext = RuntimeEnvironment.getApplication() + val root = FileKeystoreRoot(File(androidContext.filesDir, "tmp-keystore")) + val store = AndroidPrivateKeyStore(root, androidContext) + val id = KeyPairSet.PRIVATE_ENDPOINT.private + val certificate = PDACertPath.PRIVATE_ENDPOINT - store.saveIdentityKey(id) - val retrievedId = store.retrieveIdentityKey(certificate.subjectId) - assertEquals(id, retrievedId) - } + store.saveIdentityKey(id) + val retrievedId = store.retrieveIdentityKey(certificate.subjectId) + assertEquals(id, retrievedId) + } @Test(expected = EncryptionInitializationException::class) - public fun failWithAEADBadTagException(): Unit = runTest { - val androidContext = RuntimeEnvironment.getApplication() - val root = FileKeystoreRoot(File(androidContext.filesDir, "tmp-keystore")) - val store = AndroidPrivateKeyStore(root, androidContext) { _, _ -> - throw AEADBadTagException("") + public fun failWithAEADBadTagException(): Unit = + runTest { + val androidContext = RuntimeEnvironment.getApplication() + val root = FileKeystoreRoot(File(androidContext.filesDir, "tmp-keystore")) + val store = + AndroidPrivateKeyStore(root, androidContext) { _, _ -> + throw AEADBadTagException("") + } + store.saveIdentityKey(KeyPairSet.PRIVATE_ENDPOINT.private) } - store.saveIdentityKey(KeyPairSet.PRIVATE_ENDPOINT.private) - } } diff --git a/lib/src/test/java/tech/relaycorp/awaladroid/AwalaTest.kt b/lib/src/test/java/tech/relaycorp/awaladroid/AwalaTest.kt index 017dc116..6dda61ea 100644 --- a/lib/src/test/java/tech/relaycorp/awaladroid/AwalaTest.kt +++ b/lib/src/test/java/tech/relaycorp/awaladroid/AwalaTest.kt @@ -43,109 +43,120 @@ public class AwalaTest { } @Test - public fun useAfterSetup(): Unit = runTest { - Awala.setUp(RuntimeEnvironment.getApplication()) + public fun useAfterSetup(): Unit = + runTest { + Awala.setUp(RuntimeEnvironment.getApplication()) - Awala.getContextOrThrow() - } + Awala.getContextOrThrow() + } @Test(expected = SetupPendingException::class) - public fun awaitWithoutSetup(): Unit = runTest { - Awala.awaitContextOrThrow(100.milliseconds) - } + public fun awaitWithoutSetup(): Unit = + runTest { + Awala.awaitContextOrThrow(100.milliseconds) + } @Test(expected = SetupPendingException::class) - public fun awaitWithLateSetup(): Unit = runTest { - CoroutineScope(UnconfinedTestDispatcher()).launch { - delay(200.milliseconds) - Awala.setUp(RuntimeEnvironment.getApplication()) + public fun awaitWithLateSetup(): Unit = + runTest { + CoroutineScope(UnconfinedTestDispatcher()).launch { + delay(200.milliseconds) + Awala.setUp(RuntimeEnvironment.getApplication()) + } + Awala.awaitContextOrThrow(100.milliseconds) } - Awala.awaitContextOrThrow(100.milliseconds) - } @Test(expected = SetupPendingException::class) - public fun awaitAfterSetup(): Unit = runTest { - CoroutineScope(UnconfinedTestDispatcher()).launch { - delay(500.milliseconds) - Awala.setUp(RuntimeEnvironment.getApplication()) + public fun awaitAfterSetup(): Unit = + runTest { + CoroutineScope(UnconfinedTestDispatcher()).launch { + delay(500.milliseconds) + Awala.setUp(RuntimeEnvironment.getApplication()) + } + Awala.awaitContextOrThrow(1000.milliseconds) } - Awala.awaitContextOrThrow(1000.milliseconds) - } @Test - public fun keystores(): Unit = runTest { - val androidContext = RuntimeEnvironment.getApplication() - Awala.setUp(androidContext) - - val context = Awala.getContextOrThrow() - - assertTrue(context.privateKeyStore is AndroidPrivateKeyStore) - assertTrue(context.sessionPublicKeyStore is FileSessionPublicKeystore) - assertTrue(context.certificateStore is FileCertificateStore) - val expectedRoot = File(androidContext.filesDir, "awaladroid${File.separator}keystores") - assertEquals( - expectedRoot, - (context.privateKeyStore as AndroidPrivateKeyStore).rootDirectory.parentFile, - ) - assertEquals( - expectedRoot, - (context.sessionPublicKeyStore as FileSessionPublicKeystore).rootDirectory.parentFile, - ) - assertEquals( - expectedRoot, - (context.certificateStore as FileCertificateStore).rootDirectory.parentFile, - ) - } - - @Test - public fun channelManager(): Unit = runTest { - val androidContextSpy = spy(RuntimeEnvironment.getApplication()) - Awala.setUp(androidContextSpy) + public fun keystores(): Unit = + runTest { + val androidContext = RuntimeEnvironment.getApplication() + Awala.setUp(androidContext) - val context = Awala.getContextOrThrow() + val ctx = Awala.getContextOrThrow() + + assertTrue(ctx.privateKeyStore is AndroidPrivateKeyStore) + assertTrue(ctx.sessionPublicKeyStore is FileSessionPublicKeystore) + assertTrue(ctx.certificateStore is FileCertificateStore) + val expectedRoot = + File(androidContext.filesDir, "awaladroid${File.separator}keystores") + assertEquals( + expectedRoot, + (ctx.privateKeyStore as AndroidPrivateKeyStore).rootDirectory.parentFile, + ) + assertEquals( + expectedRoot, + (ctx.sessionPublicKeyStore as FileSessionPublicKeystore).rootDirectory.parentFile, + ) + assertEquals( + expectedRoot, + (ctx.certificateStore as FileCertificateStore).rootDirectory.parentFile, + ) + } - assertEquals(Dispatchers.IO, context.channelManager.coroutineContext) - // Cause shared preferences to be resolved before inspecting it - context.channelManager.sharedPreferences - verify(androidContextSpy).getSharedPreferences("awaladroid-channels", Context.MODE_PRIVATE) - } + @Test + public fun channelManager(): Unit = + runTest { + val androidContextSpy = spy(RuntimeEnvironment.getApplication()) + Awala.setUp(androidContextSpy) + + val context = Awala.getContextOrThrow() + + assertEquals(Dispatchers.IO, context.channelManager.coroutineContext) + // Cause shared preferences to be resolved before inspecting it + context.channelManager.sharedPreferences + verify( + androidContextSpy, + ).getSharedPreferences("awaladroid-channels", Context.MODE_PRIVATE) + } @Test - public fun deleteExpiredOnSetUp(): Unit = runTest { - val androidContext = RuntimeEnvironment.getApplication() - Awala.setUp(androidContext) - val originalAwalaContext = Awala.getContextOrThrow() - val interval = Duration.ofSeconds(3) - val expiringCertificate = issueEndpointCertificate( - subjectPublicKey = KeyPairSet.PRIVATE_ENDPOINT.public, - issuerPrivateKey = KeyPairSet.PRIVATE_GW.private, - validityEndDate = ZonedDateTime.now().plus(interval), - ) - - val certificateStore = originalAwalaContext.certificateStore - certificateStore.save( - CertificationPath(expiringCertificate, emptyList()), - expiringCertificate.issuerCommonName, - ) - - advanceUntilIdle() - assertNotNull( - certificateStore.retrieveLatest( - expiringCertificate.subjectId, + public fun deleteExpiredOnSetUp(): Unit = + runTest { + val androidContext = RuntimeEnvironment.getApplication() + Awala.setUp(androidContext) + val originalAwalaContext = Awala.getContextOrThrow() + val interval = Duration.ofSeconds(3) + val expiringCertificate = + issueEndpointCertificate( + subjectPublicKey = KeyPairSet.PRIVATE_ENDPOINT.public, + issuerPrivateKey = KeyPairSet.PRIVATE_GW.private, + validityEndDate = ZonedDateTime.now().plus(interval), + ) + + val certificateStore = originalAwalaContext.certificateStore + certificateStore.save( + CertificationPath(expiringCertificate, emptyList()), expiringCertificate.issuerCommonName, - ), - ) + ) - // Retry until expiration - repeat(3) { - runCatching { Thread.sleep(interval.toMillis()) } - Awala.setUp(androidContext) advanceUntilIdle() - certificateStore.retrieveLatest( - KeyPairSet.PRIVATE_ENDPOINT.public.nodeId, - KeyPairSet.PRIVATE_GW.private.nodeId, - ) ?: return@runTest + assertNotNull( + certificateStore.retrieveLatest( + expiringCertificate.subjectId, + expiringCertificate.issuerCommonName, + ), + ) + + // Retry until expiration + repeat(3) { + runCatching { Thread.sleep(interval.toMillis()) } + Awala.setUp(androidContext) + advanceUntilIdle() + certificateStore.retrieveLatest( + KeyPairSet.PRIVATE_ENDPOINT.public.nodeId, + KeyPairSet.PRIVATE_GW.private.nodeId, + ) ?: return@runTest + } + throw AssertionError("Expired certificate not deleted") } - throw AssertionError("Expired certificate not deleted") - } } diff --git a/lib/src/test/java/tech/relaycorp/awaladroid/GatewayClientImplTest.kt b/lib/src/test/java/tech/relaycorp/awaladroid/GatewayClientImplTest.kt index e3bf41c1..fbbdbd8b 100644 --- a/lib/src/test/java/tech/relaycorp/awaladroid/GatewayClientImplTest.kt +++ b/lib/src/test/java/tech/relaycorp/awaladroid/GatewayClientImplTest.kt @@ -45,103 +45,123 @@ import kotlin.time.Duration.Companion.seconds @RunWith(RobolectricTestRunner::class) internal class GatewayClientImplTest : MockContextTestCase() { - private lateinit var pdcClient: MockPDCClient private val coroutineScope = TestScope() private val serviceInteractor = mock() private val sendMessage = mock() private val receiveMessages = mock() - override val gatewayClient = GatewayClientImpl( - coroutineScope.coroutineContext, - { serviceInteractor }, - { pdcClient }, - sendMessage, - receiveMessages, - ) + override val gatewayClient = + GatewayClientImpl( + coroutineScope.coroutineContext, + { serviceInteractor }, + { pdcClient }, + sendMessage, + receiveMessages, + ) // Binding @Test - fun bind_successful() = coroutineScope.runTest { - gatewayClient.bind() + fun bind_successful() = + coroutineScope.runTest { + gatewayClient.bind() - verify(serviceInteractor).bind( - Awala.GATEWAY_SYNC_ACTION, - Awala.GATEWAY_PACKAGE, - Awala.GATEWAY_SYNC_COMPONENT, - ) - } + verify(serviceInteractor).bind( + Awala.GATEWAY_SYNC_ACTION, + Awala.GATEWAY_PACKAGE, + Awala.GATEWAY_SYNC_COMPONENT, + ) + } @Test - fun secondBindIsSkipped() = coroutineScope.runTest { - gatewayClient.bind() - gatewayClient.bind() - - verify(serviceInteractor, times(1)) - .bind(Awala.GATEWAY_SYNC_ACTION, Awala.GATEWAY_PACKAGE, Awala.GATEWAY_SYNC_COMPONENT) - } + fun secondBindIsSkipped() = + coroutineScope.runTest { + gatewayClient.bind() + gatewayClient.bind() + + verify(serviceInteractor, times(1)) + .bind( + Awala.GATEWAY_SYNC_ACTION, + Awala.GATEWAY_PACKAGE, + Awala.GATEWAY_SYNC_COMPONENT, + ) + } @Test - fun reBind_successful() = coroutineScope.runTest { - gatewayClient.bind() - gatewayClient.unbind() - gatewayClient.bind() - - verify(serviceInteractor, times(2)) - .bind(Awala.GATEWAY_SYNC_ACTION, Awala.GATEWAY_PACKAGE, Awala.GATEWAY_SYNC_COMPONENT) - } + fun reBind_successful() = + coroutineScope.runTest { + gatewayClient.bind() + gatewayClient.unbind() + gatewayClient.bind() + + verify(serviceInteractor, times(2)) + .bind( + Awala.GATEWAY_SYNC_ACTION, + Awala.GATEWAY_PACKAGE, + Awala.GATEWAY_SYNC_COMPONENT, + ) + } @Test(expected = GatewayBindingException::class) - fun bind_unsuccessful() = coroutineScope.runTest { - whenever(serviceInteractor.bind(any(), any(), any())) - .thenThrow(ServiceInteractor.BindFailedException("")) + fun bind_unsuccessful() = + coroutineScope.runTest { + whenever(serviceInteractor.bind(any(), any(), any())) + .thenThrow(ServiceInteractor.BindFailedException("")) - gatewayClient.bind() - } + gatewayClient.bind() + } // Registration @Test - internal fun registerEndpoint_successful() = coroutineScope.runTest { - val replyMessage = buildAuthorizationReplyMessage() - whenever(serviceInteractor.sendMessage(any(), any())).thenAnswer { - it.getArgument<((Message) -> Unit)?>(1)(replyMessage) - } - - val pnr = PrivateNodeRegistration(PDACertPath.PRIVATE_ENDPOINT, PDACertPath.PRIVATE_GW, "") - pdcClient = MockPDCClient(RegisterNodeCall(Result.success(pnr))) - - val result = gatewayClient.registerEndpoint(KeyPairSet.PRIVATE_ENDPOINT) - - verify(serviceInteractor) - .bind( - Awala.GATEWAY_PRE_REGISTER_ACTION, - Awala.GATEWAY_PACKAGE, - Awala.GATEWAY_PRE_REGISTER_COMPONENT, - ) - verify(serviceInteractor) - .bind(Awala.GATEWAY_SYNC_ACTION, Awala.GATEWAY_PACKAGE, Awala.GATEWAY_SYNC_COMPONENT) + internal fun registerEndpoint_successful() = + coroutineScope.runTest { + val replyMessage = buildAuthorizationReplyMessage() + whenever(serviceInteractor.sendMessage(any(), any())).thenAnswer { + it.getArgument<((Message) -> Unit)?>(1)(replyMessage) + } - assertEquals(PDACertPath.PRIVATE_ENDPOINT, result.privateNodeCertificate) - assertEquals(PDACertPath.PRIVATE_GW, result.gatewayCertificate) - } + val pnr = + PrivateNodeRegistration(PDACertPath.PRIVATE_ENDPOINT, PDACertPath.PRIVATE_GW, "") + pdcClient = MockPDCClient(RegisterNodeCall(Result.success(pnr))) + + val result = gatewayClient.registerEndpoint(KeyPairSet.PRIVATE_ENDPOINT) + + verify(serviceInteractor) + .bind( + Awala.GATEWAY_PRE_REGISTER_ACTION, + Awala.GATEWAY_PACKAGE, + Awala.GATEWAY_PRE_REGISTER_COMPONENT, + ) + verify(serviceInteractor) + .bind( + Awala.GATEWAY_SYNC_ACTION, + Awala.GATEWAY_PACKAGE, + Awala.GATEWAY_SYNC_COMPONENT, + ) + + assertEquals(PDACertPath.PRIVATE_ENDPOINT, result.privateNodeCertificate) + assertEquals(PDACertPath.PRIVATE_GW, result.gatewayCertificate) + } @Test(expected = RegistrationFailedException::class) - internal fun registerEndpoint_withFailedPreRegisterBind() = coroutineScope.runTest { - whenever(serviceInteractor.sendMessage(any(), any())) - .thenThrow(ServiceInteractor.BindFailedException("")) + internal fun registerEndpoint_withFailedPreRegisterBind() = + coroutineScope.runTest { + whenever(serviceInteractor.sendMessage(any(), any())) + .thenThrow(ServiceInteractor.BindFailedException("")) - gatewayClient.registerEndpoint(KeyPairSet.PRIVATE_ENDPOINT) - } + gatewayClient.registerEndpoint(KeyPairSet.PRIVATE_ENDPOINT) + } @Test(expected = RegistrationFailedException::class) - internal fun registerEndpoint_withFailedPreRegisterSend() = coroutineScope.runTest { - whenever(serviceInteractor.sendMessage(any(), any())) - .thenThrow(ServiceInteractor.SendFailedException(Exception())) + internal fun registerEndpoint_withFailedPreRegisterSend() = + coroutineScope.runTest { + whenever(serviceInteractor.sendMessage(any(), any())) + .thenThrow(ServiceInteractor.SendFailedException(Exception())) - gatewayClient.registerEndpoint(KeyPairSet.PRIVATE_ENDPOINT) - } + gatewayClient.registerEndpoint(KeyPairSet.PRIVATE_ENDPOINT) + } @Test(expected = RegistrationFailedException::class) internal fun registerEndpoint_withFailedRegistrationDueToServer() = @@ -183,10 +203,11 @@ internal class GatewayClientImplTest : MockContextTestCase() { gatewayClient.registerEndpoint(KeyPairSet.PRIVATE_ENDPOINT) } - private fun buildPnra() = PrivateNodeRegistrationAuthorization( - ZonedDateTime.now().plusDays(1), - PDACertPath.PRIVATE_GW.serialize(), - ) + private fun buildPnra() = + PrivateNodeRegistrationAuthorization( + ZonedDateTime.now().plusDays(1), + PDACertPath.PRIVATE_GW.serialize(), + ) private fun buildAuthorizationReplyMessage(): Message { val pnra = buildPnra() @@ -199,119 +220,134 @@ internal class GatewayClientImplTest : MockContextTestCase() { // Messaging @Test - fun sendMessage_successful() = coroutineScope.runTest { - val message = - MessageFactory.buildOutgoing(createEndpointChannel(RecipientAddressType.PUBLIC)) + fun sendMessage_successful() = + coroutineScope.runTest { + val message = + MessageFactory.buildOutgoing(createEndpointChannel(RecipientAddressType.PUBLIC)) - gatewayClient.bind() - gatewayClient.sendMessage(message) - } + gatewayClient.bind() + gatewayClient.sendMessage(message) + } @Test(expected = GatewayBindingException::class) - fun sendMessage_withoutBind() = coroutineScope.runTest { - val message = - MessageFactory.buildOutgoing(createEndpointChannel(RecipientAddressType.PUBLIC)) + fun sendMessage_withoutBind() = + coroutineScope.runTest { + val message = + MessageFactory.buildOutgoing(createEndpointChannel(RecipientAddressType.PUBLIC)) - gatewayClient.sendMessage(message) - } + gatewayClient.sendMessage(message) + } @Test(expected = SendMessageException::class) - fun sendMessage_unsuccessful() = coroutineScope.runTest { - whenever(sendMessage.send(any())).thenThrow(SendMessageException("")) - val message = - MessageFactory.buildOutgoing(createEndpointChannel(RecipientAddressType.PUBLIC)) + fun sendMessage_unsuccessful() = + coroutineScope.runTest { + whenever(sendMessage.send(any())).thenThrow(SendMessageException("")) + val message = + MessageFactory.buildOutgoing(createEndpointChannel(RecipientAddressType.PUBLIC)) - gatewayClient.bind() - gatewayClient.sendMessage(message) - } + gatewayClient.bind() + gatewayClient.sendMessage(message) + } @Test(expected = GatewayProtocolException::class) - fun sendMessage_unsuccessfulDueToClient() = coroutineScope.runTest { - whenever(sendMessage.send(any())).thenThrow(GatewayProtocolException("")) - val message = - MessageFactory.buildOutgoing(createEndpointChannel(RecipientAddressType.PUBLIC)) + fun sendMessage_unsuccessfulDueToClient() = + coroutineScope.runTest { + whenever(sendMessage.send(any())).thenThrow(GatewayProtocolException("")) + val message = + MessageFactory.buildOutgoing(createEndpointChannel(RecipientAddressType.PUBLIC)) - gatewayClient.bind() - gatewayClient.sendMessage(message) - } + gatewayClient.bind() + gatewayClient.sendMessage(message) + } @Test(expected = RejectedMessageException::class) - fun sendMessage_unsuccessfulDueToRejection() = coroutineScope.runTest { - whenever(sendMessage.send(any())).thenThrow(RejectedMessageException("")) - val message = - MessageFactory.buildOutgoing(createEndpointChannel(RecipientAddressType.PUBLIC)) + fun sendMessage_unsuccessfulDueToRejection() = + coroutineScope.runTest { + whenever(sendMessage.send(any())).thenThrow(RejectedMessageException("")) + val message = + MessageFactory.buildOutgoing(createEndpointChannel(RecipientAddressType.PUBLIC)) - gatewayClient.bind() - gatewayClient.sendMessage(message) - } + gatewayClient.bind() + gatewayClient.sendMessage(message) + } @Test - fun checkForNewMessages_bindsIfNeeded() = coroutineScope.runTest { - whenever(receiveMessages.receive()).thenReturn(emptyFlow()) - - gatewayClient.checkForNewMessages() - - verify(serviceInteractor) - .bind( - eq(Awala.GATEWAY_SYNC_ACTION), - eq(Awala.GATEWAY_PACKAGE), - eq(Awala.GATEWAY_SYNC_COMPONENT), - ) - verify(serviceInteractor) - .unbind() - } + fun checkForNewMessages_bindsIfNeeded() = + coroutineScope.runTest { + whenever(receiveMessages.receive()).thenReturn(emptyFlow()) + + gatewayClient.checkForNewMessages() + + verify(serviceInteractor) + .bind( + eq(Awala.GATEWAY_SYNC_ACTION), + eq(Awala.GATEWAY_PACKAGE), + eq(Awala.GATEWAY_SYNC_COMPONENT), + ) + verify(serviceInteractor) + .unbind() + } @Test - fun checkForNewMessages_doesNotRebind() = coroutineScope.runTest { - whenever(receiveMessages.receive()).thenReturn(emptyFlow()) + fun checkForNewMessages_doesNotRebind() = + coroutineScope.runTest { + whenever(receiveMessages.receive()).thenReturn(emptyFlow()) - gatewayClient.bind() - gatewayClient.checkForNewMessages() + gatewayClient.bind() + gatewayClient.checkForNewMessages() - verify(serviceInteractor, times(1)).bind(any(), any(), any()) - } + verify(serviceInteractor, times(1)).bind(any(), any(), any()) + } @Test - fun checkForNewMessages_relaysIncomingMessages() = coroutineScope.runTest { - val message = MessageFactory.buildIncoming() - whenever(receiveMessages.receive()).thenReturn(flowOf(message)) + fun checkForNewMessages_relaysIncomingMessages() = + coroutineScope.runTest { + val message = MessageFactory.buildIncoming() + whenever(receiveMessages.receive()).thenReturn(flowOf(message)) - val messagesReceived = mutableListOf() - CoroutineScope(UnconfinedTestDispatcher()).launch { - gatewayClient.receiveMessages().toCollection(messagesReceived) - } + val messagesReceived = mutableListOf() + CoroutineScope(UnconfinedTestDispatcher()).launch { + gatewayClient.receiveMessages().toCollection(messagesReceived) + } - gatewayClient.checkForNewMessages() + gatewayClient.checkForNewMessages() - assertEquals(listOf(message), messagesReceived) - } + assertEquals(listOf(message), messagesReceived) + } @Test - fun checkForNewMessages_handlesReceiveException() = coroutineScope.runTest { - whenever(receiveMessages.receive()).thenReturn(flow { throw ReceiveMessageException("") }) + fun checkForNewMessages_handlesReceiveException() = + coroutineScope.runTest { + whenever( + receiveMessages.receive(), + ).thenReturn(flow { throw ReceiveMessageException("") }) - gatewayClient.checkForNewMessages() - } + gatewayClient.checkForNewMessages() + } @Test - fun checkForNewMessages_handlesProtocolException() = coroutineScope.runTest { - whenever(receiveMessages.receive()).thenReturn(flow { throw GatewayProtocolException("") }) + fun checkForNewMessages_handlesProtocolException() = + coroutineScope.runTest { + whenever( + receiveMessages.receive(), + ).thenReturn(flow { throw GatewayProtocolException("") }) - gatewayClient.checkForNewMessages() - } + gatewayClient.checkForNewMessages() + } @Test - fun checkForNewMessages_doesStartSimultaneousReceiveMessages() = coroutineScope.runTest { - whenever(receiveMessages.receive()).thenReturn(flow { delay(1.seconds) }) + fun checkForNewMessages_doesStartSimultaneousReceiveMessages() = + coroutineScope.runTest { + whenever(receiveMessages.receive()).thenReturn(flow { delay(1.seconds) }) - repeat(10) { - coroutineScope.launch { - gatewayClient.checkForNewMessages() + repeat(10) { + coroutineScope.launch { + gatewayClient.checkForNewMessages() + } } - } - delay(1.seconds) + delay(1.seconds) - verify(receiveMessages, times(1)).receive() - } + verify(receiveMessages, times(1)).receive() + } } diff --git a/lib/src/test/java/tech/relaycorp/awaladroid/background/IncomingParcelBroadcastReceiverTest.kt b/lib/src/test/java/tech/relaycorp/awaladroid/background/IncomingParcelBroadcastReceiverTest.kt index dd005a19..cd554641 100644 --- a/lib/src/test/java/tech/relaycorp/awaladroid/background/IncomingParcelBroadcastReceiverTest.kt +++ b/lib/src/test/java/tech/relaycorp/awaladroid/background/IncomingParcelBroadcastReceiverTest.kt @@ -13,11 +13,12 @@ import tech.relaycorp.awaladroid.test.MockContextTestCase @RunWith(RobolectricTestRunner::class) internal class IncomingParcelBroadcastReceiverTest : MockContextTestCase() { @Test - fun name() = runTest { - val receiver = IncomingParcelBroadcastReceiver() - receiver.coroutineContext = coroutineContext - receiver.onReceive(RuntimeEnvironment.getApplication(), Intent()) - advanceUntilIdle() - verify(gatewayClient).checkForNewMessages() - } + fun name() = + runTest { + val receiver = IncomingParcelBroadcastReceiver() + receiver.coroutineContext = coroutineContext + receiver.onReceive(RuntimeEnvironment.getApplication(), Intent()) + advanceUntilIdle() + verify(gatewayClient).checkForNewMessages() + } } diff --git a/lib/src/test/java/tech/relaycorp/awaladroid/endpoint/ChannelManagerTest.kt b/lib/src/test/java/tech/relaycorp/awaladroid/endpoint/ChannelManagerTest.kt index e1db3154..2e58a4ef 100644 --- a/lib/src/test/java/tech/relaycorp/awaladroid/endpoint/ChannelManagerTest.kt +++ b/lib/src/test/java/tech/relaycorp/awaladroid/endpoint/ChannelManagerTest.kt @@ -15,10 +15,11 @@ import tech.relaycorp.awaladroid.test.ThirdPartyEndpointFactory @RunWith(RobolectricTestRunner::class) internal class ChannelManagerTest { private val androidContext = RuntimeEnvironment.getApplication() - private val sharedPreferences = androidContext.getSharedPreferences( - "channel-test", - Context.MODE_PRIVATE, - ) + private val sharedPreferences = + androidContext.getSharedPreferences( + "channel-test", + Context.MODE_PRIVATE, + ) private val firstPartyEndpoint = FirstPartyEndpointFactory.build() private val thirdPartyEndpoint = ThirdPartyEndpointFactory.buildPrivate() @@ -39,168 +40,179 @@ internal class ChannelManagerTest { } @Test - fun create_non_existing() = runTest { - assertEquals( - null, - sharedPreferences.getStringSet(firstPartyEndpoint.nodeId, null), - ) - val manager = ChannelManager(coroutineContext) { sharedPreferences } + fun create_non_existing() = + runTest { + assertEquals( + null, + sharedPreferences.getStringSet(firstPartyEndpoint.nodeId, null), + ) + val manager = ChannelManager(coroutineContext) { sharedPreferences } - manager.create(firstPartyEndpoint, thirdPartyEndpoint) + manager.create(firstPartyEndpoint, thirdPartyEndpoint) - assertEquals( - setOf(thirdPartyEndpoint.nodeId), - sharedPreferences.getStringSet(firstPartyEndpoint.nodeId, null), - ) - } + assertEquals( + setOf(thirdPartyEndpoint.nodeId), + sharedPreferences.getStringSet(firstPartyEndpoint.nodeId, null), + ) + } @Test - fun create_existing() = runTest { - val manager = ChannelManager(coroutineContext) { sharedPreferences } - manager.create(firstPartyEndpoint, thirdPartyEndpoint) + fun create_existing() = + runTest { + val manager = ChannelManager(coroutineContext) { sharedPreferences } + manager.create(firstPartyEndpoint, thirdPartyEndpoint) - manager.create(firstPartyEndpoint, thirdPartyEndpoint) + manager.create(firstPartyEndpoint, thirdPartyEndpoint) - assertEquals( - setOf(thirdPartyEndpoint.nodeId), - sharedPreferences.getStringSet(firstPartyEndpoint.nodeId, null), - ) - } + assertEquals( + setOf(thirdPartyEndpoint.nodeId), + sharedPreferences.getStringSet(firstPartyEndpoint.nodeId, null), + ) + } @Test - fun create_with_thirdPartyEndpointPublicKey() = runTest { - val manager = ChannelManager(coroutineContext) { sharedPreferences } - manager.create(firstPartyEndpoint, thirdPartyEndpoint.identityKey) + fun create_with_thirdPartyEndpointPublicKey() = + runTest { + val manager = ChannelManager(coroutineContext) { sharedPreferences } + manager.create(firstPartyEndpoint, thirdPartyEndpoint.identityKey) - manager.create(firstPartyEndpoint, thirdPartyEndpoint) + manager.create(firstPartyEndpoint, thirdPartyEndpoint) - assertEquals( - setOf(thirdPartyEndpoint.nodeId), - sharedPreferences.getStringSet(firstPartyEndpoint.nodeId, null), - ) - } + assertEquals( + setOf(thirdPartyEndpoint.nodeId), + sharedPreferences.getStringSet(firstPartyEndpoint.nodeId, null), + ) + } @Test - fun delete_first_party_non_existing() = runTest { - val manager = ChannelManager(coroutineContext) { sharedPreferences } + fun delete_first_party_non_existing() = + runTest { + val manager = ChannelManager(coroutineContext) { sharedPreferences } - manager.delete(firstPartyEndpoint) + manager.delete(firstPartyEndpoint) - assertEquals( - null, - sharedPreferences.getStringSet(firstPartyEndpoint.nodeId, null), - ) - } + assertEquals( + null, + sharedPreferences.getStringSet(firstPartyEndpoint.nodeId, null), + ) + } @Test - fun delete_first_party_existing() = runTest { - val manager = ChannelManager(coroutineContext) { sharedPreferences } - manager.create(firstPartyEndpoint, thirdPartyEndpoint) + fun delete_first_party_existing() = + runTest { + val manager = ChannelManager(coroutineContext) { sharedPreferences } + manager.create(firstPartyEndpoint, thirdPartyEndpoint) - manager.delete(firstPartyEndpoint) + manager.delete(firstPartyEndpoint) - assertEquals( - null, - sharedPreferences.getStringSet(firstPartyEndpoint.nodeId, null), - ) - } + assertEquals( + null, + sharedPreferences.getStringSet(firstPartyEndpoint.nodeId, null), + ) + } @Test - fun delete_third_party_non_existing() = runTest { - val manager = ChannelManager(coroutineContext) { sharedPreferences } - val unrelatedThirdPartyEndpointAddress = "i-have-nothing-to-do-with-the-other" - with(sharedPreferences.edit()) { - putStringSet( - firstPartyEndpoint.nodeId, + fun delete_third_party_non_existing() = + runTest { + val manager = ChannelManager(coroutineContext) { sharedPreferences } + val unrelatedThirdPartyEndpointAddress = "i-have-nothing-to-do-with-the-other" + with(sharedPreferences.edit()) { + putStringSet( + firstPartyEndpoint.nodeId, + mutableSetOf(unrelatedThirdPartyEndpointAddress), + ) + apply() + } + + manager.delete(thirdPartyEndpoint) + + assertEquals( mutableSetOf(unrelatedThirdPartyEndpointAddress), + sharedPreferences.getStringSet(firstPartyEndpoint.nodeId, null), ) - apply() } - manager.delete(thirdPartyEndpoint) - - assertEquals( - mutableSetOf(unrelatedThirdPartyEndpointAddress), - sharedPreferences.getStringSet(firstPartyEndpoint.nodeId, null), - ) - } - @Test - fun delete_third_party_existing() = runTest { - val manager = ChannelManager(coroutineContext) { sharedPreferences } - val unrelatedThirdPartyEndpointAddress = "i-have-nothing-to-do-with-the-other" - with(sharedPreferences.edit()) { - putStringSet( - firstPartyEndpoint.nodeId, - mutableSetOf(unrelatedThirdPartyEndpointAddress, thirdPartyEndpoint.nodeId), + fun delete_third_party_existing() = + runTest { + val manager = ChannelManager(coroutineContext) { sharedPreferences } + val unrelatedThirdPartyEndpointAddress = "i-have-nothing-to-do-with-the-other" + with(sharedPreferences.edit()) { + putStringSet( + firstPartyEndpoint.nodeId, + mutableSetOf(unrelatedThirdPartyEndpointAddress, thirdPartyEndpoint.nodeId), + ) + apply() + } + + manager.delete(thirdPartyEndpoint) + + assertEquals( + setOf(unrelatedThirdPartyEndpointAddress), + sharedPreferences.getStringSet(firstPartyEndpoint.nodeId, null), ) - apply() } - manager.delete(thirdPartyEndpoint) - - assertEquals( - setOf(unrelatedThirdPartyEndpointAddress), - sharedPreferences.getStringSet(firstPartyEndpoint.nodeId, null), - ) - } - @Test - fun delete_third_party_single_valued() = runTest { - val manager = ChannelManager(coroutineContext) { sharedPreferences } - val malformedValue = "i-should-not-be-here" - with(sharedPreferences.edit()) { - putString( - firstPartyEndpoint.nodeId, + fun delete_third_party_single_valued() = + runTest { + val manager = ChannelManager(coroutineContext) { sharedPreferences } + val malformedValue = "i-should-not-be-here" + with(sharedPreferences.edit()) { + putString( + firstPartyEndpoint.nodeId, + malformedValue, + ) + apply() + } + + manager.delete(thirdPartyEndpoint) + + assertEquals( malformedValue, + sharedPreferences.getString(firstPartyEndpoint.nodeId, null), ) - apply() } - manager.delete(thirdPartyEndpoint) - - assertEquals( - malformedValue, - sharedPreferences.getString(firstPartyEndpoint.nodeId, null), - ) - } - @Test - fun delete_third_party_invalid_type() = runTest { - val manager = ChannelManager(coroutineContext) { sharedPreferences } - val malformedValue = 42 - with(sharedPreferences.edit()) { - putInt( - firstPartyEndpoint.nodeId, + fun delete_third_party_invalid_type() = + runTest { + val manager = ChannelManager(coroutineContext) { sharedPreferences } + val malformedValue = 42 + with(sharedPreferences.edit()) { + putInt( + firstPartyEndpoint.nodeId, + malformedValue, + ) + apply() + } + + manager.delete(thirdPartyEndpoint) + + assertEquals( malformedValue, + sharedPreferences.getInt(firstPartyEndpoint.nodeId, 0), ) - apply() } - manager.delete(thirdPartyEndpoint) - - assertEquals( - malformedValue, - sharedPreferences.getInt(firstPartyEndpoint.nodeId, 0), - ) - } - @Test - fun getLinkedEndpointAddresses_empty() = runTest { - val manager = ChannelManager(coroutineContext) { sharedPreferences } + fun getLinkedEndpointAddresses_empty() = + runTest { + val manager = ChannelManager(coroutineContext) { sharedPreferences } - val linkedEndpoints = manager.getLinkedEndpointAddresses(firstPartyEndpoint) + val linkedEndpoints = manager.getLinkedEndpointAddresses(firstPartyEndpoint) - assertEquals(0, linkedEndpoints.size) - } + assertEquals(0, linkedEndpoints.size) + } @Test - fun getLinkedEndpointAddresses_matches() = runTest { - val manager = ChannelManager(coroutineContext) { sharedPreferences } - manager.create(firstPartyEndpoint, thirdPartyEndpoint) + fun getLinkedEndpointAddresses_matches() = + runTest { + val manager = ChannelManager(coroutineContext) { sharedPreferences } + manager.create(firstPartyEndpoint, thirdPartyEndpoint) - val linkedEndpoints = manager.getLinkedEndpointAddresses(firstPartyEndpoint) + val linkedEndpoints = manager.getLinkedEndpointAddresses(firstPartyEndpoint) - assertEquals(setOf(thirdPartyEndpoint.nodeId), linkedEndpoints) - } + assertEquals(setOf(thirdPartyEndpoint.nodeId), linkedEndpoints) + } } diff --git a/lib/src/test/java/tech/relaycorp/awaladroid/endpoint/FirstPartyEndpointTest.kt b/lib/src/test/java/tech/relaycorp/awaladroid/endpoint/FirstPartyEndpointTest.kt index a9a618db..0832e664 100644 --- a/lib/src/test/java/tech/relaycorp/awaladroid/endpoint/FirstPartyEndpointTest.kt +++ b/lib/src/test/java/tech/relaycorp/awaladroid/endpoint/FirstPartyEndpointTest.kt @@ -64,382 +64,433 @@ internal class FirstPartyEndpointTest : MockContextTestCase() { } @Test - fun register() = runTest { - val internetGatewayAddress = "example.org" - whenever(gatewayClient.registerEndpoint(any())).thenReturn( - PrivateNodeRegistration( - PDACertPath.PRIVATE_ENDPOINT, - PDACertPath.PRIVATE_GW, - internetGatewayAddress, - ), - ) - - val endpoint = FirstPartyEndpoint.register() - - val identityPrivateKey = - privateKeyStore.retrieveIdentityKey(endpoint.nodeId) - assertEquals(endpoint.identityPrivateKey, identityPrivateKey) - val identityCertificatePath = certificateStore.retrieveLatest( - endpoint.identityCertificate.subjectId, - PDACertPath.PRIVATE_GW.subjectId, - ) - assertEquals(PDACertPath.PRIVATE_ENDPOINT, identityCertificatePath!!.leafCertificate) - verify(storage.gatewayId).set( - endpoint.nodeId, - PDACertPath.PRIVATE_GW.subjectId, - ) - verify(storage.internetAddress).set(internetGatewayAddress) - } + fun register() = + runTest { + val internetGatewayAddress = "example.org" + whenever(gatewayClient.registerEndpoint(any())).thenReturn( + PrivateNodeRegistration( + PDACertPath.PRIVATE_ENDPOINT, + PDACertPath.PRIVATE_GW, + internetGatewayAddress, + ), + ) + + val endpoint = FirstPartyEndpoint.register() + + val identityPrivateKey = + privateKeyStore.retrieveIdentityKey(endpoint.nodeId) + assertEquals(endpoint.identityPrivateKey, identityPrivateKey) + val identityCertificatePath = + certificateStore.retrieveLatest( + endpoint.identityCertificate.subjectId, + PDACertPath.PRIVATE_GW.subjectId, + ) + assertEquals(PDACertPath.PRIVATE_ENDPOINT, identityCertificatePath!!.leafCertificate) + verify(storage.gatewayId).set( + endpoint.nodeId, + PDACertPath.PRIVATE_GW.subjectId, + ) + verify(storage.internetAddress).set(internetGatewayAddress) + } @Test - fun reRegister() = runTest { - val endpoint = FirstPartyEndpointFactory.build() - val newCertificate = issueEndpointCertificate( - subjectPublicKey = endpoint.identityPrivateKey.toPublicKey(), - issuerPrivateKey = KeyPairSet.PRIVATE_GW.private, - validityEndDate = ZonedDateTime.now().plusYears(1), - ) - whenever(gatewayClient.registerEndpoint(any())).thenReturn( - PrivateNodeRegistration( - newCertificate, - PDACertPath.PRIVATE_GW, - "", - ), - ) - - endpoint.reRegister() - - val identityCertificatePath = certificateStore.retrieveLatest( - endpoint.identityPrivateKey.nodeId, - PDACertPath.PRIVATE_GW.subjectId, - ) - assertEquals(newCertificate, identityCertificatePath!!.leafCertificate) - } + fun reRegister() = + runTest { + val endpoint = FirstPartyEndpointFactory.build() + val newCertificate = + issueEndpointCertificate( + subjectPublicKey = endpoint.identityPrivateKey.toPublicKey(), + issuerPrivateKey = KeyPairSet.PRIVATE_GW.private, + validityEndDate = ZonedDateTime.now().plusYears(1), + ) + whenever(gatewayClient.registerEndpoint(any())).thenReturn( + PrivateNodeRegistration( + newCertificate, + PDACertPath.PRIVATE_GW, + "", + ), + ) + + endpoint.reRegister() + + val identityCertificatePath = + certificateStore.retrieveLatest( + endpoint.identityPrivateKey.nodeId, + PDACertPath.PRIVATE_GW.subjectId, + ) + assertEquals(newCertificate, identityCertificatePath!!.leafCertificate) + } @Test(expected = RegistrationFailedException::class) - fun register_failed() = runTest { - whenever(gatewayClient.registerEndpoint(any())).thenThrow(RegistrationFailedException("")) + fun register_failed() = + runTest { + whenever( + gatewayClient.registerEndpoint(any()), + ).thenThrow(RegistrationFailedException("")) - FirstPartyEndpoint.register() + FirstPartyEndpoint.register() - verifyZeroInteractions(storage) - assertEquals(0, privateKeyStore.identityKeys.size) - } + verifyZeroInteractions(storage) + assertEquals(0, privateKeyStore.identityKeys.size) + } @Test(expected = GatewayProtocolException::class) - fun register_failedDueToProtocol(): Unit = runTest { - whenever(gatewayClient.registerEndpoint(any())).thenThrow(GatewayProtocolException("")) + fun register_failedDueToProtocol(): Unit = + runTest { + whenever(gatewayClient.registerEndpoint(any())).thenThrow(GatewayProtocolException("")) - FirstPartyEndpoint.register() + FirstPartyEndpoint.register() - verifyZeroInteractions(storage) - assertEquals(0, privateKeyStore.identityKeys.size) - } - - @Test - fun register_failedDueToPrivateKeystore(): Unit = runTest { - whenever(gatewayClient.registerEndpoint(any())).thenReturn( - PrivateNodeRegistration( - PDACertPath.PRIVATE_ENDPOINT, - PDACertPath.PRIVATE_GW, - "", - ), - ) - val savingException = Exception("Oh noes") - setAwalaContext( - Awala.getContextOrThrow().copy( - privateKeyStore = MockPrivateKeyStore(savingException = savingException), - ), - ) - - val exception = assertThrows(PersistenceException::class.java) { - runBlocking { - FirstPartyEndpoint.register() - } + verifyZeroInteractions(storage) + assertEquals(0, privateKeyStore.identityKeys.size) } - assertEquals("Failed to save identity key", exception.message) - assertTrue(exception.cause is KeyStoreBackendException) - assertEquals(savingException, exception.cause!!.cause) - } - @Test - fun register_failedDueToCertStore(): Unit = runTest { - whenever(gatewayClient.registerEndpoint(any())).thenReturn( - PrivateNodeRegistration( - PDACertPath.PRIVATE_ENDPOINT, - PDACertPath.PRIVATE_GW, - "", - ), - ) - val savingException = Exception("Oh noes") - setAwalaContext( - Awala.getContextOrThrow().copy( - certificateStore = MockCertificateStore(savingException = savingException), - ), - ) - - val exception = assertThrows(PersistenceException::class.java) { - runBlocking { - FirstPartyEndpoint.register() - } - } + fun register_failedDueToPrivateKeystore(): Unit = + runTest { + whenever(gatewayClient.registerEndpoint(any())).thenReturn( + PrivateNodeRegistration( + PDACertPath.PRIVATE_ENDPOINT, + PDACertPath.PRIVATE_GW, + "", + ), + ) + val savingException = Exception("Oh noes") + setAwalaContext( + Awala.getContextOrThrow().copy( + privateKeyStore = MockPrivateKeyStore(savingException = savingException), + ), + ) - assertEquals("Failed to save certificate", exception.message) - assertTrue(exception.cause is KeyStoreBackendException) - assertEquals(savingException, exception.cause!!.cause) - } + val exception = + assertThrows(PersistenceException::class.java) { + runBlocking { + FirstPartyEndpoint.register() + } + } - @Test - fun load_withResult(): Unit = runTest { - createFirstPartyEndpoint() - - val nodeId = KeyPairSet.PRIVATE_ENDPOINT.public.nodeId - with(FirstPartyEndpoint.load(nodeId)) { - assertNotNull(this) - assertEquals(KeyPairSet.PRIVATE_ENDPOINT.private, this?.identityPrivateKey) - assertEquals(PDACertPath.PRIVATE_ENDPOINT, this?.identityCertificate) - assertEquals(listOf(PDACertPath.PRIVATE_GW), this?.identityCertificateChain) - assertEquals("example.org", this?.internetAddress) + assertEquals("Failed to save identity key", exception.message) + assertTrue(exception.cause is KeyStoreBackendException) + assertEquals(savingException, exception.cause!!.cause) } - } @Test - fun load_withMissingPrivateKey() = runTest { - whenever(storage.gatewayId.get()) - .thenReturn(PDACertPath.PRIVATE_GW.subjectId) + fun register_failedDueToCertStore(): Unit = + runTest { + whenever(gatewayClient.registerEndpoint(any())).thenReturn( + PrivateNodeRegistration( + PDACertPath.PRIVATE_ENDPOINT, + PDACertPath.PRIVATE_GW, + "", + ), + ) + val savingException = Exception("Oh noes") + setAwalaContext( + Awala.getContextOrThrow().copy( + certificateStore = MockCertificateStore(savingException = savingException), + ), + ) - assertNull(FirstPartyEndpoint.load("non-existent")) - } + val exception = + assertThrows(PersistenceException::class.java) { + runBlocking { + FirstPartyEndpoint.register() + } + } - @Test - fun load_withKeystoreError(): Unit = runTest { - setAwalaContext( - Awala.getContextOrThrow().copy( - privateKeyStore = MockPrivateKeyStore(retrievalException = Exception("Oh noes")), - ), - ) - whenever(storage.gatewayId.get()) - .thenReturn(PDACertPath.PRIVATE_GW.subjectId) - - val exception = assertThrows(PersistenceException::class.java) { - runBlocking { - FirstPartyEndpoint.load(KeyPairSet.PRIVATE_ENDPOINT.public.nodeId) - } + assertEquals("Failed to save certificate", exception.message) + assertTrue(exception.cause is KeyStoreBackendException) + assertEquals(savingException, exception.cause!!.cause) } - assertEquals("Failed to load private key of endpoint", exception.message) - assertTrue(exception.cause is KeyStoreBackendException) - } - @Test - fun load_withMissingGatewayId(): Unit = runTest { - val firstPartyEndpoint = createFirstPartyEndpoint() - whenever(storage.gatewayId.get(firstPartyEndpoint.nodeId)) - .thenReturn(null) - - val exception = assertThrows(PersistenceException::class.java) { - runBlocking { - FirstPartyEndpoint.load(KeyPairSet.PRIVATE_ENDPOINT.public.nodeId) + fun load_withResult(): Unit = + runTest { + createFirstPartyEndpoint() + + val nodeId = KeyPairSet.PRIVATE_ENDPOINT.public.nodeId + with(FirstPartyEndpoint.load(nodeId)) { + assertNotNull(this) + assertEquals(KeyPairSet.PRIVATE_ENDPOINT.private, this?.identityPrivateKey) + assertEquals(PDACertPath.PRIVATE_ENDPOINT, this?.identityCertificate) + assertEquals(listOf(PDACertPath.PRIVATE_GW), this?.identityCertificateChain) + assertEquals("example.org", this?.internetAddress) } } - assertEquals("Failed to load gateway address for endpoint", exception.message) - } - @Test - fun load_withMissingInternetAddress() = runTest { - createFirstPartyEndpoint() - whenever(storage.internetAddress.get()) - .thenReturn(null) - - val exception = assertThrows(PersistenceException::class.java) { - runBlocking { - FirstPartyEndpoint.load(KeyPairSet.PRIVATE_ENDPOINT.public.nodeId) - } + fun load_withMissingPrivateKey() = + runTest { + whenever(storage.gatewayId.get()) + .thenReturn(PDACertPath.PRIVATE_GW.subjectId) + + assertNull(FirstPartyEndpoint.load("non-existent")) } - assertEquals("Failed to load gateway internet address for endpoint", exception.message) - } + @Test + fun load_withKeystoreError(): Unit = + runTest { + setAwalaContext( + Awala.getContextOrThrow().copy( + privateKeyStore = + MockPrivateKeyStore( + retrievalException = Exception("Oh noes"), + ), + ), + ) + whenever(storage.gatewayId.get()) + .thenReturn(PDACertPath.PRIVATE_GW.subjectId) + + val exception = + assertThrows(PersistenceException::class.java) { + runBlocking { + FirstPartyEndpoint.load(KeyPairSet.PRIVATE_ENDPOINT.public.nodeId) + } + } + + assertEquals("Failed to load private key of endpoint", exception.message) + assertTrue(exception.cause is KeyStoreBackendException) + } @Test - fun load_withCertStoreError(): Unit = runTest { - val firstPartyEndpoint = createFirstPartyEndpoint() - val retrievalException = Exception("Oh noes") - setAwalaContext( - Awala.getContextOrThrow().copy( - certificateStore = MockCertificateStore(retrievalException = retrievalException), - ), - ) - - val exception = assertThrows(PersistenceException::class.java) { - runBlocking { - FirstPartyEndpoint.load(firstPartyEndpoint.nodeId) - } + fun load_withMissingGatewayId(): Unit = + runTest { + val firstPartyEndpoint = createFirstPartyEndpoint() + whenever(storage.gatewayId.get(firstPartyEndpoint.nodeId)) + .thenReturn(null) + + val exception = + assertThrows(PersistenceException::class.java) { + runBlocking { + FirstPartyEndpoint.load(KeyPairSet.PRIVATE_ENDPOINT.public.nodeId) + } + } + + assertEquals("Failed to load gateway address for endpoint", exception.message) } - assertEquals("Failed to load certificate for endpoint", exception.message) - assertEquals(retrievalException, exception.cause?.cause) - } + @Test + fun load_withMissingInternetAddress() = + runTest { + createFirstPartyEndpoint() + whenever(storage.internetAddress.get()) + .thenReturn(null) + + val exception = + assertThrows(PersistenceException::class.java) { + runBlocking { + FirstPartyEndpoint.load(KeyPairSet.PRIVATE_ENDPOINT.public.nodeId) + } + } + + assertEquals("Failed to load gateway internet address for endpoint", exception.message) + } @Test - fun issueAuthorization_thirdPartyEndpoint() = runTest { - val firstPartyEndpoint = createFirstPartyEndpoint() - val thirdPartyEndpoint = ThirdPartyEndpointFactory.buildPublic() - val expiryDate = ZonedDateTime.now().plusDays(1) + fun load_withCertStoreError(): Unit = + runTest { + val firstPartyEndpoint = createFirstPartyEndpoint() + val retrievalException = Exception("Oh noes") + setAwalaContext( + Awala.getContextOrThrow().copy( + certificateStore = + MockCertificateStore( + retrievalException = retrievalException, + ), + ), + ) - val authorization = firstPartyEndpoint.issueAuthorization(thirdPartyEndpoint, expiryDate) + val exception = + assertThrows(PersistenceException::class.java) { + runBlocking { + FirstPartyEndpoint.load(firstPartyEndpoint.nodeId) + } + } - validateAuthorization(authorization, firstPartyEndpoint, expiryDate) - } + assertEquals("Failed to load certificate for endpoint", exception.message) + assertEquals(retrievalException, exception.cause?.cause) + } @Test - fun issueAuthorization_publicKey_valid() = runTest { - val firstPartyEndpoint = createFirstPartyEndpoint() - val expiryDate = ZonedDateTime.now().plusDays(1) + fun issueAuthorization_thirdPartyEndpoint() = + runTest { + val firstPartyEndpoint = createFirstPartyEndpoint() + val thirdPartyEndpoint = ThirdPartyEndpointFactory.buildPublic() + val expiryDate = ZonedDateTime.now().plusDays(1) - val authorization = firstPartyEndpoint.issueAuthorization( - KeyPairSet.PDA_GRANTEE.public.encoded, - expiryDate, - ) + val authorization = + firstPartyEndpoint.issueAuthorization( + thirdPartyEndpoint, + expiryDate, + ) - validateAuthorization(authorization, firstPartyEndpoint, expiryDate) - } + validateAuthorization(authorization, firstPartyEndpoint, expiryDate) + } @Test - fun issueAuthorization_publicKey_invalid() = runTest { - val firstPartyEndpoint = createFirstPartyEndpoint() - val expiryDate = ZonedDateTime.now().plusDays(1) + fun issueAuthorization_publicKey_valid() = + runTest { + val firstPartyEndpoint = createFirstPartyEndpoint() + val expiryDate = ZonedDateTime.now().plusDays(1) - val exception = assertThrows(AuthorizationIssuanceException::class.java) { - runBlocking { + val authorization = firstPartyEndpoint.issueAuthorization( - "This is not a key".toByteArray(), + KeyPairSet.PDA_GRANTEE.public.encoded, expiryDate, ) - } - } - assertEquals("PDA grantee public key is not a valid RSA public key", exception.message) - } + validateAuthorization(authorization, firstPartyEndpoint, expiryDate) + } @Test - fun authorizeIndefinitely_thirdPartyEndpoint() = runTest { - val firstPartyEndpoint = createFirstPartyEndpoint() - val thirdPartyEndpoint = ThirdPartyEndpointFactory.buildPublic() - val expiryDate = ZonedDateTime.now().plusDays(1) + fun issueAuthorization_publicKey_invalid() = + runTest { + val firstPartyEndpoint = createFirstPartyEndpoint() + val expiryDate = ZonedDateTime.now().plusDays(1) + + val exception = + assertThrows(AuthorizationIssuanceException::class.java) { + runBlocking { + firstPartyEndpoint.issueAuthorization( + "This is not a key".toByteArray(), + expiryDate, + ) + } + } + + assertEquals("PDA grantee public key is not a valid RSA public key", exception.message) + } - val authorization = firstPartyEndpoint.authorizeIndefinitely(thirdPartyEndpoint) + @Test + fun authorizeIndefinitely_thirdPartyEndpoint() = + runTest { + val firstPartyEndpoint = createFirstPartyEndpoint() + val thirdPartyEndpoint = ThirdPartyEndpointFactory.buildPublic() + val expiryDate = ZonedDateTime.now().plusDays(1) - validateAuthorization(authorization, firstPartyEndpoint, expiryDate) - verify(channelManager).create(firstPartyEndpoint, thirdPartyEndpoint.identityKey) - } + val authorization = firstPartyEndpoint.authorizeIndefinitely(thirdPartyEndpoint) - @Test - fun authorizeIndefinitely_publicKey_valid() = runTest { - val firstPartyEndpoint = createFirstPartyEndpoint() - val expiryDate = ZonedDateTime.now().plusDays(1) - - val authorization = firstPartyEndpoint.authorizeIndefinitely( - KeyPairSet.PDA_GRANTEE.public.encoded, - ) - - validateAuthorization(authorization, firstPartyEndpoint, expiryDate) - verify(channelManager).create( - eq(firstPartyEndpoint), - argThat { - encoded.asList() == KeyPairSet.PDA_GRANTEE.public.encoded.asList() - }, - ) - } + validateAuthorization(authorization, firstPartyEndpoint, expiryDate) + verify(channelManager).create(firstPartyEndpoint, thirdPartyEndpoint.identityKey) + } @Test - fun authorizeIndefinitely_publicKey_invalid() = runTest { - val firstPartyEndpoint = createFirstPartyEndpoint() + fun authorizeIndefinitely_publicKey_valid() = + runTest { + val firstPartyEndpoint = createFirstPartyEndpoint() + val expiryDate = ZonedDateTime.now().plusDays(1) - val exception = assertThrows(AuthorizationIssuanceException::class.java) { - runBlocking { + val authorization = firstPartyEndpoint.authorizeIndefinitely( - "This is not a key".toByteArray(), + KeyPairSet.PDA_GRANTEE.public.encoded, ) - } + + validateAuthorization(authorization, firstPartyEndpoint, expiryDate) + verify(channelManager).create( + eq(firstPartyEndpoint), + argThat { + encoded.asList() == KeyPairSet.PDA_GRANTEE.public.encoded.asList() + }, + ) } - assertEquals("PDA grantee public key is not a valid RSA public key", exception.message) - verify(channelManager, never()).create(any(), any()) - } + @Test + fun authorizeIndefinitely_publicKey_invalid() = + runTest { + val firstPartyEndpoint = createFirstPartyEndpoint() + + val exception = + assertThrows(AuthorizationIssuanceException::class.java) { + runBlocking { + firstPartyEndpoint.authorizeIndefinitely( + "This is not a key".toByteArray(), + ) + } + } + + assertEquals("PDA grantee public key is not a valid RSA public key", exception.message) + verify(channelManager, never()).create(any(), any()) + } @Test - fun reissuePDAs_with_no_channel() = runTest { - val firstPartyEndpoint = createFirstPartyEndpoint() - whenever(channelManager.getLinkedEndpointAddresses(firstPartyEndpoint)) - .thenReturn(emptySet()) + fun reissuePDAs_with_no_channel() = + runTest { + val firstPartyEndpoint = createFirstPartyEndpoint() + whenever(channelManager.getLinkedEndpointAddresses(firstPartyEndpoint)) + .thenReturn(emptySet()) - firstPartyEndpoint.reissuePDAs() + firstPartyEndpoint.reissuePDAs() - verify(gatewayClient, never()).sendMessage(any()) - } + verify(gatewayClient, never()).sendMessage(any()) + } @Test - fun reissuePDAs_with_missing_third_party_endpoint() = runTest { - val firstPartyEndpoint = createFirstPartyEndpoint() - val missingAddress = "non existing address" - whenever(channelManager.getLinkedEndpointAddresses(firstPartyEndpoint)) - .thenReturn(setOf(missingAddress)) - val logCaptor = LogCaptor.forClass(FirstPartyEndpoint::class.java) - - firstPartyEndpoint.reissuePDAs() - - verify(gatewayClient, never()).sendMessage(any()) - assertTrue( - logCaptor.infoLogs.contains("Ignoring missing third-party endpoint $missingAddress"), - ) - } + fun reissuePDAs_with_missing_third_party_endpoint() = + runTest { + val firstPartyEndpoint = createFirstPartyEndpoint() + val missingAddress = "non existing address" + whenever(channelManager.getLinkedEndpointAddresses(firstPartyEndpoint)) + .thenReturn(setOf(missingAddress)) + val logCaptor = LogCaptor.forClass(FirstPartyEndpoint::class.java) + + firstPartyEndpoint.reissuePDAs() + + verify(gatewayClient, never()).sendMessage(any()) + assertTrue( + logCaptor.infoLogs.contains( + "Ignoring missing third-party endpoint $missingAddress", + ), + ) + } @Test - fun reissuePDAs_with_existing_third_party_endpoint() = runTest { - val channel = createEndpointChannel(RecipientAddressType.PRIVATE) - val firstPartyEndpoint = channel.firstPartyEndpoint - - firstPartyEndpoint.reissuePDAs() - - argumentCaptor().apply { - verify(gatewayClient, times(1)).sendMessage(capture()) - - val outgoingMessage = firstValue - // Verify the parcel - assertEquals(firstPartyEndpoint, outgoingMessage.senderEndpoint) - assertEquals( - channel.thirdPartyEndpoint.nodeId, - outgoingMessage.recipientEndpoint.nodeId, - ) - // Verify the PDA - val (serviceMessage) = - outgoingMessage.parcel.unwrapPayload(channel.thirdPartySessionKeyPair.privateKey) - assertEquals("application/vnd+relaycorp.awala.pda-path", serviceMessage.type) - val params = PrivateEndpointConnParams.deserialize(serviceMessage.content) - val pdaPath = params.deliveryAuth - pdaPath.validate() - assertEquals( - channel.thirdPartyEndpoint.identityKey, - pdaPath.leafCertificate.subjectPublicKey, - ) - assertEquals(firstPartyEndpoint.pdaChain, pdaPath.certificateAuthorities) - assertEquals(pdaPath.leafCertificate.expiryDate, outgoingMessage.parcelExpiryDate) + fun reissuePDAs_with_existing_third_party_endpoint() = + runTest { + val channel = createEndpointChannel(RecipientAddressType.PRIVATE) + val firstPartyEndpoint = channel.firstPartyEndpoint + + firstPartyEndpoint.reissuePDAs() + + argumentCaptor().apply { + verify(gatewayClient, times(1)).sendMessage(capture()) + + val outgoingMessage = firstValue + // Verify the parcel + assertEquals(firstPartyEndpoint, outgoingMessage.senderEndpoint) + assertEquals( + channel.thirdPartyEndpoint.nodeId, + outgoingMessage.recipientEndpoint.nodeId, + ) + // Verify the PDA + val (serviceMessage) = + outgoingMessage.parcel.unwrapPayload( + channel.thirdPartySessionKeyPair.privateKey, + ) + assertEquals("application/vnd+relaycorp.awala.pda-path", serviceMessage.type) + val params = PrivateEndpointConnParams.deserialize(serviceMessage.content) + val pdaPath = params.deliveryAuth + pdaPath.validate() + assertEquals( + channel.thirdPartyEndpoint.identityKey, + pdaPath.leafCertificate.subjectPublicKey, + ) + assertEquals(firstPartyEndpoint.pdaChain, pdaPath.certificateAuthorities) + assertEquals(pdaPath.leafCertificate.expiryDate, outgoingMessage.parcelExpiryDate) + } } - } @Test - fun delete() = runTest { - val channel = createEndpointChannel(RecipientAddressType.PRIVATE) - val endpoint = channel.firstPartyEndpoint + fun delete() = + runTest { + val channel = createEndpointChannel(RecipientAddressType.PRIVATE) + val endpoint = channel.firstPartyEndpoint - endpoint.delete() + endpoint.delete() - assertEquals(0, privateKeyStore.identityKeys.size) - assertEquals(0, certificateStore.certificationPaths.size) - verify(channelManager).delete(endpoint) - } + assertEquals(0, privateKeyStore.identityKeys.size) + assertEquals(0, certificateStore.certificationPaths.size) + verify(channelManager).delete(endpoint) + } } private fun validateAuthorization( diff --git a/lib/src/test/java/tech/relaycorp/awaladroid/endpoint/PrivateThirdPartyEndpointTest.kt b/lib/src/test/java/tech/relaycorp/awaladroid/endpoint/PrivateThirdPartyEndpointTest.kt index e065d49d..9dc2a5e6 100644 --- a/lib/src/test/java/tech/relaycorp/awaladroid/endpoint/PrivateThirdPartyEndpointTest.kt +++ b/lib/src/test/java/tech/relaycorp/awaladroid/endpoint/PrivateThirdPartyEndpointTest.kt @@ -29,18 +29,20 @@ import java.time.ZonedDateTime import java.util.UUID internal class PrivateThirdPartyEndpointTest : MockContextTestCase() { - private val thirdPartyEndpointCertificate = issueEndpointCertificate( - KeyPairSet.PDA_GRANTEE.public, - KeyPairSet.PRIVATE_GW.private, - ZonedDateTime.now().plusDays(1), - PDACertPath.PRIVATE_GW, - ) - private val pda = issueDeliveryAuthorization( - subjectPublicKey = KeyPairSet.PRIVATE_ENDPOINT.public, - issuerPrivateKey = KeyPairSet.PDA_GRANTEE.private, - validityEndDate = ZonedDateTime.now().plusDays(1), - issuerCertificate = thirdPartyEndpointCertificate, - ) + private val thirdPartyEndpointCertificate = + issueEndpointCertificate( + KeyPairSet.PDA_GRANTEE.public, + KeyPairSet.PRIVATE_GW.private, + ZonedDateTime.now().plusDays(1), + PDACertPath.PRIVATE_GW, + ) + private val pda = + issueDeliveryAuthorization( + subjectPublicKey = KeyPairSet.PRIVATE_ENDPOINT.public, + issuerPrivateKey = KeyPairSet.PDA_GRANTEE.private, + validityEndDate = ZonedDateTime.now().plusDays(1), + issuerCertificate = thirdPartyEndpointCertificate, + ) private val sessionKey = SessionKeyPair.generate().sessionKey @@ -48,13 +50,14 @@ internal class PrivateThirdPartyEndpointTest : MockContextTestCase() { @Test fun recipient() { - val endpoint = PrivateThirdPartyEndpoint( - "the id", - KeyPairSet.PDA_GRANTEE.public, - pda, - listOf(PDACertPath.PRIVATE_ENDPOINT, PDACertPath.PRIVATE_GW), - internetGatewayAddress, - ) + val endpoint = + PrivateThirdPartyEndpoint( + "the id", + KeyPairSet.PDA_GRANTEE.public, + pda, + listOf(PDACertPath.PRIVATE_ENDPOINT, PDACertPath.PRIVATE_GW), + internetGatewayAddress, + ) val recipient = endpoint.recipient assertEquals(endpoint.nodeId, recipient.id) @@ -62,208 +65,225 @@ internal class PrivateThirdPartyEndpointTest : MockContextTestCase() { } @Test - fun load_successful() = runTest { - whenever(storage.privateThirdParty.get(any())).thenReturn( - PrivateThirdPartyEndpointData( - KeyPairSet.PRIVATE_ENDPOINT.public, - CertificationPath( - PDACertPath.PDA, - listOf(PDACertPath.PRIVATE_ENDPOINT, PDACertPath.PRIVATE_GW), + fun load_successful() = + runTest { + whenever(storage.privateThirdParty.get(any())).thenReturn( + PrivateThirdPartyEndpointData( + KeyPairSet.PRIVATE_ENDPOINT.public, + CertificationPath( + PDACertPath.PDA, + listOf(PDACertPath.PRIVATE_ENDPOINT, PDACertPath.PRIVATE_GW), + ), + internetGatewayAddress, ), - internetGatewayAddress, - ), - ) - val firstAddress = UUID.randomUUID().toString() - val thirdAddress = UUID.randomUUID().toString() - - with(PrivateThirdPartyEndpoint.load(thirdAddress, firstAddress)!!) { - assertEquals(firstAddress, firstPartyEndpointAddress) - assertEquals(PDACertPath.PRIVATE_ENDPOINT.subjectId, nodeId) - assertEquals(PDACertPath.PDA, pda) - assertEquals(listOf(PDACertPath.PRIVATE_ENDPOINT, PDACertPath.PRIVATE_GW), pdaChain) - assertEquals(internetGatewayAddress, internetAddress) + ) + val firstAddress = UUID.randomUUID().toString() + val thirdAddress = UUID.randomUUID().toString() + + with(PrivateThirdPartyEndpoint.load(thirdAddress, firstAddress)!!) { + assertEquals(firstAddress, firstPartyEndpointAddress) + assertEquals(PDACertPath.PRIVATE_ENDPOINT.subjectId, nodeId) + assertEquals(PDACertPath.PDA, pda) + assertEquals(listOf(PDACertPath.PRIVATE_ENDPOINT, PDACertPath.PRIVATE_GW), pdaChain) + assertEquals(internetGatewayAddress, internetAddress) + } + + verify(storage.privateThirdParty).get("${firstAddress}_$thirdAddress") } - verify(storage.privateThirdParty).get("${firstAddress}_$thirdAddress") - } - @Test - fun load_nonExistent() = runTest { - whenever(storage.privateThirdParty.get(any())).thenReturn(null) - - assertNull( - PrivateThirdPartyEndpoint.load( - UUID.randomUUID().toString(), - UUID.randomUUID().toString(), - ), - ) - } + fun load_nonExistent() = + runTest { + whenever(storage.privateThirdParty.get(any())).thenReturn(null) + + assertNull( + PrivateThirdPartyEndpoint.load( + UUID.randomUUID().toString(), + UUID.randomUUID().toString(), + ), + ) + } @Test - fun import_successful() = runTest { - val firstPartyEndpoint = createFirstPartyEndpoint() + fun import_successful() = + runTest { + val firstPartyEndpoint = createFirstPartyEndpoint() - val deliveryAuth = CertificationPath( - pda, - listOf(thirdPartyEndpointCertificate), - ) - val paramsSerialized = serializeConnectionParams(deliveryAuth) - val endpoint = PrivateThirdPartyEndpoint.import(paramsSerialized) + val delivAuth = + CertificationPath( + pda, + listOf(thirdPartyEndpointCertificate), + ) + val paramsSerialized = serializeConnectionParams(delivAuth) + val endpoint = PrivateThirdPartyEndpoint.import(paramsSerialized) - assertEquals( - firstPartyEndpoint.nodeId, - endpoint.firstPartyEndpointAddress, - ) - assertEquals( - KeyPairSet.PDA_GRANTEE.public.nodeId, - endpoint.nodeId, - ) - assertEquals( - KeyPairSet.PDA_GRANTEE.public, - endpoint.identityKey, - ) - assertEquals(pda, endpoint.pda) - assertArrayEquals( - arrayOf(thirdPartyEndpointCertificate), - endpoint.pdaChain.toTypedArray(), - ) + assertEquals( + firstPartyEndpoint.nodeId, + endpoint.firstPartyEndpointAddress, + ) + assertEquals( + KeyPairSet.PDA_GRANTEE.public.nodeId, + endpoint.nodeId, + ) + assertEquals( + KeyPairSet.PDA_GRANTEE.public, + endpoint.identityKey, + ) + assertEquals(pda, endpoint.pda) + assertArrayEquals( + arrayOf(thirdPartyEndpointCertificate), + endpoint.pdaChain.toTypedArray(), + ) - verify(storage.privateThirdParty).set( - eq("${firstPartyEndpoint.nodeId}_${endpoint.nodeId}"), - argThat { - identityKey == KeyPairSet.PDA_GRANTEE.public && - this.pdaPath.leafCertificate == pda && - this.pdaPath.certificateAuthorities == deliveryAuth.certificateAuthorities && - this.internetGatewayAddress == internetGatewayAddress - }, - ) + verify(storage.privateThirdParty).set( + eq("${firstPartyEndpoint.nodeId}_${endpoint.nodeId}"), + argThat { + identityKey == KeyPairSet.PDA_GRANTEE.public && + this.pdaPath.leafCertificate == pda && + this.pdaPath.certificateAuthorities == delivAuth.certificateAuthorities && + this.internetGatewayAddress == internetGatewayAddress + }, + ) - assertEquals(sessionKey, sessionPublicKeystore.retrieve(endpoint.nodeId)) - } + assertEquals(sessionKey, sessionPublicKeystore.retrieve(endpoint.nodeId)) + } @Test - fun import_invalidFirstParty() = runTest { - val firstPartyCert = PDACertPath.PRIVATE_ENDPOINT - val pdaPath = CertificationPath(firstPartyCert, emptyList()) - val paramsSerialized = serializeConnectionParams(pdaPath) - try { - PrivateThirdPartyEndpoint.import(paramsSerialized) - } catch (exception: UnknownFirstPartyEndpointException) { - assertEquals( - "First-party endpoint ${firstPartyCert.subjectId} is not registered", - exception.message, - ) - return@runTest + fun import_invalidFirstParty() = + runTest { + val firstPartyCert = PDACertPath.PRIVATE_ENDPOINT + val pdaPath = CertificationPath(firstPartyCert, emptyList()) + val paramsSerialized = serializeConnectionParams(pdaPath) + try { + PrivateThirdPartyEndpoint.import(paramsSerialized) + } catch (exception: UnknownFirstPartyEndpointException) { + assertEquals( + "First-party endpoint ${firstPartyCert.subjectId} is not registered", + exception.message, + ) + return@runTest + } + + assert(false) } - assert(false) - } - @Test - fun import_wrongAuthorizationIssuer() = runTest { - val firstPartyEndpoint = createFirstPartyEndpoint() - - val unrelatedKeyPair = generateRSAKeyPair() - val unrelatedCertificate = issueEndpointCertificate( - unrelatedKeyPair.public, - unrelatedKeyPair.private, - ZonedDateTime.now().plusDays(1), - ) - - val invalidPDA = issueDeliveryAuthorization( - subjectPublicKey = firstPartyEndpoint.identityCertificate.subjectPublicKey, - issuerPrivateKey = unrelatedKeyPair.private, - validityEndDate = ZonedDateTime.now().plusDays(1), - issuerCertificate = unrelatedCertificate, - ) - - val pdaPath = CertificationPath( - invalidPDA, - listOf(thirdPartyEndpointCertificate), - ) - val paramsSerialized = serializeConnectionParams(pdaPath) - try { - PrivateThirdPartyEndpoint.import(paramsSerialized) - } catch (exception: InvalidAuthorizationException) { - assertEquals("PDA path is invalid", exception.message) - assertTrue(exception.cause is CertificationPathException) - assertTrue(exception.cause?.cause is CertificateException) - return@runTest + fun import_wrongAuthorizationIssuer() = + runTest { + val firstPartyEndpoint = createFirstPartyEndpoint() + + val unrelatedKeyPair = generateRSAKeyPair() + val unrelatedCertificate = + issueEndpointCertificate( + unrelatedKeyPair.public, + unrelatedKeyPair.private, + ZonedDateTime.now().plusDays(1), + ) + + val invalidPDA = + issueDeliveryAuthorization( + subjectPublicKey = firstPartyEndpoint.identityCertificate.subjectPublicKey, + issuerPrivateKey = unrelatedKeyPair.private, + validityEndDate = ZonedDateTime.now().plusDays(1), + issuerCertificate = unrelatedCertificate, + ) + + val pdaPath = + CertificationPath( + invalidPDA, + listOf(thirdPartyEndpointCertificate), + ) + val paramsSerialized = serializeConnectionParams(pdaPath) + try { + PrivateThirdPartyEndpoint.import(paramsSerialized) + } catch (exception: InvalidAuthorizationException) { + assertEquals("PDA path is invalid", exception.message) + assertTrue(exception.cause is CertificationPathException) + assertTrue(exception.cause?.cause is CertificateException) + return@runTest + } + + assert(false) } - assert(false) - } - @Test - fun import_malformedParams() = runTest { - try { - PrivateThirdPartyEndpoint.import("malformed".toByteArray()) - } catch (exception: InvalidThirdPartyEndpoint) { - assertEquals("Malformed connection params", exception.message) - assertTrue(exception.cause is InvalidNodeConnectionParams) - return@runTest + fun import_malformedParams() = + runTest { + try { + PrivateThirdPartyEndpoint.import("malformed".toByteArray()) + } catch (exception: InvalidThirdPartyEndpoint) { + assertEquals("Malformed connection params", exception.message) + assertTrue(exception.cause is InvalidNodeConnectionParams) + return@runTest + } + + assert(false) } - assert(false) - } - @Test - fun import_invalidPDAPath() = runTest { - createFirstPartyEndpoint() - val pdaPath = CertificationPath( - pda, - emptyList(), // Shouldn't be empty - ) - val paramsSerialized = serializeConnectionParams(pdaPath) - try { - PrivateThirdPartyEndpoint.import(paramsSerialized) - } catch (exception: InvalidAuthorizationException) { - assertEquals("PDA path is invalid", exception.message) - return@runTest + fun import_invalidPDAPath() = + runTest { + createFirstPartyEndpoint() + val pdaPath = + CertificationPath( + pda, + // Shouldn't be empty + emptyList(), + ) + val paramsSerialized = serializeConnectionParams(pdaPath) + try { + PrivateThirdPartyEndpoint.import(paramsSerialized) + } catch (exception: InvalidAuthorizationException) { + assertEquals("PDA path is invalid", exception.message) + return@runTest + } + + assert(false) } - assert(false) - } - @Test - fun import_expiredPDA() = runTest { - val firstPartyEndpoint = createFirstPartyEndpoint() - - val now = ZonedDateTime.now() - val expiredPDA = issueDeliveryAuthorization( - firstPartyEndpoint.identityCertificate.subjectPublicKey, - KeyPairSet.PDA_GRANTEE.private, - now.minusSeconds(1), - thirdPartyEndpointCertificate, - now.minusSeconds(2), - ) - - val pdaPath = CertificationPath(expiredPDA, listOf(thirdPartyEndpointCertificate)) - val paramsSerialized = serializeConnectionParams(pdaPath) - try { - PrivateThirdPartyEndpoint.import(paramsSerialized) - } catch (exception: InvalidAuthorizationException) { - assertEquals("PDA path is invalid", exception.message) - assertTrue(exception.cause is CertificationPathException) - return@runTest + fun import_expiredPDA() = + runTest { + val firstPartyEndpoint = createFirstPartyEndpoint() + + val now = ZonedDateTime.now() + val expiredPDA = + issueDeliveryAuthorization( + firstPartyEndpoint.identityCertificate.subjectPublicKey, + KeyPairSet.PDA_GRANTEE.private, + now.minusSeconds(1), + thirdPartyEndpointCertificate, + now.minusSeconds(2), + ) + + val pdaPath = CertificationPath(expiredPDA, listOf(thirdPartyEndpointCertificate)) + val paramsSerialized = serializeConnectionParams(pdaPath) + try { + PrivateThirdPartyEndpoint.import(paramsSerialized) + } catch (exception: InvalidAuthorizationException) { + assertEquals("PDA path is invalid", exception.message) + assertTrue(exception.cause is CertificationPathException) + return@runTest + } + + assert(false) } - assert(false) - } - @Test fun dataSerialization() { val pda = PDACertPath.PDA val identityKey = KeyPairSet.PRIVATE_ENDPOINT.public - val pdaPath = CertificationPath( - pda, - listOf(PDACertPath.PRIVATE_GW, PDACertPath.INTERNET_GW), - ) - val dataSerialized = PrivateThirdPartyEndpointData( - identityKey, - pdaPath, - internetGatewayAddress, - ).serialize() + val pdaPath = + CertificationPath( + pda, + listOf(PDACertPath.PRIVATE_GW, PDACertPath.INTERNET_GW), + ) + val dataSerialized = + PrivateThirdPartyEndpointData( + identityKey, + pdaPath, + internetGatewayAddress, + ).serialize() val data = PrivateThirdPartyEndpointData.deserialize(dataSerialized) assertEquals(identityKey, data.identityKey) @@ -276,111 +296,119 @@ internal class PrivateThirdPartyEndpointTest : MockContextTestCase() { } @Test - fun updateConnectionParams_invalidPath() = runTest { - val channel = createEndpointChannel(RecipientAddressType.PRIVATE) - val thirdPartyEndpoint = channel.thirdPartyEndpoint as PrivateThirdPartyEndpoint - val deliveryAuth = CertificationPath(pda, listOf()) - val params = makeConnectionParams(thirdPartyEndpoint, deliveryAuth) - - try { - thirdPartyEndpoint.updateParams(params) - } catch (exception: InvalidAuthorizationException) { - assertEquals("PDA path is invalid", exception.message) - assertTrue(exception.cause is CertificationPathException) - return@runTest + fun updateConnectionParams_invalidPath() = + runTest { + val channel = createEndpointChannel(RecipientAddressType.PRIVATE) + val thirdPartyEndpoint = channel.thirdPartyEndpoint as PrivateThirdPartyEndpoint + val deliveryAuth = CertificationPath(pda, listOf()) + val params = makeConnectionParams(thirdPartyEndpoint, deliveryAuth) + + try { + thirdPartyEndpoint.updateParams(params) + } catch (exception: InvalidAuthorizationException) { + assertEquals("PDA path is invalid", exception.message) + assertTrue(exception.cause is CertificationPathException) + return@runTest + } + + assert(false) } - assert(false) - } - @Test - fun updateConnectionParams_differentFirstPartyEndpoint() = runTest { - val channel = createEndpointChannel(RecipientAddressType.PRIVATE) - val thirdPartyEndpoint = channel.thirdPartyEndpoint as PrivateThirdPartyEndpoint - val invalidSubjectPublicKey = KeyPairSet.INTERNET_GW.public - val invalidPDA = issueDeliveryAuthorization( - invalidSubjectPublicKey, - KeyPairSet.PDA_GRANTEE.private, - thirdPartyEndpointCertificate.expiryDate, - thirdPartyEndpointCertificate, - ) - val deliveryAuth = CertificationPath(invalidPDA, listOf(thirdPartyEndpointCertificate)) - val params = makeConnectionParams(thirdPartyEndpoint, deliveryAuth) - - try { - thirdPartyEndpoint.updateParams(params) - } catch (exception: InvalidAuthorizationException) { - assertEquals( - "PDA subject (${invalidSubjectPublicKey.nodeId}) " + - "is not first-party endpoint", - exception.message, - ) - return@runTest + fun updateConnectionParams_differentFirstPartyEndpoint() = + runTest { + val channel = createEndpointChannel(RecipientAddressType.PRIVATE) + val thirdPartyEndpoint = channel.thirdPartyEndpoint as PrivateThirdPartyEndpoint + val invalidSubjectPublicKey = KeyPairSet.INTERNET_GW.public + val invalidPDA = + issueDeliveryAuthorization( + invalidSubjectPublicKey, + KeyPairSet.PDA_GRANTEE.private, + thirdPartyEndpointCertificate.expiryDate, + thirdPartyEndpointCertificate, + ) + val deliveryAuth = CertificationPath(invalidPDA, listOf(thirdPartyEndpointCertificate)) + val params = makeConnectionParams(thirdPartyEndpoint, deliveryAuth) + + try { + thirdPartyEndpoint.updateParams(params) + } catch (exception: InvalidAuthorizationException) { + assertEquals( + "PDA subject (${invalidSubjectPublicKey.nodeId}) " + + "is not first-party endpoint", + exception.message, + ) + return@runTest + } + + assert(false) } - assert(false) - } - @Test - fun updateConnectionParams_differentThirdPartyEndpoint() = runTest { - val channel = createEndpointChannel(RecipientAddressType.PRIVATE) - val thirdPartyEndpoint = channel.thirdPartyEndpoint as PrivateThirdPartyEndpoint - val invalidIssuer = PDACertPath.INTERNET_GW - val invalidPDA = issueDeliveryAuthorization( - channel.firstPartyEndpoint.publicKey, - KeyPairSet.INTERNET_GW.private, // Invalid issuer - invalidIssuer.expiryDate, - invalidIssuer, - ) - val deliveryAuth = CertificationPath(invalidPDA, listOf(invalidIssuer)) - val params = makeConnectionParams(thirdPartyEndpoint, deliveryAuth) - - try { - thirdPartyEndpoint.updateParams(params) - } catch (exception: InvalidAuthorizationException) { - assertEquals( - "PDA issuer (${invalidIssuer.subjectId}) is not third-party endpoint", - exception.message, - ) - return@runTest + fun updateConnectionParams_differentThirdPartyEndpoint() = + runTest { + val channel = createEndpointChannel(RecipientAddressType.PRIVATE) + val thirdPartyEndpoint = channel.thirdPartyEndpoint as PrivateThirdPartyEndpoint + val invalidIssuer = PDACertPath.INTERNET_GW + val invalidPDA = + issueDeliveryAuthorization( + channel.firstPartyEndpoint.publicKey, + // Invalid issuer + KeyPairSet.INTERNET_GW.private, + invalidIssuer.expiryDate, + invalidIssuer, + ) + val deliveryAuth = CertificationPath(invalidPDA, listOf(invalidIssuer)) + val params = makeConnectionParams(thirdPartyEndpoint, deliveryAuth) + + try { + thirdPartyEndpoint.updateParams(params) + } catch (exception: InvalidAuthorizationException) { + assertEquals( + "PDA issuer (${invalidIssuer.subjectId}) is not third-party endpoint", + exception.message, + ) + return@runTest + } + + assert(false) } - assert(false) - } - @Test - fun updateConnectionParams_valid() = runTest { - val channel = createEndpointChannel(RecipientAddressType.PRIVATE) - val thirdPartyEndpoint = channel.thirdPartyEndpoint as PrivateThirdPartyEndpoint - val deliveryAuth = CertificationPath(pda, listOf(thirdPartyEndpointCertificate)) - val params = makeConnectionParams(thirdPartyEndpoint, deliveryAuth) + fun updateConnectionParams_valid() = + runTest { + val channel = createEndpointChannel(RecipientAddressType.PRIVATE) + val thirdPartyEndpoint = channel.thirdPartyEndpoint as PrivateThirdPartyEndpoint + val deliveryAuth = CertificationPath(pda, listOf(thirdPartyEndpointCertificate)) + val params = makeConnectionParams(thirdPartyEndpoint, deliveryAuth) - thirdPartyEndpoint.updateParams(params) + thirdPartyEndpoint.updateParams(params) - verify(storage.privateThirdParty).set( - "${channel.firstPartyEndpoint.nodeId}_${thirdPartyEndpoint.nodeId}", - PrivateThirdPartyEndpointData( - KeyPairSet.PDA_GRANTEE.public, - deliveryAuth, - thirdPartyEndpoint.internetAddress, - ), - ) - } + verify(storage.privateThirdParty).set( + "${channel.firstPartyEndpoint.nodeId}_${thirdPartyEndpoint.nodeId}", + PrivateThirdPartyEndpointData( + KeyPairSet.PDA_GRANTEE.public, + deliveryAuth, + thirdPartyEndpoint.internetAddress, + ), + ) + } @Test - fun delete() = runTest { - val channel = createEndpointChannel(RecipientAddressType.PRIVATE) - val endpoint = channel.thirdPartyEndpoint as PrivateThirdPartyEndpoint - val firstPartyEndpoint = channel.firstPartyEndpoint - - endpoint.delete() - - verify(storage.privateThirdParty) - .delete("${firstPartyEndpoint.nodeId}_${endpoint.nodeId}") - assertEquals(0, privateKeyStore.sessionKeys[firstPartyEndpoint.nodeId]!!.size) - assertEquals(0, sessionPublicKeystore.keys.size) - verify(channelManager).delete(endpoint) - } + fun delete() = + runTest { + val channel = createEndpointChannel(RecipientAddressType.PRIVATE) + val endpoint = channel.thirdPartyEndpoint as PrivateThirdPartyEndpoint + val firstPartyEndpoint = channel.firstPartyEndpoint + + endpoint.delete() + + verify(storage.privateThirdParty) + .delete("${firstPartyEndpoint.nodeId}_${endpoint.nodeId}") + assertEquals(0, privateKeyStore.sessionKeys[firstPartyEndpoint.nodeId]!!.size) + assertEquals(0, sessionPublicKeystore.keys.size) + verify(channelManager).delete(endpoint) + } private fun serializeConnectionParams(deliveryAuth: CertificationPath) = PrivateEndpointConnParams( diff --git a/lib/src/test/java/tech/relaycorp/awaladroid/endpoint/PublicThirdPartyEndpointTest.kt b/lib/src/test/java/tech/relaycorp/awaladroid/endpoint/PublicThirdPartyEndpointTest.kt index 47c6b300..5d9e4839 100644 --- a/lib/src/test/java/tech/relaycorp/awaladroid/endpoint/PublicThirdPartyEndpointTest.kt +++ b/lib/src/test/java/tech/relaycorp/awaladroid/endpoint/PublicThirdPartyEndpointTest.kt @@ -23,20 +23,22 @@ internal class PublicThirdPartyEndpointTest : MockContextTestCase() { @Test fun nodeId() { val identityKey = KeyPairSet.PDA_GRANTEE.public - val thirdPartyEndpoint = PublicThirdPartyEndpoint( - internetAddress, - identityKey, - ) + val thirdPartyEndpoint = + PublicThirdPartyEndpoint( + internetAddress, + identityKey, + ) assertEquals(identityKey.nodeId, thirdPartyEndpoint.nodeId) } @Test fun recipient() { - val thirdPartyEndpoint = PublicThirdPartyEndpoint( - internetAddress, - KeyPairSet.PDA_GRANTEE.public, - ) + val thirdPartyEndpoint = + PublicThirdPartyEndpoint( + internetAddress, + KeyPairSet.PDA_GRANTEE.public, + ) val recipient = thirdPartyEndpoint.recipient assertEquals(thirdPartyEndpoint.nodeId, recipient.id) @@ -44,64 +46,69 @@ internal class PublicThirdPartyEndpointTest : MockContextTestCase() { } @Test - fun load_successful() = runTest { - val id = UUID.randomUUID().toString() - whenever(storage.publicThirdParty.get(any())) - .thenReturn( - PublicThirdPartyEndpointData( - internetAddress, - KeyPairSet.PDA_GRANTEE.public, - ), - ) - - val endpoint = PublicThirdPartyEndpoint.load(id)!! - assertEquals(internetAddress, endpoint.internetAddress) - assertEquals(KeyPairSet.PDA_GRANTEE.public, endpoint.identityKey) - } + fun load_successful() = + runTest { + val id = UUID.randomUUID().toString() + whenever(storage.publicThirdParty.get(any())) + .thenReturn( + PublicThirdPartyEndpointData( + internetAddress, + KeyPairSet.PDA_GRANTEE.public, + ), + ) + + val endpoint = PublicThirdPartyEndpoint.load(id)!! + assertEquals(internetAddress, endpoint.internetAddress) + assertEquals(KeyPairSet.PDA_GRANTEE.public, endpoint.identityKey) + } @Test - fun load_nonExistent() = runTest { - whenever(storage.publicThirdParty.get(any())).thenReturn(null) + fun load_nonExistent() = + runTest { + whenever(storage.publicThirdParty.get(any())).thenReturn(null) - assertNull(PublicThirdPartyEndpoint.load(UUID.randomUUID().toString())) - } + assertNull(PublicThirdPartyEndpoint.load(UUID.randomUUID().toString())) + } @Test - fun import_validConnectionParams() = runTest { - val connectionParams = NodeConnectionParams( - internetAddress, - KeyPairSet.PDA_GRANTEE.public, - SessionKeyPair.generate().sessionKey, - ) - - val thirdPartyEndpoint = PublicThirdPartyEndpoint.import(connectionParams.serialize()) - - assertEquals(connectionParams.internetAddress, thirdPartyEndpoint.internetAddress) - assertEquals(connectionParams.identityKey, thirdPartyEndpoint.identityKey) - verify(storage.publicThirdParty).set( - PDACertPath.PDA.subjectId, - PublicThirdPartyEndpointData( - connectionParams.internetAddress, - connectionParams.identityKey, - ), - ) - sessionPublicKeystore.retrieve(thirdPartyEndpoint.nodeId) - } + fun import_validConnectionParams() = + runTest { + val connectionParams = + NodeConnectionParams( + internetAddress, + KeyPairSet.PDA_GRANTEE.public, + SessionKeyPair.generate().sessionKey, + ) - @Test - fun import_invalidConnectionParams() = runTest { - try { - PublicThirdPartyEndpoint.import( - "malformed".toByteArray(), + val thirdPartyEndpoint = PublicThirdPartyEndpoint.import(connectionParams.serialize()) + + assertEquals(connectionParams.internetAddress, thirdPartyEndpoint.internetAddress) + assertEquals(connectionParams.identityKey, thirdPartyEndpoint.identityKey) + verify(storage.publicThirdParty).set( + PDACertPath.PDA.subjectId, + PublicThirdPartyEndpointData( + connectionParams.internetAddress, + connectionParams.identityKey, + ), ) - } catch (exception: InvalidThirdPartyEndpoint) { - assertEquals("Connection params serialization is malformed", exception.message) - assertEquals(0, sessionPublicKeystore.keys.size) - return@runTest + sessionPublicKeystore.retrieve(thirdPartyEndpoint.nodeId) } - assert(false) - } + @Test + fun import_invalidConnectionParams() = + runTest { + try { + PublicThirdPartyEndpoint.import( + "malformed".toByteArray(), + ) + } catch (exception: InvalidThirdPartyEndpoint) { + assertEquals("Connection params serialization is malformed", exception.message) + assertEquals(0, sessionPublicKeystore.keys.size) + return@runTest + } + + assert(false) + } @Test fun dataSerialization() { @@ -115,24 +122,25 @@ internal class PublicThirdPartyEndpointTest : MockContextTestCase() { } @Test - fun delete() = runTest { - val firstPartyEndpoint = FirstPartyEndpointFactory.build() - val thirdPartyEndpoint = ThirdPartyEndpointFactory.buildPublic() - val ownSessionKeyPair = SessionKeyPair.generate() - privateKeyStore.saveSessionKey( - ownSessionKeyPair.privateKey, - ownSessionKeyPair.sessionKey.keyId, - firstPartyEndpoint.nodeId, - thirdPartyEndpoint.nodeId, - ) - val peerSessionKey = SessionKeyPair.generate().sessionKey - sessionPublicKeystore.save(peerSessionKey, thirdPartyEndpoint.nodeId) - - thirdPartyEndpoint.delete() - - verify(storage.publicThirdParty).delete(thirdPartyEndpoint.nodeId) - assertEquals(0, privateKeyStore.sessionKeys[firstPartyEndpoint.nodeId]!!.size) - assertEquals(0, sessionPublicKeystore.keys.size) - verify(channelManager).delete(thirdPartyEndpoint) - } + fun delete() = + runTest { + val firstPartyEndpoint = FirstPartyEndpointFactory.build() + val thirdPartyEndpoint = ThirdPartyEndpointFactory.buildPublic() + val ownSessionKeyPair = SessionKeyPair.generate() + privateKeyStore.saveSessionKey( + ownSessionKeyPair.privateKey, + ownSessionKeyPair.sessionKey.keyId, + firstPartyEndpoint.nodeId, + thirdPartyEndpoint.nodeId, + ) + val peerSessionKey = SessionKeyPair.generate().sessionKey + sessionPublicKeystore.save(peerSessionKey, thirdPartyEndpoint.nodeId) + + thirdPartyEndpoint.delete() + + verify(storage.publicThirdParty).delete(thirdPartyEndpoint.nodeId) + assertEquals(0, privateKeyStore.sessionKeys[firstPartyEndpoint.nodeId]!!.size) + assertEquals(0, sessionPublicKeystore.keys.size) + verify(channelManager).delete(thirdPartyEndpoint) + } } diff --git a/lib/src/test/java/tech/relaycorp/awaladroid/endpoint/RenewExpiringCertificatesTest.kt b/lib/src/test/java/tech/relaycorp/awaladroid/endpoint/RenewExpiringCertificatesTest.kt index 3e3d4afb..ee8bcbd4 100644 --- a/lib/src/test/java/tech/relaycorp/awaladroid/endpoint/RenewExpiringCertificatesTest.kt +++ b/lib/src/test/java/tech/relaycorp/awaladroid/endpoint/RenewExpiringCertificatesTest.kt @@ -13,42 +13,45 @@ import tech.relaycorp.relaynet.testing.pki.KeyPairSet import java.time.ZonedDateTime internal class RenewExpiringCertificatesTest() { - private val privateKeyStore = mock() @Before - fun setUp() = runTest { - whenever(privateKeyStore.retrieveAllIdentityKeys()) - .thenReturn(listOf(KeyPairSet.PRIVATE_ENDPOINT.private)) - } + fun setUp() = + runTest { + whenever(privateKeyStore.retrieveAllIdentityKeys()) + .thenReturn(listOf(KeyPairSet.PRIVATE_ENDPOINT.private)) + } @Test - fun `renews expiring certificates`() = runTest { - val expiringEndpoint = buildFirstPartyEndpoint(ZonedDateTime.now().plusDays(50)) - val subject = RenewExpiringCertificates(privateKeyStore) { expiringEndpoint } + fun `renews expiring certificates`() = + runTest { + val expiringEndpoint = buildFirstPartyEndpoint(ZonedDateTime.now().plusDays(50)) + val subject = RenewExpiringCertificates(privateKeyStore) { expiringEndpoint } - subject() + subject() - verify(expiringEndpoint).reRegister() - } + verify(expiringEndpoint).reRegister() + } @Test - fun `does not renew not expiring certificates`() = runTest { - val notExpiringEndpoint = buildFirstPartyEndpoint(ZonedDateTime.now().plusDays(70)) - val subject = RenewExpiringCertificates(privateKeyStore) { notExpiringEndpoint } + fun `does not renew not expiring certificates`() = + runTest { + val notExpiringEndpoint = buildFirstPartyEndpoint(ZonedDateTime.now().plusDays(70)) + val subject = RenewExpiringCertificates(privateKeyStore) { notExpiringEndpoint } - subject() + subject() - verify(notExpiringEndpoint, never()).reRegister() - } + verify(notExpiringEndpoint, never()).reRegister() + } private fun buildFirstPartyEndpoint(certExpiryDate: ZonedDateTime): FirstPartyEndpoint { val firstPartyEndpoint = mock() - val expiringCert = issueEndpointCertificate( - KeyPairSet.PRIVATE_ENDPOINT.public, - KeyPairSet.PRIVATE_GW.private, - certExpiryDate, - ) + val expiringCert = + issueEndpointCertificate( + KeyPairSet.PRIVATE_ENDPOINT.public, + KeyPairSet.PRIVATE_GW.private, + certExpiryDate, + ) whenever(firstPartyEndpoint.identityCertificate).thenReturn(expiringCert) return firstPartyEndpoint } diff --git a/lib/src/test/java/tech/relaycorp/awaladroid/messaging/IncomingMessageTest.kt b/lib/src/test/java/tech/relaycorp/awaladroid/messaging/IncomingMessageTest.kt index 134610a0..f0873503 100644 --- a/lib/src/test/java/tech/relaycorp/awaladroid/messaging/IncomingMessageTest.kt +++ b/lib/src/test/java/tech/relaycorp/awaladroid/messaging/IncomingMessageTest.kt @@ -38,198 +38,224 @@ import tech.relaycorp.relaynet.testing.pki.PDACertPath import java.time.ZonedDateTime internal class IncomingMessageTest : MockContextTestCase() { - private val thirdPartyEndpointCertificate = issueEndpointCertificate( - KeyPairSet.PDA_GRANTEE.public, - KeyPairSet.PRIVATE_GW.private, - ZonedDateTime.now().plusDays(1), - PDACertPath.PRIVATE_GW, - ) + private val thirdPartyEndpointCertificate = + issueEndpointCertificate( + KeyPairSet.PDA_GRANTEE.public, + KeyPairSet.PRIVATE_GW.private, + ZonedDateTime.now().plusDays(1), + PDACertPath.PRIVATE_GW, + ) @After fun clearLogs() = logCaptor.clearLogs() @Test - fun build_valid() = runTest { - val channel = createEndpointChannel(RecipientAddressType.PUBLIC) - val thirdPartyEndpointManager = makeThirdPartyEndpointManager(channel) - val serviceMessage = ServiceMessage("the type", "the content".toByteArray()) - val parcel = Parcel( - recipient = Recipient( - channel.firstPartyEndpoint.nodeId, - channel.firstPartyEndpoint.nodeId, - ), - payload = thirdPartyEndpointManager.wrapMessagePayload( - serviceMessage, - channel.firstPartyEndpoint.nodeId, - channel.thirdPartyEndpoint.nodeId, - ), - senderCertificate = PDACertPath.PDA, - ) + fun build_valid() = + runTest { + val channel = createEndpointChannel(RecipientAddressType.PUBLIC) + val thirdPartyEndpointManager = makeThirdPartyEndpointManager(channel) + val serviceMessage = ServiceMessage("the type", "the content".toByteArray()) + val parcel = + Parcel( + recipient = + Recipient( + channel.firstPartyEndpoint.nodeId, + channel.firstPartyEndpoint.nodeId, + ), + payload = + thirdPartyEndpointManager.wrapMessagePayload( + serviceMessage, + channel.firstPartyEndpoint.nodeId, + channel.thirdPartyEndpoint.nodeId, + ), + senderCertificate = PDACertPath.PDA, + ) - val message = IncomingMessage.build(parcel) {} + val message = IncomingMessage.build(parcel) {} - assertEquals(PDACertPath.PRIVATE_ENDPOINT, message!!.recipientEndpoint.identityCertificate) - assertEquals(serviceMessage.type, message.type) - assertArrayEquals(serviceMessage.content, message.content) - } + assertEquals( + PDACertPath.PRIVATE_ENDPOINT, + message!!.recipientEndpoint.identityCertificate, + ) + assertEquals(serviceMessage.type, message.type) + assertArrayEquals(serviceMessage.content, message.content) + } @Test - fun build_unknownRecipient() = runTest { - val parcel = Parcel( - Recipient("0deadbeef"), // Non-existing first-party endpoint - "payload".toByteArray(), - PDACertPath.PDA, - ) + fun build_unknownRecipient() = + runTest { + val parcel = + Parcel( + // Non-existing first-party endpoint + Recipient("0deadbeef"), + "payload".toByteArray(), + PDACertPath.PDA, + ) - val exception = assertThrows(UnknownFirstPartyEndpointException::class.java) { - runBlocking { - IncomingMessage.build(parcel) {} - } - } + val exception = + assertThrows(UnknownFirstPartyEndpointException::class.java) { + runBlocking { + IncomingMessage.build(parcel) {} + } + } - assertEquals("Unknown first-party endpoint ${parcel.recipient.id}", exception.message) - } + assertEquals("Unknown first-party endpoint ${parcel.recipient.id}", exception.message) + } @Test - fun build_unknownSender() = runTest { - val firstPartyEndpoint = createFirstPartyEndpoint() - val parcel = Parcel( - Recipient(firstPartyEndpoint.nodeId, firstPartyEndpoint.nodeId), - "payload".toByteArray(), - PDACertPath.PDA, - ) + fun build_unknownSender() = + runTest { + val firstPartyEndpoint = createFirstPartyEndpoint() + val parcel = + Parcel( + Recipient(firstPartyEndpoint.nodeId, firstPartyEndpoint.nodeId), + "payload".toByteArray(), + PDACertPath.PDA, + ) - val exception = assertThrows(UnknownThirdPartyEndpointException::class.java) { - runBlocking { - IncomingMessage.build(parcel) {} - } - } + val exception = + assertThrows(UnknownThirdPartyEndpointException::class.java) { + runBlocking { + IncomingMessage.build(parcel) {} + } + } - assertEquals( - "Unknown third-party endpoint ${PDACertPath.PDA.subjectId} for " + - "first-party endpoint ${firstPartyEndpoint.nodeId}", - exception.message, - ) - } + assertEquals( + "Unknown third-party endpoint ${PDACertPath.PDA.subjectId} for " + + "first-party endpoint ${firstPartyEndpoint.nodeId}", + exception.message, + ) + } @Test - fun build_pdaPath_fromPublicEndpoint() = runTest { - val channel = createEndpointChannel(RecipientAddressType.PUBLIC) - val parcel = Parcel( - Recipient(channel.firstPartyEndpoint.nodeId, channel.firstPartyEndpoint.nodeId), - encryptParcelPayload(channel, "doesn't matter".toByteArray()), - PDACertPath.PDA, - ) - val ack = StubACK() + fun build_pdaPath_fromPublicEndpoint() = + runTest { + val channel = createEndpointChannel(RecipientAddressType.PUBLIC) + val parcel = + Parcel( + Recipient(channel.firstPartyEndpoint.nodeId, channel.firstPartyEndpoint.nodeId), + encryptParcelPayload(channel, "doesn't matter".toByteArray()), + PDACertPath.PDA, + ) + val ack = StubACK() - val message = IncomingMessage.build(parcel, ack::run) + val message = IncomingMessage.build(parcel, ack::run) - assertNull(message) - assertTrue(ack.wasCalled) - val thirdPartyEndpoint = channel.thirdPartyEndpoint as PublicThirdPartyEndpoint - assertTrue( - logCaptor.infoLogs.contains( - "Ignoring connection params from public endpoint ${thirdPartyEndpoint.nodeId} " + - "(${thirdPartyEndpoint.internetAddress})", - ), - ) - } + assertNull(message) + assertTrue(ack.wasCalled) + val thirdPartyEndpoint = channel.thirdPartyEndpoint as PublicThirdPartyEndpoint + assertTrue( + logCaptor.infoLogs.contains( + "Ignoring connection params from public endpoint " + + "${thirdPartyEndpoint.nodeId} (${thirdPartyEndpoint.internetAddress})", + ), + ) + } @Test - fun build_connParams_malformed() = runTest { - val channel = createEndpointChannel(RecipientAddressType.PRIVATE) - val parcel = Parcel( - Recipient(channel.firstPartyEndpoint.nodeId, channel.firstPartyEndpoint.nodeId), - encryptParcelPayload(channel, "malformed".toByteArray()), - PDACertPath.PDA, - ) - val ack = StubACK() + fun build_connParams_malformed() = + runTest { + val channel = createEndpointChannel(RecipientAddressType.PRIVATE) + val parcel = + Parcel( + Recipient(channel.firstPartyEndpoint.nodeId, channel.firstPartyEndpoint.nodeId), + encryptParcelPayload(channel, "malformed".toByteArray()), + PDACertPath.PDA, + ) + val ack = StubACK() - val message = IncomingMessage.build(parcel, ack::run) + val message = IncomingMessage.build(parcel, ack::run) - assertNull(message) - assertTrue(ack.wasCalled) - verify(storage.privateThirdParty, never()).set(any(), any()) - assertTrue( - logCaptor.infoLogs.contains( - "Ignoring malformed connection params for ${channel.firstPartyEndpoint.nodeId} " + - "from ${channel.thirdPartyEndpoint.nodeId}", - ), - ) - } + assertNull(message) + assertTrue(ack.wasCalled) + verify(storage.privateThirdParty, never()).set(any(), any()) + assertTrue( + logCaptor.infoLogs.contains( + "Ignoring malformed connection params " + + "for ${channel.firstPartyEndpoint.nodeId} " + + "from ${channel.thirdPartyEndpoint.nodeId}", + ), + ) + } @Test - fun build_connParams_invalid() = runTest { - val channel = createEndpointChannel(RecipientAddressType.PRIVATE) - val now = ZonedDateTime.now() - val expiredPDA = issueDeliveryAuthorization( - channel.firstPartyEndpoint.publicKey, - KeyPairSet.PDA_GRANTEE.private, - now.minusSeconds(1), - thirdPartyEndpointCertificate, - now.minusSeconds(2), - ) - val deliveryAuth = CertificationPath(expiredPDA, listOf(thirdPartyEndpointCertificate)) - val params = makeConnParams(channel, deliveryAuth) - val parcel = Parcel( - Recipient(channel.firstPartyEndpoint.nodeId, channel.firstPartyEndpoint.nodeId), - encryptConnectionParams(channel, params), - PDACertPath.PDA, - ) - val ack = StubACK() + fun build_connParams_invalid() = + runTest { + val channel = createEndpointChannel(RecipientAddressType.PRIVATE) + val now = ZonedDateTime.now() + val expiredPDA = + issueDeliveryAuthorization( + channel.firstPartyEndpoint.publicKey, + KeyPairSet.PDA_GRANTEE.private, + now.minusSeconds(1), + thirdPartyEndpointCertificate, + now.minusSeconds(2), + ) + val deliveryAuth = CertificationPath(expiredPDA, listOf(thirdPartyEndpointCertificate)) + val params = makeConnParams(channel, deliveryAuth) + val parcel = + Parcel( + Recipient(channel.firstPartyEndpoint.nodeId, channel.firstPartyEndpoint.nodeId), + encryptConnectionParams(channel, params), + PDACertPath.PDA, + ) + val ack = StubACK() - val message = IncomingMessage.build(parcel, ack::run) + val message = IncomingMessage.build(parcel, ack::run) - assertNull(message) - assertTrue(ack.wasCalled) - verify(storage.privateThirdParty, never()).set(any(), any()) - assertTrue( - logCaptor.infoLogs.contains( - "Ignoring invalid connection params for ${channel.firstPartyEndpoint.nodeId} " + - "from ${channel.thirdPartyEndpoint.nodeId}", - ), - ) - } + assertNull(message) + assertTrue(ack.wasCalled) + verify(storage.privateThirdParty, never()).set(any(), any()) + assertTrue( + logCaptor.infoLogs.contains( + "Ignoring invalid connection params for ${channel.firstPartyEndpoint.nodeId} " + + "from ${channel.thirdPartyEndpoint.nodeId}", + ), + ) + } @Test - fun build_connParams_valid() = runTest { - val channel = createEndpointChannel(RecipientAddressType.PRIVATE) - val pda = issueDeliveryAuthorization( - channel.firstPartyEndpoint.publicKey, - KeyPairSet.PDA_GRANTEE.private, - thirdPartyEndpointCertificate.expiryDate, - thirdPartyEndpointCertificate, - ) - val deliveryAuth = CertificationPath(pda, listOf(thirdPartyEndpointCertificate)) - val connectionParams = makeConnParams(channel, deliveryAuth) - val parcel = Parcel( - Recipient(channel.firstPartyEndpoint.nodeId), - encryptConnectionParams(channel, connectionParams), - PDACertPath.PDA, - ) - val ack = StubACK() + fun build_connParams_valid() = + runTest { + val channel = createEndpointChannel(RecipientAddressType.PRIVATE) + val pda = + issueDeliveryAuthorization( + channel.firstPartyEndpoint.publicKey, + KeyPairSet.PDA_GRANTEE.private, + thirdPartyEndpointCertificate.expiryDate, + thirdPartyEndpointCertificate, + ) + val delivAuth = CertificationPath(pda, listOf(thirdPartyEndpointCertificate)) + val connectionParams = makeConnParams(channel, delivAuth) + val parcel = + Parcel( + Recipient(channel.firstPartyEndpoint.nodeId), + encryptConnectionParams(channel, connectionParams), + PDACertPath.PDA, + ) + val ack = StubACK() - val message = IncomingMessage.build(parcel, ack::run) + val message = IncomingMessage.build(parcel, ack::run) - val thirdPartyEndpoint = channel.thirdPartyEndpoint - assertNull(message) - assertTrue(ack.wasCalled) - assertTrue( - logCaptor.infoLogs.contains( - "Updated connection params from ${thirdPartyEndpoint.nodeId} for " + - channel.firstPartyEndpoint.nodeId, - ), - ) - verify(storage.privateThirdParty).set( - eq("${channel.firstPartyEndpoint.nodeId}_${thirdPartyEndpoint.nodeId}"), - argThat { - identityKey == thirdPartyEndpoint.identityKey && - this.pdaPath.leafCertificate == pda && - this.pdaPath.certificateAuthorities == deliveryAuth.certificateAuthorities && - this.internetGatewayAddress == thirdPartyEndpoint.internetAddress - }, - ) - } + val thirdPartyEndpoint = channel.thirdPartyEndpoint + assertNull(message) + assertTrue(ack.wasCalled) + assertTrue( + logCaptor.infoLogs.contains( + "Updated connection params from ${thirdPartyEndpoint.nodeId} for " + + channel.firstPartyEndpoint.nodeId, + ), + ) + verify(storage.privateThirdParty).set( + eq("${channel.firstPartyEndpoint.nodeId}_${thirdPartyEndpoint.nodeId}"), + argThat { + identityKey == thirdPartyEndpoint.identityKey && + this.pdaPath.leafCertificate == pda && + this.pdaPath.certificateAuthorities == delivAuth.certificateAuthorities && + this.internetGatewayAddress == thirdPartyEndpoint.internetAddress + }, + ) + } private fun makeConnParams( channel: EndpointChannel, diff --git a/lib/src/test/java/tech/relaycorp/awaladroid/messaging/OutgoingMessageTest.kt b/lib/src/test/java/tech/relaycorp/awaladroid/messaging/OutgoingMessageTest.kt index fc0a0348..5aaa4bb3 100644 --- a/lib/src/test/java/tech/relaycorp/awaladroid/messaging/OutgoingMessageTest.kt +++ b/lib/src/test/java/tech/relaycorp/awaladroid/messaging/OutgoingMessageTest.kt @@ -22,161 +22,184 @@ import kotlin.random.Random internal class OutgoingMessageTest : MockContextTestCase() { @Test - fun build_creationDate() = runTest { - val channel = createEndpointChannel(RecipientAddressType.PRIVATE) - val dateBeforeCreation = ZonedDateTime.now() + fun build_creationDate() = + runTest { + val channel = createEndpointChannel(RecipientAddressType.PRIVATE) + val dateBeforeCreation = ZonedDateTime.now() - val message = MessageFactory.buildOutgoing(channel) + val message = MessageFactory.buildOutgoing(channel) - assertTrue(dateBeforeCreation.minusMinutes(5) <= message.parcel.creationDate) - assertTrue(message.parcel.creationDate <= ZonedDateTime.now().minusMinutes(5)) - } + assertTrue(dateBeforeCreation.minusMinutes(5) <= message.parcel.creationDate) + assertTrue(message.parcel.creationDate <= ZonedDateTime.now().minusMinutes(5)) + } @Test - fun build_defaultExpiryDate() = runTest { - val channel = createEndpointChannel(RecipientAddressType.PUBLIC) - - val message = MessageFactory.buildOutgoing(channel) + fun build_defaultExpiryDate() = + runTest { + val channel = createEndpointChannel(RecipientAddressType.PUBLIC) - val difference = Duration.between( - message.parcel.expiryDate, - message.parcel.creationDate.plusDays(180), - ) - assertTrue(abs(difference.toDays()) == 0L) - } + val message = MessageFactory.buildOutgoing(channel) - @Test - fun build_customExpiryDate() = runTest { - val (senderEndpoint, recipientEndpoint) = createEndpointChannel(RecipientAddressType.PUBLIC) - val parcelExpiryDate = ZonedDateTime.now().plusMinutes(1) - - val message = OutgoingMessage.build( - "the type", - Random.Default.nextBytes(10), - senderEndpoint, - recipientEndpoint, - parcelExpiryDate, - ) - - val differenceSeconds = - Duration.between(message.parcel.expiryDate, parcelExpiryDate).seconds - assertTrue(abs(differenceSeconds) < 3) - } + val difference = + Duration.between( + message.parcel.expiryDate, + message.parcel.creationDate.plusDays(180), + ) + assertTrue(abs(difference.toDays()) == 0L) + } @Test - fun build_bigServiceMessage() = runTest { - val (senderEndpoint, recipientEndpoint) = createEndpointChannel(RecipientAddressType.PUBLIC) + fun build_customExpiryDate() = + runTest { + val (senderEndpoint, recipientEndpoint) = + createEndpointChannel( + RecipientAddressType.PUBLIC, + ) + val parcelExpiryDate = ZonedDateTime.now().plusMinutes(1) - val exception = assertThrows(InvalidMessageException::class.java) { - runBlocking { + val message = OutgoingMessage.build( "the type", - ByteArray(RAMFMessage.MAX_PAYLOAD_LENGTH + 1), + Random.Default.nextBytes(10), senderEndpoint, recipientEndpoint, + parcelExpiryDate, ) - } + + val differenceSeconds = + Duration.between(message.parcel.expiryDate, parcelExpiryDate).seconds + assertTrue(abs(differenceSeconds) < 3) } - assertEquals("Failed to create parcel", exception.message) - assertTrue(exception.cause is RAMFException) - } + @Test + fun build_bigServiceMessage() = + runTest { + val (senderEndpoint, recipientEndpoint) = + createEndpointChannel( + RecipientAddressType.PUBLIC, + ) + + val exception = + assertThrows(InvalidMessageException::class.java) { + runBlocking { + OutgoingMessage.build( + "the type", + ByteArray(RAMFMessage.MAX_PAYLOAD_LENGTH + 1), + senderEndpoint, + recipientEndpoint, + ) + } + } + + assertEquals("Failed to create parcel", exception.message) + assertTrue(exception.cause is RAMFException) + } // Public Recipient @Test - fun buildForPublicRecipient_checkBaseValues() = runTest { - val channel = createEndpointChannel(RecipientAddressType.PUBLIC) - val recipientPublicEndpoint = channel.thirdPartyEndpoint as PublicThirdPartyEndpoint - - val message = MessageFactory.buildOutgoing(channel) - - assertEquals(message.recipientEndpoint.nodeId, message.parcel.recipient.id) - assertEquals( - recipientPublicEndpoint.internetAddress, - message.parcel.recipient.internetAddress, - ) - assertEquals(message.parcelId.value, message.parcel.id) - assertSameDateTime(message.parcelCreationDate, message.parcel.creationDate) - assertEquals(message.ttl, message.parcel.ttl) - } + fun buildForPublicRecipient_checkBaseValues() = + runTest { + val channel = createEndpointChannel(RecipientAddressType.PUBLIC) + val recipientPublicEndpoint = channel.thirdPartyEndpoint as PublicThirdPartyEndpoint - @Test - fun buildForPublicRecipient_checkServiceMessage() = runTest { - val channel = createEndpointChannel(RecipientAddressType.PUBLIC) + val message = MessageFactory.buildOutgoing(channel) - val message = MessageFactory.buildOutgoing(channel) + assertEquals(message.recipientEndpoint.nodeId, message.parcel.recipient.id) + assertEquals( + recipientPublicEndpoint.internetAddress, + message.parcel.recipient.internetAddress, + ) + assertEquals(message.parcelId.value, message.parcel.id) + assertSameDateTime(message.parcelCreationDate, message.parcel.creationDate) + assertEquals(message.ttl, message.parcel.ttl) + } - val (serviceMessageDecrypted) = - message.parcel.unwrapPayload(channel.thirdPartySessionKeyPair.privateKey) - assertEquals(MessageFactory.serviceMessage.type, serviceMessageDecrypted.type) - assertArrayEquals(MessageFactory.serviceMessage.content, serviceMessageDecrypted.content) - } + @Test + fun buildForPublicRecipient_checkServiceMessage() = + runTest { + val channel = createEndpointChannel(RecipientAddressType.PUBLIC) + + val message = MessageFactory.buildOutgoing(channel) + + val (serviceMessageDecrypted) = + message.parcel.unwrapPayload(channel.thirdPartySessionKeyPair.privateKey) + assertEquals(MessageFactory.serviceMessage.type, serviceMessageDecrypted.type) + assertArrayEquals( + MessageFactory.serviceMessage.content, + serviceMessageDecrypted.content, + ) + } @Test - internal fun buildForPublicRecipient_checkSenderCertificate() = runTest { - val channel = createEndpointChannel(RecipientAddressType.PUBLIC) + internal fun buildForPublicRecipient_checkSenderCertificate() = + runTest { + val channel = createEndpointChannel(RecipientAddressType.PUBLIC) - val message = MessageFactory.buildOutgoing(channel) + val message = MessageFactory.buildOutgoing(channel) - message.parcel.senderCertificate.let { cert -> - cert.validate() - assertEquals( - message.senderEndpoint.identityCertificate.subjectPublicKey, - cert.subjectPublicKey, - ) - assertSameDateTime(message.parcelCreationDate, cert.startDate) - assertSameDateTime(message.parcelExpiryDate, cert.expiryDate) + message.parcel.senderCertificate.let { cert -> + cert.validate() + assertEquals( + message.senderEndpoint.identityCertificate.subjectPublicKey, + cert.subjectPublicKey, + ) + assertSameDateTime(message.parcelCreationDate, cert.startDate) + assertSameDateTime(message.parcelExpiryDate, cert.expiryDate) + } } - } @Test - internal fun buildForPublicRecipient_checkSenderCertificateChain() = runTest { - val channel = createEndpointChannel(RecipientAddressType.PUBLIC) + internal fun buildForPublicRecipient_checkSenderCertificateChain() = + runTest { + val channel = createEndpointChannel(RecipientAddressType.PUBLIC) - val message = MessageFactory.buildOutgoing(channel) + val message = MessageFactory.buildOutgoing(channel) - assertTrue(message.parcel.senderCertificateChain.isEmpty()) - } + assertTrue(message.parcel.senderCertificateChain.isEmpty()) + } // Private Recipient @Test - fun buildForPrivateRecipient_checkBaseValues() = runTest { - val channel = createEndpointChannel(RecipientAddressType.PRIVATE) - val message = MessageFactory.buildOutgoing(channel) - - assertEquals(message.recipientEndpoint.nodeId, message.parcel.recipient.id) - assertEquals( - message.recipientEndpoint.internetAddress, - message.parcel.recipient.internetAddress, - ) - assertEquals(message.parcelId.value, message.parcel.id) - assertSameDateTime(message.parcelCreationDate, message.parcel.creationDate) - assertEquals(message.ttl, message.parcel.ttl) - } + fun buildForPrivateRecipient_checkBaseValues() = + runTest { + val channel = createEndpointChannel(RecipientAddressType.PRIVATE) + val message = MessageFactory.buildOutgoing(channel) + + assertEquals(message.recipientEndpoint.nodeId, message.parcel.recipient.id) + assertEquals( + message.recipientEndpoint.internetAddress, + message.parcel.recipient.internetAddress, + ) + assertEquals(message.parcelId.value, message.parcel.id) + assertSameDateTime(message.parcelCreationDate, message.parcel.creationDate) + assertEquals(message.ttl, message.parcel.ttl) + } @Test - internal fun buildForPrivateRecipient_checkSenderCertificate() = runTest { - val channel = createEndpointChannel(RecipientAddressType.PRIVATE) + internal fun buildForPrivateRecipient_checkSenderCertificate() = + runTest { + val channel = createEndpointChannel(RecipientAddressType.PRIVATE) - val message = MessageFactory.buildOutgoing(channel) + val message = MessageFactory.buildOutgoing(channel) - assertEquals( - (message.recipientEndpoint as PrivateThirdPartyEndpoint).pda, - message.parcel.senderCertificate, - ) - } + assertEquals( + (message.recipientEndpoint as PrivateThirdPartyEndpoint).pda, + message.parcel.senderCertificate, + ) + } @Test - internal fun buildForPrivateRecipient_checkSenderCertificateChain() = runTest { - val channel = createEndpointChannel(RecipientAddressType.PRIVATE) + internal fun buildForPrivateRecipient_checkSenderCertificateChain() = + runTest { + val channel = createEndpointChannel(RecipientAddressType.PRIVATE) - val message = MessageFactory.buildOutgoing(channel) + val message = MessageFactory.buildOutgoing(channel) - assertArrayEquals( - (message.recipientEndpoint as PrivateThirdPartyEndpoint).pdaChain.toTypedArray(), - message.parcel.senderCertificateChain.toTypedArray(), - ) - } + assertArrayEquals( + (message.recipientEndpoint as PrivateThirdPartyEndpoint).pdaChain.toTypedArray(), + message.parcel.senderCertificateChain.toTypedArray(), + ) + } } diff --git a/lib/src/test/java/tech/relaycorp/awaladroid/messaging/ReceiveMessagesTest.kt b/lib/src/test/java/tech/relaycorp/awaladroid/messaging/ReceiveMessagesTest.kt index 55070735..142fd2a7 100644 --- a/lib/src/test/java/tech/relaycorp/awaladroid/messaging/ReceiveMessagesTest.kt +++ b/lib/src/test/java/tech/relaycorp/awaladroid/messaging/ReceiveMessagesTest.kt @@ -37,7 +37,6 @@ import tech.relaycorp.relaynet.wrappers.nodeId import java.time.ZonedDateTime internal class ReceiveMessagesTest : MockContextTestCase() { - private lateinit var pdcClient: MockPDCClient private val subject = ReceiveMessages { pdcClient } @@ -45,277 +44,318 @@ internal class ReceiveMessagesTest : MockContextTestCase() { private val logCaptor = LogCaptor.forClass(ParcelCollection::class.java) @Test - fun receiveParcelSuccessfully() = runTest { - val channel = createEndpointChannel(RecipientAddressType.PUBLIC) - val parcel = buildParcel(channel) - val parcelCollection = parcel.toParcelCollection() - val collectParcelsCall = CollectParcelsCall(Result.success(flowOf(parcelCollection))) - pdcClient = MockPDCClient(collectParcelsCall) - - val messages = subject.receive().toCollection(mutableListOf()) - - assertTrue(pdcClient.wasClosed) - assertTrue(collectParcelsCall.wasCalled) - assertEquals(1, messages.size) - } + fun receiveParcelSuccessfully() = + runTest { + val channel = createEndpointChannel(RecipientAddressType.PUBLIC) + val parcel = buildParcel(channel) + val parcelCollection = parcel.toParcelCollection() + val collectParcelsCall = CollectParcelsCall(Result.success(flowOf(parcelCollection))) + pdcClient = MockPDCClient(collectParcelsCall) + + val messages = subject.receive().toCollection(mutableListOf()) + + assertTrue(pdcClient.wasClosed) + assertTrue(collectParcelsCall.wasCalled) + assertEquals(1, messages.size) + } @Test - fun collectParcelsWithCorrectNonceSigners() = runTest { - val channel = createEndpointChannel(RecipientAddressType.PUBLIC) - val parcel = buildParcel(channel) - val parcelCollection = parcel.toParcelCollection() - val collectParcelsCall = CollectParcelsCall(Result.success(flowOf(parcelCollection))) - pdcClient = MockPDCClient(collectParcelsCall) - - subject.receive().collect() - - assertTrue(pdcClient.wasClosed) - assertTrue(collectParcelsCall.wasCalled) - val nonceSigners = collectParcelsCall.arguments!!.nonceSigners - assertEquals(1, nonceSigners.size) - assertEquals(PDACertPath.PRIVATE_ENDPOINT, nonceSigners.first().certificate) - } + fun collectParcelsWithCorrectNonceSigners() = + runTest { + val channel = createEndpointChannel(RecipientAddressType.PUBLIC) + val parcel = buildParcel(channel) + val parcelCollection = parcel.toParcelCollection() + val collectParcelsCall = CollectParcelsCall(Result.success(flowOf(parcelCollection))) + pdcClient = MockPDCClient(collectParcelsCall) + + subject.receive().collect() + + assertTrue(pdcClient.wasClosed) + assertTrue(collectParcelsCall.wasCalled) + val nonceSigners = collectParcelsCall.arguments!!.nonceSigners + assertEquals(1, nonceSigners.size) + assertEquals(PDACertPath.PRIVATE_ENDPOINT, nonceSigners.first().certificate) + } @Test(expected = ReceiveMessageException::class) - fun collectParcelsGetsServerError() = runTest { - createFirstPartyEndpoint() - val collectParcelsCall = CollectParcelsCall( - Result.success(flow { throw ServerBindingException("") }), - ) - pdcClient = MockPDCClient(collectParcelsCall) - - subject.receive().collect() - } + fun collectParcelsGetsServerError() = + runTest { + createFirstPartyEndpoint() + val collectParcelsCall = + CollectParcelsCall( + Result.success(flow { throw ServerBindingException("") }), + ) + pdcClient = MockPDCClient(collectParcelsCall) + + subject.receive().collect() + } @Test(expected = GatewayProtocolException::class) - fun collectParcelsGetsClientError() = runTest { - createFirstPartyEndpoint() - val collectParcelsCall = CollectParcelsCall( - Result.success(flow { throw ClientBindingException("") }), - ) - pdcClient = MockPDCClient(collectParcelsCall) - - subject.receive().collect() - } + fun collectParcelsGetsClientError() = + runTest { + createFirstPartyEndpoint() + val collectParcelsCall = + CollectParcelsCall( + Result.success(flow { throw ClientBindingException("") }), + ) + pdcClient = MockPDCClient(collectParcelsCall) + + subject.receive().collect() + } @Test(expected = GatewayProtocolException::class) - fun collectParcelsGetsSigningError() = runTest { - createFirstPartyEndpoint() - val collectParcelsCall = CollectParcelsCall( - Result.success(flow { throw NonceSignerException("") }), - ) - pdcClient = MockPDCClient(collectParcelsCall) - - subject.receive().collect() - } + fun collectParcelsGetsSigningError() = + runTest { + createFirstPartyEndpoint() + val collectParcelsCall = + CollectParcelsCall( + Result.success(flow { throw NonceSignerException("") }), + ) + pdcClient = MockPDCClient(collectParcelsCall) + + subject.receive().collect() + } @Test - fun collectParcelsWithoutFirstPartyEndpoints() = runTest { - val logCaptor = LogCaptor.forClass(ReceiveMessages::class.java) - val collectParcelsCall = CollectParcelsCall(Result.success(emptyFlow())) - pdcClient = MockPDCClient(collectParcelsCall) - - subject.receive().collect() - - assertFalse(collectParcelsCall.wasCalled) - assertTrue( - logCaptor.warnLogs.contains( - "Skipping parcel collection because there are no first-party endpoints", - ), - ) - } + fun collectParcelsWithoutFirstPartyEndpoints() = + runTest { + val logCaptor = LogCaptor.forClass(ReceiveMessages::class.java) + val collectParcelsCall = CollectParcelsCall(Result.success(emptyFlow())) + pdcClient = MockPDCClient(collectParcelsCall) + + subject.receive().collect() + + assertFalse(collectParcelsCall.wasCalled) + assertTrue( + logCaptor.warnLogs.contains( + "Skipping parcel collection because there are no first-party endpoints", + ), + ) + } @Test - fun receiveInvalidParcel_ackedButNotDeliveredToApp() = runTest { - createFirstPartyEndpoint() - val invalidParcel = Parcel( - recipient = Recipient(KeyPairSet.PRIVATE_ENDPOINT.public.nodeId), - payload = "".toByteArray(), - senderCertificate = PDACertPath.PRIVATE_ENDPOINT, - ) - var ackWasCalled = false - val parcelCollection = ParcelCollection( - parcelSerialized = invalidParcel.serialize(KeyPairSet.PRIVATE_ENDPOINT.private), - trustedCertificates = emptyList(), // sender won't be trusted - ack = { ackWasCalled = true }, - ) - val collectParcelsCall = CollectParcelsCall(Result.success(flowOf(parcelCollection))) - pdcClient = MockPDCClient(collectParcelsCall) - - val messages = subject.receive().toCollection(mutableListOf()) - - assertTrue(pdcClient.wasClosed) - assertTrue(collectParcelsCall.wasCalled) - assertTrue(messages.isEmpty()) - assertTrue(ackWasCalled) - assertTrue(logCaptor.warnLogs.contains("Invalid incoming parcel")) - } + fun receiveInvalidParcel_ackedButNotDeliveredToApp() = + runTest { + createFirstPartyEndpoint() + val invalidParcel = + Parcel( + recipient = Recipient(KeyPairSet.PRIVATE_ENDPOINT.public.nodeId), + payload = "".toByteArray(), + senderCertificate = PDACertPath.PRIVATE_ENDPOINT, + ) + var ackWasCalled = false + val parcelCollection = + ParcelCollection( + parcelSerialized = invalidParcel.serialize(KeyPairSet.PRIVATE_ENDPOINT.private), + // sender won't be trusted + trustedCertificates = emptyList(), + ack = { ackWasCalled = true }, + ) + val collectParcelsCall = CollectParcelsCall(Result.success(flowOf(parcelCollection))) + pdcClient = MockPDCClient(collectParcelsCall) + + val messages = subject.receive().toCollection(mutableListOf()) + + assertTrue(pdcClient.wasClosed) + assertTrue(collectParcelsCall.wasCalled) + assertTrue(messages.isEmpty()) + assertTrue(ackWasCalled) + assertTrue(logCaptor.warnLogs.contains("Invalid incoming parcel")) + } @Test - fun receiveMalformedParcel_ackedButNotDeliveredToApp() = runTest { - createFirstPartyEndpoint() - var ackWasCalled = false - val parcelCollection = ParcelCollection( - parcelSerialized = "1234".toByteArray(), - trustedCertificates = emptyList(), - ack = { ackWasCalled = true }, - ) - val collectParcelsCall = CollectParcelsCall(Result.success(flowOf(parcelCollection))) - pdcClient = MockPDCClient(collectParcelsCall) - - val messages = subject.receive().toCollection(mutableListOf()) - - assertTrue(pdcClient.wasClosed) - assertTrue(collectParcelsCall.wasCalled) - assertTrue(messages.isEmpty()) - assertTrue(ackWasCalled) - assertTrue(logCaptor.warnLogs.contains("Malformed incoming parcel")) - } + fun receiveMalformedParcel_ackedButNotDeliveredToApp() = + runTest { + createFirstPartyEndpoint() + var ackWasCalled = false + val parcelCollection = + ParcelCollection( + parcelSerialized = "1234".toByteArray(), + trustedCertificates = emptyList(), + ack = { ackWasCalled = true }, + ) + val collectParcelsCall = CollectParcelsCall(Result.success(flowOf(parcelCollection))) + pdcClient = MockPDCClient(collectParcelsCall) + + val messages = subject.receive().toCollection(mutableListOf()) + + assertTrue(pdcClient.wasClosed) + assertTrue(collectParcelsCall.wasCalled) + assertTrue(messages.isEmpty()) + assertTrue(ackWasCalled) + assertTrue(logCaptor.warnLogs.contains("Malformed incoming parcel")) + } @Test - fun receiveParcelWithUnknownRecipient_ackedButNotDeliveredToApp() = runTest { - val channel = createEndpointChannel(RecipientAddressType.PUBLIC) - val parcel = buildParcel(channel) - var ackWasCalled = false - val parcelCollection = parcel.toParcelCollection { ackWasCalled = true } - val collectParcelsCall = CollectParcelsCall(Result.success(flowOf(parcelCollection))) - pdcClient = MockPDCClient(collectParcelsCall) - - channel.firstPartyEndpoint.delete() - createAnotherFirstPartyEndpoint() - - val messages = subject.receive().toCollection(mutableListOf()) - - assertTrue(pdcClient.wasClosed) - assertTrue(collectParcelsCall.wasCalled) - assertTrue(messages.isEmpty()) - assertTrue(ackWasCalled) - assertTrue(logCaptor.warnLogs.contains("Incoming parcel with invalid recipient")) - } + fun receiveParcelWithUnknownRecipient_ackedButNotDeliveredToApp() = + runTest { + val channel = createEndpointChannel(RecipientAddressType.PUBLIC) + val parcel = buildParcel(channel) + var ackWasCalled = false + val parcelCollection = parcel.toParcelCollection { ackWasCalled = true } + val collectParcelsCall = CollectParcelsCall(Result.success(flowOf(parcelCollection))) + pdcClient = MockPDCClient(collectParcelsCall) + + channel.firstPartyEndpoint.delete() + createAnotherFirstPartyEndpoint() + + val messages = subject.receive().toCollection(mutableListOf()) + + assertTrue(pdcClient.wasClosed) + assertTrue(collectParcelsCall.wasCalled) + assertTrue(messages.isEmpty()) + assertTrue(ackWasCalled) + assertTrue(logCaptor.warnLogs.contains("Incoming parcel with invalid recipient")) + } @Test - fun receiveParcelWithUnknownSender_ackedButNotDeliveredToApp() = runTest { - val channel = createEndpointChannel(RecipientAddressType.PUBLIC) - val parcel = buildParcel(channel) - var ackWasCalled = false - val parcelCollection = parcel.toParcelCollection { ackWasCalled = true } - val collectParcelsCall = CollectParcelsCall(Result.success(flowOf(parcelCollection))) - pdcClient = MockPDCClient(collectParcelsCall) - - channel.thirdPartyEndpoint.delete() - - val messages = subject.receive().toCollection(mutableListOf()) - - assertTrue(pdcClient.wasClosed) - assertTrue(collectParcelsCall.wasCalled) - assertTrue(messages.isEmpty()) - assertTrue(ackWasCalled) - assertTrue(logCaptor.warnLogs.contains("Incoming parcel issues with invalid sender")) - } + fun receiveParcelWithUnknownSender_ackedButNotDeliveredToApp() = + runTest { + val channel = createEndpointChannel(RecipientAddressType.PUBLIC) + val parcel = buildParcel(channel) + var ackWasCalled = false + val parcelCollection = parcel.toParcelCollection { ackWasCalled = true } + val collectParcelsCall = CollectParcelsCall(Result.success(flowOf(parcelCollection))) + pdcClient = MockPDCClient(collectParcelsCall) + + channel.thirdPartyEndpoint.delete() + + val messages = subject.receive().toCollection(mutableListOf()) + + assertTrue(pdcClient.wasClosed) + assertTrue(collectParcelsCall.wasCalled) + assertTrue(messages.isEmpty()) + assertTrue(ackWasCalled) + assertTrue(logCaptor.warnLogs.contains("Incoming parcel issues with invalid sender")) + } @Test - fun receiveValidParcel_invalidPayloadEncryption() = runTest { - val channel = createEndpointChannel(RecipientAddressType.PUBLIC) - storage.publicThirdParty.set( - channel.thirdPartyEndpoint.nodeId, - PublicThirdPartyEndpointData( + fun receiveValidParcel_invalidPayloadEncryption() = + runTest { + val channel = createEndpointChannel(RecipientAddressType.PUBLIC) + storage.publicThirdParty.set( channel.thirdPartyEndpoint.nodeId, - channel.thirdPartyEndpoint.identityKey, - ), - ) - val parcelPayload = serviceMessage.encrypt( - channel.firstPartySessionKeyPair.sessionKey.copy( - publicKey = generateECDHKeyPair().public, // Invalid encryption key - ), - channel.thirdPartySessionKeyPair, - ) - val parcel = Parcel( - recipient = Recipient(PDACertPath.PRIVATE_ENDPOINT.subjectId), - payload = parcelPayload, - senderCertificate = PDACertPath.PDA, - senderCertificateChain = setOf(PDACertPath.PRIVATE_ENDPOINT, PDACertPath.PRIVATE_GW), - ) - var ackWasCalled = false - val parcelCollection = parcel.toParcelCollection { ackWasCalled = true } - val collectParcelsCall = CollectParcelsCall(Result.success(flowOf(parcelCollection))) - pdcClient = MockPDCClient(collectParcelsCall) - - val messages = subject.receive().toCollection(mutableListOf()) - - assertTrue(pdcClient.wasClosed) - assertTrue(messages.isEmpty()) - assertTrue(ackWasCalled) - assertTrue( - logCaptor.warnLogs.contains( - "Failed to decrypt parcel; sender might have used wrong key", - ), - ) - } + PublicThirdPartyEndpointData( + channel.thirdPartyEndpoint.nodeId, + channel.thirdPartyEndpoint.identityKey, + ), + ) + val parcelPayload = + serviceMessage.encrypt( + channel.firstPartySessionKeyPair.sessionKey.copy( + // Invalid encryption key + publicKey = generateECDHKeyPair().public, + ), + channel.thirdPartySessionKeyPair, + ) + val parcel = + Parcel( + recipient = Recipient(PDACertPath.PRIVATE_ENDPOINT.subjectId), + payload = parcelPayload, + senderCertificate = PDACertPath.PDA, + senderCertificateChain = + setOf( + PDACertPath.PRIVATE_ENDPOINT, + PDACertPath.PRIVATE_GW, + ), + ) + var ackWasCalled = false + val parcelCollection = parcel.toParcelCollection { ackWasCalled = true } + val collectParcelsCall = CollectParcelsCall(Result.success(flowOf(parcelCollection))) + pdcClient = MockPDCClient(collectParcelsCall) + + val messages = subject.receive().toCollection(mutableListOf()) + + assertTrue(pdcClient.wasClosed) + assertTrue(messages.isEmpty()) + assertTrue(ackWasCalled) + assertTrue( + logCaptor.warnLogs.contains( + "Failed to decrypt parcel; sender might have used wrong key", + ), + ) + } @Test - fun receiveValidParcel_invalidServiceMessage() = runTest { - val invalidServiceMessage = CargoMessageSet(emptyArray()) - val channel = createEndpointChannel(RecipientAddressType.PUBLIC) - storage.publicThirdParty.set( - channel.thirdPartyEndpoint.nodeId, - PublicThirdPartyEndpointData( + fun receiveValidParcel_invalidServiceMessage() = + runTest { + val invalidServiceMessage = CargoMessageSet(emptyArray()) + val channel = createEndpointChannel(RecipientAddressType.PUBLIC) + storage.publicThirdParty.set( channel.thirdPartyEndpoint.nodeId, - channel.thirdPartyEndpoint.identityKey, - ), - ) - val parcel = Parcel( - recipient = Recipient(PDACertPath.PRIVATE_ENDPOINT.subjectId), - payload = invalidServiceMessage.encrypt( - channel.firstPartySessionKeyPair.sessionKey, - channel.thirdPartySessionKeyPair, - ), - senderCertificate = PDACertPath.PDA, - senderCertificateChain = setOf(PDACertPath.PRIVATE_ENDPOINT, PDACertPath.PRIVATE_GW), - ) - var ackWasCalled = false - val parcelCollection = parcel.toParcelCollection { ackWasCalled = true } - val collectParcelsCall = CollectParcelsCall(Result.success(flowOf(parcelCollection))) - pdcClient = MockPDCClient(collectParcelsCall) - - val messages = subject.receive().toCollection(mutableListOf()) - - assertTrue(pdcClient.wasClosed) - assertTrue(messages.isEmpty()) - assertTrue(ackWasCalled) - assertTrue( - logCaptor.warnLogs.contains( - "Incoming parcel did not encapsulate a valid service message", - ), + PublicThirdPartyEndpointData( + channel.thirdPartyEndpoint.nodeId, + channel.thirdPartyEndpoint.identityKey, + ), + ) + val parcel = + Parcel( + recipient = Recipient(PDACertPath.PRIVATE_ENDPOINT.subjectId), + payload = + invalidServiceMessage.encrypt( + channel.firstPartySessionKeyPair.sessionKey, + channel.thirdPartySessionKeyPair, + ), + senderCertificate = PDACertPath.PDA, + senderCertificateChain = + setOf( + PDACertPath.PRIVATE_ENDPOINT, + PDACertPath.PRIVATE_GW, + ), + ) + var ackWasCalled = false + val parcelCollection = parcel.toParcelCollection { ackWasCalled = true } + val collectParcelsCall = CollectParcelsCall(Result.success(flowOf(parcelCollection))) + pdcClient = MockPDCClient(collectParcelsCall) + + val messages = subject.receive().toCollection(mutableListOf()) + + assertTrue(pdcClient.wasClosed) + assertTrue(messages.isEmpty()) + assertTrue(ackWasCalled) + assertTrue( + logCaptor.warnLogs.contains( + "Incoming parcel did not encapsulate a valid service message", + ), + ) + } + + private fun buildParcel(channel: EndpointChannel) = + Parcel( + recipient = Recipient(KeyPairSet.PRIVATE_ENDPOINT.public.nodeId), + payload = + serviceMessage.encrypt( + channel.firstPartySessionKeyPair.sessionKey, + channel.thirdPartySessionKeyPair, + ), + senderCertificate = + issueDeliveryAuthorization( + subjectPublicKey = KeyPairSet.PDA_GRANTEE.public, + issuerPrivateKey = KeyPairSet.PRIVATE_ENDPOINT.private, + issuerCertificate = PDACertPath.PRIVATE_ENDPOINT, + validityStartDate = ZonedDateTime.now().minusDays(1), + validityEndDate = ZonedDateTime.now().plusDays(1), + ), + senderCertificateChain = + setOf( + PDACertPath.PRIVATE_ENDPOINT, + PDACertPath.PRIVATE_GW, + ), ) - } - private fun buildParcel(channel: EndpointChannel) = Parcel( - recipient = Recipient(KeyPairSet.PRIVATE_ENDPOINT.public.nodeId), - payload = serviceMessage.encrypt( - channel.firstPartySessionKeyPair.sessionKey, - channel.thirdPartySessionKeyPair, - ), - senderCertificate = issueDeliveryAuthorization( - subjectPublicKey = KeyPairSet.PDA_GRANTEE.public, - issuerPrivateKey = KeyPairSet.PRIVATE_ENDPOINT.private, - issuerCertificate = PDACertPath.PRIVATE_ENDPOINT, - validityStartDate = ZonedDateTime.now().minusDays(1), - validityEndDate = ZonedDateTime.now().plusDays(1), - ), - senderCertificateChain = setOf(PDACertPath.PRIVATE_ENDPOINT, PDACertPath.PRIVATE_GW), - ) - - private fun Parcel.toParcelCollection(ack: suspend () -> Unit = {}) = ParcelCollection( - parcelSerialized = serialize(KeyPairSet.PDA_GRANTEE.private), - trustedCertificates = listOf(PDACertPath.PRIVATE_ENDPOINT, PDACertPath.PRIVATE_GW), - ack = ack, - ) + private fun Parcel.toParcelCollection(ack: suspend () -> Unit = {}) = + ParcelCollection( + parcelSerialized = serialize(KeyPairSet.PDA_GRANTEE.private), + trustedCertificates = listOf(PDACertPath.PRIVATE_ENDPOINT, PDACertPath.PRIVATE_GW), + ack = ack, + ) private suspend fun createAnotherFirstPartyEndpoint() { val anotherKey = generateRSAKeyPair() createFirstPartyEndpoint( FirstPartyEndpoint( - anotherKey.private, // Different key + // Different key + anotherKey.private, issueEndpointCertificate( anotherKey.public, KeyPairSet.PRIVATE_GW.private, diff --git a/lib/src/test/java/tech/relaycorp/awaladroid/messaging/SendMessageTest.kt b/lib/src/test/java/tech/relaycorp/awaladroid/messaging/SendMessageTest.kt index 7db90a83..9da756ec 100644 --- a/lib/src/test/java/tech/relaycorp/awaladroid/messaging/SendMessageTest.kt +++ b/lib/src/test/java/tech/relaycorp/awaladroid/messaging/SendMessageTest.kt @@ -17,69 +17,73 @@ import tech.relaycorp.relaynet.testing.pdc.DeliverParcelCall import tech.relaycorp.relaynet.testing.pdc.MockPDCClient internal class SendMessageTest : MockContextTestCase() { - private lateinit var pdcClient: MockPDCClient private val coroutineScope = TestScope() private val subject = SendMessage({ pdcClient }, coroutineScope.coroutineContext) @Test - fun deliverParcelToPublicEndpoint() = coroutineScope.runTest { - val deliverParcelCall = DeliverParcelCall() - pdcClient = MockPDCClient(deliverParcelCall) - val message = - MessageFactory.buildOutgoing(createEndpointChannel(RecipientAddressType.PUBLIC)) + fun deliverParcelToPublicEndpoint() = + coroutineScope.runTest { + val deliverParcelCall = DeliverParcelCall() + pdcClient = MockPDCClient(deliverParcelCall) + val message = + MessageFactory.buildOutgoing(createEndpointChannel(RecipientAddressType.PUBLIC)) - subject.send(message) + subject.send(message) - assertTrue(deliverParcelCall.wasCalled) - val parcel = Parcel.deserialize(deliverParcelCall.arguments!!.parcelSerialized) - assertEquals(message.parcel.id, parcel.id) - } + assertTrue(deliverParcelCall.wasCalled) + val parcel = Parcel.deserialize(deliverParcelCall.arguments!!.parcelSerialized) + assertEquals(message.parcel.id, parcel.id) + } @Test - fun deliverParcelSigner() = coroutineScope.runTest { - val deliverParcelCall = DeliverParcelCall() - pdcClient = MockPDCClient(deliverParcelCall) - val message = - MessageFactory.buildOutgoing(createEndpointChannel(RecipientAddressType.PUBLIC)) + fun deliverParcelSigner() = + coroutineScope.runTest { + val deliverParcelCall = DeliverParcelCall() + pdcClient = MockPDCClient(deliverParcelCall) + val message = + MessageFactory.buildOutgoing(createEndpointChannel(RecipientAddressType.PUBLIC)) - subject.send(message) + subject.send(message) - assertTrue(deliverParcelCall.wasCalled) - val signer = deliverParcelCall.arguments!!.deliverySigner - assertEquals( - message.senderEndpoint.identityCertificate.subjectId, - signer.certificate.subjectId, - ) - } + assertTrue(deliverParcelCall.wasCalled) + val signer = deliverParcelCall.arguments!!.deliverySigner + assertEquals( + message.senderEndpoint.identityCertificate.subjectId, + signer.certificate.subjectId, + ) + } @Test(expected = SendMessageException::class) - fun deliverParcelWithServerError() = coroutineScope.runTest { - val deliverParcelCall = DeliverParcelCall(ServerConnectionException("")) - pdcClient = MockPDCClient(deliverParcelCall) + fun deliverParcelWithServerError() = + coroutineScope.runTest { + val deliverParcelCall = DeliverParcelCall(ServerConnectionException("")) + pdcClient = MockPDCClient(deliverParcelCall) - val message = - MessageFactory.buildOutgoing(createEndpointChannel(RecipientAddressType.PUBLIC)) - subject.send(message) - } + val message = + MessageFactory.buildOutgoing(createEndpointChannel(RecipientAddressType.PUBLIC)) + subject.send(message) + } @Test(expected = GatewayProtocolException::class) - fun deliverParcelWithClientError() = coroutineScope.runTest { - val deliverParcelCall = DeliverParcelCall(ClientBindingException("")) - pdcClient = MockPDCClient(deliverParcelCall) + fun deliverParcelWithClientError() = + coroutineScope.runTest { + val deliverParcelCall = DeliverParcelCall(ClientBindingException("")) + pdcClient = MockPDCClient(deliverParcelCall) - val message = - MessageFactory.buildOutgoing(createEndpointChannel(RecipientAddressType.PUBLIC)) - subject.send(message) - } + val message = + MessageFactory.buildOutgoing(createEndpointChannel(RecipientAddressType.PUBLIC)) + subject.send(message) + } @Test(expected = RejectedMessageException::class) - fun deliverParcelWithRejectedParcelError() = coroutineScope.runTest { - val deliverParcelCall = DeliverParcelCall(RejectedParcelException("")) - pdcClient = MockPDCClient(deliverParcelCall) + fun deliverParcelWithRejectedParcelError() = + coroutineScope.runTest { + val deliverParcelCall = DeliverParcelCall(RejectedParcelException("")) + pdcClient = MockPDCClient(deliverParcelCall) - val message = - MessageFactory.buildOutgoing(createEndpointChannel(RecipientAddressType.PUBLIC)) - subject.send(message) - } + val message = + MessageFactory.buildOutgoing(createEndpointChannel(RecipientAddressType.PUBLIC)) + subject.send(message) + } } diff --git a/lib/src/test/java/tech/relaycorp/awaladroid/storage/MockStorage.kt b/lib/src/test/java/tech/relaycorp/awaladroid/storage/MockStorage.kt index a630f287..8c48cbb0 100644 --- a/lib/src/test/java/tech/relaycorp/awaladroid/storage/MockStorage.kt +++ b/lib/src/test/java/tech/relaycorp/awaladroid/storage/MockStorage.kt @@ -3,9 +3,10 @@ package tech.relaycorp.awaladroid.storage import com.nhaarman.mockitokotlin2.doReturn import com.nhaarman.mockitokotlin2.mock -internal fun mockStorage() = mock { - on { gatewayId } doReturn mock() - on { internetAddress } doReturn mock() - on { publicThirdParty } doReturn mock() - on { privateThirdParty } doReturn mock() -} +internal fun mockStorage() = + mock { + on { gatewayId } doReturn mock() + on { internetAddress } doReturn mock() + on { publicThirdParty } doReturn mock() + on { privateThirdParty } doReturn mock() + } diff --git a/lib/src/test/java/tech/relaycorp/awaladroid/storage/StorageImplTest.kt b/lib/src/test/java/tech/relaycorp/awaladroid/storage/StorageImplTest.kt index dff7c501..a9ebe6cb 100644 --- a/lib/src/test/java/tech/relaycorp/awaladroid/storage/StorageImplTest.kt +++ b/lib/src/test/java/tech/relaycorp/awaladroid/storage/StorageImplTest.kt @@ -19,62 +19,66 @@ import java.nio.charset.Charset import java.util.UUID internal class StorageImplTest { - private val persistence = mock() private val storage = StorageImpl(persistence) @Test - fun gatewayId() = runTest { - val charset = Charset.forName("ASCII") - storage.gatewayId.testGet( - PDACertPath.PRIVATE_GW.subjectId.toByteArray(charset), - PDACertPath.PRIVATE_GW.subjectId, - ) - storage.gatewayId.testSet( - PDACertPath.PRIVATE_GW.subjectId, - PDACertPath.PRIVATE_GW.subjectId.toByteArray(charset), - ) - storage.gatewayId.testDelete() - } + fun gatewayId() = + runTest { + val charset = Charset.forName("ASCII") + storage.gatewayId.testGet( + PDACertPath.PRIVATE_GW.subjectId.toByteArray(charset), + PDACertPath.PRIVATE_GW.subjectId, + ) + storage.gatewayId.testSet( + PDACertPath.PRIVATE_GW.subjectId, + PDACertPath.PRIVATE_GW.subjectId.toByteArray(charset), + ) + storage.gatewayId.testDelete() + } @Test - fun privateThirdParty() = runTest { - val data = PrivateThirdPartyEndpointData( - KeyPairSet.PRIVATE_ENDPOINT.public, - CertificationPath( - PDACertPath.PDA, - listOf(PDACertPath.PRIVATE_GW), - ), - "gateway.com", - ) - val rawData = data.serialize() - - storage.privateThirdParty.testGet(rawData, data) { a, b -> - a.identityKey == b.identityKey && - a.pdaPath.leafCertificate == b.pdaPath.leafCertificate && - a.pdaPath.certificateAuthorities == b.pdaPath.certificateAuthorities && - a.internetGatewayAddress == b.internetGatewayAddress + fun privateThirdParty() = + runTest { + val data = + PrivateThirdPartyEndpointData( + KeyPairSet.PRIVATE_ENDPOINT.public, + CertificationPath( + PDACertPath.PDA, + listOf(PDACertPath.PRIVATE_GW), + ), + "gateway.com", + ) + val rawData = data.serialize() + + storage.privateThirdParty.testGet(rawData, data) { a, b -> + a.identityKey == b.identityKey && + a.pdaPath.leafCertificate == b.pdaPath.leafCertificate && + a.pdaPath.certificateAuthorities == b.pdaPath.certificateAuthorities && + a.internetGatewayAddress == b.internetGatewayAddress + } + storage.privateThirdParty.testSet(data, rawData) + storage.privateThirdParty.testDelete() + storage.privateThirdParty.testDeleteAll() + storage.privateThirdParty.testList() } - storage.privateThirdParty.testSet(data, rawData) - storage.privateThirdParty.testDelete() - storage.privateThirdParty.testDeleteAll() - storage.privateThirdParty.testList() - } @Test - fun publicThirdParty() = runTest { - val data = PublicThirdPartyEndpointData( - "example.org", - KeyPairSet.INTERNET_GW.public, - ) - val rawData = data.serialize() - - storage.publicThirdParty.testGet(rawData, data) - storage.publicThirdParty.testSet(data, rawData) - storage.publicThirdParty.testDelete() - storage.publicThirdParty.testDeleteAll() - storage.publicThirdParty.testList() - } + fun publicThirdParty() = + runTest { + val data = + PublicThirdPartyEndpointData( + "example.org", + KeyPairSet.INTERNET_GW.public, + ) + val rawData = data.serialize() + + storage.publicThirdParty.testGet(rawData, data) + storage.publicThirdParty.testSet(data, rawData) + storage.publicThirdParty.testDelete() + storage.publicThirdParty.testDeleteAll() + storage.publicThirdParty.testList() + } // Helpers diff --git a/lib/src/test/java/tech/relaycorp/awaladroid/storage/persistence/DiskPersistenceTest.kt b/lib/src/test/java/tech/relaycorp/awaladroid/storage/persistence/DiskPersistenceTest.kt index deab9606..e8b5e807 100644 --- a/lib/src/test/java/tech/relaycorp/awaladroid/storage/persistence/DiskPersistenceTest.kt +++ b/lib/src/test/java/tech/relaycorp/awaladroid/storage/persistence/DiskPersistenceTest.kt @@ -23,55 +23,62 @@ internal class DiskPersistenceTest { private lateinit var subject: DiskPersistence @Before - fun initDiskPersistence(): Unit = runBlocking { - filesDir = createTempDirectory("rootDir").toString() - subject = DiskPersistence( - filesDir, - coroutineScope.coroutineContext, - rootFolder, - ) - } + fun initDiskPersistence(): Unit = + runBlocking { + filesDir = createTempDirectory("rootDir").toString() + subject = + DiskPersistence( + filesDir, + coroutineScope.coroutineContext, + rootFolder, + ) + } @Test - fun getNonExistingFile() = coroutineScope.runTest { - assertNull(subject.get("file")) - } + fun getNonExistingFile() = + coroutineScope.runTest { + assertNull(subject.get("file")) + } @Test - fun setNonExistingFileAndGetIt() = coroutineScope.runTest { - val data = "test" - subject.set("file", data.toByteArray()) - assertEquals(data, subject.get("file")?.toString(Charset.defaultCharset())) - } + fun setNonExistingFileAndGetIt() = + coroutineScope.runTest { + val data = "test" + subject.set("file", data.toByteArray()) + assertEquals(data, subject.get("file")?.toString(Charset.defaultCharset())) + } @Test - fun setOnExistingFile() = coroutineScope.runTest { - val data1 = "test1" - val data2 = "test2" - subject.set("file", data1.toByteArray()) - subject.set("file", data2.toByteArray()) - assertEquals(data2, subject.get("file")?.toString(Charset.defaultCharset())) - } + fun setOnExistingFile() = + coroutineScope.runTest { + val data1 = "test1" + val data2 = "test2" + subject.set("file", data1.toByteArray()) + subject.set("file", data2.toByteArray()) + assertEquals(data2, subject.get("file")?.toString(Charset.defaultCharset())) + } @Test - fun setContent() = coroutineScope.runTest { - val location = "file" - val data = "test" - subject.set(location, data.toByteArray()) - val fileContent = - File(filesDir, "$rootFolder${File.separator}$location") - .readBytes() - .toString(Charset.defaultCharset()) - assertEquals(data, fileContent) - } + fun setContent() = + coroutineScope.runTest { + val location = "file" + val data = "test" + subject.set(location, data.toByteArray()) + val fileContent = + File(filesDir, "$rootFolder${File.separator}$location") + .readBytes() + .toString(Charset.defaultCharset()) + assertEquals(data, fileContent) + } @Test - fun deleteExistingFile() = coroutineScope.runTest { - subject.set("file", "test".toByteArray()) - assertNotNull(subject.get("file")) - subject.delete("file") - assertNull(subject.get("file")) - } + fun deleteExistingFile() = + coroutineScope.runTest { + subject.set("file", "test".toByteArray()) + assertNotNull(subject.get("file")) + subject.delete("file") + assertNull(subject.get("file")) + } @Test fun deleteNonExistingFile() { @@ -83,41 +90,44 @@ internal class DiskPersistenceTest { } @Test - fun deleteAll() = coroutineScope.runTest { - subject.set("file1", "test".toByteArray()) - subject.set("file2", "test".toByteArray()) - subject.deleteAll() - assertNull(subject.get("file1")) - assertNull(subject.get("file2")) - } + fun deleteAll() = + coroutineScope.runTest { + subject.set("file1", "test".toByteArray()) + subject.set("file2", "test".toByteArray()) + subject.deleteAll() + assertNull(subject.get("file1")) + assertNull(subject.get("file2")) + } @Test - fun deleteAll_withPrefix() = coroutineScope.runTest { - subject.set("file1", "test".toByteArray()) - subject.set("different2", "test".toByteArray()) - subject.deleteAll("file") - assertNull(subject.get("file1")) - assertNotNull(subject.get("different2")) - } + fun deleteAll_withPrefix() = + coroutineScope.runTest { + subject.set("file1", "test".toByteArray()) + subject.set("different2", "test".toByteArray()) + subject.deleteAll("file") + assertNull(subject.get("file1")) + assertNotNull(subject.get("different2")) + } @Test - fun list() = coroutineScope.runTest { - subject.set("file1", "test".toByteArray()) - subject.set("file2", "test".toByteArray()) - subject.set("another", "test".toByteArray()) + fun list() = + coroutineScope.runTest { + subject.set("file1", "test".toByteArray()) + subject.set("file2", "test".toByteArray()) + subject.set("another", "test".toByteArray()) - with(subject.list()) { - assertEquals(3, size) - assertTrue(contains("file1")) - assertTrue(contains("file2")) - assertTrue(contains("another")) - } + with(subject.list()) { + assertEquals(3, size) + assertTrue(contains("file1")) + assertTrue(contains("file2")) + assertTrue(contains("another")) + } - with(subject.list("file")) { - assertEquals(2, size) - assertTrue(contains("file1")) - assertTrue(contains("file2")) - assertFalse(contains("another")) + with(subject.list("file")) { + assertEquals(2, size) + assertTrue(contains("file1")) + assertTrue(contains("file2")) + assertFalse(contains("another")) + } } - } } diff --git a/lib/src/test/java/tech/relaycorp/awaladroid/test/AssertUtils.kt b/lib/src/test/java/tech/relaycorp/awaladroid/test/AssertUtils.kt index f9bbc3d9..29918f0e 100644 --- a/lib/src/test/java/tech/relaycorp/awaladroid/test/AssertUtils.kt +++ b/lib/src/test/java/tech/relaycorp/awaladroid/test/AssertUtils.kt @@ -4,5 +4,7 @@ import org.junit.Assert import java.time.Duration import java.time.ZonedDateTime -internal fun assertSameDateTime(date1: ZonedDateTime, date2: ZonedDateTime) = - Assert.assertTrue(Duration.between(date1, date2).seconds < 2) +internal fun assertSameDateTime( + date1: ZonedDateTime, + date2: ZonedDateTime, +) = Assert.assertTrue(Duration.between(date1, date2).seconds < 2) diff --git a/lib/src/test/java/tech/relaycorp/awaladroid/test/FakeAndroidKeyStore.kt b/lib/src/test/java/tech/relaycorp/awaladroid/test/FakeAndroidKeyStore.kt index 6510315d..b6d9825d 100644 --- a/lib/src/test/java/tech/relaycorp/awaladroid/test/FakeAndroidKeyStore.kt +++ b/lib/src/test/java/tech/relaycorp/awaladroid/test/FakeAndroidKeyStore.kt @@ -35,14 +35,15 @@ import javax.crypto.SecretKey // Source: https://proandroiddev.com/testing-jetpack-security-with-robolectric-9f9cf2aa4f61 public object FakeAndroidKeyStore { - public val setup: Int by lazy { - Security.addProvider(object : Provider("AndroidKeyStore", 1.0, "") { - init { - put("KeyStore.AndroidKeyStore", FakeKeyStore::class.java.name) - put("KeyGenerator.AES", FakeAesKeyGenerator::class.java.name) - } - }) + Security.addProvider( + object : Provider("AndroidKeyStore", 1.0, "") { + init { + put("KeyStore.AndroidKeyStore", FakeKeyStore::class.java.name) + put("KeyGenerator.AES", FakeAesKeyGenerator::class.java.name) + } + }, + ) } @Suppress("unused") @@ -50,6 +51,7 @@ public object FakeAndroidKeyStore { private val wrapped = KeyStore.getInstance(KeyStore.getDefaultType()) override fun engineIsKeyEntry(alias: String?): Boolean = wrapped.isKeyEntry(alias) + override fun engineIsCertificateEntry(alias: String?): Boolean = wrapped.isCertificateEntry(alias) @@ -57,14 +59,15 @@ public object FakeAndroidKeyStore { wrapped.getCertificate(alias) override fun engineGetCreationDate(alias: String?): Date = wrapped.getCreationDate(alias) + override fun engineDeleteEntry(alias: String?): Unit = wrapped.deleteEntry(alias) + override fun engineSetKeyEntry( alias: String?, key: Key?, password: CharArray?, chain: Array?, - ): Unit = - wrapped.setKeyEntry(alias, key, password, chain) + ): Unit = wrapped.setKeyEntry(alias, key, password, chain) override fun engineSetKeyEntry( alias: String?, @@ -72,26 +75,37 @@ public object FakeAndroidKeyStore { chain: Array?, ): Unit = wrapped.setKeyEntry(alias, key, chain) - override fun engineStore(stream: OutputStream?, password: CharArray?): Unit = - wrapped.store(stream, password) + override fun engineStore( + stream: OutputStream?, + password: CharArray?, + ): Unit = wrapped.store(stream, password) override fun engineSize(): Int = wrapped.size() + override fun engineAliases(): Enumeration = wrapped.aliases() + override fun engineContainsAlias(alias: String?): Boolean = wrapped.containsAlias(alias) - override fun engineLoad(stream: InputStream?, password: CharArray?): Unit = - wrapped.load(stream, password) + + override fun engineLoad( + stream: InputStream?, + password: CharArray?, + ): Unit = wrapped.load(stream, password) override fun engineGetCertificateChain(alias: String?): Array = wrapped.getCertificateChain(alias) - override fun engineSetCertificateEntry(alias: String?, cert: Certificate?): Unit = - wrapped.setCertificateEntry(alias, cert) + override fun engineSetCertificateEntry( + alias: String?, + cert: Certificate?, + ): Unit = wrapped.setCertificateEntry(alias, cert) override fun engineGetCertificateAlias(cert: Certificate?): String = wrapped.getCertificateAlias(cert) - override fun engineGetKey(alias: String?, password: CharArray?): Key? = - wrapped.getKey(alias, password) + override fun engineGetKey( + alias: String?, + password: CharArray?, + ): Key? = wrapped.getKey(alias, password) } @Suppress("unused") @@ -99,8 +113,17 @@ public object FakeAndroidKeyStore { private val wrapped = KeyGenerator.getInstance("AES") override fun engineInit(random: SecureRandom?): Unit = Unit - override fun engineInit(params: AlgorithmParameterSpec?, random: SecureRandom?): Unit = Unit - override fun engineInit(keysize: Int, random: SecureRandom?): Unit = Unit + + override fun engineInit( + params: AlgorithmParameterSpec?, + random: SecureRandom?, + ): Unit = Unit + + override fun engineInit( + keysize: Int, + random: SecureRandom?, + ): Unit = Unit + override fun engineGenerateKey(): SecretKey = wrapped.generateKey() } } diff --git a/lib/src/test/java/tech/relaycorp/awaladroid/test/FirstPartyEndpointFactory.kt b/lib/src/test/java/tech/relaycorp/awaladroid/test/FirstPartyEndpointFactory.kt index 6f03a447..ebd59723 100644 --- a/lib/src/test/java/tech/relaycorp/awaladroid/test/FirstPartyEndpointFactory.kt +++ b/lib/src/test/java/tech/relaycorp/awaladroid/test/FirstPartyEndpointFactory.kt @@ -5,10 +5,11 @@ import tech.relaycorp.relaynet.testing.pki.KeyPairSet import tech.relaycorp.relaynet.testing.pki.PDACertPath internal object FirstPartyEndpointFactory { - fun build(): FirstPartyEndpoint = FirstPartyEndpoint( - KeyPairSet.PRIVATE_ENDPOINT.private, - PDACertPath.PRIVATE_ENDPOINT, - listOf(PDACertPath.PRIVATE_GW), - "frankfurt.relaycorp.cloud", - ) + fun build(): FirstPartyEndpoint = + FirstPartyEndpoint( + KeyPairSet.PRIVATE_ENDPOINT.private, + PDACertPath.PRIVATE_ENDPOINT, + listOf(PDACertPath.PRIVATE_GW), + "frankfurt.relaycorp.cloud", + ) } diff --git a/lib/src/test/java/tech/relaycorp/awaladroid/test/MessageFactory.kt b/lib/src/test/java/tech/relaycorp/awaladroid/test/MessageFactory.kt index 853670a2..a8915fdf 100644 --- a/lib/src/test/java/tech/relaycorp/awaladroid/test/MessageFactory.kt +++ b/lib/src/test/java/tech/relaycorp/awaladroid/test/MessageFactory.kt @@ -7,17 +7,19 @@ import tech.relaycorp.relaynet.messages.payloads.ServiceMessage internal object MessageFactory { val serviceMessage = ServiceMessage("application/foo", "the content".toByteArray()) - suspend fun buildOutgoing(channel: EndpointChannel) = OutgoingMessage.build( - serviceMessage.type, - serviceMessage.content, - senderEndpoint = channel.firstPartyEndpoint, - recipientEndpoint = channel.thirdPartyEndpoint, - ) + suspend fun buildOutgoing(channel: EndpointChannel) = + OutgoingMessage.build( + serviceMessage.type, + serviceMessage.content, + senderEndpoint = channel.firstPartyEndpoint, + recipientEndpoint = channel.thirdPartyEndpoint, + ) - fun buildIncoming() = IncomingMessage( - type = serviceMessage.type, - content = serviceMessage.content, - senderEndpoint = ThirdPartyEndpointFactory.buildPublic(), - recipientEndpoint = FirstPartyEndpointFactory.build(), - ) {} + fun buildIncoming() = + IncomingMessage( + type = serviceMessage.type, + content = serviceMessage.content, + senderEndpoint = ThirdPartyEndpointFactory.buildPublic(), + recipientEndpoint = FirstPartyEndpointFactory.build(), + ) {} } diff --git a/lib/src/test/java/tech/relaycorp/awaladroid/test/MockContextTestCase.kt b/lib/src/test/java/tech/relaycorp/awaladroid/test/MockContextTestCase.kt index 788e2223..1406c1ac 100644 --- a/lib/src/test/java/tech/relaycorp/awaladroid/test/MockContextTestCase.kt +++ b/lib/src/test/java/tech/relaycorp/awaladroid/test/MockContextTestCase.kt @@ -67,11 +67,12 @@ internal abstract class MockContextTestCase { val firstPartyEndpoint = createFirstPartyEndpoint() val thirdPartySessionKeyPair = SessionKeyPair.generate() - val thirdPartyEndpoint = createThirdPartyEndpoint( - thirdPartyEndpointType, - thirdPartySessionKeyPair.sessionKey, - firstPartyEndpoint, - ) + val thirdPartyEndpoint = + createThirdPartyEndpoint( + thirdPartyEndpointType, + thirdPartySessionKeyPair.sessionKey, + firstPartyEndpoint, + ) val firstPartySessionKeyPair = SessionKeyPair.generate() privateKeyStore.saveSessionKey( @@ -138,10 +139,11 @@ internal abstract class MockContextTestCase { when (thirdPartyEndpointType) { RecipientAddressType.PRIVATE -> { thirdPartyEndpoint = ThirdPartyEndpointFactory.buildPrivate() - val authBundle = CertificationPath( - PDACertPath.PDA, - listOf(PDACertPath.PRIVATE_ENDPOINT, PDACertPath.PRIVATE_GW), - ) + val authBundle = + CertificationPath( + PDACertPath.PDA, + listOf(PDACertPath.PRIVATE_ENDPOINT, PDACertPath.PRIVATE_GW), + ) whenever( storage.privateThirdParty.get( "${firstPartyEndpoint.nodeId}_${thirdPartyEndpoint.nodeId}", diff --git a/lib/src/test/java/tech/relaycorp/awaladroid/test/MockPersistence.kt b/lib/src/test/java/tech/relaycorp/awaladroid/test/MockPersistence.kt index 25ad8ef2..560bd4e1 100644 --- a/lib/src/test/java/tech/relaycorp/awaladroid/test/MockPersistence.kt +++ b/lib/src/test/java/tech/relaycorp/awaladroid/test/MockPersistence.kt @@ -5,7 +5,10 @@ import tech.relaycorp.awaladroid.storage.persistence.Persistence internal class MockPersistence : Persistence { private val values: MutableMap = mutableMapOf() - override suspend fun set(location: String, data: ByteArray) { + override suspend fun set( + location: String, + data: ByteArray, + ) { values[location] = data } diff --git a/lib/src/test/java/tech/relaycorp/awaladroid/test/RecipientAddressType.kt b/lib/src/test/java/tech/relaycorp/awaladroid/test/RecipientAddressType.kt index 23f141a8..b06a397f 100644 --- a/lib/src/test/java/tech/relaycorp/awaladroid/test/RecipientAddressType.kt +++ b/lib/src/test/java/tech/relaycorp/awaladroid/test/RecipientAddressType.kt @@ -1,5 +1,6 @@ package tech.relaycorp.awaladroid.test public enum class RecipientAddressType { - PRIVATE, PUBLIC + PRIVATE, + PUBLIC, } diff --git a/lib/src/test/java/tech/relaycorp/awaladroid/test/ThirdPartyEndpointFactory.kt b/lib/src/test/java/tech/relaycorp/awaladroid/test/ThirdPartyEndpointFactory.kt index 01030422..ae34744e 100644 --- a/lib/src/test/java/tech/relaycorp/awaladroid/test/ThirdPartyEndpointFactory.kt +++ b/lib/src/test/java/tech/relaycorp/awaladroid/test/ThirdPartyEndpointFactory.kt @@ -6,20 +6,21 @@ import tech.relaycorp.relaynet.testing.pki.KeyPairSet import tech.relaycorp.relaynet.testing.pki.PDACertPath internal object ThirdPartyEndpointFactory { - private const val internetAddress = "example.org" + private const val INTERNET_ADDRESS = "example.org" fun buildPublic(): PublicThirdPartyEndpoint { return PublicThirdPartyEndpoint( - internetAddress, + INTERNET_ADDRESS, KeyPairSet.PDA_GRANTEE.public, ) } - fun buildPrivate(): PrivateThirdPartyEndpoint = PrivateThirdPartyEndpoint( - PDACertPath.PRIVATE_ENDPOINT.subjectId, - KeyPairSet.PDA_GRANTEE.public, - PDACertPath.PDA, - listOf(PDACertPath.PRIVATE_GW), - internetAddress, - ) + fun buildPrivate(): PrivateThirdPartyEndpoint = + PrivateThirdPartyEndpoint( + PDACertPath.PRIVATE_ENDPOINT.subjectId, + KeyPairSet.PDA_GRANTEE.public, + PDACertPath.PDA, + listOf(PDACertPath.PRIVATE_GW), + INTERNET_ADDRESS, + ) }