diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ffd30524..cd8f660a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,9 +16,14 @@ jobs: java-version: 8 - name: Validate Gradle wrapper uses: gradle/wrapper-validation-action@v1 - - uses: eskatos/gradle-command-action@v1 + - name: Build project + uses: eskatos/gradle-command-action@v1 + with: + arguments: build dokkaHtml --scan + - name: Generate documentation + uses: eskatos/gradle-command-action@v1 with: - arguments: build --scan + arguments: dokkaHtml # - uses: eskatos/gradle-command-action@v1 # with: # arguments: jacocoTestCoverageVerification @@ -48,11 +53,11 @@ jobs: run: | set -x -o nounset -o errexit -o pipefail cd ./build/dokka/html - mv ./styles ./images ./scripts navigation.html ./relaydroid/ - find ./relaydroid/ -name '*.html' -print0 | xargs -0 sed -i 's;../styles/;styles/;g' - find ./relaydroid/ -name '*.html' -print0 | xargs -0 sed -i 's;../images/;images/;g' - find ./relaydroid/ -name '*.html' -print0 | xargs -0 sed -i 's;../scripts/;scripts/;g' - find ./relaydroid/ -name '*.html' -print0 | xargs -0 sed -i 's;pathToRoot = "../;pathToRoot = ";g' + mv ./styles ./images ./scripts navigation.html ./lib/ + find ./lib/ -name '*.html' -print0 | xargs -0 sed -i 's;../styles/;styles/;g' + find ./lib/ -name '*.html' -print0 | xargs -0 sed -i 's;../images/;images/;g' + find ./lib/ -name '*.html' -print0 | xargs -0 sed -i 's;../scripts/;scripts/;g' + find ./lib/ -name '*.html' -print0 | xargs -0 sed -i 's;pathToRoot = "../;pathToRoot = ";g' sed -i 's;href="relaydroid/;href=";g' ./relaydroid/navigation.html - name: Release diff --git a/api-docs.md b/api-docs.md deleted file mode 100644 index 32d0dad4..00000000 --- a/api-docs.md +++ /dev/null @@ -1,3 +0,0 @@ -# Module tech.relaycorp.relaydroid - -Lorem ipsum diff --git a/build.gradle b/build.gradle index 764912b7..9a243e7e 100644 --- a/build.gradle +++ b/build.gradle @@ -15,7 +15,6 @@ buildscript { plugins { id("org.jetbrains.kotlin.jvm") version "$kotlinVersion" - id("org.jetbrains.dokka") version "1.4.20" id('idea') } @@ -33,15 +32,6 @@ java { withSourcesJar() } -dokkaHtml.configure { - dokkaSourceSets { - configureEach { - reportUndocumented.set(true) - includes.from("api-docs.md") - } - } -} - gradleEnterprise { buildScan { termsOfServiceUrl = "https://gradle.com/terms-of-service" diff --git a/gradle.properties b/gradle.properties index c830cb27..fc193d9a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,15 +1,13 @@ ## For more details on how to configure your build environment visit # http://www.gradle.org/docs/current/userguide/build_environment.html -# -# Specifies the JVM arguments used for the daemon process. -# The setting is particularly useful for tweaking memory settings. -# Default value: -Xmx1024m -XX:MaxPermSize=256m -# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 -# + +org.gradle.jvmargs=-Xmx4096M -XX:MaxPermSize=512m + # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true + #Mon Feb 01 16:19:46 WET 2021 android.useAndroidX=true diff --git a/lib/api-docs.md b/lib/api-docs.md new file mode 100644 index 00000000..ef5682a5 --- /dev/null +++ b/lib/api-docs.md @@ -0,0 +1,13 @@ +# Module lib + +# Package tech.relaycorp.relaydroid + +Root package for the endpoint library. + +# Package tech.relaycorp.relaydroid.endpoint + +Handling of first- and third-party endpoints. + +# Package tech.relaycorp.relaydroid.messaging + +Handling of service messages. diff --git a/lib/build.gradle b/lib/build.gradle index 184faebd..149c7310 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -4,6 +4,7 @@ plugins { id 'kotlin-kapt' id 'maven-publish' id 'org.jlleitschuh.gradle.ktlint' version "10.0.0" + id 'org.jetbrains.dokka' version "1.4.20" } apply from: 'jacoco.gradle' @@ -105,6 +106,15 @@ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { } } +dokkaHtml.configure { + dokkaSourceSets { + configureEach { + reportUndocumented.set(true) + includes.from("api-docs.md") + } + } +} + ktlint { verbose = true android = true diff --git a/lib/src/main/java/tech/relaycorp/relaydroid/GatewayClientImpl.kt b/lib/src/main/java/tech/relaycorp/relaydroid/GatewayClientImpl.kt index 99a5f5f2..b0b13761 100644 --- a/lib/src/main/java/tech/relaycorp/relaydroid/GatewayClientImpl.kt +++ b/lib/src/main/java/tech/relaycorp/relaydroid/GatewayClientImpl.kt @@ -18,8 +18,8 @@ import tech.relaycorp.relaydroid.background.ServiceInteractor import tech.relaycorp.relaydroid.common.Logging.logger import tech.relaycorp.relaydroid.messaging.IncomingMessage import tech.relaycorp.relaydroid.messaging.OutgoingMessage +import tech.relaycorp.relaydroid.messaging.ReceiveMessageException import tech.relaycorp.relaydroid.messaging.ReceiveMessages -import tech.relaycorp.relaydroid.messaging.ReceiveMessagesException import tech.relaycorp.relaydroid.messaging.RejectedMessageException import tech.relaycorp.relaydroid.messaging.SendMessage import tech.relaycorp.relaydroid.messaging.SendMessageException @@ -30,6 +30,9 @@ import tech.relaycorp.relaynet.bindings.pdc.ServerException import tech.relaycorp.relaynet.messages.control.PrivateNodeRegistration import tech.relaycorp.relaynet.messages.control.PrivateNodeRegistrationRequest +/** + * Private gateway client. + */ public class GatewayClientImpl internal constructor( private val coroutineContext: CoroutineContext = Dispatchers.IO, @@ -44,6 +47,9 @@ internal constructor( private var gwServiceInteractor: ServiceInteractor? = null + /** + * Bind to the gateway to be able to communicate with it. + */ @Throws(GatewayBindingException::class) public suspend fun bind() { withContext(coroutineContext) { @@ -66,6 +72,11 @@ internal constructor( } } + /** + * 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 @@ -148,6 +159,10 @@ internal constructor( } private val incomingMessageChannel = BroadcastChannel(1) + + /** + * Receive messages from the gateway. + */ public fun receiveMessages(): Flow = incomingMessageChannel.asFlow() // Internal @@ -172,7 +187,7 @@ internal constructor( receiveMessages .receive() .collect(incomingMessageChannel::send) - } catch (exp: ReceiveMessagesException) { + } 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) @@ -190,17 +205,26 @@ internal constructor( } } -// General class for all exceptions deriving from interactions with the Gateway +/** + * General class for all exceptions deriving from interactions with the gateway. + */ public open class GatewayException(message: String, cause: Throwable? = null) : RelaydroidException(message, cause) -// Non-recoverable protocol-level discrepancies when interacting with the Gateway +/** + * Non-recoverable, protocol-level discrepancies when interacting with the gateway. + */ public open class GatewayProtocolException(message: String, cause: Throwable? = null) : GatewayException(message, cause) -// Not bound or unable to bind to the Gateway +/** + * Not bound or failure to bind to the gateway. + */ public class GatewayBindingException(message: String, cause: Throwable? = null) : GatewayException(message, cause) +/** + * Failure to register a first-party endpoint. + */ public class RegistrationFailedException(message: String, cause: Throwable? = null) : GatewayException(message, cause) diff --git a/lib/src/main/java/tech/relaycorp/relaydroid/Relaynet.kt b/lib/src/main/java/tech/relaycorp/relaydroid/Relaynet.kt index 9767ca43..54aff246 100644 --- a/lib/src/main/java/tech/relaycorp/relaydroid/Relaynet.kt +++ b/lib/src/main/java/tech/relaycorp/relaydroid/Relaynet.kt @@ -13,6 +13,9 @@ public object Relaynet { internal const val GATEWAY_SYNC_COMPONENT = "tech.relaycorp.gateway.background.endpoint.GatewaySyncService" + /** + * Set up the endpoint library. + */ public suspend fun setup(context: Context) { storage = StorageImpl(EncryptedDiskPersistence(context)) gatewayClientImpl = GatewayClientImpl( @@ -24,5 +27,9 @@ public object Relaynet { internal lateinit var gatewayClientImpl: GatewayClientImpl } +/** + * Private gateway client. + */ public val GatewayClient: GatewayClientImpl get() = Relaynet.gatewayClientImpl + internal val Storage get() = Relaynet.storage diff --git a/lib/src/main/java/tech/relaycorp/relaydroid/endpoint/AuthorizationBundle.kt b/lib/src/main/java/tech/relaycorp/relaydroid/endpoint/AuthorizationBundle.kt index 0e661ed7..8ab69009 100644 --- a/lib/src/main/java/tech/relaycorp/relaydroid/endpoint/AuthorizationBundle.kt +++ b/lib/src/main/java/tech/relaycorp/relaydroid/endpoint/AuthorizationBundle.kt @@ -1,6 +1,16 @@ package tech.relaycorp.relaydroid.endpoint +/** + * Parcel Delivery Authorization (PDA) and support data for the grantee to use the PDA. + */ public class AuthorizationBundle( + /** + * The ASN.1 DER encoding of the PDA. + */ public val pdaSerialized: ByteArray, + + /** + * The ASN.1 DER encoding of each certificate in the PDA chain (excluding the PDA itself). + */ public val pdaChainSerialized: List ) diff --git a/lib/src/main/java/tech/relaycorp/relaydroid/endpoint/Endpoint.kt b/lib/src/main/java/tech/relaycorp/relaydroid/endpoint/Endpoint.kt index 7a846841..a96e76e8 100644 --- a/lib/src/main/java/tech/relaycorp/relaydroid/endpoint/Endpoint.kt +++ b/lib/src/main/java/tech/relaycorp/relaydroid/endpoint/Endpoint.kt @@ -1,5 +1,11 @@ package tech.relaycorp.relaydroid.endpoint +/** + * Relaynet endpoint. + */ public interface Endpoint { + /** + * The private or public address of a private or public endpoint, respectively. + */ public val address: String } diff --git a/lib/src/main/java/tech/relaycorp/relaydroid/endpoint/FirstPartyEndpoint.kt b/lib/src/main/java/tech/relaycorp/relaydroid/endpoint/FirstPartyEndpoint.kt index a43967b2..3060ccfe 100644 --- a/lib/src/main/java/tech/relaycorp/relaydroid/endpoint/FirstPartyEndpoint.kt +++ b/lib/src/main/java/tech/relaycorp/relaydroid/endpoint/FirstPartyEndpoint.kt @@ -17,6 +17,9 @@ import tech.relaycorp.relaynet.wrappers.privateAddress import tech.relaycorp.relaynet.wrappers.x509.Certificate import tech.relaycorp.relaynet.wrappers.x509.CertificateException +/** + * An endpoint owned by the current instance of the app. + */ public class FirstPartyEndpoint internal constructor( internal val keyPair: KeyPair, @@ -26,10 +29,16 @@ internal constructor( public override val address: String get() = keyPair.public.privateAddress + /** + * The RSA public key of the endpoint. + */ public val publicKey: PublicKey get() = keyPair.public internal val pdaChain: List get() = listOf(identityCertificate, gatewayCertificate) + /** + * Issue a PDA for a third-party endpoint. + */ @Throws(CertificateException::class) public fun issueAuthorization( thirdPartyEndpoint: ThirdPartyEndpoint, @@ -41,6 +50,9 @@ internal constructor( ) } + /** + * Issue a PDA for a third-party endpoint using its public key. + */ @Throws(CertificateException::class) public fun issueAuthorization( thirdPartyEndpointPublicKeySerialized: ByteArray, @@ -74,6 +86,9 @@ internal constructor( ) } + /** + * Delete the endpoint. + */ @Throws(PersistenceException::class) public suspend fun delete() { Storage.identityKeyPair.delete(address) @@ -88,6 +103,9 @@ internal constructor( } public companion object { + /** + * Generate endpoint and register it with the private gateway. + */ @Throws( RegistrationFailedException::class, GatewayProtocolException::class, @@ -105,10 +123,13 @@ internal constructor( return endpoint } + /** + * Load an endpoint by its address. + */ @Throws(PersistenceException::class) - public suspend fun load(address: String): FirstPartyEndpoint? { - return Storage.identityKeyPair.get(address)?.let { keyPair -> - Storage.identityCertificate.get(address)?.let { certificate -> + public suspend fun load(privateAddress: String): FirstPartyEndpoint? { + return Storage.identityKeyPair.get(privateAddress)?.let { keyPair -> + Storage.identityCertificate.get(privateAddress)?.let { certificate -> Storage.gatewayCertificate.get()?.let { gwCertificate -> FirstPartyEndpoint( keyPair, @@ -122,5 +143,8 @@ internal constructor( } } +/** + * Failure to issue a PDA. + */ public class AuthorizationIssuanceException(message: String, cause: Throwable) : RelaydroidException(message, cause) diff --git a/lib/src/main/java/tech/relaycorp/relaydroid/endpoint/ThirdPartyEndpoint.kt b/lib/src/main/java/tech/relaycorp/relaydroid/endpoint/ThirdPartyEndpoint.kt index d72ea898..721176a6 100644 --- a/lib/src/main/java/tech/relaycorp/relaydroid/endpoint/ThirdPartyEndpoint.kt +++ b/lib/src/main/java/tech/relaycorp/relaydroid/endpoint/ThirdPartyEndpoint.kt @@ -12,10 +12,16 @@ import tech.relaycorp.relaydroid.storage.persistence.PersistenceException import tech.relaycorp.relaynet.wrappers.x509.Certificate import tech.relaycorp.relaynet.wrappers.x509.CertificateException +/** + * An endpoint owned by a different instance of this app, or a different app in the same service. + */ public sealed class ThirdPartyEndpoint( internal val identityCertificate: Certificate ) : Endpoint { + /** + * The private address of the endpoint. + */ public val privateAddress: String get() = identityCertificate.subjectPrivateAddress internal companion object { @@ -29,8 +35,14 @@ public sealed class ThirdPartyEndpoint( } } +/** + * A private third-party endpoint (i.e., one behind a different private gateway). + * + * @property firstPartyEndpointAddress The private address of the first-party endpoint linked to + * this endpoint. + */ public class PrivateThirdPartyEndpoint internal constructor( - public val firstPartyAddress: String, + public val firstPartyEndpointAddress: String, internal val pda: Certificate, identityCertificate: Certificate ) : ThirdPartyEndpoint(identityCertificate) { @@ -38,7 +50,9 @@ public class PrivateThirdPartyEndpoint internal constructor( override val address: String get() = privateAddress public companion object { - + /** + * Load an endpoint. + */ @Throws(PersistenceException::class) public suspend fun load( firstPartyAddress: String, @@ -52,12 +66,15 @@ public class PrivateThirdPartyEndpoint internal constructor( } } + /** + * Create third-party endpoint by importing its PDA and chain. + */ @Throws( PersistenceException::class, UnknownFirstPartyEndpointException::class, InvalidAuthorizationException::class ) - public suspend fun importAuthorization( + public suspend fun import( pda: Certificate, identityCertificate: Certificate ): PrivateThirdPartyEndpoint { @@ -90,6 +107,11 @@ public class PrivateThirdPartyEndpoint internal constructor( } } +/** + * A public third-party endpoint (i.e., an Internet host in a centralized service). + * + * @property publicAddress The public address of the endpoint (e.g., "ping.awala.services"). + */ public class PublicThirdPartyEndpoint internal constructor( public val publicAddress: String, identityCertificate: Certificate @@ -98,12 +120,21 @@ public class PublicThirdPartyEndpoint internal constructor( override val address: String get() = "https://$publicAddress" public companion object { + /** + * Load an endpoint by its [publicAddress]. + */ @Throws(PersistenceException::class) public suspend fun load(publicAddress: String): PublicThirdPartyEndpoint? = Storage.publicThirdPartyCertificate.get(publicAddress)?.let { PublicThirdPartyEndpoint(it.publicAddress, it.identityCertificate) } + /** + * Import the public endpoint at [publicAddress]. + * + * @param publicAddress The public address of the endpoint (e.g., `ping.awala.services`). + * @param identityCertificate The identity certificate of the endpoint. + */ @Throws( PersistenceException::class, InvalidThirdPartyEndpoint::class diff --git a/lib/src/main/java/tech/relaycorp/relaydroid/messaging/IncomingMessage.kt b/lib/src/main/java/tech/relaycorp/relaydroid/messaging/IncomingMessage.kt index 89194d6d..16dcfe53 100644 --- a/lib/src/main/java/tech/relaycorp/relaydroid/messaging/IncomingMessage.kt +++ b/lib/src/main/java/tech/relaycorp/relaydroid/messaging/IncomingMessage.kt @@ -9,14 +9,23 @@ import tech.relaycorp.relaynet.messages.InvalidMessageException import tech.relaycorp.relaynet.messages.Parcel import tech.relaycorp.relaynet.wrappers.cms.EnvelopedDataException +/** + * An incoming service message. + * + * @property type The type of the service message (e.g., "application/vnd.relaynet.ping-v1.ping"). + * @property content The contents of the service message. + * @property senderEndpoint The third-party endpoint that created the message. + * @property recipientEndpoint The first-party endpoint that should receive the message. + * @property ack The function to call as soon as the message has been processed. + */ public class IncomingMessage internal constructor( - id: MessageId, + parcelId: ParcelId, public val type: String, public val content: ByteArray, public val senderEndpoint: ThirdPartyEndpoint, public val recipientEndpoint: FirstPartyEndpoint, public val ack: suspend () -> Unit -) : Message(id) { +) : Message(parcelId) { internal companion object { @Throws( @@ -43,7 +52,7 @@ public class IncomingMessage internal constructor( val serviceMessage = parcel.unwrapPayload(recipientEndpoint.keyPair.private) return IncomingMessage( - id = MessageId(parcel.id), + parcelId = ParcelId(parcel.id), type = serviceMessage.type, content = serviceMessage.content, senderEndpoint = sender, diff --git a/lib/src/main/java/tech/relaycorp/relaydroid/messaging/Message.kt b/lib/src/main/java/tech/relaycorp/relaydroid/messaging/Message.kt index c06dadd4..54bd3d5a 100644 --- a/lib/src/main/java/tech/relaycorp/relaydroid/messaging/Message.kt +++ b/lib/src/main/java/tech/relaycorp/relaydroid/messaging/Message.kt @@ -1,3 +1,8 @@ package tech.relaycorp.relaydroid.messaging -public abstract class Message(public val id: MessageId) +/** + * A service message. + * + * @property parcelId The parcel id. + */ +public abstract class Message internal constructor(public val parcelId: ParcelId) diff --git a/lib/src/main/java/tech/relaycorp/relaydroid/messaging/MessageId.kt b/lib/src/main/java/tech/relaycorp/relaydroid/messaging/MessageId.kt deleted file mode 100644 index d12b7e0d..00000000 --- a/lib/src/main/java/tech/relaycorp/relaydroid/messaging/MessageId.kt +++ /dev/null @@ -1,21 +0,0 @@ -package tech.relaycorp.relaydroid.messaging - -import java.util.UUID - -public class MessageId -internal constructor( - public val value: String -) { - public companion object { - public fun generate(): MessageId = MessageId(UUID.randomUUID().toString()) - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other !is MessageId) return false - if (value != other.value) return false - return true - } - - override fun hashCode(): Int = value.hashCode() -} diff --git a/lib/src/main/java/tech/relaycorp/relaydroid/messaging/OutgoingMessage.kt b/lib/src/main/java/tech/relaycorp/relaydroid/messaging/OutgoingMessage.kt index be0ca3c2..0be41769 100644 --- a/lib/src/main/java/tech/relaycorp/relaydroid/messaging/OutgoingMessage.kt +++ b/lib/src/main/java/tech/relaycorp/relaydroid/messaging/OutgoingMessage.kt @@ -11,32 +11,54 @@ import tech.relaycorp.relaynet.messages.Parcel import tech.relaycorp.relaynet.messages.payloads.ServiceMessage import tech.relaycorp.relaynet.wrappers.x509.Certificate +/** + * An outgoing service message. + * + * @property senderEndpoint The first-party endpoint that created the message. + * @property recipientEndpoint The third-party endpoint that should receive the message. + * @property parcelExpiryDate The expiry date of the parcel. + */ public class OutgoingMessage private constructor( public val senderEndpoint: FirstPartyEndpoint, public val recipientEndpoint: ThirdPartyEndpoint, - public val expiryDate: ZonedDateTime = maxExpiryDate(), - id: MessageId, - internal val creationDate: ZonedDateTime = ZonedDateTime.now() -) : Message(id) { + public val parcelExpiryDate: ZonedDateTime = maxExpiryDate(), + parcelId: ParcelId, + internal val parcelCreationDate: ZonedDateTime = ZonedDateTime.now() +) : Message(parcelId) { internal lateinit var parcel: Parcel private set - internal val ttl get() = Duration.between(creationDate, expiryDate).seconds.toInt() + internal val ttl get() = Duration.between(parcelCreationDate, parcelExpiryDate).seconds.toInt() public companion object { internal fun maxExpiryDate() = ZonedDateTime.now().plusDays(30) + /** + * 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 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. + */ public suspend fun build( type: String, content: ByteArray, senderEndpoint: FirstPartyEndpoint, recipientEndpoint: ThirdPartyEndpoint, - expiryDate: ZonedDateTime = maxExpiryDate(), - id: MessageId = MessageId.generate() + parcelExpiryDate: ZonedDateTime = maxExpiryDate(), + parcelId: ParcelId = ParcelId.generate() ): OutgoingMessage { - val message = OutgoingMessage(senderEndpoint, recipientEndpoint, expiryDate, id) + val message = OutgoingMessage( + senderEndpoint, + recipientEndpoint, + parcelExpiryDate, + parcelId + ) message.parcel = message.buildParcel(type, content) return message } @@ -51,8 +73,8 @@ private constructor( recipientAddress = recipientEndpoint.address, payload = serviceMessage.encrypt(recipientEndpoint.identityCertificate), senderCertificate = getSenderCertificate(), - messageId = id.value, - creationDate = creationDate, + messageId = parcelId.value, + creationDate = parcelCreationDate, ttl = ttl, senderCertificateChain = getSenderCertificateChain() ) @@ -70,8 +92,8 @@ private constructor( return issueEndpointCertificate( senderEndpoint.keyPair.public, senderEndpoint.keyPair.private, - validityStartDate = creationDate, - validityEndDate = expiryDate + validityStartDate = parcelCreationDate, + validityEndDate = parcelExpiryDate ) } diff --git a/lib/src/main/java/tech/relaycorp/relaydroid/messaging/ParcelId.kt b/lib/src/main/java/tech/relaycorp/relaydroid/messaging/ParcelId.kt new file mode 100644 index 00000000..e0241b6c --- /dev/null +++ b/lib/src/main/java/tech/relaycorp/relaydroid/messaging/ParcelId.kt @@ -0,0 +1,35 @@ +package tech.relaycorp.relaydroid.messaging + +import java.util.UUID + +/** + * The id of a parcel. + * + * @property value The string representation of the parcel. + * + * You should only ever use these ids if you wish to replace an in-transit parcel. That is, if you + * wish to replace a parcel currently held by a gateway. If the older parcel already reached the + * final destination, subsequent parcels with the same id will be ignored. + * + * 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()) + } + + 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() +} diff --git a/lib/src/main/java/tech/relaycorp/relaydroid/messaging/ReceiveMessages.kt b/lib/src/main/java/tech/relaycorp/relaydroid/messaging/ReceiveMessages.kt index 1431d6c7..ec318fcb 100644 --- a/lib/src/main/java/tech/relaycorp/relaydroid/messaging/ReceiveMessages.kt +++ b/lib/src/main/java/tech/relaycorp/relaydroid/messaging/ReceiveMessages.kt @@ -29,7 +29,7 @@ internal class ReceiveMessages( ) { @Throws( - ReceiveMessagesException::class, + ReceiveMessageException::class, GatewayProtocolException::class, PersistenceException::class ) @@ -40,7 +40,7 @@ internal class ReceiveMessages( try { collectParcels(it, nonceSigners) } catch (exp: ServerException) { - throw ReceiveMessagesException("Server error", exp) + throw ReceiveMessageException("Server error", exp) } catch (exp: ClientBindingException) { throw GatewayProtocolException("Client error", exp) } catch (exp: NonceSignerException) { @@ -106,5 +106,10 @@ private suspend fun ParcelCollection.disregard(reason: String, exc: Throwable) { ack() } -public class ReceiveMessagesException(message: String, throwable: Throwable? = null) : +/** + * The private gateway failed to give us incoming messages. + * + * This is most likely to be a bug in the gateway and retrying later may work. + */ +public class ReceiveMessageException(message: String, throwable: Throwable? = null) : GatewayException(message, throwable) diff --git a/lib/src/main/java/tech/relaycorp/relaydroid/messaging/SendMessage.kt b/lib/src/main/java/tech/relaycorp/relaydroid/messaging/SendMessage.kt index 0ec432f3..e245e420 100644 --- a/lib/src/main/java/tech/relaycorp/relaydroid/messaging/SendMessage.kt +++ b/lib/src/main/java/tech/relaycorp/relaydroid/messaging/SendMessage.kt @@ -48,8 +48,21 @@ internal class SendMessage( } } +/** + * The private gateway failed to process the outgoing message. + * + * This is most likely to be a bug in the private gateway. You should retry later. + */ public class SendMessageException(message: String, cause: Throwable? = null) : GatewayException(message, cause) +/** + * The private gateway refused to accept an outgoing message. + * + * It could be that the first-party endpoint certificate isn't valid anymore or the message + * already expired, for example. + * + * Retrying won't make any difference. + */ public class RejectedMessageException(message: String, cause: Throwable? = null) : GatewayException(message, cause) diff --git a/lib/src/test/java/tech/relaycorp/relaydroid/GatewayClientImplTest.kt b/lib/src/test/java/tech/relaycorp/relaydroid/GatewayClientImplTest.kt index 7f05c29e..5cadfd42 100644 --- a/lib/src/test/java/tech/relaycorp/relaydroid/GatewayClientImplTest.kt +++ b/lib/src/test/java/tech/relaycorp/relaydroid/GatewayClientImplTest.kt @@ -22,8 +22,8 @@ import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner import tech.relaycorp.relaydroid.background.ServiceInteractor import tech.relaycorp.relaydroid.messaging.IncomingMessage +import tech.relaycorp.relaydroid.messaging.ReceiveMessageException import tech.relaycorp.relaydroid.messaging.ReceiveMessages -import tech.relaycorp.relaydroid.messaging.ReceiveMessagesException import tech.relaycorp.relaydroid.messaging.RejectedMessageException import tech.relaycorp.relaydroid.messaging.SendMessage import tech.relaycorp.relaydroid.messaging.SendMessageException @@ -249,7 +249,7 @@ internal class GatewayClientImplTest { @Test fun checkForNewMessages_handlesReceiveException() = coroutineScope.runBlockingTest { - whenever(receiveMessages.receive()).thenReturn(flow { throw ReceiveMessagesException("") }) + whenever(receiveMessages.receive()).thenReturn(flow { throw ReceiveMessageException("") }) subject.checkForNewMessages() } diff --git a/lib/src/test/java/tech/relaycorp/relaydroid/endpoint/PrivateThirdPartyEndpointTest.kt b/lib/src/test/java/tech/relaycorp/relaydroid/endpoint/PrivateThirdPartyEndpointTest.kt index 63acb2ce..a158f08f 100644 --- a/lib/src/test/java/tech/relaycorp/relaydroid/endpoint/PrivateThirdPartyEndpointTest.kt +++ b/lib/src/test/java/tech/relaycorp/relaydroid/endpoint/PrivateThirdPartyEndpointTest.kt @@ -46,7 +46,7 @@ internal class PrivateThirdPartyEndpointTest { val thirdAddress = UUID.randomUUID().toString() with(PrivateThirdPartyEndpoint.load(firstAddress, thirdAddress)!!) { - assertEquals(firstAddress, firstPartyAddress) + assertEquals(firstAddress, firstPartyEndpointAddress) assertEquals(PDACertPath.PRIVATE_ENDPOINT.subjectPrivateAddress, address) assertEquals(PDACertPath.PRIVATE_ENDPOINT, pda) assertEquals(PDACertPath.PRIVATE_ENDPOINT, identityCertificate) @@ -68,7 +68,7 @@ internal class PrivateThirdPartyEndpointTest { } @Test - fun importAuthorization_successful() = runBlockingTest { + fun import_successful() = runBlockingTest { val firstPartyEndpoint = FirstPartyEndpointFactory.build() val firstPartyAddress = firstPartyEndpoint.identityCertificate.subjectPrivateAddress whenever(storage.identityCertificate.get(any())) @@ -82,13 +82,13 @@ internal class PrivateThirdPartyEndpointTest { issuerCertificate = PDACertPath.PRIVATE_ENDPOINT ) - val endpoint = PrivateThirdPartyEndpoint.importAuthorization( + val endpoint = PrivateThirdPartyEndpoint.import( authorization, PDACertPath.PRIVATE_ENDPOINT ) assertEquals( firstPartyAddress, - endpoint.firstPartyAddress + endpoint.firstPartyEndpointAddress ) assertEquals( thirdPartyAddress, @@ -115,7 +115,7 @@ internal class PrivateThirdPartyEndpointTest { } @Test(expected = UnknownFirstPartyEndpointException::class) - fun importAuthorization_invalidFirstParty() = runBlockingTest { + fun import_invalidFirstParty() = runBlockingTest { val firstPartyEndpoint = FirstPartyEndpointFactory.build() val authorization = issueDeliveryAuthorization( subjectPublicKey = firstPartyEndpoint.keyPair.public, @@ -124,11 +124,11 @@ internal class PrivateThirdPartyEndpointTest { issuerCertificate = PDACertPath.PRIVATE_ENDPOINT ) - PrivateThirdPartyEndpoint.importAuthorization(authorization, PDACertPath.PRIVATE_ENDPOINT) + PrivateThirdPartyEndpoint.import(authorization, PDACertPath.PRIVATE_ENDPOINT) } @Test - fun importAuthorization_wrongAuthorizationIssuer() = runBlockingTest { + fun import_wrongAuthorizationIssuer() = runBlockingTest { val firstPartyEndpoint = FirstPartyEndpointFactory.build() whenever(storage.identityCertificate.get(any())) .thenReturn(firstPartyEndpoint.identityCertificate) @@ -149,11 +149,11 @@ internal class PrivateThirdPartyEndpointTest { expectedException.expect(InvalidAuthorizationException::class.java) expectedException.expectMessage("PDA was not issued by third-party endpoint") - PrivateThirdPartyEndpoint.importAuthorization(authorization, unrelatedCertificate) + PrivateThirdPartyEndpoint.import(authorization, unrelatedCertificate) } @Test - fun importAuthorization_invalidAuthorization() = runBlockingTest { + fun import_invalidAuthorization() = runBlockingTest { val firstPartyEndpoint = FirstPartyEndpointFactory.build() whenever(storage.identityCertificate.get(any())) .thenReturn(firstPartyEndpoint.identityCertificate) @@ -168,11 +168,11 @@ internal class PrivateThirdPartyEndpointTest { expectedException.expect(InvalidAuthorizationException::class.java) expectedException.expectMessage("PDA is invalid") - PrivateThirdPartyEndpoint.importAuthorization(authorization, PDACertPath.PRIVATE_ENDPOINT) + PrivateThirdPartyEndpoint.import(authorization, PDACertPath.PRIVATE_ENDPOINT) } @Test(expected = PersistenceException::class) - fun importAuthorization_persistenceException() = runBlockingTest { + fun import_persistenceException() = runBlockingTest { val firstPartyEndpoint = FirstPartyEndpointFactory.build() whenever(storage.identityCertificate.get(any())).thenThrow(PersistenceException("")) @@ -183,6 +183,6 @@ internal class PrivateThirdPartyEndpointTest { issuerCertificate = PDACertPath.PRIVATE_ENDPOINT ) - PrivateThirdPartyEndpoint.importAuthorization(authorization, PDACertPath.PRIVATE_ENDPOINT) + PrivateThirdPartyEndpoint.import(authorization, PDACertPath.PRIVATE_ENDPOINT) } } diff --git a/lib/src/test/java/tech/relaycorp/relaydroid/messaging/IncomingMessageTest.kt b/lib/src/test/java/tech/relaycorp/relaydroid/messaging/IncomingMessageTest.kt index ccd755b3..f6290ecd 100644 --- a/lib/src/test/java/tech/relaycorp/relaydroid/messaging/IncomingMessageTest.kt +++ b/lib/src/test/java/tech/relaycorp/relaydroid/messaging/IncomingMessageTest.kt @@ -51,6 +51,6 @@ internal class IncomingMessageTest { assertEquals(PDACertPath.PRIVATE_ENDPOINT, message.recipientEndpoint.identityCertificate) assertEquals(serviceMessage.type, message.type) assertArrayEquals(serviceMessage.content, message.content) - assertEquals(parcel.id, message.id.value) + assertEquals(parcel.id, message.parcelId.value) } } diff --git a/lib/src/test/java/tech/relaycorp/relaydroid/messaging/OutgoingMessageTest.kt b/lib/src/test/java/tech/relaycorp/relaydroid/messaging/OutgoingMessageTest.kt index 33808bd6..2e6ab783 100644 --- a/lib/src/test/java/tech/relaycorp/relaydroid/messaging/OutgoingMessageTest.kt +++ b/lib/src/test/java/tech/relaycorp/relaydroid/messaging/OutgoingMessageTest.kt @@ -22,8 +22,8 @@ internal class OutgoingMessageTest { val parcel = message.parcel assertEquals(message.recipientEndpoint.address, parcel.recipientAddress) - assertEquals(message.id.value, parcel.id) - assertSameDateTime(message.creationDate, parcel.creationDate) + assertEquals(message.parcelId.value, parcel.id) + assertSameDateTime(message.parcelCreationDate, parcel.creationDate) assertEquals(message.ttl, parcel.ttl) } @@ -37,7 +37,7 @@ internal class OutgoingMessageTest { Random.Default.nextBytes(10), senderEndpoint = senderEndpoint, recipientEndpoint = recipientEndpoint, - expiryDate = ZonedDateTime.now().plusMinutes(1) + parcelExpiryDate = ZonedDateTime.now().plusMinutes(1) ) assertTrue(58 < message.ttl) @@ -62,8 +62,8 @@ internal class OutgoingMessageTest { parcel.senderCertificate.let { cert -> cert.validate() assertEquals(message.senderEndpoint.keyPair.public, cert.subjectPublicKey) - assertSameDateTime(message.creationDate, cert.startDate) - assertSameDateTime(message.expiryDate, cert.expiryDate) + assertSameDateTime(message.parcelCreationDate, cert.startDate) + assertSameDateTime(message.parcelExpiryDate, cert.expiryDate) } } diff --git a/lib/src/test/java/tech/relaycorp/relaydroid/messaging/MessageIdTest.kt b/lib/src/test/java/tech/relaycorp/relaydroid/messaging/ParcelIdTest.kt similarity index 63% rename from lib/src/test/java/tech/relaycorp/relaydroid/messaging/MessageIdTest.kt rename to lib/src/test/java/tech/relaycorp/relaydroid/messaging/ParcelIdTest.kt index d50dad30..7a0b54ae 100644 --- a/lib/src/test/java/tech/relaycorp/relaydroid/messaging/MessageIdTest.kt +++ b/lib/src/test/java/tech/relaycorp/relaydroid/messaging/ParcelIdTest.kt @@ -4,17 +4,17 @@ import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull import org.junit.Test -internal class MessageIdTest { +internal class ParcelIdTest { @Test fun generate() { - val messageId = MessageId.generate() + val messageId = ParcelId.generate() assertNotNull(messageId.value) } @Test fun equals() { - val messageId1 = MessageId.generate() - val messageId2 = MessageId(messageId1.value) + val messageId1 = ParcelId.generate() + val messageId2 = ParcelId(messageId1.value) assertEquals(messageId1, messageId2) } } diff --git a/lib/src/test/java/tech/relaycorp/relaydroid/messaging/ReceiveMessagesTest.kt b/lib/src/test/java/tech/relaycorp/relaydroid/messaging/ReceiveMessagesTest.kt index 67b42cc4..73b6599e 100644 --- a/lib/src/test/java/tech/relaycorp/relaydroid/messaging/ReceiveMessagesTest.kt +++ b/lib/src/test/java/tech/relaycorp/relaydroid/messaging/ReceiveMessagesTest.kt @@ -65,7 +65,7 @@ internal class ReceiveMessagesTest { assertTrue(pdcClient.wasClosed) assertTrue(collectParcelsCall.wasCalled) assertEquals(1, messages.size) - assertEquals(parcel.id, messages.first().id.value) + assertEquals(parcel.id, messages.first().parcelId.value) } @Test @@ -84,7 +84,7 @@ internal class ReceiveMessagesTest { assertEquals(PDACertPath.PRIVATE_ENDPOINT, nonceSigners.first().certificate) } - @Test(expected = ReceiveMessagesException::class) + @Test(expected = ReceiveMessageException::class) fun collectParcelsGetsServerError() = runBlockingTest { val collectParcelsCall = CollectParcelsCall(Result.failure(ServerBindingException(""))) pdcClient = MockPDCClient(collectParcelsCall) diff --git a/lib/src/test/java/tech/relaycorp/relaydroid/test/MessageFactory.kt b/lib/src/test/java/tech/relaycorp/relaydroid/test/MessageFactory.kt index d2e63810..07eb3ac6 100644 --- a/lib/src/test/java/tech/relaycorp/relaydroid/test/MessageFactory.kt +++ b/lib/src/test/java/tech/relaycorp/relaydroid/test/MessageFactory.kt @@ -2,8 +2,8 @@ package tech.relaycorp.relaydroid.test import java.util.UUID import tech.relaycorp.relaydroid.messaging.IncomingMessage -import tech.relaycorp.relaydroid.messaging.MessageId import tech.relaycorp.relaydroid.messaging.OutgoingMessage +import tech.relaycorp.relaydroid.messaging.ParcelId import tech.relaycorp.relaynet.messages.payloads.ServiceMessage import tech.relaycorp.relaynet.ramf.RecipientAddressType @@ -18,7 +18,7 @@ internal object MessageFactory { ) fun buildIncoming() = IncomingMessage( - id = MessageId(UUID.randomUUID().toString()), + parcelId = ParcelId(UUID.randomUUID().toString()), type = serviceMessage.type, content = serviceMessage.content, senderEndpoint = ThirdPartyEndpointFactory.buildPublic(),