Android: send X-Kvitt-App app key on every server request (D-126)

The reverse proxy closes requests from outside the home LAN that lack
X-Kvitt-App: <key>. The key is the build property kvitt.appKey
(-Pkvitt.appKey, else android/local.properties, else empty = no header),
validated as 16-128 printable ASCII without whitespace, exposed as
BuildConfig.APP_KEY and passed via AppBuildInfo.appKey to KvittApi
(every call, /health and other unauthenticated calls included) and the
Coil picture client (PictureAuth). AppBuildInfo.toString hides the key.
Tests use a made-up key; README documents the property.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
This commit is contained in:
2026-10-21 18:50:57 +02:00
parent d9e189abd5
commit 34e5cad02a
9 changed files with 172 additions and 11 deletions
+16 -1
View File
@@ -32,6 +32,19 @@ require(Regex("[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+"
"kvitt.publicHost must be a host name such as kvitt.duckdns.org (no scheme, port or path), was '$publicHost'"
}
// D-126: the app key for the reverse proxy (SPEC §8.5, §14.3.1). Every request to the server carries X-Kvitt-App: <key>;
// outside the home LAN the proxy closes any request without it. From -Pkvitt.appKey, else the key kvitt.appKey in
// android/local.properties (the owner sets it there; never committed), else empty: no header (debug builds against the
// dev server need none). A secret: never logged, and the failure below names its length only, never its value.
val appKey: String =
providers.gradleProperty("kvitt.appKey").orNull?.trim()?.takeIf { it.isNotEmpty() }
?: localProperties.getProperty("kvitt.appKey")?.trim()?.takeIf { it.isNotEmpty() }
?: ""
require(appKey.isEmpty() || Regex("[!-~]{16,128}").matches(appKey)) {
"kvitt.appKey must be 16 to 128 printable ASCII characters without whitespace (D-126); the value given has " +
"${appKey.length} characters"
}
// SPEC §10.5.3 (D-082): the pinned NPM root (D-120) replaces the system anchors for kvitt.publicHost only when this is true.
val pinnedRoot = providers.gradleProperty("kvitt.tls.pinnedRoot").orElse("false").get().toBoolean()
@@ -65,6 +78,7 @@ android {
versionCode = providers.gradleProperty("kvitt.versionCode").orElse("1").get().toInt()
versionName = providers.gradleProperty("kvitt.versionName").orElse("dev").get()
buildConfigField("String", "PUBLIC_HOST", "\"$publicHost\"")
buildConfigField("String", "APP_KEY", "\"${appKey.replace("\\", "\\\\").replace("\"", "\\\"")}\"")
manifestPlaceholders["publicHost"] = publicHost
}
// SPEC G-8 and AC-19.1: the app is English (fallback) and Swedish only; library translations of other languages
@@ -72,7 +86,8 @@ android {
androidResources {
localeFilters += listOf("en", "sv")
}
// BuildConfig carries versionCode, versionName and DEBUG into AppBuildInfo (:data, D-041), and PUBLIC_HOST (D-121).
// BuildConfig carries versionCode, versionName, DEBUG and APP_KEY (D-126) into AppBuildInfo (:data, D-041), and
// PUBLIC_HOST (D-121).
buildFeatures {
buildConfig = true
}