diff --git a/build.gradle b/build.gradle index c4af8a64492ab7e43c17cf37458b714df3e4e6cb..85ad63cd5782cd2b608a5946fbd5bb1487255ddb 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.5.21' + ext.kotlin_version = '1.5.30' ext.hilt_version = '2.38.1' ext.tor_version = '0.3.5.15' ext.obfs4_version = '0.0.12-dev-40245c4a' @@ -11,7 +11,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.0.0' + classpath 'com.android.tools.build:gradle:7.0.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" } diff --git a/mailbox-core/build.gradle b/mailbox-core/build.gradle index 33f0927ea6db736a0c4093673186d6525bff656e..cbf2b5ce91682019cceb3b02207fc2646bfa496b 100644 --- a/mailbox-core/build.gradle +++ b/mailbox-core/build.gradle @@ -19,9 +19,10 @@ dependencies { implementation 'org.briarproject:jtorctl:0.3' - def ktorVersion = '1.6.2' - implementation "io.ktor:ktor-server-core:$ktorVersion" - implementation "io.ktor:ktor-server-netty:$ktorVersion" + def ktor_version = '1.6.2' + implementation "io.ktor:ktor-server-core:$ktor_version" + implementation "io.ktor:ktor-server-netty:$ktor_version" + implementation "io.ktor:ktor-auth:$ktor_version" api "org.slf4j:slf4j-api:1.7.32" implementation 'com.h2database:h2:1.4.192' // The last version that supports Java 1.6 @@ -32,7 +33,7 @@ dependencies { testImplementation "org.junit.jupiter:junit-jupiter-engine:$junit_version" testImplementation "io.mockk:mockk:$mockk_version" testImplementation "ch.qos.logback:logback-classic:1.2.5" - testImplementation "io.ktor:ktor-client-cio:$ktorVersion" + testImplementation "io.ktor:ktor-client-cio:$ktor_version" testImplementation "com.google.dagger:hilt-core:$hilt_version" kaptTest "com.google.dagger:dagger-compiler:$hilt_version" } diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/AuthenticationManager.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/AuthenticationManager.kt new file mode 100644 index 0000000000000000000000000000000000000000..1f5ee5ad3e85c41fd5dfe592ccbae89c83395e71 --- /dev/null +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/AuthenticationManager.kt @@ -0,0 +1,23 @@ +package org.briarproject.mailbox.core.server + +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class AuthenticationManager @Inject constructor() { + + fun canOwnerAccess(credentials: Credentials): Boolean { + // TODO check credentials: + // * token must be from owner + // * if credentials.mailboxId is not null, must have accessType right to mailboxId + return credentials.token == "test123" + } + + fun canOwnerOrContactAccess(credentials: Credentials): Boolean { + require(credentials.mailboxId != null) + // TODO check credentials: + // * token must have credentials.accessType right to mailboxId + return credentials.token == "test123" || credentials.token == "test124" + } + +} diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/BearerAuthenticationProvider.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/BearerAuthenticationProvider.kt new file mode 100644 index 0000000000000000000000000000000000000000..22330145be34f24d3a7b3a862586e08d69bbd4cd --- /dev/null +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/BearerAuthenticationProvider.kt @@ -0,0 +1,126 @@ +package org.briarproject.mailbox.core.server + +import io.ktor.application.ApplicationCall +import io.ktor.application.call +import io.ktor.auth.Authentication +import io.ktor.auth.AuthenticationContext +import io.ktor.auth.AuthenticationFailedCause +import io.ktor.auth.AuthenticationFunction +import io.ktor.auth.AuthenticationPipeline +import io.ktor.auth.AuthenticationProvider +import io.ktor.auth.Principal +import io.ktor.auth.UnauthorizedResponse +import io.ktor.auth.parseAuthorizationHeader +import io.ktor.http.auth.HttpAuthHeader +import io.ktor.request.ApplicationRequest +import io.ktor.request.httpMethod +import io.ktor.response.respond +import org.briarproject.mailbox.core.util.LogUtils.debug +import org.slf4j.LoggerFactory.getLogger + +private val BearerAuthKey: Any = "BearerAuth" +private val LOG = getLogger(BearerAuthenticationProvider::class.java) + +internal class BearerAuthenticationProvider constructor(config: Configuration) : + AuthenticationProvider(config) { + + internal var realm: String = config.realm ?: "Ktor Server" + internal val authHeader: (ApplicationCall) -> HttpAuthHeader? = config.authHeader + internal val authenticationFunction = config.authenticationFunction + + internal class Configuration internal constructor(name: String?) : + AuthenticationProvider.Configuration(name) { + + var realm: String? = null + val authHeader: (ApplicationCall) -> HttpAuthHeader? = { call -> + call.request.parseAuthorizationHeaderOrNull() + } + var authenticationFunction: AuthenticationFunction<Credentials> = { + throw NotImplementedError( + "Bearer auth validate function is not specified." + + "Use bearer { validate { ... } } to fix." + ) + } + + /** + * Apply [validate] function to every call with [String] + * @return a principal (usually an instance of [Principal]) or `null` + */ + fun validate(validate: suspend ApplicationCall.(Credentials) -> Principal?) { + authenticationFunction = validate + } + + internal fun build() = BearerAuthenticationProvider(this) + } + +} + +/** + * Installs Bearer token Authentication mechanism + */ +internal fun Authentication.Configuration.bearer( + name: String? = null, + configure: BearerAuthenticationProvider.Configuration.() -> Unit, +) { + val provider = BearerAuthenticationProvider.Configuration(name).apply(configure).build() + provider.pipeline.intercept(AuthenticationPipeline.RequestAuthentication) { context -> + val authHeader = provider.authHeader(call) + if (authHeader == null) { + context.unauthorizedResponse(AuthenticationFailedCause.NoCredentials, provider) + return@intercept + } + + try { + // TODO try faking accessType with X-Http-Method-Override header + val accessType = call.request.httpMethod.toAccessType() + val token = (authHeader as? HttpAuthHeader.Single)?.blob + if (accessType == null || token == null) { + context.unauthorizedResponse(AuthenticationFailedCause.InvalidCredentials, provider) + return@intercept + } + val mailboxId = call.parameters["mailboxId"] + + LOG.debug("name: $name") + LOG.debug("httpMethod: ${call.request.httpMethod}") + + val credentials = Credentials(accessType, token, mailboxId) + val principal = provider.authenticationFunction(call, credentials) + if (principal == null) { + context.unauthorizedResponse(AuthenticationFailedCause.InvalidCredentials, provider) + } else { + context.principal(principal) + } + } catch (cause: Throwable) { + val message = cause.message ?: cause.javaClass.simpleName + LOG.debug { "Bearer verification failed: $message" } + context.error(BearerAuthKey, AuthenticationFailedCause.Error(message)) + } + } + register(provider) +} + +private fun AuthenticationContext.unauthorizedResponse( + cause: AuthenticationFailedCause, + provider: BearerAuthenticationProvider, +) { + challenge(BearerAuthKey, cause) { + call.respond( + UnauthorizedResponse( + HttpAuthHeader.Parameterized( + authScheme = "Bearer", + parameters = mapOf(HttpAuthHeader.Parameters.Realm to provider.realm) + ) + ) + ) + if (!it.completed && call.response.status() != null) { + it.complete() + } + } +} + +private fun ApplicationRequest.parseAuthorizationHeaderOrNull() = try { + parseAuthorizationHeader() +} catch (ex: IllegalArgumentException) { + LOG.warn("Illegal HTTP auth header", ex) + null +} diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/Credentials.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/Credentials.kt new file mode 100644 index 0000000000000000000000000000000000000000..55c63079341959304fb46f5d9e03110e879b8e95 --- /dev/null +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/Credentials.kt @@ -0,0 +1,24 @@ +package org.briarproject.mailbox.core.server + +import io.ktor.http.HttpMethod + +data class Credentials( + val accessType: AccessType, + val token: String, + val mailboxId: String?, +) + +enum class AccessType { UPLOAD, DOWNLOAD_DELETE } + +internal fun HttpMethod.toAccessType(): AccessType? = when (this) { + HttpMethod.Get -> AccessType.DOWNLOAD_DELETE + HttpMethod.Delete -> AccessType.DOWNLOAD_DELETE + HttpMethod.Post -> AccessType.UPLOAD + HttpMethod.Put -> AccessType.UPLOAD + else -> null +} + +internal object AuthContext { + const val ownerOnly = "ownerOnly" + const val ownerAndContacts = "ownerAndContacts" +} diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/Routing.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/Routing.kt index 2c0aa0cef0aa271230c322ec9262d53d6d088398..c48ac4de9c5dd74a8347873fed6c4614f8d19d41 100644 --- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/Routing.kt +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/Routing.kt @@ -2,7 +2,10 @@ package org.briarproject.mailbox.core.server import io.ktor.application.Application import io.ktor.application.call +import io.ktor.auth.authenticate import io.ktor.http.ContentType +import io.ktor.http.HttpStatusCode +import io.ktor.response.respond import io.ktor.response.respondText import io.ktor.routing.delete import io.ktor.routing.get @@ -14,65 +17,86 @@ import io.ktor.routing.routing internal const val V = "/" // TODO set to "/v1" for release internal fun Application.configureBasicApi() = routing { - route("$V/") { get { call.respondText("Hello world!", ContentType.Text.Plain) } - delete { - TODO("Not yet implemented") + authenticate(AuthContext.ownerOnly) { + delete { + call.respond(HttpStatusCode.OK, "delete: Not yet implemented") + } + put("setup") { + call.respond(HttpStatusCode.OK, "put: Not yet implemented") + } } } - - put("$V/setup") { - TODO("Not yet implemented") - } - } internal fun Application.configureContactApi() = routing { - - route("$V/contacts") { - put("{contactId}") { - TODO("Not yet implemented. contactId: ${call.parameters["contactId"]}") - } - delete("{contactId}") { - TODO("Not yet implemented. contactId: ${call.parameters["contactId"]}") - } - get { - TODO("Not yet implemented") + authenticate(AuthContext.ownerOnly) { + route("$V/contacts") { + put("{contactId}") { + call.respond( + HttpStatusCode.OK, + "get: Not yet implemented. " + + "contactId: ${call.parameters["contactId"]}" + ) + } + delete("{contactId}") { + call.respond( + HttpStatusCode.OK, + "delete: Not yet implemented. " + + "contactId: ${call.parameters["contactId"]}" + ) + } + get { + call.respond(HttpStatusCode.OK, "get: Not yet implemented") + } } } - } internal fun Application.configureFilesApi() = routing { - route("$V/files/{mailboxId}") { - post { - TODO("Not yet implemented. mailboxId: ${call.parameters["mailboxId"]}") - } - get { - TODO("Not yet implemented. mailboxId: ${call.parameters["mailboxId"]}") - } - route("/{fileId}") { - get { - TODO( - "Not yet implemented. mailboxId: ${call.parameters["mailboxId"]}" + - "fileId: ${call.parameters["fileId"]}" + authenticate(AuthContext.ownerAndContacts) { + route("$V/files/{mailboxId}") { + post { + call.respond( + HttpStatusCode.OK, + "post: Not yet implemented. " + + "mailboxId: ${call.parameters["mailboxId"]}" ) } - delete { - TODO( - "Not yet implemented. mailboxId: ${call.parameters["mailboxId"]}" + - "fileId: ${call.parameters["fileId"]}" + get { + call.respond( + HttpStatusCode.OK, + "get: Not yet implemented. " + + "mailboxId: ${call.parameters["mailboxId"]}" ) } + route("/{fileId}") { + get { + call.respond( + HttpStatusCode.OK, + "get: Not yet implemented. " + + "mailboxId: ${call.parameters["mailboxId"]} " + + "fileId: ${call.parameters["fileId"]}" + ) + } + delete { + call.respond( + HttpStatusCode.OK, + "delete: Not yet implemented. " + + "mailboxId: ${call.parameters["mailboxId"]} " + + "fileId: ${call.parameters["fileId"]}" + ) + } + } } } - - get("$V/mailboxes") { - TODO("Not yet implemented") + authenticate(AuthContext.ownerOnly) { + get("$V/mailboxes") { + call.respond(HttpStatusCode.OK, "get: Not yet implemented") + } } - } diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/WebServerManager.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/WebServerManager.kt index 00d9ddcb9b7cfcc8776276eb3406701bc5560a5b..86ac2825d9cb21158ee3afc3ec6ca941b3c07000 100644 --- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/WebServerManager.kt +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/WebServerManager.kt @@ -1,6 +1,8 @@ package org.briarproject.mailbox.core.server import io.ktor.application.install +import io.ktor.auth.Authentication +import io.ktor.auth.UserIdPrincipal import io.ktor.features.CallLogging import io.ktor.server.engine.embeddedServer import io.ktor.server.netty.Netty @@ -9,8 +11,12 @@ import org.slf4j.LoggerFactory.getLogger import javax.inject.Inject import javax.inject.Singleton +interface WebServerManager : Service + @Singleton -class WebServerManager @Inject constructor() : Service { +internal class WebServerManagerImpl @Inject constructor( + private val authManager: AuthenticationManager, +) : WebServerManager { internal companion object { internal const val PORT = 8000 @@ -20,6 +26,32 @@ class WebServerManager @Inject constructor() : Service { private val server by lazy { embeddedServer(Netty, PORT, watchPaths = emptyList()) { install(CallLogging) + // TODO validate mailboxId and fileId somewhere + install(Authentication) { + bearer(AuthContext.ownerOnly) { + realm = "Briar Mailbox Owner" + validate { credentials -> + LOG.error("credentials: $credentials") + if (authManager.canOwnerAccess(credentials)) { + UserIdPrincipal(AuthContext.ownerOnly) + } else null // not authenticated + } + } + bearer(AuthContext.ownerAndContacts) { + realm = "Briar Mailbox" + validate { credentials -> + LOG.error("credentials: $credentials") + val mailboxId = credentials.mailboxId + // we must have a mailboxId for this AuthContext + if (mailboxId == null) { + LOG.warn("No mailboxId found in request") + null + } else if (authManager.canOwnerOrContactAccess(credentials)) { + UserIdPrincipal(AuthContext.ownerAndContacts) + } else null // not authenticated + } + } + } configureBasicApi() configureContactApi() configureFilesApi() diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/WebServerModule.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/WebServerModule.kt index 37550a3823539a0ab420a0b060c9c0f6a9a6b0aa..be2c92aa2bde8e1495fa490b75971aed979c18b6 100644 --- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/WebServerModule.kt +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/WebServerModule.kt @@ -13,8 +13,10 @@ internal class WebServerModule { @Provides @Singleton - fun provideWebServer(lifecycleManager: LifecycleManager): WebServerManager { - val webServerManager = WebServerManager() + fun provideWebServerManager( + lifecycleManager: LifecycleManager, + webServerManager: WebServerManagerImpl, + ): WebServerManager { lifecycleManager.registerService(webServerManager) return webServerManager } diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/tor/TorPlugin.java b/mailbox-core/src/main/java/org/briarproject/mailbox/core/tor/TorPlugin.java index 05929a6ef0dd4fdd7bcbf6fb2eaab759ddc7c1bc..88285497c5717b971c7a599355cffc2900beeeeb 100644 --- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/tor/TorPlugin.java +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/tor/TorPlugin.java @@ -1,5 +1,25 @@ package org.briarproject.mailbox.core.tor; +import static net.freehaven.tor.control.TorControlCommands.HS_ADDRESS; +import static net.freehaven.tor.control.TorControlCommands.HS_PRIVKEY; +import static org.briarproject.mailbox.core.tor.TorConstants.CONTROL_PORT; +import static org.briarproject.mailbox.core.tor.TorPlugin.State.ACTIVE; +import static org.briarproject.mailbox.core.tor.TorPlugin.State.DISABLED; +import static org.briarproject.mailbox.core.tor.TorPlugin.State.ENABLING; +import static org.briarproject.mailbox.core.tor.TorPlugin.State.INACTIVE; +import static org.briarproject.mailbox.core.tor.TorPlugin.State.STARTING_STOPPING; +import static org.briarproject.mailbox.core.util.IoUtils.copyAndClose; +import static org.briarproject.mailbox.core.util.IoUtils.tryToClose; +import static org.briarproject.mailbox.core.util.LogUtils.info; +import static org.briarproject.mailbox.core.util.LogUtils.logException; +import static org.briarproject.mailbox.core.util.LogUtils.warn; +import static org.briarproject.mailbox.core.util.PrivacyUtils.scrubOnion; +import static org.slf4j.LoggerFactory.getLogger; +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; +import static java.util.Collections.singletonMap; +import static java.util.Objects.requireNonNull; + import net.freehaven.tor.control.EventHandler; import net.freehaven.tor.control.TorControlConnection; @@ -10,7 +30,7 @@ import org.briarproject.mailbox.core.event.EventListener; import org.briarproject.mailbox.core.lifecycle.IoExecutor; import org.briarproject.mailbox.core.lifecycle.Service; import org.briarproject.mailbox.core.lifecycle.ServiceException; -import org.briarproject.mailbox.core.server.WebServerManager; +import org.briarproject.mailbox.core.server.WebServerManagerImpl; import org.briarproject.mailbox.core.settings.Settings; import org.briarproject.mailbox.core.settings.SettingsManager; import org.briarproject.mailbox.core.system.Clock; @@ -228,7 +248,7 @@ abstract class TorPlugin implements Service, EventHandler, EventListener { // Check whether we're online updateConnectionStatus(networkManager.getNetworkStatus()); // Create a hidden service if necessary - ioExecutor.execute(() -> publishHiddenService(String.valueOf(WebServerManager.PORT))); + ioExecutor.execute(() -> publishHiddenService(String.valueOf(WebServerManagerImpl.PORT))); } private boolean assetsAreUpToDate() { diff --git a/mailbox-core/src/test/java/org/briarproject/mailbox/core/server/WebServerIntegrationTest.kt b/mailbox-core/src/test/java/org/briarproject/mailbox/core/server/WebServerIntegrationTest.kt index 753642437efe430a862737084eff97296ec2cf33..904455c82cd6e74d58b9ec02654066958c82b086 100644 --- a/mailbox-core/src/test/java/org/briarproject/mailbox/core/server/WebServerIntegrationTest.kt +++ b/mailbox-core/src/test/java/org/briarproject/mailbox/core/server/WebServerIntegrationTest.kt @@ -9,7 +9,7 @@ import kotlinx.coroutines.runBlocking import org.briarproject.mailbox.core.DaggerTestComponent import org.briarproject.mailbox.core.TestComponent import org.briarproject.mailbox.core.TestModule -import org.briarproject.mailbox.core.server.WebServerManager.Companion.PORT +import org.briarproject.mailbox.core.server.WebServerManagerImpl.Companion.PORT import org.junit.jupiter.api.AfterAll import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.Test