From 2b9996ff3f191222e949ff18ff562bb40079d47dcfb91e886c89673dec2834ab Mon Sep 17 00:00:00 2001 From: Yuvomi Mobile Contributors Date: Wed, 21 Oct 2026 07:25:00 +0200 Subject: [PATCH] fix(android): harden origin isolation and native file lifecycles Block cross-origin POSTs, preserve bounded generated exports without relaxing CSP, bind picker results to their initiating context, erase camera cache and constrain platform permissions. Upgrade vulnerable build/test tooling and lock runtime/build dependencies. JVM regressions, lint and unsigned release builds pass; extended emulator validation remains separate. --- .editorconfig | 18 + android/app/build.gradle.kts | 65 ++- android/app/gradle.lockfile | 169 ++++++ .../community/yuvomi/mobile/BlobExportTest.kt | 111 ++++ .../yuvomi/mobile/BrowserSecurityTest.kt | 66 +++ .../mobile/LocalNetworkPermissionTest.kt | 71 +++ .../community/yuvomi/mobile/OnboardingTest.kt | 93 ++++ android/app/src/main/AndroidManifest.xml | 3 +- .../community/yuvomi/mobile/BlobTransfer.kt | 165 ++++++ .../community/yuvomi/mobile/FileTransfer.kt | 7 +- .../community/yuvomi/mobile/MainActivity.kt | 521 ++++++++++++++---- .../community/yuvomi/mobile/SelectionGate.kt | 23 + .../community/yuvomi/mobile/ServerAddress.kt | 7 +- .../community/yuvomi/mobile/ServerProbe.kt | 10 +- .../yuvomi/mobile/SelectionGateTest.kt | 31 ++ .../yuvomi/mobile/ServerAddressTest.kt | 47 +- android/build.gradle.kts | 26 +- android/buildscript-gradle.lockfile | 116 ++++ android/gradle.properties | 2 + android/gradle/wrapper/gradle-wrapper.jar | Bin 43705 -> 48462 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 +- android/gradlew | 13 +- android/gradlew.bat | 36 +- android/settings.gradle.kts | 13 +- 24 files changed, 1446 insertions(+), 173 deletions(-) create mode 100644 .editorconfig create mode 100644 android/app/gradle.lockfile create mode 100644 android/app/src/androidTest/java/community/yuvomi/mobile/BlobExportTest.kt create mode 100644 android/app/src/androidTest/java/community/yuvomi/mobile/BrowserSecurityTest.kt create mode 100644 android/app/src/androidTest/java/community/yuvomi/mobile/LocalNetworkPermissionTest.kt create mode 100644 android/app/src/androidTest/java/community/yuvomi/mobile/OnboardingTest.kt create mode 100644 android/app/src/main/java/community/yuvomi/mobile/BlobTransfer.kt create mode 100644 android/app/src/main/java/community/yuvomi/mobile/SelectionGate.kt create mode 100644 android/app/src/test/java/community/yuvomi/mobile/SelectionGateTest.kt create mode 100644 android/buildscript-gradle.lockfile diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..cfd4de7 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,18 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 4 + +[*.{yml,yaml,json,xml}] +indent_size = 2 + +[*.{kt,kts}] +max_line_length = 140 +ktlint_code_style = android_studio +ij_kotlin_allow_trailing_comma = true +ij_kotlin_allow_trailing_comma_on_call_site = true diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 7fb1fa2..839c5b1 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -1,15 +1,14 @@ plugins { id("com.android.application") - id("org.jetbrains.kotlin.android") } android { namespace = "community.yuvomi.mobile" - compileSdk = 36 + compileSdk { version = release(37) { minorApiLevel = 0 } } defaultConfig { applicationId = "community.yuvomi.mobile" minSdk = 26 - targetSdk = 36 + targetSdk = 37 versionCode = 1 versionName = "0.1.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" @@ -18,19 +17,75 @@ android { release { isMinifyEnabled = true isShrinkResources = true - proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro", + ) // Signing is deliberately supplied by the authorized release maintainer. } + create("integration") { + initWith(getByName("debug")) + applicationIdSuffix = ".integration" + matchingFallbacks += "debug" + } } compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } - lint { abortOnError = true; warningsAsErrors = true } + lint { + abortOnError = true + warningsAsErrors = true + } + if (providers.gradleProperty("integration").isPresent) { + testBuildType = "integration" + sourceSets.getByName( + "androidTest", + ).kotlin.directories.add("src/androidTestIntegration/java") + val testResources = providers.environmentVariable("YUVOMI_TEST_RES").get() + sourceSets.getByName("integration").res.directories.add(testResources) + defaultConfig.testInstrumentationRunnerArguments["server"] = + providers.environmentVariable("YUVOMI_TEST_URL").get() + defaultConfig.testInstrumentationRunnerArguments["password"] = + providers.environmentVariable("YUVOMI_TEST_PASSWORD").get() + } } kotlin { compilerOptions { jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17) } } +dependencyLocking { lockAllConfigurations() } + +// AGP's device-test tools still request vulnerable older wire libraries. These +// are host-only, not APK dependencies; stay within binary-compatible minor lines. +configurations.configureEach { + if (name.startsWith("_internal-unified-test-platform") || name == "androidLintTool" || + name == "unified-test-platform-gradle-work-action" + ) { + resolutionStrategy.eachDependency { + when (requested.group) { + "io.netty" -> { + useVersion("4.1.139.Final") + because("Remediate published host-test HTTP/TLS advisories") + } + "com.google.protobuf" -> { + useVersion("3.25.9") + because("Remediate protobuf recursion/DoS advisories") + } + "org.bouncycastle" -> { + useVersion("1.86") + because("Remediate published build-tool cryptography advisories") + } + "org.apache.commons" -> if (requested.name == "commons-lang3") useVersion("3.21.0") + "org.apache.httpcomponents" -> if (requested.name == + "httpclient" + ) { + useVersion("4.5.14") + } + } + } + } +} + dependencies { implementation("androidx.activity:activity-ktx:1.10.1") implementation("androidx.core:core-ktx:1.16.0") diff --git a/android/app/gradle.lockfile b/android/app/gradle.lockfile new file mode 100644 index 0000000..96100c3 --- /dev/null +++ b/android/app/gradle.lockfile @@ -0,0 +1,169 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +# To regenerate this file, run: ./gradlew :app:dependencies --write-locks +androidx.activity:activity-ktx:1.10.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,integrationCompileClasspath,integrationLintChecksClasspath,integrationRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.activity:activity:1.10.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,integrationCompileClasspath,integrationLintChecksClasspath,integrationRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.annotation:annotation-experimental:1.4.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,integrationCompileClasspath,integrationLintChecksClasspath,integrationRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.annotation:annotation-jvm:1.8.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,integrationCompileClasspath,integrationLintChecksClasspath,integrationRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.annotation:annotation:1.8.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,integrationCompileClasspath,integrationLintChecksClasspath,integrationRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.arch.core:core-common:2.2.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,integrationCompileClasspath,integrationLintChecksClasspath,integrationRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.arch.core:core-runtime:2.2.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,integrationLintChecksClasspath,integrationRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.collection:collection-jvm:1.4.2=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,integrationLintChecksClasspath,integrationRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.collection:collection:1.0.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugUnitTestCompileClasspath,integrationCompileClasspath,releaseCompileClasspath +androidx.collection:collection:1.4.2=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,integrationLintChecksClasspath,integrationRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.concurrent:concurrent-futures-ktx:1.1.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath +androidx.concurrent:concurrent-futures:1.1.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,integrationLintChecksClasspath,integrationRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.core:core-ktx:1.16.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,integrationCompileClasspath,integrationLintChecksClasspath,integrationRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.core:core-viewtree:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,integrationCompileClasspath,integrationLintChecksClasspath,integrationRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.core:core:1.16.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,integrationCompileClasspath,integrationLintChecksClasspath,integrationRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.interpolator:interpolator:1.0.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,integrationLintChecksClasspath,integrationRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-common:2.6.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,integrationCompileClasspath,integrationLintChecksClasspath,integrationRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-livedata-core:2.6.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,integrationCompileClasspath,integrationLintChecksClasspath,integrationRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-runtime-ktx:2.6.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,integrationCompileClasspath,integrationLintChecksClasspath,integrationRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-runtime:2.6.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,integrationCompileClasspath,integrationLintChecksClasspath,integrationRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,integrationCompileClasspath,integrationLintChecksClasspath,integrationRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-viewmodel-savedstate:2.6.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,integrationCompileClasspath,integrationLintChecksClasspath,integrationRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-viewmodel:2.6.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,integrationCompileClasspath,integrationLintChecksClasspath,integrationRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.profileinstaller:profileinstaller:1.4.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,integrationLintChecksClasspath,integrationRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.savedstate:savedstate-ktx:1.2.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,integrationCompileClasspath,integrationLintChecksClasspath,integrationRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.savedstate:savedstate:1.2.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,integrationCompileClasspath,integrationLintChecksClasspath,integrationRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.startup:startup-runtime:1.1.1=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,integrationLintChecksClasspath,integrationRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.test.espresso:espresso-core:3.6.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath +androidx.test.espresso:espresso-idling-resource:3.6.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath +androidx.test.ext:junit:1.2.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath +androidx.test.services:storage:1.5.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath +androidx.test:core:1.6.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath +androidx.test:monitor:1.7.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath +androidx.test:runner:1.6.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath +androidx.tracing:tracing:1.1.0=debugAndroidTestCompileClasspath +androidx.tracing:tracing:1.2.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,integrationLintChecksClasspath,integrationRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.versionedparcelable:versionedparcelable:1.1.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,integrationCompileClasspath,integrationLintChecksClasspath,integrationRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.webkit:webkit:1.14.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,integrationCompileClasspath,integrationLintChecksClasspath,integrationRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.android.tools.analytics-library:protos:32.4.1=androidLintTool +com.android.tools.analytics-library:shared:32.4.1=androidLintTool +com.android.tools.analytics-library:tracker:32.4.1=androidLintTool +com.android.tools.androidtest:android-test-engine-result-listener:1.0.1=unified-test-platform-gradle-work-action +com.android.tools.androidtest:android-test-engine:1.0.1=unified-test-platform-gradle-work-action +com.android.tools.androidtest:test-result-event-proto:1.0.1=unified-test-platform-gradle-work-action +com.android.tools.androidtest:test-result-proto:1.0.1=unified-test-platform-gradle-work-action +com.android.tools.build:aapt2-proto:9.4.1-15978811=androidLintTool +com.android.tools.build:builder-model:9.4.1=androidLintTool +com.android.tools.build:manifest-merger:32.4.1=androidLintTool +com.android.tools.ddms:ddmlib:32.4.1=androidLintTool +com.android.tools.external.com-intellij:intellij-core:32.4.1=androidLintTool +com.android.tools.external.com-intellij:kotlin-compiler:32.4.1=androidLintTool +com.android.tools.external.org-jetbrains:uast:32.4.1=androidLintTool +com.android.tools.layoutlib:layoutlib-api:32.4.1=androidLintTool +com.android.tools.lint:lint-api:32.4.1=androidLintTool +com.android.tools.lint:lint-checks:32.4.1=androidLintTool +com.android.tools.lint:lint-gradle:32.4.1=androidLintTool +com.android.tools.lint:lint-model:32.4.1=androidLintTool +com.android.tools.lint:lint-typedef-remover:32.4.1=androidLintTool +com.android.tools.lint:lint:32.4.1=androidLintTool +com.android.tools.utp:gradle-work-action:32.4.1=unified-test-platform-gradle-work-action +com.android.tools:annotations:32.4.1=androidLintTool +com.android.tools:common:32.4.1=androidLintTool +com.android.tools:dvlib:32.4.1=androidLintTool +com.android.tools:play-sdk-proto:32.4.1=androidLintTool +com.android.tools:repository:32.4.1=androidLintTool +com.android.tools:sdk-common:32.4.1=androidLintTool +com.android.tools:sdklib:32.4.1=androidLintTool +com.google.code.findbugs:jsr305:3.0.2=androidLintTool,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath +com.google.code.gson:gson:2.11.0=androidLintTool +com.google.errorprone:error_prone_annotations:2.18.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath +com.google.errorprone:error_prone_annotations:2.36.0=androidLintTool +com.google.guava:failureaccess:1.0.2=androidLintTool +com.google.guava:guava:33.4.0-jre=androidLintTool +com.google.guava:listenablefuture:1.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,integrationLintChecksClasspath,integrationRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=androidLintTool +com.google.j2objc:j2objc-annotations:3.0.0=androidLintTool +com.google.jimfs:jimfs:1.1=androidLintTool +com.google.protobuf:protobuf-java:3.25.9=androidLintTool,unified-test-platform-gradle-work-action +com.sun.istack:istack-commons-runtime:3.0.8=androidLintTool +com.sun.xml.fastinfoset:FastInfoset:1.2.16=androidLintTool +commons-codec:commons-codec:1.17.1=androidLintTool +commons-io:commons-io:2.16.1=androidLintTool +commons-logging:commons-logging:1.2=androidLintTool +jakarta.activation:jakarta.activation-api:1.2.1=androidLintTool +jakarta.xml.bind:jakarta.xml.bind-api:2.3.2=androidLintTool +javax.inject:javax.inject:1=androidLintTool,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath +junit:junit:4.13.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +net.java.dev.jna:jna-platform:5.6.0=androidLintTool +net.java.dev.jna:jna:5.6.0=androidLintTool +net.sf.kxml:kxml2:2.3.0=androidLintTool +org.apache.commons:commons-compress:1.27.1=androidLintTool +org.apache.commons:commons-lang3:3.21.0=androidLintTool +org.apache.groovy:groovy-bom:4.0.29=androidLintTool +org.apache.groovy:groovy:4.0.29=androidLintTool +org.apache.httpcomponents:httpclient:4.5.14=androidLintTool +org.apache.httpcomponents:httpcore:4.4.16=androidLintTool +org.apache.httpcomponents:httpmime:4.5.6=androidLintTool +org.bouncycastle:bcpkix-jdk18on:1.86=androidLintTool +org.bouncycastle:bcprov-jdk18on:1.86=androidLintTool +org.bouncycastle:bcutil-jdk18on:1.86=androidLintTool +org.checkerframework:checker-qual:3.43.0=androidLintTool +org.glassfish.jaxb:jaxb-runtime:2.3.2=androidLintTool +org.glassfish.jaxb:txw2:2.3.2=androidLintTool +org.hamcrest:hamcrest-core:1.3=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +org.hamcrest:hamcrest-library:1.3=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath +org.jetbrains.kotlin:kotlin-build-tools-api:2.4.0=kotlinAbiValidationCompatClasspath +org.jetbrains.kotlin:kotlin-build-tools-api:2.4.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath +org.jetbrains.kotlin:kotlin-build-tools-compat:2.4.21=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath +org.jetbrains.kotlin:kotlin-build-tools-cri-impl:2.4.0=kotlinAbiValidationCompatClasspath +org.jetbrains.kotlin:kotlin-build-tools-cri-impl:2.4.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath +org.jetbrains.kotlin:kotlin-build-tools-impl:2.4.0=kotlinAbiValidationCompatClasspath +org.jetbrains.kotlin:kotlin-build-tools-impl:2.4.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath +org.jetbrains.kotlin:kotlin-compiler-embeddable:2.4.0=kotlinAbiValidationCompatClasspath +org.jetbrains.kotlin:kotlin-compiler-embeddable:2.4.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath +org.jetbrains.kotlin:kotlin-compiler-runner:2.4.0=kotlinAbiValidationCompatClasspath +org.jetbrains.kotlin:kotlin-compiler-runner:2.4.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath +org.jetbrains.kotlin:kotlin-daemon-client:2.4.0=kotlinAbiValidationCompatClasspath +org.jetbrains.kotlin:kotlin-daemon-client:2.4.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath +org.jetbrains.kotlin:kotlin-daemon-embeddable:2.4.0=kotlinAbiValidationCompatClasspath +org.jetbrains.kotlin:kotlin-daemon-embeddable:2.4.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath +org.jetbrains.kotlin:kotlin-reflect:1.6.10=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath +org.jetbrains.kotlin:kotlin-reflect:2.2.10=androidLintTool +org.jetbrains.kotlin:kotlin-script-runtime:2.4.0=kotlinAbiValidationCompatClasspath +org.jetbrains.kotlin:kotlin-script-runtime:2.4.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath +org.jetbrains.kotlin:kotlin-stdlib-common:2.4.21=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,integrationCompileClasspath,integrationLintChecksClasspath,integrationRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.0=debugCompileClasspath,debugUnitTestCompileClasspath,integrationCompileClasspath,releaseCompileClasspath +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.20=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,integrationLintChecksClasspath,integrationRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib-jdk7:2.2.10=androidLintTool,unified-test-platform-gradle-work-action +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.0=debugCompileClasspath,debugUnitTestCompileClasspath,integrationCompileClasspath,releaseCompileClasspath +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.20=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,integrationLintChecksClasspath,integrationRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.2.10=androidLintTool,unified-test-platform-gradle-work-action +org.jetbrains.kotlin:kotlin-stdlib:2.2.10=androidLintTool,unified-test-platform-gradle-work-action +org.jetbrains.kotlin:kotlin-stdlib:2.4.0=kotlinAbiValidationCompatClasspath +org.jetbrains.kotlin:kotlin-stdlib:2.4.21=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,integrationCompileClasspath,integrationLintChecksClasspath,integrationRuntimeClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.kotlin:kotlin-tooling-core:2.4.21=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath +org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4=debugCompileClasspath,debugUnitTestCompileClasspath,integrationCompileClasspath,releaseCompileClasspath +org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1=debugAndroidTestCompileClasspath +org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,integrationLintChecksClasspath,integrationRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.6.4=debugCompileClasspath,debugUnitTestCompileClasspath,integrationCompileClasspath,releaseCompileClasspath +org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.1=debugAndroidTestCompileClasspath +org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,integrationLintChecksClasspath,integrationRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.9.0=androidLintTool,unified-test-platform-gradle-work-action +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.6.4=debugCompileClasspath,debugUnitTestCompileClasspath,integrationCompileClasspath,releaseCompileClasspath +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.1=debugAndroidTestCompileClasspath +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.3=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,integrationLintChecksClasspath,integrationRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.8.0=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.9.0=androidLintTool,unified-test-platform-gradle-work-action +org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4=debugCompileClasspath,debugUnitTestCompileClasspath,integrationCompileClasspath,releaseCompileClasspath +org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1=debugAndroidTestCompileClasspath +org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,integrationLintChecksClasspath,integrationRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0=androidLintTool,unified-test-platform-gradle-work-action +org.jetbrains:annotations:13.0=debugCompileClasspath,debugUnitTestCompileClasspath,integrationCompileClasspath,kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,releaseCompileClasspath +org.jetbrains:annotations:23.0.0=androidLintTool,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,integrationLintChecksClasspath,integrationRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath,unified-test-platform-gradle-work-action +org.jspecify:jspecify:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,integrationCompileClasspath,integrationLintChecksClasspath,integrationRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.junit.platform:junit-platform-commons:1.13.3=unified-test-platform-gradle-work-action +org.junit.platform:junit-platform-engine:1.13.3=unified-test-platform-gradle-work-action +org.junit.platform:junit-platform-launcher:1.13.3=unified-test-platform-gradle-work-action +org.junit:junit-bom:5.13.3=unified-test-platform-gradle-work-action +org.jvnet.staxex:stax-ex:1.8.1=androidLintTool +org.opentest4j:opentest4j:1.3.0=unified-test-platform-gradle-work-action +org.ow2.asm:asm-analysis:9.9=androidLintTool +org.ow2.asm:asm-commons:9.9=androidLintTool +org.ow2.asm:asm-tree:9.9=androidLintTool +org.ow2.asm:asm:9.9=androidLintTool +empty=androidApis,androidJdkImage,androidTestUtil,coreLibraryDesugaring,debugAndroidTestAnnotationProcessorClasspath,debugAndroidTestImplementationDependenciesMetadata,debugAnnotationProcessorClasspath,debugImplementationDependenciesMetadata,debugReverseMetadataValues,debugUnitTestAnnotationProcessorClasspath,debugUnitTestImplementationDependenciesMetadata,integrationAnnotationProcessorClasspath,integrationImplementationDependenciesMetadata,integrationReverseMetadataValues,kotlinCompilerPluginClasspath,kotlinCompilerPluginClasspathDebug,kotlinCompilerPluginClasspathDebugAndroidTest,kotlinCompilerPluginClasspathDebugUnitTest,kotlinCompilerPluginClasspathIntegration,kotlinCompilerPluginClasspathRelease,lintChecks,lintPublish,releaseAnnotationProcessorClasspath,releaseImplementationDependenciesMetadata,releaseReverseMetadataValues diff --git a/android/app/src/androidTest/java/community/yuvomi/mobile/BlobExportTest.kt b/android/app/src/androidTest/java/community/yuvomi/mobile/BlobExportTest.kt new file mode 100644 index 0000000..f9a37ec --- /dev/null +++ b/android/app/src/androidTest/java/community/yuvomi/mobile/BlobExportTest.kt @@ -0,0 +1,111 @@ +package community.yuvomi.mobile + +import android.content.Context +import android.webkit.WebView +import androidx.core.content.FileProvider +import androidx.test.core.app.ActivityScenario +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import java.io.File +import java.util.concurrent.CountDownLatch +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit +import org.json.JSONArray +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class BlobExportTest { + @Test fun rejectsBlobExportFromOpaqueNativePageEvenWithClaimedOrigin() { + val context = ApplicationProvider.getApplicationContext() + context.getSharedPreferences("server", Context.MODE_PRIVATE).edit() + .putString("origin", "https://family.example").commit() + val file = File(File(context.cacheDir, "captures").apply { mkdirs() }, "blob-test.csv") + val uri = FileProvider.getUriForFile(context, "${context.packageName}.files", file) + val worker = Executors.newSingleThreadExecutor() + try { + ActivityScenario.launch(MainActivity::class.java).use { scenario -> + lateinit var web: WebView + val created = CountDownLatch(1) + var blob = "" + scenario.onActivity { + // A real WebView with a controlled document, not a network mock. + // The HTTPS upstream suite separately exercises the app's client. + web = WebView(it) + web.settings.javaScriptEnabled = true + web.loadDataWithBaseURL( + "https://family.example/", + "Export test", + "text/html", + "UTF-8", + null, + ) + } + val deadline = System.nanoTime() + 10_000_000_000L + var ready = false + while (!ready && System.nanoTime() < deadline) { + val checked = CountDownLatch(1) + scenario.onActivity { + web.evaluateJavascript("location.origin") { + ready = + it == "\"https://family.example\"" + checked.countDown() + } + } + assertTrue(checked.await(2, TimeUnit.SECONDS)) + if (!ready) Thread.sleep(50) + } + assertTrue("Trusted document loaded", ready) + scenario.onActivity { + web.evaluateJavascript( + "URL.createObjectURL(new Blob(['name,value\\nYuvomi,Grüße\\n'],{type:'text/csv'}))", + ) { + blob = JSONArray("[$it]").getString(0) + created.countDown() + } + } + assertTrue(created.await(10, TimeUnit.SECONDS)) + assertTrue( + "Blob inherits trusted origin: $blob", + BlobTransfer.owns(ServerAddress.parse("https://family.example"), blob), + ) + assertNotNull( + context.contentResolver.openOutputStream(uri, "w")?.also { + it.close() + }, + ) + val saved = CountDownLatch(1) + var success = false + scenario.onActivity { + assertFalse( + "Native page URL remains opaque despite loadDataWithBaseURL", + ServerAddress.parse("https://family.example").owns(web.url.orEmpty()), + ) + BlobTransfer.save( + ServerAddress.parse("https://family.example"), + web, + blob, + context.contentResolver, + uri, + worker, + { true }, + ) { + success = it + saved.countDown() + } + } + assertTrue(saved.await(50, TimeUnit.SECONDS)) + assertFalse(success) + assertEquals(0L, file.length()) + scenario.onActivity { web.destroy() } + } + } finally { + worker.shutdownNow() + file.delete() + } + } +} diff --git a/android/app/src/androidTest/java/community/yuvomi/mobile/BrowserSecurityTest.kt b/android/app/src/androidTest/java/community/yuvomi/mobile/BrowserSecurityTest.kt new file mode 100644 index 0000000..4189669 --- /dev/null +++ b/android/app/src/androidTest/java/community/yuvomi/mobile/BrowserSecurityTest.kt @@ -0,0 +1,66 @@ +package community.yuvomi.mobile + +import android.content.Context +import android.net.Uri +import android.view.View +import android.view.ViewGroup +import android.webkit.WebResourceRequest +import android.webkit.WebView +import androidx.test.core.app.ActivityScenario +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class BrowserSecurityTest { + @Test fun blocksCrossOriginPostAndSubresourceBeforeNetwork() { + val context = ApplicationProvider.getApplicationContext() + context.getSharedPreferences("server", Context.MODE_PRIVATE).edit() + .putString("origin", "https://family.example").commit() + ActivityScenario.launch(MainActivity::class.java).use { scenario -> + lateinit var web: WebView + lateinit var client: android.webkit.WebViewClient + scenario.onActivity { + web = requireNotNull(findWeb(it.window.decorView)) + client = web.webViewClient + } + // Interception runs off the UI thread in WebView. POST navigations do + // not reliably call shouldOverrideUrlLoading, so this is a separate gate. + for (mainFrame in listOf(true, false)) { + val response = client.shouldInterceptRequest( + web, + Request("https://evil.example/collect", mainFrame), + ) + assertNotNull(response) + assertEquals(403, response!!.statusCode) + } + assertNull( + client.shouldInterceptRequest(web, Request("https://family.example/tasks", true)), + ) + } + } + + private fun findWeb(view: View): WebView? { + if (view is WebView) return view + if (view is ViewGroup) { + for (i in 0 until view.childCount) { + findWeb(view.getChildAt(i))?.let { + return it + } + } + } + return null + } + private class Request(private val value: String, private val mainFrame: Boolean) : WebResourceRequest { + override fun getUrl(): Uri = Uri.parse(value) + override fun isForMainFrame() = mainFrame + override fun isRedirect() = false + override fun hasGesture() = true + override fun getMethod() = "POST" + override fun getRequestHeaders(): MutableMap = mutableMapOf() + } +} diff --git a/android/app/src/androidTest/java/community/yuvomi/mobile/LocalNetworkPermissionTest.kt b/android/app/src/androidTest/java/community/yuvomi/mobile/LocalNetworkPermissionTest.kt new file mode 100644 index 0000000..e87100b --- /dev/null +++ b/android/app/src/androidTest/java/community/yuvomi/mobile/LocalNetworkPermissionTest.kt @@ -0,0 +1,71 @@ +package community.yuvomi.mobile + +import android.Manifest +import android.accessibilityservice.AccessibilityServiceInfo +import android.content.Context +import android.content.pm.PackageManager +import android.os.Build +import android.view.accessibility.AccessibilityNodeInfo +import androidx.test.core.app.ActivityScenario +import androidx.test.core.app.ApplicationProvider +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.assertion.ViewAssertions.doesNotExist +import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class LocalNetworkPermissionTest { + @Test fun permissionIsOptionalAndHonorsSystemDenialAndGrant() { + val context = ApplicationProvider.getApplicationContext() + context.getSharedPreferences("server", Context.MODE_PRIVATE).edit().clear().commit() + ActivityScenario.launch(MainActivity::class.java).use { + if (Build.VERSION.SDK_INT < 37 || + context.checkSelfPermission(Manifest.permission.ACCESS_LOCAL_NETWORK) == PackageManager.PERMISSION_GRANTED + ) { + onView(withText("Allow private-network servers")).check(doesNotExist()) + return@use + } + val automation = InstrumentationRegistry.getInstrumentation().uiAutomation + automation.serviceInfo = automation.serviceInfo.apply { flags = flags or AccessibilityServiceInfo.FLAG_REPORT_VIEW_IDS } + onView(withText("Allow private-network servers")).perform(click()) + onView(withText("Cancel")).perform(click()) + assertEquals(PackageManager.PERMISSION_DENIED, context.checkSelfPermission(Manifest.permission.ACCESS_LOCAL_NETWORK)) + onView(withText("Allow private-network servers")).perform(click()) + onView(withText("Continue")).perform(click()) + chooseSystemButton("permission_deny_button") + await { context.checkSelfPermission(Manifest.permission.ACCESS_LOCAL_NETWORK) == PackageManager.PERMISSION_DENIED } + onView(withText("Allow private-network servers")).perform(click()) + onView(withText("Continue")).perform(click()) + chooseSystemButton("permission_allow_button") + await { context.checkSelfPermission(Manifest.permission.ACCESS_LOCAL_NETWORK) == PackageManager.PERMISSION_GRANTED } + } + } + + private fun chooseSystemButton(id: String) { + val automation = InstrumentationRegistry.getInstrumentation().uiAutomation + await { + val node = find(automation.rootInActiveWindow, id) ?: return@await false + node.performAction(AccessibilityNodeInfo.ACTION_CLICK) + } + } + private fun find(node: AccessibilityNodeInfo?, id: String): AccessibilityNodeInfo? { + if (node == null) return null + if (node.viewIdResourceName?.endsWith("/$id") == true) return node + for (index in 0 until node.childCount) find(node.getChild(index), id)?.let { return it } + return null + } + private fun await(condition: () -> Boolean) { + val deadline = System.nanoTime() + 10_000_000_000L + while (System.nanoTime() < deadline) { + if (condition()) return + Thread.sleep(100) + } + assertTrue("System permission response timed out", condition()) + } +} diff --git a/android/app/src/androidTest/java/community/yuvomi/mobile/OnboardingTest.kt b/android/app/src/androidTest/java/community/yuvomi/mobile/OnboardingTest.kt new file mode 100644 index 0000000..6651fde --- /dev/null +++ b/android/app/src/androidTest/java/community/yuvomi/mobile/OnboardingTest.kt @@ -0,0 +1,93 @@ +package community.yuvomi.mobile + +import android.content.Context +import android.webkit.WebView +import androidx.test.core.app.ActivityScenario +import androidx.test.core.app.ApplicationProvider +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.action.ViewActions.closeSoftKeyboard +import androidx.test.espresso.action.ViewActions.replaceText +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.hasErrorText +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.isEnabled +import androidx.test.espresso.matcher.ViewMatchers.withContentDescription +import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class OnboardingTest { + private val context: Context get() = ApplicationProvider.getApplicationContext() + + @Before fun clearConfiguration() { + context.getSharedPreferences("server", Context.MODE_PRIVATE).edit().clear().commit() + } + + @Test fun rejectsHttpAndSubpathsLocally() { + ActivityScenario.launch(MainActivity::class.java).use { + onView( + withContentDescription("Yuvomi server URL"), + ).perform(replaceText("http://family.example"), closeSoftKeyboard()) + onView(withText("Connect securely")).perform(click()) + onView( + withContentDescription("Yuvomi server URL"), + ).check(matches(hasErrorText("Use HTTPS with a valid server hostname."))) + onView( + withContentDescription("Yuvomi server URL"), + ).perform(replaceText("https://family.example/subpath"), closeSoftKeyboard()) + onView(withText("Connect securely")).perform(click()) + onView( + withContentDescription("Yuvomi server URL"), + ).check(matches(hasErrorText("Yuvomi requires a dedicated hostname, not a subpath."))) + } + } + + @Test fun survivesRecreationWithoutLosingOnboarding() { + ActivityScenario.launch(MainActivity::class.java).use { scenario -> + scenario.recreate() + onView(withText("Connect securely")).check(matches(isDisplayed())) + onView(withContentDescription("Yuvomi server URL")).check(matches(isEnabled())) + } + } + + @Test fun webViewIsHardenedAndBackgroundingPreservesInstance() { + context.getSharedPreferences("server", Context.MODE_PRIVATE).edit() + .putString("origin", "https://family.example").commit() + ActivityScenario.launch(MainActivity::class.java).use { scenario -> + var initial: WebView? = null + scenario.onActivity { activity -> + fun find(view: android.view.View): WebView? { + if (view is WebView) return view + if (view is android.view.ViewGroup) { + for (i in 0 until view.childCount) { + find(view.getChildAt(i))?.let { + return it + } + } + } + return null + } + initial = find(activity.window.decorView) + val web = requireNotNull(initial) + assertFalse(web.settings.allowFileAccess) + assertFalse(web.settings.allowContentAccess) + assertEquals( + android.webkit.WebSettings.MIXED_CONTENT_NEVER_ALLOW, + web.settings.mixedContentMode, + ) + assertTrue(web.settings.javaScriptEnabled) + } + scenario.moveToState(androidx.lifecycle.Lifecycle.State.CREATED) + scenario.moveToState(androidx.lifecycle.Lifecycle.State.RESUMED) + scenario.onActivity { assertNotNull(initial?.parent) } + } + } +} diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index fa06748..1ffa84a 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,7 @@ + diff --git a/android/app/src/main/java/community/yuvomi/mobile/BlobTransfer.kt b/android/app/src/main/java/community/yuvomi/mobile/BlobTransfer.kt new file mode 100644 index 0000000..17ad224 --- /dev/null +++ b/android/app/src/main/java/community/yuvomi/mobile/BlobTransfer.kt @@ -0,0 +1,165 @@ +package community.yuvomi.mobile + +import android.content.ContentResolver +import android.net.Uri +import android.os.Handler +import android.os.Looper +import android.util.Base64 +import android.webkit.WebView +import androidx.webkit.WebViewCompat +import androidx.webkit.WebViewFeature +import java.io.OutputStream +import java.util.UUID +import java.util.WeakHashMap +import java.util.concurrent.Executor +import org.json.JSONObject + +/** Same-origin, user-selected blob exports. No JS bridge or CSP/network bypass. */ +object BlobTransfer { + private val stores = WeakHashMap() + + fun owns(server: ServerAddress, value: String): Boolean = value.startsWith("blob:") && server.owns(value.substring(5)) + + fun install(web: WebView, server: ServerAddress) { + if (!WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT)) return + val key = "__yuvomi_blobs_${UUID.randomUUID().toString().replace("-", "") }" + stores[web] = key + val script = """ + (function(){if(window.top!==window)return; + const entries=new Map();let total=0;const limit=25*1024*1024; + const original=URL.createObjectURL.bind(URL); + URL.createObjectURL=function(blob){const url=original(blob); + if(blob instanceof Blob && blob.size<=limit){ + while(entries.size && (total+blob.size>limit || entries.size>=8)){ + const first=entries.keys().next().value;total-=entries.get(first).size;entries.delete(first); + } + entries.set(url,blob);total+=blob.size; + }return url;}; + Object.defineProperty(window,${JSONObject.quote(key)},{value:{take:function(url){ + const blob=entries.get(url);if(blob){total-=blob.size;entries.delete(url);}return blob; + }}}); + })(); + """.trimIndent() + // Retain a bounded Blob reference even if upstream revokes the object URL + // immediately after clicking its download anchor. Never fetch a blob URL: + // upstream's CSP correctly refuses that operation in a WebView. + WebViewCompat.addDocumentStartJavaScript(web, script, setOf(server.origin)) + } + + fun forget(web: WebView) { + stores.remove(web) + } + + fun save( + server: ServerAddress, + web: WebView, + value: String, + resolver: ContentResolver, + destination: Uri, + worker: Executor, + authorized: () -> Boolean, + completed: (Boolean) -> Unit, + ) { + val store = stores[web] + if (store == null || !authorized() || !server.owns(web.url.orEmpty()) || !owns(server, value)) { + completed(false) + return + } + val handler = Handler(Looper.getMainLooper()) + val key = JSONObject.quote("__yuvomi_export_${UUID.randomUUID().toString().replace("-", "")}") + val maxBytes = 25 * 1024 * 1024 + var output: OutputStream? = null + var offset = 0 + var finished = false + lateinit var expiry: Runnable + fun allowed() = authorized() && server.owns(web.url.orEmpty()) + fun finish(success: Boolean) { + if (finished) return + finished = true + handler.removeCallbacks(expiry) + if (runCatching { allowed() }.getOrDefault(false)) { + runCatching { web.evaluateJavascript("delete window[$key]", null) } + } + val close = Runnable { + val closed = runCatching { output?.close() }.isSuccess + handler.post { completed(success && closed) } + } + runCatching { worker.execute(close) }.onFailure { close.run() } + } + expiry = Runnable { finish(false) } + lateinit var pull: Runnable + pull = Runnable { + if (!runCatching { allowed() }.getOrDefault(false)) { + finish(false) + return@Runnable + } + val script = """ + (function(){const x=window[$key]; + if(!x || x.state!=='ready')return {state:x?x.state:'failed'}; + const b=x.bytes;if(b.length>$maxBytes)return {state:'failed'}; + const end=Math.min($offset+32768,b.length); + return {state:'ready',data:btoa(String.fromCharCode.apply(null,b.subarray($offset,end))),done:end===b.length};})() + """.trimIndent() + runCatching { + web.evaluateJavascript(script) { result -> + if (finished) return@evaluateJavascript + val data = runCatching { + require(result.length < 50_000) + JSONObject(result) + }.getOrNull() + when (data?.optString("state")) { + "loading" -> handler.postDelayed(pull, 50) + "ready" -> { + val chunk = runCatching { Base64.decode(data.getString("data"), Base64.NO_WRAP) }.getOrNull() + if (chunk == null || chunk.size > 32768 || offset + chunk.size > maxBytes) { + finish(false) + } else { + offset += chunk.size + runCatching { + worker.execute { + val success = runCatching { requireNotNull(output).write(chunk) }.isSuccess + handler.post { + if (!success) { + finish(false) + } else if (data.optBoolean("done")) { + finish(true) + } else { + handler.post(pull) + } + } + } + }.onFailure { finish(false) } + } + } + else -> finish(false) + } + } + }.onFailure { finish(false) } + } + handler.postDelayed(expiry, 45_000) + runCatching { + worker.execute { + output = runCatching { resolver.openOutputStream(destination, "w") }.getOrNull() + handler.post { + if (finished) { + runCatching { output?.close() } + return@post + } + if (output == null || !allowed()) { + finish(false) + return@post + } + val script = """ + (function(){window[$key]={state:'loading'}; + try{const b=window[${JSONObject.quote(store)}].take(${JSONObject.quote(value)}); + if(!b || b.size>$maxBytes)throw new Error('size'); + b.arrayBuffer().then(a=>window[$key]={state:'ready',bytes:new Uint8Array(a)}) + .catch(()=>window[$key]={state:'failed'}); + }catch(e){window[$key]={state:'failed'};}})() + """.trimIndent() + runCatching { web.evaluateJavascript(script) { handler.post(pull) } }.onFailure { finish(false) } + } + } + }.onFailure { finish(false) } + } +} diff --git a/android/app/src/main/java/community/yuvomi/mobile/FileTransfer.kt b/android/app/src/main/java/community/yuvomi/mobile/FileTransfer.kt index 4188f7e..ed4001d 100644 --- a/android/app/src/main/java/community/yuvomi/mobile/FileTransfer.kt +++ b/android/app/src/main/java/community/yuvomi/mobile/FileTransfer.kt @@ -12,7 +12,8 @@ object FileTransfer { val connection = URI(url).toURL().openConnection() as HttpsURLConnection try { connection.instanceFollowRedirects = false - connection.connectTimeout = 15_000; connection.readTimeout = 20_000 + connection.connectTimeout = 15_000 + connection.readTimeout = 20_000 cookie?.let { connection.setRequestProperty("Cookie", it) } require(connection.responseCode == 200) { "Download refused" } require(connection.contentLengthLong <= 100L * 1024 * 1024) { "Download too large" } @@ -31,6 +32,8 @@ object FileTransfer { } } } - } finally { connection.disconnect() } + } finally { + connection.disconnect() + } } } diff --git a/android/app/src/main/java/community/yuvomi/mobile/MainActivity.kt b/android/app/src/main/java/community/yuvomi/mobile/MainActivity.kt index ffc0e66..805b733 100644 --- a/android/app/src/main/java/community/yuvomi/mobile/MainActivity.kt +++ b/android/app/src/main/java/community/yuvomi/mobile/MainActivity.kt @@ -14,8 +14,29 @@ import android.text.InputType import android.view.View import android.view.ViewGroup import android.view.WindowManager -import android.webkit.* -import android.widget.* +import android.webkit.CookieManager +import android.webkit.GeolocationPermissions +import android.webkit.PermissionRequest +import android.webkit.RenderProcessGoneDetail +import android.webkit.SslErrorHandler +import android.webkit.URLUtil +import android.webkit.ValueCallback +import android.webkit.WebChromeClient +import android.webkit.WebResourceError +import android.webkit.WebResourceRequest +import android.webkit.WebResourceResponse +import android.webkit.WebSettings +import android.webkit.WebStorage +import android.webkit.WebView +import android.webkit.WebViewClient +import android.widget.Button +import android.widget.EditText +import android.widget.FrameLayout +import android.widget.LinearLayout +import android.widget.ProgressBar +import android.widget.ScrollView +import android.widget.TextView +import android.widget.Toast import androidx.activity.ComponentActivity import androidx.activity.OnBackPressedCallback import androidx.activity.result.contract.ActivityResultContracts @@ -32,7 +53,7 @@ import java.net.URI import java.util.concurrent.Executors class MainActivity : ComponentActivity() { - private val worker = Executors.newSingleThreadExecutor() + private var worker = Executors.newSingleThreadExecutor() private val handler = Handler(Looper.getMainLooper()) private val preferences by lazy { getSharedPreferences("server", MODE_PRIVATE) } private var server: ServerAddress? = null @@ -45,62 +66,132 @@ class MainActivity : ComponentActivity() { private var capture: Uri? = null private var pendingDownload: String? = null private var generation = 0 - private val loadTimeout = Runnable { showError("The server is taking too long. Check your connection or try again.") } - - private val picker = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> - val callback = fileCallback - fileCallback = null - val uris = if (result.resultCode != RESULT_OK) null else { - val data = result.data - data?.clipData?.let { clip -> Array(clip.itemCount.coerceAtMost(50)) { clip.getItemAt(it).uri } } - ?: data?.data?.let { arrayOf(it) } ?: capture?.let { arrayOf(it) } + private val uploadSelection = SelectionGate() + private val downloadSelection = SelectionGate() + private val loadTimeout = + Runnable { showError("The server is taking too long. Check your connection or try again.") } + private val localNetworkPermission = + registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted -> + if (granted) { + toast("Private-network access allowed. Retry your server connection.") + } else { + toast("Private-network access was not granted. Public HTTPS servers can still be used.") + } } - callback?.onReceiveValue(uris?.filter { it.scheme == "content" }?.toTypedArray()) - capture = null - } - private val saveDocument = registerForActivityResult(ActivityResultContracts.CreateDocument("application/octet-stream")) { destination -> - val url = pendingDownload - pendingDownload = null - val current = server - if (destination != null && url != null && current != null && current.owns(url)) { - val cookies = CookieManager.getInstance().getCookie(url) - val epoch = generation - worker.execute { - val success = runCatching { FileTransfer.save(current, url, cookies, contentResolver, destination) }.isSuccess - runOnUiThread { - if (!isDestroyed && epoch == generation) { - if (success) toast("Download saved") else { - runCatching { contentResolver.delete(destination, null, null) } - toast("Download failed. Check your connection and sign-in, then retry.") + + private val picker = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + val accepted = uploadSelection.consume(generation) + val callback = fileCallback + fileCallback = null + val uris = if (!accepted || result.resultCode != RESULT_OK) { + null + } else { + val data = result.data + data?.clipData?.let { clip -> + Array(clip.itemCount.coerceAtMost(50)) { clip.getItemAt(it).uri } + } + ?: data?.data?.let { arrayOf(it) } ?: capture?.let { arrayOf(it) } + } + callback?.onReceiveValue(uris?.filter { it.scheme == "content" }?.toTypedArray()) + revokeCapture() + capture = null + } + private val saveDocument = + registerForActivityResult( + ActivityResultContracts.CreateDocument("application/octet-stream"), + ) { destination -> + val accepted = downloadSelection.consume(generation) + val url = pendingDownload + pendingDownload = null + if (!accepted) return@registerForActivityResult + val current = server + if (destination != null && url != null && current != null && + BlobTransfer.owns(current, url) + ) { + val view = web ?: return@registerForActivityResult + val epoch = generation + BlobTransfer.save( + current, + view, + url, + contentResolver, + destination, + worker, + { !isDestroyed && epoch == generation }, + ) { success -> + if (!success) runCatching { contentResolver.delete(destination, null, null) } + if (!isDestroyed && + epoch == generation + ) { + toast( + if (success) "Export saved" else "Export failed or exceeded the 25 MB limit.", + ) + } + } + } else if (destination != null && url != null && current != null && current.owns(url)) { + val cookies = CookieManager.getInstance().getCookie(url) + val epoch = generation + worker.execute { + val success = runCatching { + FileTransfer.save(current, url, cookies, contentResolver, destination) + }.isSuccess + if (!success) runCatching { contentResolver.delete(destination, null, null) } + runOnUiThread { + if (!isDestroyed && epoch == generation) { + if (success) { + toast("Download saved") + } else { + toast( + "Download failed. Check your connection and sign-in, then retry.", + ) + } } } } } } - } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + if (savedInstanceState?.getBoolean("uploadOutstanding") == true) uploadSelection.restoreOutstanding() + if (savedInstanceState?.getBoolean("downloadOutstanding") == true) downloadSelection.restoreOutstanding() // Household data is hidden from screenshots and the recent-apps preview. window.addFlags(WindowManager.LayoutParams.FLAG_SECURE) WebView.setWebContentsDebuggingEnabled(false) root = LinearLayout(this).apply { orientation = LinearLayout.VERTICAL } ViewCompat.setOnApplyWindowInsetsListener(root) { view, insets -> - val bars = insets.getInsets(WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.ime()) + val bars = insets.getInsets( + WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.ime(), + ) view.setPadding(bars.left, bars.top, bars.right, bars.bottom) insets } setContentView(root) - onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - if (web?.canGoBack() == true) web?.goBack() - else if (web != null) settings() - else { isEnabled = false; onBackPressedDispatcher.onBackPressed() } - } - }) - File(cacheDir, "captures").listFiles()?.filter { it.lastModified() < System.currentTimeMillis() - 86_400_000 } + onBackPressedDispatcher.addCallback( + this, + object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + if (web?.canGoBack() == true) { + web?.goBack() + } else if (web != null) { + moveTaskToBack(true) + } else { + isEnabled = false + onBackPressedDispatcher.onBackPressed() + } + } + }, + ) + File(cacheDir, "captures").listFiles()?.filter { + it.lastModified() < + System.currentTimeMillis() - 86_400_000 + } ?.forEach { it.delete() } - server = preferences.getString("origin", null)?.let { runCatching { ServerAddress.parse(it) }.getOrNull() } + server = + preferences.getString("origin", null)?.let { + runCatching { ServerAddress.parse(it) }.getOrNull() + } if (server == null) onboarding() else browser() } @@ -109,10 +200,15 @@ class MainActivity : ComponentActivity() { setPadding(dp(24), dp(20), dp(24), dp(20)) } private fun text(value: String, size: Float = 17f) = TextView(this).apply { - text = value; textSize = size; setPadding(0, dp(8), 0, dp(8)) + text = value + textSize = size + setPadding(0, dp(8), 0, dp(8)) } private fun button(label: String, action: () -> Unit) = Button(this).apply { - text = label; isAllCaps = false; minHeight = dp(48); setOnClickListener { action() } + text = label + isAllCaps = false + minHeight = dp(48) + setOnClickListener { action() } } private fun dp(value: Int) = (value * resources.displayMetrics.density).toInt() @@ -120,7 +216,9 @@ class MainActivity : ComponentActivity() { root.removeAllViews() val form = column() form.addView(text("Your home, connected", 30f)) - form.addView(text("Connect to your own Yuvomi server. Your data stays with your household.")) + form.addView( + text("Connect to your own Yuvomi server. Your data stays with your household."), + ) val address = EditText(this).apply { hint = "https://family.example.com" inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_URI @@ -129,23 +227,31 @@ class MainActivity : ComponentActivity() { setText(server?.origin.orEmpty()) id = View.generateViewId() } - val label = text("HTTPS required. Custom ports supported. Use a dedicated hostname, not a subpath.") + val label = + text("HTTPS required. Custom ports supported. Use a dedicated hostname, not a subpath.") label.accessibilityLiveRegion = View.ACCESSIBILITY_LIVE_REGION_POLITE lateinit var connect: Button connect = button("Connect securely") { val candidate = runCatching { ServerAddress.parse(address.text.toString()) }.getOrElse { - address.error = it.message; return@button + address.error = it.message + return@button } - connect.isEnabled = false; address.isEnabled = false; label.setText(R.string.checking_server) + connect.isEnabled = false + address.isEnabled = false + label.setText(R.string.checking_server) val epoch = generation worker.execute { val result = runCatching { ServerProbe.check(candidate) } runOnUiThread { if (isDestroyed || epoch != generation) return@runOnUiThread - connect.isEnabled = true; address.isEnabled = true + connect.isEnabled = true + address.isEnabled = true if (result.isSuccess) { server = candidate - preferences.edit { putString("origin", candidate.origin); remove("path") } + preferences.edit { + putString("origin", candidate.origin) + remove("path") + } browser() } else { // Exception details may contain URL parameters; never expose them. @@ -156,8 +262,11 @@ class MainActivity : ComponentActivity() { } form.addView(address) form.addView(label) + localNetworkButton()?.let { form.addView(it) } form.addView(connect) - form.addView(text("Independent community app. No telemetry or mandatory cloud service.", 14f)) + form.addView( + text("Independent community app. No telemetry or mandatory cloud service.", 14f), + ) root.addView(ScrollView(this).apply { addView(form) }) } @@ -166,14 +275,23 @@ class MainActivity : ComponentActivity() { root.removeAllViews() val toolbar = LinearLayout(this) toolbar.addView(button("Server settings") { settings() }) - toolbar.addView(button("Share server") { - startActivity(Intent.createChooser(Intent(Intent.ACTION_SEND).apply { - type = "text/plain"; putExtra(Intent.EXTRA_TEXT, current.origin) - }, "Share server address")) - }) + toolbar.addView( + button("Share server") { + startActivity( + Intent.createChooser( + Intent(Intent.ACTION_SEND).apply { + type = "text/plain" + putExtra(Intent.EXTRA_TEXT, current.origin) + }, + "Share server address", + ), + ) + }, + ) root.addView(toolbar) progress = ProgressBar(this, null, android.R.attr.progressBarStyleHorizontal).apply { - contentDescription = "Loading Yuvomi"; max = 100 + contentDescription = "Loading Yuvomi" + max = 100 } root.addView(progress, LinearLayout.LayoutParams(-1, dp(4))) content = FrameLayout(this) @@ -181,12 +299,20 @@ class MainActivity : ComponentActivity() { val view = WebView(this) web = view secure(view) + BlobTransfer.install(view, current) CookieManager.getInstance().setAcceptCookie(true) CookieManager.getInstance().setAcceptThirdPartyCookies(view, false) view.webViewClient = object : WebViewClient() { override fun shouldInterceptRequest(v: WebView, request: WebResourceRequest): WebResourceResponse? { - if (!request.isForMainFrame && !current.owns(request.url.toString())) { - return WebResourceResponse("text/plain", "UTF-8", 403, "Blocked", emptyMap(), ByteArrayInputStream(ByteArray(0))) + if (!current.owns(request.url.toString())) { + return WebResourceResponse( + "text/plain", + "UTF-8", + 403, + "Blocked", + emptyMap(), + ByteArrayInputStream(ByteArray(0)), + ) } return null } @@ -197,8 +323,14 @@ class MainActivity : ComponentActivity() { return true } override fun onPageStarted(v: WebView, url: String, icon: Bitmap?) { + if (!current.owns(url)) { + v.stopLoading() + external(url) + return + } progress.visibility = View.VISIBLE - handler.removeCallbacks(loadTimeout); handler.postDelayed(loadTimeout, 30_000) + handler.removeCallbacks(loadTimeout) + handler.postDelayed(loadTimeout, 30_000) } override fun onPageFinished(v: WebView, url: String) { handler.removeCallbacks(loadTimeout) @@ -206,74 +338,140 @@ class MainActivity : ComponentActivity() { CookieManager.getInstance().flush() if (current.owns(url)) { val path = URI(url).rawPath.orEmpty() - if (!listOf("auth", "reset", "invite", "login").any { path.contains(it, true) }) { + if (!listOf("auth", "reset", "invite", "login").any { + path.contains(it, true) + } + ) { preferences.edit { putString("path", path) } } } } override fun onReceivedError(v: WebView, r: WebResourceRequest, e: WebResourceError) { - if (r.isForMainFrame) showError("Unable to connect. Cached content may still be available. Check your network or VPN.") + if (r.isForMainFrame) { + showError( + "Unable to connect. Cached content may still be available. Check your network or VPN.", + ) + } } override fun onReceivedHttpError(v: WebView, r: WebResourceRequest, e: WebResourceResponse) { - if (r.isForMainFrame && e.statusCode >= 400) showError("The server is unavailable (HTTP ${e.statusCode}).") + if (r.isForMainFrame && + e.statusCode >= 400 + ) { + showError("The server is unavailable (HTTP ${e.statusCode}).") + } } override fun onReceivedSslError(v: WebView, h: SslErrorHandler, e: SslError) { - h.cancel(); showError("The server certificate could not be verified. Contact your server administrator.") + h.cancel() + showError( + "The server certificate could not be verified. Contact your server administrator.", + ) } override fun onRenderProcessGone(v: WebView, detail: RenderProcessGoneDetail): Boolean { - releaseWeb(); browser(); toast("Yuvomi recovered after a browser interruption."); return true + releaseWeb() + browser() + toast("Yuvomi recovered after a browser interruption.") + return true } } view.webChromeClient = object : WebChromeClient() { - override fun onProgressChanged(v: WebView, value: Int) { progress.progress = value } - override fun onPermissionRequest(request: PermissionRequest) { request.deny() } + override fun onProgressChanged(v: WebView, value: Int) { + progress.progress = value + } + override fun onPermissionRequest(request: PermissionRequest) { + request.deny() + } override fun onGeolocationPermissionsShowPrompt(origin: String, callback: GeolocationPermissions.Callback) { callback.invoke(origin, false, false) } override fun onShowFileChooser(v: WebView, callback: ValueCallback>, params: FileChooserParams): Boolean { if (!current.owns(v.url.orEmpty())) return false + if (!uploadSelection.begin(generation)) { + callback.onReceiveValue(null) + toast("Finish or cancel the existing system file picker first.") + return true + } fileCallback?.onReceiveValue(null) fileCallback = callback - val types = params.acceptTypes.filter { it.matches(Regex("[a-zA-Z0-9.+*-]+/[a-zA-Z0-9.+*-]+")) } - val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply { - addCategory(Intent.CATEGORY_OPENABLE) - type = types.singleOrNull() ?: "*/*" - if (types.size > 1) putExtra(Intent.EXTRA_MIME_TYPES, types.toTypedArray()) - putExtra(Intent.EXTRA_ALLOW_MULTIPLE, params.mode == FileChooserParams.MODE_OPEN_MULTIPLE) - } - val chooser = Intent.createChooser(intent, "Select a file") - if (types.any { it.startsWith("image/") }) { - val directory = File(cacheDir, "captures").apply { mkdirs() } - capture = FileProvider.getUriForFile(this@MainActivity, "$packageName.files", File.createTempFile("capture-", ".jpg", directory)) - chooser.putExtra(Intent.EXTRA_INITIAL_INTENTS, arrayOf(Intent(MediaStore.ACTION_IMAGE_CAPTURE).apply { - putExtra(MediaStore.EXTRA_OUTPUT, capture) - addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION) - })) - } - runCatching { picker.launch(chooser) }.onFailure { - fileCallback?.onReceiveValue(null); fileCallback = null; toast("No compatible file picker is available.") + runCatching { + val types = params.acceptTypes.filter { + it.matches(Regex("[a-zA-Z0-9.+*-]+/[a-zA-Z0-9.+*-]+")) + } + val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + type = types.singleOrNull() ?: "*/*" + if (types.size > 1) putExtra(Intent.EXTRA_MIME_TYPES, types.toTypedArray()) + putExtra( + Intent.EXTRA_ALLOW_MULTIPLE, + params.mode == FileChooserParams.MODE_OPEN_MULTIPLE, + ) + } + val chooser = Intent.createChooser(intent, "Select a file") + if (types.any { it.startsWith("image/") }) { + val directory = File(cacheDir, "captures").apply { mkdirs() } + capture = + FileProvider.getUriForFile( + this@MainActivity, + "$packageName.files", + File.createTempFile("capture-", ".jpg", directory), + ) + chooser.putExtra( + Intent.EXTRA_INITIAL_INTENTS, + arrayOf( + Intent(MediaStore.ACTION_IMAGE_CAPTURE).apply { + putExtra(MediaStore.EXTRA_OUTPUT, capture) + clipData = + android.content.ClipData.newRawUri("Yuvomi photo", capture) + addFlags( + Intent.FLAG_GRANT_WRITE_URI_PERMISSION or + Intent.FLAG_GRANT_READ_URI_PERMISSION, + ) + }, + ), + ) + } + picker.launch(chooser) + }.onFailure { + uploadSelection.consume(generation) + fileCallback?.onReceiveValue(null) + fileCallback = null + toast("No compatible file picker is available.") } return true } } view.setDownloadListener { url, _, disposition, mime, _ -> - if (!current.owns(url)) toast("Only HTTPS downloads from your server are supported.") - else if (pendingDownload == null) { + if (!current.owns(url) && + !BlobTransfer.owns(current, url) + ) { + toast("Only downloads from your server are supported.") + } else if (pendingDownload == null && downloadSelection.begin(generation)) { pendingDownload = url - val name = URLUtil.guessFileName(url, disposition, mime).replace(Regex("[^a-zA-Z0-9._ -]"), "_").take(120) - saveDocument.launch(name.ifBlank { "download" }) + val name = URLUtil.guessFileName( + url, + disposition, + mime, + ).replace(Regex("[^a-zA-Z0-9._ -]"), "_").take(120) + runCatching { saveDocument.launch(name.ifBlank { "download" }) }.onFailure { + downloadSelection.consume(generation) + pendingDownload = null + toast("No compatible document provider is available.") + } } } content.addView(view, FrameLayout.LayoutParams(-1, -1)) val path = preferences.getString("path", "/").orEmpty() - view.loadUrl(current.origin + if (path.startsWith("/") && !path.startsWith("//")) path else "/") + view.loadUrl( + current.origin + if (path.startsWith("/") && !path.startsWith("//")) path else "/", + ) } @SuppressLint("SetJavaScriptEnabled") private fun secure(view: WebView) { view.settings.apply { - javaScriptEnabled = true; domStorageEnabled = true - allowFileAccess = false; allowContentAccess = false + javaScriptEnabled = true + domStorageEnabled = true + allowFileAccess = false + allowContentAccess = false mixedContentMode = WebSettings.MIXED_CONTENT_NEVER_ALLOW javaScriptCanOpenWindowsAutomatically = false setSupportMultipleWindows(false) @@ -284,12 +482,17 @@ class MainActivity : ComponentActivity() { } private fun external(value: String) { + handler.removeCallbacks(loadTimeout) + progress.visibility = View.GONE val uri = runCatching { URI(value) }.getOrNull() ?: return if (uri.scheme != "https" || uri.host.isNullOrBlank() || uri.rawUserInfo != null) { - toast("This link type is blocked for your security."); return + toast("This link type is blocked for your security.") + return } AlertDialog.Builder(this).setTitle("Leave your Yuvomi server?") - .setMessage("Destination: ${uri.host}\n\nOpen normal links in your browser. For SSO, use the isolated sign-in window only if you trust this provider. Providers that require a system browser cannot transfer a session back to this app.") + .setMessage( + "Destination: ${uri.host}\n\nOpen normal links in your browser. For SSO, use the isolated sign-in window only if you trust this provider. Providers that require a system browser cannot transfer a session back to this app.", + ) .setPositiveButton("Browser") { _, _ -> openBrowser(value) } .setNeutralButton("Sign-in window") { _, _ -> authentication(value) } .setNegativeButton("Cancel", null).show() @@ -301,34 +504,56 @@ class MainActivity : ComponentActivity() { secure(auth) CookieManager.getInstance().setAcceptThirdPartyCookies(auth, false) auth.webChromeClient = object : WebChromeClient() { - override fun onPermissionRequest(request: PermissionRequest) { request.deny() } + override fun onPermissionRequest(request: PermissionRequest) { + request.deny() + } } auth.webViewClient = object : WebViewClient() { + override fun onPageStarted(v: WebView, url: String, favicon: Bitmap?) { + authDialog?.setTitle( + "Sign in: ${runCatching { + URI(url).host + }.getOrNull().orEmpty()}", + ) + } override fun shouldOverrideUrlLoading(v: WebView, request: WebResourceRequest): Boolean { val uri = request.url return uri.scheme != "https" || !uri.userInfo.isNullOrEmpty() } - override fun onReceivedSslError(v: WebView, h: SslErrorHandler, e: SslError) { h.cancel() } + override fun onReceivedSslError(v: WebView, h: SslErrorHandler, e: SslError) { + h.cancel() + } override fun onPageFinished(v: WebView, url: String) { if (server?.owns(url) == true && !URI(url).path.startsWith("/api/")) { CookieManager.getInstance().flush() - authDialog?.dismiss(); web?.loadUrl(url) + authDialog?.dismiss() + web?.loadUrl(url) } } } authDialog = AlertDialog.Builder(this).setTitle("Isolated web sign-in") .setView(auth).setNegativeButton("Close", null).create().also { dialog -> dialog.setOnDismissListener { - auth.stopLoading(); auth.removeAllViews(); auth.destroy(); authDialog = null + auth.stopLoading() + auth.removeAllViews() + auth.destroy() + authDialog = null } dialog.show() - dialog.window?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) + dialog.window?.setLayout( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT, + ) } auth.loadUrl(value) } private fun openBrowser(value: String) { - runCatching { startActivity(Intent(Intent.ACTION_VIEW, value.toUri()).addCategory(Intent.CATEGORY_BROWSABLE)) } + runCatching { + startActivity( + Intent(Intent.ACTION_VIEW, value.toUri()).addCategory(Intent.CATEGORY_BROWSABLE), + ) + } .onFailure { toast("No browser is available.") } } @@ -338,12 +563,21 @@ class MainActivity : ComponentActivity() { progress.visibility = View.GONE if (content.childCount > 1) return val panel = column().apply { - addView(text("Connection interrupted", 25f)); addView(text(message)) - addView(button("Retry") { content.removeView(this); web?.reload() }) + addView(text("Connection interrupted", 25f)) + addView(text(message)) + addView( + button("Retry") { + content.removeView(this) + web?.reload() + }, + ) addView(button("Keep viewing") { content.removeView(this) }) addView(button("Server settings") { settings() }) + localNetworkButton()?.let { addView(it) } } - val dark = resources.configuration.uiMode and android.content.res.Configuration.UI_MODE_NIGHT_MASK == android.content.res.Configuration.UI_MODE_NIGHT_YES + val dark = + resources.configuration.uiMode and android.content.res.Configuration.UI_MODE_NIGHT_MASK == + android.content.res.Configuration.UI_MODE_NIGHT_YES panel.setBackgroundColor(if (dark) 0xff191816.toInt() else 0xfff5f3ed.toInt()) content.addView(panel, FrameLayout.LayoutParams(-1, -1)) panel.accessibilityLiveRegion = View.ACCESSIBILITY_LIVE_REGION_POLITE @@ -352,41 +586,104 @@ class MainActivity : ComponentActivity() { private fun settings() { AlertDialog.Builder(this).setTitle("Server settings") - .setMessage("${server?.origin.orEmpty()}\n\nSwitching servers erases all app cookies, storage and cached household data. Downloads saved outside the app remain. This does not revoke the server session; sign out in Yuvomi first where possible.") + .setMessage( + "${server?.origin.orEmpty()}\n\nSwitching servers erases all app cookies, storage and cached household data. Downloads saved outside the app remain. This does not revoke the server session; sign out in Yuvomi first where possible.", + ) .setPositiveButton("Erase and change server") { _, _ -> generation++ - authDialog?.dismiss(); releaseWeb() - if (WebViewFeature.isFeatureSupported(WebViewFeature.DELETE_BROWSING_DATA)) { + worker.shutdownNow() + worker = Executors.newSingleThreadExecutor() + authDialog?.dismiss() + releaseWeb() + val captures = File(cacheDir, "captures") + val capturesErased = !captures.exists() || captures.listFiles()?.all { !it.exists() || it.delete() } == true + if (capturesErased && WebViewFeature.isFeatureSupported(WebViewFeature.DELETE_BROWSING_DATA)) { WebStorageCompat.deleteBrowsingData(WebStorage.getInstance()) { CookieManager.getInstance().removeAllCookies { CookieManager.getInstance().flush() - preferences.edit { clear() }; server = null; onboarding() + preferences.edit { clear() } + server = null + onboarding() } } } else { // Older engines cannot reliably erase CacheStorage/IndexedDB. // OS reset is complete and scoped to this application, but closes it. - getSystemService(android.app.ActivityManager::class.java).clearApplicationUserData() + val reset = getSystemService( + android.app.ActivityManager::class.java, + ).clearApplicationUserData() + if (!reset) { + toast("Local data could not be erased. Clear this app's storage in Android settings before switching servers.") + finish() + } } } - .setNeutralButton("Open server in browser") { _, _ -> server?.let { openBrowser(it.origin) } } + .setNeutralButton("Open server in browser") { _, _ -> + server?.let { openBrowser(it.origin) } + } .setNegativeButton("Continue", null).show() } + private fun localNetworkButton(): Button? { + if (android.os.Build.VERSION.SDK_INT < 37 || + checkSelfPermission(android.Manifest.permission.ACCESS_LOCAL_NETWORK) == + android.content.pm.PackageManager.PERMISSION_GRANTED + ) { + return null + } + return button("Allow private-network servers") { + AlertDialog.Builder(this).setTitle("Private server access") + .setMessage( + "Android requires permission to connect to a Yuvomi server on your local network. The app connects only to your configured server; it does not scan for devices. Public internet servers do not need this permission.", + ) + .setPositiveButton("Continue") { _, _ -> + localNetworkPermission.launch(android.Manifest.permission.ACCESS_LOCAL_NETWORK) + } + .setNegativeButton("Cancel", null).show() + } + } + private fun releaseWeb() { handler.removeCallbacks(loadTimeout) - fileCallback?.onReceiveValue(null); fileCallback = null + fileCallback?.onReceiveValue(null) + fileCallback = null + pendingDownload = null + revokeCapture() + capture = null web?.let { view -> + BlobTransfer.forget(view) view.stopLoading() (view.parent as? ViewGroup)?.removeView(view) - view.removeAllViews(); view.destroy() + view.removeAllViews() + view.destroy() } web = null } + private fun revokeCapture() { + capture?.let { + revokeUriPermission(it, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION) + } + } + override fun onSaveInstanceState(outState: Bundle) { + outState.putBoolean("uploadOutstanding", uploadSelection.outstanding) + outState.putBoolean("downloadOutstanding", downloadSelection.outstanding) + super.onSaveInstanceState(outState) + } private fun toast(message: String) = Toast.makeText(this, message, Toast.LENGTH_LONG).show() - override fun onPause() { web?.onPause(); CookieManager.getInstance().flush(); super.onPause() } - override fun onResume() { super.onResume(); web?.onResume() } + override fun onPause() { + web?.onPause() + CookieManager.getInstance().flush() + super.onPause() + } + override fun onResume() { + super.onResume() + web?.onResume() + } override fun onDestroy() { - generation++; authDialog?.dismiss(); releaseWeb(); worker.shutdownNow(); super.onDestroy() + generation++ + authDialog?.dismiss() + releaseWeb() + worker.shutdownNow() + super.onDestroy() } } diff --git a/android/app/src/main/java/community/yuvomi/mobile/SelectionGate.kt b/android/app/src/main/java/community/yuvomi/mobile/SelectionGate.kt new file mode 100644 index 0000000..7d804af --- /dev/null +++ b/android/app/src/main/java/community/yuvomi/mobile/SelectionGate.kt @@ -0,0 +1,23 @@ +package community.yuvomi.mobile + +/** Never delivers a late system-picker result into a different server/web context. */ +class SelectionGate { + private var owner: Int? = null + val outstanding: Boolean get() = owner != null + + fun begin(generation: Int): Boolean { + if (outstanding) return false + owner = generation + return true + } + + fun consume(generation: Int): Boolean { + val previous = owner + owner = null + return previous == generation + } + + fun restoreOutstanding() { + owner = -1 + } +} diff --git a/android/app/src/main/java/community/yuvomi/mobile/ServerAddress.kt b/android/app/src/main/java/community/yuvomi/mobile/ServerAddress.kt index fc53947..3ca5170 100644 --- a/android/app/src/main/java/community/yuvomi/mobile/ServerAddress.kt +++ b/android/app/src/main/java/community/yuvomi/mobile/ServerAddress.kt @@ -8,7 +8,8 @@ class ServerAddress private constructor(val origin: String) { fun owns(value: String): Boolean = runCatching { val uri = URI(value) uri.rawUserInfo == null && uri.scheme.equals("https", true) && - uri.host.equals(URI(origin).host, true) && effectivePort(uri) == effectivePort(URI(origin)) + uri.host.equals(URI(origin).host, true) && + effectivePort(uri) == effectivePort(URI(origin)) }.getOrDefault(false) companion object { @@ -24,7 +25,9 @@ class ServerAddress private constructor(val origin: String) { require(uri.rawPath.isNullOrEmpty() || uri.rawPath == "/") { "Yuvomi requires a dedicated hostname, not a subpath." } - require(uri.port == -1 || uri.port in 1..65535) { "The port must be between 1 and 65535." } + require(uri.port == -1 || uri.port in 1..65535) { + "The port must be between 1 and 65535." + } val port = if (uri.port == -1 || uri.port == 443) "" else ":${uri.port}" return ServerAddress("https://${uri.host.lowercase(Locale.ROOT)}$port") } diff --git a/android/app/src/main/java/community/yuvomi/mobile/ServerProbe.kt b/android/app/src/main/java/community/yuvomi/mobile/ServerProbe.kt index 0489ceb..64b7186 100644 --- a/android/app/src/main/java/community/yuvomi/mobile/ServerProbe.kt +++ b/android/app/src/main/java/community/yuvomi/mobile/ServerProbe.kt @@ -1,16 +1,18 @@ package community.yuvomi.mobile -import org.json.JSONObject +import java.io.ByteArrayOutputStream import java.net.URI import javax.net.ssl.HttpsURLConnection -import java.io.ByteArrayOutputStream +import org.json.JSONObject object ServerProbe { /** No cookies, no automatic redirects, bounded response and deadlines. */ fun check(server: ServerAddress) { val version = json(server, "/api/v1/version") - require(version.opt("app_name") is String && version.opt("setup_required") is Boolean && - version.opt("password_reset_enabled") is Boolean) { + require( + version.opt("app_name") is String && version.opt("setup_required") is Boolean && + version.opt("password_reset_enabled") is Boolean, + ) { "This server does not expose compatible Yuvomi bootstrap metadata." } val auth = json(server, "/api/v1/auth/oidc/config") diff --git a/android/app/src/test/java/community/yuvomi/mobile/SelectionGateTest.kt b/android/app/src/test/java/community/yuvomi/mobile/SelectionGateTest.kt new file mode 100644 index 0000000..96cdc93 --- /dev/null +++ b/android/app/src/test/java/community/yuvomi/mobile/SelectionGateTest.kt @@ -0,0 +1,31 @@ +package community.yuvomi.mobile + +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +class SelectionGateTest { + @Test fun acceptsOnlyTheInitiatingContextOnce() { + val gate = SelectionGate() + assertTrue(gate.begin(0)) + assertTrue(gate.consume(0)) + assertFalse(gate.consume(0)) + } + + @Test fun serverSwitchCannotDeliverAnOldSelectionToTheNewServer() { + val gate = SelectionGate() + assertTrue(gate.begin(0)) + assertFalse(gate.begin(1)) + assertFalse(gate.consume(1)) + assertTrue(gate.begin(1)) + assertTrue(gate.consume(1)) + } + + @Test fun processRecreationCancelsSelectionWithoutReusingItsCallback() { + val gate = SelectionGate() + gate.restoreOutstanding() + assertFalse(gate.begin(0)) + assertFalse(gate.consume(0)) + assertTrue(gate.begin(0)) + } +} diff --git a/android/app/src/test/java/community/yuvomi/mobile/ServerAddressTest.kt b/android/app/src/test/java/community/yuvomi/mobile/ServerAddressTest.kt index 48f3ecc..36b3344 100644 --- a/android/app/src/test/java/community/yuvomi/mobile/ServerAddressTest.kt +++ b/android/app/src/test/java/community/yuvomi/mobile/ServerAddressTest.kt @@ -1,20 +1,31 @@ package community.yuvomi.mobile -import org.junit.Assert.* +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertThrows +import org.junit.Assert.assertTrue import org.junit.Test class ServerAddressTest { @Test fun canonicalizesAndSupportsPortsAndIPv6() { - assertEquals("https://family.example", ServerAddress.parse(" HTTPS://Family.Example:443/ ").origin) - assertEquals("https://family.example:8443", ServerAddress.parse("https://family.example:8443").origin) + assertEquals( + "https://family.example", + ServerAddress.parse(" HTTPS://Family.Example:443/ ").origin, + ) + assertEquals( + "https://family.example:8443", + ServerAddress.parse("https://family.example:8443").origin, + ) assertEquals("https://[::1]:8443", ServerAddress.parse("https://[::1]:8443").origin) } @Test fun rejectsUnsafeConfiguration() { - listOf("http://family.example", "javascript:alert(1)", "https://user:pass@family.example", + listOf( + "http://family.example", "javascript:alert(1)", "https://user:pass@family.example", "https://family.example/subpath", "https://family.example?token=secret", "https://family.example/#token", "https://family.example:0", "https://family.example:65536", - "https://family.example/%2f", "https://family.example\\@evil.example", "https:///", "").forEach { + "https://family.example/%2f", "https://family.example\\@evil.example", "https:///", "", + ).forEach { assertThrows(it, IllegalArgumentException::class.java) { ServerAddress.parse(it) } } } @@ -22,10 +33,30 @@ class ServerAddressTest { @Test fun navigationIsOriginBoundNotStringPrefixBound() { val server = ServerAddress.parse("https://family.example") assertTrue(server.owns("https://FAMILY.example:443/tasks?id=1")) - listOf("https://family.example.evil.test/", "https://family.example@evil.test/", - "https://user@family.example/", "http://family.example/", "https://family.example:8443/", - "file:///etc/passwd", "intent://family.example", "data:text/html,hello").forEach { + listOf( + "https://family.example.evil.test/", + "https://family.example@evil.test/", + "https://user@family.example/", + "http://family.example/", + "https://family.example:8443/", + "file:///etc/passwd", + "intent://family.example", + "data:text/html,hello", + ).forEach { assertFalse(it, server.owns(it)) } } + + @Test fun blobDownloadsCannotCrossOriginsOrDowngradeTls() { + val server = ServerAddress.parse("https://family.example") + assertTrue(BlobTransfer.owns(server, "blob:https://family.example/unique-id")) + listOf( + "blob:https://evil.example/id", + "blob:http://family.example/id", + "blob:https://family.example.evil.test/id", + "data:text/plain,hi", + ).forEach { + assertFalse(it, BlobTransfer.owns(server, it)) + } + } } diff --git a/android/build.gradle.kts b/android/build.gradle.kts index fef8ec1..ec29718 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -1,8 +1,28 @@ -plugins { - id("com.android.application") version "8.13.2" apply false - id("org.jetbrains.kotlin.android") version "2.2.10" apply false +buildscript { + repositories { + google() + mavenCentral() + } + // Override AGP's bundled KGP to include CVE-2026-53914 remediation. + dependencies { classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:2.4.21") } + configurations.getByName("classpath").resolutionStrategy { + activateDependencyLocking() + eachDependency { + when (requested.group) { + "io.netty" -> useVersion("4.1.139.Final") + "com.google.protobuf" -> useVersion("3.25.9") + "org.bouncycastle" -> useVersion("1.86") + "org.apache.commons" -> if (requested.name == "commons-lang3") useVersion("3.21.0") + "org.apache.httpcomponents" -> if (requested.name == "httpclient") useVersion("4.5.14") + "org.bitbucket.b_c" -> if (requested.name == "jose4j") useVersion("0.9.6") + "org.jdom" -> if (requested.name == "jdom2") useVersion("2.0.6.1") + } + } + } } +plugins { id("com.android.application") version "9.4.1" apply false } + // WSL mounted Windows volumes may reject Gradle's POSIX file metadata operations. // Opt-in external output root; ordinary developer/CI builds retain standard paths. val outputRoot = providers.environmentVariable("YUVOMI_BUILD_DIR") diff --git a/android/buildscript-gradle.lockfile b/android/buildscript-gradle.lockfile new file mode 100644 index 0000000..f684690 --- /dev/null +++ b/android/buildscript-gradle.lockfile @@ -0,0 +1,116 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +# To regenerate this file, run: ./gradlew dependencies --write-locks +androidx.databinding:databinding-common:9.4.1=classpath +androidx.databinding:databinding-compiler-common:9.4.1=classpath +com.android.application:com.android.application.gradle.plugin:9.4.1=classpath +com.android.databinding:baseLibrary:9.4.1=classpath +com.android.tools.analytics-library:crash:32.4.1=classpath +com.android.tools.analytics-library:protos:32.4.1=classpath +com.android.tools.analytics-library:shared:32.4.1=classpath +com.android.tools.analytics-library:tracker:32.4.1=classpath +com.android.tools.build.jetifier:jetifier-core:1.0.0-beta10=classpath +com.android.tools.build.jetifier:jetifier-processor:1.0.0-beta10=classpath +com.android.tools.build:aapt2-proto:9.4.1-15978811=classpath +com.android.tools.build:aaptcompiler:9.4.1=classpath +com.android.tools.build:apksig:9.4.1=classpath +com.android.tools.build:apkzlib:9.4.1=classpath +com.android.tools.build:builder-model:9.4.1=classpath +com.android.tools.build:builder-test-api:9.4.1=classpath +com.android.tools.build:builder:9.4.1=classpath +com.android.tools.build:bundletool:1.18.3=classpath +com.android.tools.build:gradle-api:9.4.1=classpath +com.android.tools.build:gradle-common-api:9.4.1=classpath +com.android.tools.build:gradle-settings-api:9.4.1=classpath +com.android.tools.build:gradle:9.4.1=classpath +com.android.tools.build:manifest-merger:32.4.1=classpath +com.android.tools.ddms:ddmlib:32.4.1=classpath +com.android.tools.layoutlib:layoutlib-api:32.4.1=classpath +com.android.tools.lint:lint-model:32.4.1=classpath +com.android.tools.lint:lint-typedef-remover:32.4.1=classpath +com.android.tools.utp:gradle-work-action-api:32.4.1=classpath +com.android.tools:annotations:32.4.1=classpath +com.android.tools:common:32.4.1=classpath +com.android.tools:dvlib:32.4.1=classpath +com.android.tools:repository:32.4.1=classpath +com.android.tools:sdk-common:32.4.1=classpath +com.android.tools:sdklib:32.4.1=classpath +com.android:signflinger:9.4.1=classpath +com.android:zipflinger:9.4.1=classpath +com.google.auto.value:auto-value-annotations:1.6.2=classpath +com.google.code.findbugs:jsr305:3.0.2=classpath +com.google.code.gson:gson:2.11.0=classpath +com.google.crypto.tink:tink:1.7.0=classpath +com.google.dagger:dagger:2.28.3=classpath +com.google.errorprone:error_prone_annotations:2.36.0=classpath +com.google.flatbuffers:flatbuffers-java:1.12.0=classpath +com.google.guava:failureaccess:1.0.2=classpath +com.google.guava:guava:33.4.0-jre=classpath +com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=classpath +com.google.j2objc:j2objc-annotations:3.0.0=classpath +com.google.jimfs:jimfs:1.1=classpath +com.google.protobuf:protobuf-java-util:3.25.9=classpath +com.google.protobuf:protobuf-java:3.25.9=classpath +com.googlecode.juniversalchardet:juniversalchardet:1.0.3=classpath +com.squareup:javapoet:1.13.0=classpath +com.squareup:javawriter:2.5.0=classpath +com.sun.istack:istack-commons-runtime:3.0.8=classpath +com.sun.xml.fastinfoset:FastInfoset:1.2.16=classpath +commons-codec:commons-codec:1.17.1=classpath +commons-io:commons-io:2.16.1=classpath +commons-logging:commons-logging:1.2=classpath +jakarta.activation:jakarta.activation-api:1.2.1=classpath +jakarta.xml.bind:jakarta.xml.bind-api:2.3.2=classpath +javax.inject:javax.inject:1=classpath +net.java.dev.jna:jna-platform:5.6.0=classpath +net.java.dev.jna:jna:5.6.0=classpath +net.sf.jopt-simple:jopt-simple:4.9=classpath +net.sf.kxml:kxml2:2.3.0=classpath +org.apache.commons:commons-compress:1.27.1=classpath +org.apache.commons:commons-lang3:3.21.0=classpath +org.apache.httpcomponents:httpclient:4.5.14=classpath +org.apache.httpcomponents:httpcore:4.4.16=classpath +org.apache.httpcomponents:httpmime:4.5.6=classpath +org.bitbucket.b_c:jose4j:0.9.6=classpath +org.bouncycastle:bcpkix-jdk18on:1.86=classpath +org.bouncycastle:bcprov-jdk18on:1.86=classpath +org.bouncycastle:bcutil-jdk18on:1.86=classpath +org.checkerframework:checker-qual:3.43.0=classpath +org.glassfish.jaxb:jaxb-runtime:2.3.2=classpath +org.glassfish.jaxb:txw2:2.3.2=classpath +org.jdom:jdom2:2.0.6.1=classpath +org.jetbrains.kotlin:fus-statistics-gradle-plugin:2.4.21=classpath +org.jetbrains.kotlin:kotlin-build-statistics:2.4.21=classpath +org.jetbrains.kotlin:kotlin-build-tools-api:2.4.21=classpath +org.jetbrains.kotlin:kotlin-compiler-runner:2.4.21=classpath +org.jetbrains.kotlin:kotlin-daemon-client:2.4.21=classpath +org.jetbrains.kotlin:kotlin-gradle-plugin-annotations:2.4.21=classpath +org.jetbrains.kotlin:kotlin-gradle-plugin-api:2.4.21=classpath +org.jetbrains.kotlin:kotlin-gradle-plugin-idea-proto:2.4.21=classpath +org.jetbrains.kotlin:kotlin-gradle-plugin-idea:2.4.21=classpath +org.jetbrains.kotlin:kotlin-gradle-plugin:2.4.21=classpath +org.jetbrains.kotlin:kotlin-gradle-plugins-bom:2.4.21=classpath +org.jetbrains.kotlin:kotlin-klib-commonizer-api:2.4.21=classpath +org.jetbrains.kotlin:kotlin-native-utils:2.4.21=classpath +org.jetbrains.kotlin:kotlin-reflect:2.3.21=classpath +org.jetbrains.kotlin:kotlin-stdlib-jdk7:2.2.10=classpath +org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.2.10=classpath +org.jetbrains.kotlin:kotlin-stdlib:2.3.21=classpath +org.jetbrains.kotlin:kotlin-tooling-core:2.4.21=classpath +org.jetbrains.kotlin:kotlin-util-io:2.4.21=classpath +org.jetbrains.kotlin:kotlin-util-klib-metadata:2.4.21=classpath +org.jetbrains.kotlin:kotlin-util-klib:2.4.21=classpath +org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.9.0=classpath +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.9.0=classpath +org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0=classpath +org.jetbrains:annotations:13.0=classpath +org.jvnet.staxex:stax-ex:1.8.1=classpath +org.ow2.asm:asm-analysis:9.9=classpath +org.ow2.asm:asm-commons:9.9=classpath +org.ow2.asm:asm-tree:9.9=classpath +org.ow2.asm:asm-util:9.9=classpath +org.ow2.asm:asm:9.9=classpath +org.slf4j:slf4j-api:1.7.30=classpath +org.tensorflow:tensorflow-lite-metadata:0.2.0=classpath +empty= diff --git a/android/gradle.properties b/android/gradle.properties index 3735595..bfedcec 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,4 +1,6 @@ org.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8 +org.gradle.workers.max=2 +kotlin.compiler.execution.strategy=in-process android.useAndroidX=true kotlin.code.style=official android.nonTransitiveRClass=true diff --git a/android/gradle/wrapper/gradle-wrapper.jar b/android/gradle/wrapper/gradle-wrapper.jar index 096fefc5650d7c6c53255c4a421f68c5863db93c3515f597851dec629c8b0acb..e10449e7b61986daf247b50dfb2effadb8bc8d9de25dedbfff33c70a634c9649 100644 GIT binary patch delta 40165 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 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 -goto fail +"%COMSPEC%" /c exit 1 :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% @@ -65,30 +65,18 @@ echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 -goto fail +"%COMSPEC%" /c exit 1 :execute @rem Setup the command line -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* +@rem Execute gradlew +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel -:end -@rem End local scope for the variables with windows NT shell -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! -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 - -:omega +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts index 9828438..651be41 100644 --- a/android/settings.gradle.kts +++ b/android/settings.gradle.kts @@ -1,7 +1,16 @@ -pluginManagement { repositories { google(); mavenCentral(); gradlePluginPortal() } } +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) - repositories { google(); mavenCentral() } + repositories { + google() + mavenCentral() + } } rootProject.name = "YuvomiMobile" include(":app")