diff options
| -rw-r--r-- | CLAUDE.md | 3 | ||||
| -rw-r--r-- | docs/MESHBAY_DESIGN.md | 10 | ||||
| -rw-r--r-- | docs/USERGUIDE.md | 4 | ||||
| -rw-r--r-- | packages/meshbay-android/README.md | 18 | ||||
| -rw-r--r-- | packages/meshbay-android/app/build.gradle.kts | 29 | ||||
| -rw-r--r-- | packages/meshbay-hub/tests/test_android_shell.py | 11 |
6 files changed, 63 insertions, 12 deletions
@@ -1100,7 +1100,8 @@ here are kept only where they are a rule about *editing* the code. ### Android (`packages/meshbay-android/`) Built with `./gradlew assembleDebug` / `assembleRelease` (JDK 17+, an Android -SDK); `./gradlew testDebugUnitTest` runs the JVM tests, which pytest also runs +SDK; a release needs the release key's four `meshbayRelease*` properties in +`~/.gradle/gradle.properties`, README); `./gradlew testDebugUnitTest` runs the JVM tests, which pytest also runs when `ANDROID_HOME` is set (`test_android_shell.py`). | Need | File | Note | diff --git a/docs/MESHBAY_DESIGN.md b/docs/MESHBAY_DESIGN.md index 18b23cf..9c42c62 100644 --- a/docs/MESHBAY_DESIGN.md +++ b/docs/MESHBAY_DESIGN.md @@ -3444,9 +3444,11 @@ narrow bridge — and Android has all three: keeps the WebView reported visible and holds a media-playback foreground service; nowhere else, because a page never hidden is never throttled. -Release builds are signed with the development key until the release key exists -(Stage D12); §2.3's sentence about who holds a signing key applies to whichever -store distributes them. +Release builds are signed with the release key, distributed as a direct APK; +the key stays outside the repository and a release build without it fails +rather than falling back to the development key. §2.3's sentence about who +holds a signing key applies to whichever store distributes them, if one ever +does. ### 11.4 Casting @@ -4027,7 +4029,7 @@ process runs it — `systemctl --user` on Linux, Task Scheduler on Windows. | — | **Bitmap subtitles** (PGS, VOBSUB — about a fifth of the embedded streams). No WebVTT without OCR; they are not listed rather than listed and blank. Burn-in covers them and costs `-c:v copy`, which is what the eight-slot sizing assumes never happens | | — | Delegation (§3.4) | | — | Tier 3 roster attestation (§3.3) | -| — | **Android: phone behaviour and release** — the back button driving the page, recovery from a network handover, keeping a download alive with the screen off, lock-screen media controls, and a release key (§11.3) | +| — | **Android: phone behaviour and release** — the back button driving the page, recovery from a network handover, keeping a download alive with the screen off, lock-screen media controls, and an update channel (§11.3) | | — | **Federation between two hubs.** The protocol is written and switched off in the code (§7.6); what is not built is one run between two machines | ### 15.3 Open, and why each is where it is diff --git a/docs/USERGUIDE.md b/docs/USERGUIDE.md index 2c37f56..275a70d 100644 --- a/docs/USERGUIDE.md +++ b/docs/USERGUIDE.md @@ -1006,8 +1006,8 @@ Better to know now than to go looking for it: distribution. Until that ships, take them from the download page and from nowhere else. - **The Android application is not released yet.** It works — groups, chat, - films, downloads, casting — but is built and installed by hand and signed - with a development key, so there is no store page and no update channel. The + films, downloads, casting — and is signed with the release key, but is + installed by hand from an APK: there is no store page and no update channel. The back button and the lock screen do not drive it yet. Music keeps playing with the screen off, from one track to the next, with a notification shown while it plays. A phone browser works too. diff --git a/packages/meshbay-android/README.md b/packages/meshbay-android/README.md index bfd82cf..5cb474e 100644 --- a/packages/meshbay-android/README.md +++ b/packages/meshbay-android/README.md @@ -32,13 +32,29 @@ holds the same service and the same visibility, for as long as it plays # needs JDK 17+ and an Android SDK (ANDROID_HOME, or sdk.dir in local.properties) ./gradlew assembleDebug # app/build/outputs/apk/debug/app-debug.apk ./gradlew testDebugUnitTest # JVM unit tests +./gradlew assembleRelease # app/build/outputs/apk/release/app-release.apk ``` +A release is signed with the release key, which never enters the repository. +`assembleRelease` reads it from `~/.gradle/gradle.properties`, and stops if +any of these is missing rather than signing with the debug key: + +```properties +meshbayReleaseStoreFile=/path/to/meshbay-release.jks +meshbayReleaseStorePassword=... +meshbayReleaseKeyAlias=meshbay +meshbayReleaseKeyPassword=... +``` + +`apksigner verify --print-certs app-release.apk` prints the certificate's +SHA-256 fingerprint, the one the download page publishes (§8.2). A release +does not install over a debug build, or the reverse: the keys differ. + The security contract is also pinned from the Python suite by reading this source: `packages/meshbay-hub/tests/test_android_shell.py`. Not built yet: phone-specific behaviour (back button, network handover, -keeping a download alive with the screen off), signed releases. +keeping a download alive with the screen off), updates through a store. ## Icon diff --git a/packages/meshbay-android/app/build.gradle.kts b/packages/meshbay-android/app/build.gradle.kts index 3f29ac0..8afbc1a 100644 --- a/packages/meshbay-android/app/build.gradle.kts +++ b/packages/meshbay-android/app/build.gradle.kts @@ -8,6 +8,9 @@ val packageVersion = (JsonSlurper().parse(rootDir.resolve("../meshbay-client/pac as Map<*, *>)["version"] as String val versionParts = packageVersion.split(".").map { it.toInt() } +val releaseSigning = listOf("meshbayReleaseStoreFile", "meshbayReleaseStorePassword", + "meshbayReleaseKeyAlias", "meshbayReleaseKeyPassword") + android { namespace = "org.meshbay.client" compileSdk = 37 @@ -18,13 +21,21 @@ android { versionName = packageVersion versionCode = versionParts[0] * 10000 + versionParts[1] * 100 + versionParts[2] } + // The release key never enters the repository: its path and passwords come + // from ~/.gradle/gradle.properties. Without them a release build stops + // rather than signing with the debug key (see preReleaseBuild below). + if (releaseSigning.all { providers.gradleProperty(it).isPresent }) { + signingConfigs.create("release") { + storeFile = file(providers.gradleProperty("meshbayReleaseStoreFile").get()) + storePassword = providers.gradleProperty("meshbayReleaseStorePassword").get() + keyAlias = providers.gradleProperty("meshbayReleaseKeyAlias").get() + keyPassword = providers.gradleProperty("meshbayReleaseKeyPassword").get() + } + } buildTypes { getByName("release") { isMinifyEnabled = false - // A stand-in until the release key exists (Stage D12): the debug - // key, so a release build installs over a debug one and back - // without losing the account. Not a key to publish anything with. - signingConfig = signingConfigs.getByName("debug") + signingConfig = signingConfigs.findByName("release") } } compileOptions { @@ -81,6 +92,16 @@ val syncUi = tasks.register<SyncUi>("syncUi") { outputDir.set(layout.buildDirectory.dir("generated/ui-assets")) } +// Checked when a release is built, not when the project is configured, so a +// debug build and the unit tests need no key. +tasks.configureEach { + if (name == "preReleaseBuild") doFirst { + val missing = releaseSigning.filter { !providers.gradleProperty(it).isPresent } + if (missing.isNotEmpty()) throw GradleException( + "no release key: set ${missing.joinToString()} in ~/.gradle/gradle.properties") + } +} + androidComponents { onVariants { variant -> variant.sources.assets?.addGeneratedSourceDirectory(syncUi, SyncUi::outputDir) diff --git a/packages/meshbay-hub/tests/test_android_shell.py b/packages/meshbay-hub/tests/test_android_shell.py index e569bb7..71832f2 100644 --- a/packages/meshbay-hub/tests/test_android_shell.py +++ b/packages/meshbay-hub/tests/test_android_shell.py @@ -215,6 +215,17 @@ def test_the_version_is_the_packages_version(): assert not re.search(r'versionName = "\d', build) +def test_a_release_is_signed_with_the_release_key_or_not_built(): + """The key's path and passwords come from outside the repository, and a + release build without them stops instead of signing with the debug key.""" + build = _strip_js_comments(_read(APP / "build.gradle.kts")) + assert 'signingConfigs.getByName("debug")' not in build + assert 'signingConfigs.findByName("release")' in build + assert 'providers.gradleProperty("meshbayReleaseStorePassword")' in build + assert '"preReleaseBuild"' in build and "throw GradleException" in build + assert not re.search(r'storePassword = "', build) + + @pytest.mark.skipif(not os.environ.get("ANDROID_HOME") or shutil.which("java") is None, reason="no Android SDK in the environment") def test_the_jvm_unit_tests_pass(): |