From 6578dc47a7d1f7946528e9540d9cfa2e9a5d88d2 Mon Sep 17 00:00:00 2001 From: Lena Date: Sun, 16 Aug 2026 00:00:00 +0000 Subject: app: release 0.2.0 Document the multi-remote release, publish matching store metadata, and advance the version once from the released 0.1.3 base. --- README | 195 ++++++++++++++++----- app/build.gradle | 4 +- fastlane/metadata/android/en-US/changelogs/5.txt | 6 + .../metadata/android/en-US/full_description.txt | 9 +- .../android/en-US/images/phoneScreenshots/1.png | Bin 158231 -> 156595 bytes .../android/en-US/images/phoneScreenshots/2.png | Bin 70395 -> 167742 bytes .../android/en-US/images/phoneScreenshots/3.png | Bin 85264 -> 84590 bytes .../android/en-US/images/phoneScreenshots/4.png | Bin 110091 -> 122571 bytes .../android/en-US/images/phoneScreenshots/5.png | Bin 0 -> 84384 bytes .../metadata/android/en-US/short_description.txt | 2 +- metadata/reproducible-builds.md | 38 +++- versions | 4 +- 12 files changed, 201 insertions(+), 57 deletions(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/5.txt create mode 100644 fastlane/metadata/android/en-US/images/phoneScreenshots/5.png diff --git a/README b/README index 2f321c9..f9722c9 100644 --- a/README +++ b/README @@ -1,21 +1,44 @@ rsend ===== -Push configured phone folders to a home SSH host using real rsync. +Push configured phone folders to home SSH hosts using real rsync. -rsend backs up folders such as DCIM, WhatsApp media, and call recordings to a -remote host over SSH. One-way push only, on an unmetered network by default, -periodic or manual. Small, boring, self-contained: real rsync and a pure-Go SSH -transport, both built from source and shipped inside the APK. No foreign -prebuilt binaries. +rsend backs up folders such as DCIM, WhatsApp media, and call recordings to +one or more remote hosts over SSH. One-way push only, on an unmetered network +by default, periodic or manual. Small, boring, self-contained: real rsync and a +pure-Go SSH transport, both built from source and shipped inside the APK. No +downloaded application .so files are repackaged. What it does ------------ -- Pushes one or more local folders to remote paths over rsync-over-SSH. -- Incremental: only changed files move; large videos resume (rsync --partial). -- Per-folder deletion policy: additive backup (default) or mirror (--delete). -- Runs on an unmetered network, periodically (WorkManager) or on demand. -- ed25519 key auth with strict, pinned host-key verification. +- Pushes one or more local folders to one or more SSH hosts over rsync, + each folder mapped to a named remote. +- Incremental: only changed files move; large videos resume. Partial transfers + are staged in a .rsend-partial directory, so an interrupted sync never + replaces a complete file on the server with a truncated one. +- Per-folder deletion policy: additive backup (default) or mirror + (--delete-after). A mirror follows rsync exactly: an empty readable source + empties its remote destination. A missing, non-directory, root, or unreadable + source fails before rsync starts. Deletions happen only after transfers, so + an interrupted run never removes the server's copy of a file whose + replacement has not arrived. Every item rsync removes there is named in the + log, and a mirror that ends with files unaccounted for is reported as failed. +- Rejects duplicate and ancestor remote destinations on the same configured + endpoint. The check is lexical, case-insensitive, and does not allow absolute + and relative paths to be mixed for one endpoint. Root, bare home, dot, and + parent components are rejected. Separate folder mappings use separate trees. +- Regular files and directories only. A source path that is itself a symlink is + refused. Symlinks, devices and sockets inside it are skipped and named in the + log ("skipping non-regular file"), which is what media backup wants; rsync's + -l would copy them if you ever need it. +- Native executables and APK packaging are aligned for Android devices with + 4 KB or 16 KB memory pages. +- Runs on an unmetered network, periodically (WorkManager) or on demand. A + manual sync obeys the same unmetered setting, and says so and stops rather + than queueing itself until the phone next sees wifi. +- Ed25519 client-key auth with strict, pinned host-key verification. rsend + accepts modern Ed25519, ECDSA, and RSA-SHA2 host keys, pins the key type with + the key, and warns loudly if a pinned host presents a different one. Layout @@ -25,26 +48,34 @@ Layout - app/ Android app (Kotlin, classic Views); bundles both as lib*.so. - ci/ CI-agnostic build and test scripts; the Makefile drives them. - metadata/ reproducibility and F-Droid build notes. +- THIRD_PARTY complete shipped-component inventory and license notices. +- APACHE-2.0 license terms for the shipped Apache-licensed components. - versions pinned toolchain and source versions; the single source of truth. Build ----- The repository is self-contained: clone it and the build fetches everything -else. The host prerequisites are a POSIX shell, coreutils, make, curl, python3, -tar, and git. The toolchain archives and rsync source are version- and -checksum-pinned by the build (see versions for every pin). +else. Toolchain setup supports x86_64 Linux with glibc or musl and selects a +separately checksummed Temurin JDK for each. The host prerequisites are a POSIX +shell, coreutils, make, curl, python3, tar, git, a C compiler for the Go race +detector, and the commands checked by ci/test.sh. Alpine also needs bash for +the NDK compiler launchers and gcompat for the NDK host binaries. The CI file +gives exact Debian and Alpine package lists. Toolchain archives, the SDK +platform revision, and rsync source are version- and checksum-pinned by the +build; see versions. make setup # provision the toolchain into $HOME/toolchains . "$HOME/toolchains/env.sh" # put it on PATH (do this in each shell) make # rsync (NDK) -> rsh (Go) -> APK - make test # Go, real-rsync integration, and Kotlin tests - make verify-repro # build twice, compare the unsigned APKs + make test # tests, race detector, lint, vulnerability scan + make verify-repro # clean commit, two paths, byte-identical APKs -`make test` also requires host rsync. The APK lands under +`make test` also requires host rsync and lets govulncheck query the public Go +vulnerability database. No source code is uploaded. The APK lands under app/build/outputs/apk/. It is unsigned by default and cannot be installed until -signed. Continuous integration runs the same scripts: ci/setup-toolchain.sh, -ci/build.sh, and ci/test.sh. +signed. .gitlab-ci.yml runs exactly those scripts and holds no build logic of +its own: ci/setup-toolchain.sh, ci/test.sh, and ci/build.sh. For a locally installable APK, create a keystore and a gitignored keystore.properties before building: @@ -63,32 +94,51 @@ app/build/outputs/apk/release/app-release.apk. Run --- -1. Install the APK and grant all-files access and notifications. +1. Install the APK and grant all-files access and notifications. All-files + access is required: a sync refuses to start without it because configured + storage may otherwise be unreadable. 2. Generate a key in the app and add the shown public key to the home host's - ~/.ssh/authorized_keys. -3. Set the remote host, user, and port; run Test connection and accept the - pinned host-key fingerprint. -4. Add folders, choose each folder's deletion policy, set the schedule. + ~/.ssh/authorized_keys. Import accepts unencrypted Ed25519 private keys only. +3. Add a remote (name, host, user, port); run Test connection and accept the + pinned host-key fingerprint. The dialog shows the key type next to the + fingerprint; verify it against the matching key file on the server. Nothing + is saved until you accept a key, so a failed test cannot disturb a remote + that already works. Repeat for every host you push to. +4. Add folders, pick each folder's remote and deletion policy, set the + schedule. Enabling mirror shows its exact deletion semantics before saving. 5. Tap Sync now, or wait for the periodic sync. Scheduled intervals have a 15-minute minimum imposed by WorkManager. For scheduled backup to run reliably, tap Battery and allow rsend to ignore -battery optimization. Phones with aggressive power management (Samsung, Xiaomi, -and others) otherwise delay or skip background jobs; see dontkillmyapp.com. +battery optimization. Phones with aggressive power management otherwise delay +or skip background jobs. Android 16 also counts long WorkManager foreground +runs against the app's job quota; a very large first backup may need several +runs and resumes from .rsend-partial each time. Server ------ -Any host with sshd and rsync works, old rsyncs included (stock macOS); rsend -pushes over rsync-over-SSH, not SFTP. rsync creates the final component of a -folder's remote path on its own; create deeper missing parents once with -mkdir -p on the server. - -The account is confined to "rsync into one folder and nothing else" by a forced -rrsync command, not by its shell. sshd runs that command through the account's -login shell, so the shell must be real: /bin/sh works, while /bin/false or -/sbin/nologin would break rsync. The confinement comes from key-only auth, the -forced command, and rrsync's write-only root. As root on the server: +Any rsync-compatible SSH service works, including ordinary shell accounts and +older rsync releases such as stock macOS. rsend pushes over rsync-over-SSH, not +SFTP. Remote paths may be absolute, relative, or home-relative, but mappings on +one endpoint must use either absolute or relative paths consistently. A path +must name a directory below root or home and cannot contain dot or parent +components. rsync creates the final component on its own; create deeper missing +parent directories once with mkdir -p on the server. A remote path cannot begin +with a colon: rsync interprets host::path as its unencrypted daemon protocol +instead of SSH. + +Protocol compatibility is not a security promise. This release bundles rsync +3.5.0, but it cannot update the rsync receiver or rrsync wrapper on the server. +Install upstream 3.5.0 or a vendor package carrying the August 2026 security +fixes on the server before relying on rrsync confinement. + +The recommended deployment confines the account to "rsync into one folder and +nothing else" with a forced rrsync command. This is not a protocol requirement. +sshd runs the forced command through the account's login shell, so the shell +must be real: /bin/sh works, while /bin/false or /sbin/nologin breaks rsync. The +confinement comes from key-only auth, the forced command, and rrsync's +write-only root. As root on the server: useradd -m -d /srv/backup -s /bin/sh rsendbackup passwd -l rsendbackup @@ -124,17 +174,80 @@ plain rsync transfer into its root. Remote paths are relative to that root, so set a folder's remote path to DCIM, not an absolute path. rrsync may instead live at /usr/share/rsync/scripts/rrsync; check command -v rrsync. Verify the host key from Test connection against -ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub on the server. +ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub on the server. The Test +connection dialog prints the key type it pinned; if it says something other +than ssh-ed25519, the server has no ed25519 host key and you should compare +against that type's .pub file instead. + +Interrupted transfers leave a .rsend-partial directory inside the destination +folder. rsync reuses it to resume and excludes it from the transfer, so it is +never deleted by a mirror. Abandoned partials from a sync that never resumed +can be removed by hand. + + +Upgrading +--------- +A configuration written by 0.1.x is read once and rewritten in the current +shape on first launch. The single remote becomes the sole entry, named after +its host; every safe folder points at it; schedule intervals below 15 minutes +are raised to WorkManager's minimum; and the pin 0.1.x kept in its own +known_hosts file is carried over. A pin that no longer describes that host and +port is dropped rather than failing the upgrade, which costs one Test +connection instead of the whole configuration. The exact old JSON is preserved +as config.json.0.1. A folder or endpoint that violates current safety rules is +omitted, preserved in that file, and reported in the app instead of causing the +rest of the configuration to be discarded. + +This is a deliberate, time-limited exception to the project's rule against +compatibility shims, kept because 0.1.3 is the last published release and is +therefore what every upgrading install has. It will be removed in 0.4.0: a +0.1.x configuration not opened by then is read as unparsable, preserved +alongside as config.json.broken, and the app starts empty. Debug ----- -- The in-app log viewer displays the plain-text rsync log; tap Refresh to reload. -- The log records the rsync arguments and complete process output for each run. +- The in-app log viewer displays the plain-text rsync log; tap Refresh to + reload. +- The log records the rsync arguments, the errors, and every file a mirror + deleted on the server ("del. "). Transfers are counted rather than + listed one line each: on a first sync of a large library the filenames would + push everything worth reading out of the capped log. The log is capped at + 512 KB and rolls over to sync.log.1. The viewer reads at most the newest + 64 KB across both files so layout cost stays bounded. Control characters are + escaped before writing so remote output cannot forge terminal log lines. +- If config.json cannot be read or parsed, rsend atomically renames it to a + unique config.json.broken file, starts from an empty configuration, and shows + the preserved filename in the app. It never deletes unreadable config bytes. - rsh transport: RSH_KEY, RSH_KNOWN_HOSTS, and RSH_PORT select the key, - known_hosts file, and port. Run rsh by hand to isolate SSH from rsync. + known_hosts file, and port; RSH_KEY_DATA passes the key itself, which is what + the app uses so the plaintext key never reaches the filesystem. Run rsh by + hand to isolate SSH from rsync. +- During transport, rsh offers only the host-key type already pinned for that + remote. Test connection prefers the pinned type so an untouched server + reproduces the same key, but can show a newly rotated type for explicit + approval. Re-run Test connection after deliberately rotating a host key. +- An IPv6 host goes in the host field as a bare literal (2001:db8::1); rsend + adds the brackets where rsh and rsync each need them. A bracketed literal is + accepted too. +- "foreground service refused" in the log means the sync ran as a plain + background job, which the system may stop early. Grant Battery > + unrestricted; rsync resumes from .rsend-partial on the next run either way. +- A host that drops packets instead of refusing them, a firewall closed to the + phone's current network for instance, stalls the connect for the full budget. + That budget is 10 seconds; RSH_CONNECT_TIMEOUT overrides it in whole seconds. + The log line reads "rsh: unreachable:", and the remaining folders bound to + that remote are skipped for the rest of the run rather than each paying the + timeout again. License ------- -GPLv3. Bundling rsync makes the whole app GPLv3; see LICENSE. +Copyright (C) 2026 Lena. GPLv3; see LICENSE. + +Bundling rsync makes the whole app GPLv3. THIRD_PARTY records every native and +JVM component shipped in the APK, its license, and its corresponding source. +Native source and tool archives are pinned and checksummed; JVM dependencies +use exact Maven versions. Test and build-only dependencies are listed +separately because they are not part of the APK. LICENSE and THIRD_PARTY are +packaged as plain APK assets, along with APACHE-2.0. diff --git a/app/build.gradle b/app/build.gradle index df41dc5..3c73d2e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -17,8 +17,8 @@ android { applicationId 'invalid.lena.rsend' minSdk 30 targetSdk 35 - versionCode 4 - versionName '0.1.3' + versionCode 5 + versionName '0.2.0' // Ship only the ABIs we build native libs for. ndk { abiFilters 'arm64-v8a', 'x86_64' } } diff --git a/fastlane/metadata/android/en-US/changelogs/5.txt b/fastlane/metadata/android/en-US/changelogs/5.txt new file mode 100644 index 0000000..3ad2f4b --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/5.txt @@ -0,0 +1,6 @@ +Adds multiple named remotes. Each folder selects its server, and an +unreachable host no longer blocks the others. Transfers resume safely, +SSH host keys are pinned, and 0.1.x settings retain every safe entry. +Mirror deletions run only after transfer and are reported. Scheduling +survives reboots without duplicate jobs. Includes rsync 3.5.0 security +fixes and 16 KB page-size support. diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt index 95d8690..5197a78 100644 --- a/fastlane/metadata/android/en-US/full_description.txt +++ b/fastlane/metadata/android/en-US/full_description.txt @@ -1,6 +1,6 @@ Pushes configured phone folders (DCIM, WhatsApp media, call recordings, and -so on) to a home SSH host using rsync over SSH. One-way push only, on an -unmetered network by default, periodic or on demand. +so on) to one or more home SSH hosts using rsync over SSH. One-way push +only, on an unmetered network by default, periodic or on demand. rsync and the SSH transport are built from source and shipped inside the APK. The app needs all-files access to read arbitrary backup folders such as OEM @@ -8,7 +8,8 @@ call-recording directories. Features: -* Incremental transfers; large videos resume (rsync --partial). +* Multiple named remotes; each folder picks its destination host. +* Incremental transfers; large videos resume from a private partial directory. * Per-folder deletion policy: additive backup or mirror. * Runs on an unmetered network, periodically or on demand. -* ed25519 key auth with strict, pinned host-key verification. +* Ed25519 client-key auth with strict, pinned host-key verification. diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png index 8a42552..293a40f 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png index a882b86..d7085c0 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png index d9aafd8..0a8a954 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png index d77c244..8eda900 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png new file mode 100644 index 0000000..c984306 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png differ diff --git a/fastlane/metadata/android/en-US/short_description.txt b/fastlane/metadata/android/en-US/short_description.txt index b18f43a..5fdced6 100644 --- a/fastlane/metadata/android/en-US/short_description.txt +++ b/fastlane/metadata/android/en-US/short_description.txt @@ -1 +1 @@ -Push phone folders to a home SSH host with rsync over SSH +Push phone folders to home SSH hosts with rsync over SSH diff --git a/metadata/reproducible-builds.md b/metadata/reproducible-builds.md index 6885aa7..ee3102d 100644 --- a/metadata/reproducible-builds.md +++ b/metadata/reproducible-builds.md @@ -8,25 +8,43 @@ Pinning Every tool and source version lives in ./versions: the rsync source, Go, JDK, Android NDK, SDK platform, build-tools, command-line tools, Gradle, the Android Gradle Plugin, and Kotlin. Directly downloaded archives have repository-owned -checksums. The gradle files and go.mod repeat some pins because gradle cannot -source a shell file; ci/test.sh fails if they drift from ./versions. +checksums. The SDK platform revision and android.jar bytes are checked after +sdkmanager installs them. Temurin's glibc and musl archives have separate +checksums and cache directories. The Gradle wrapper records its distribution +checksum too. The +gradle files and go.mod repeat some pins because gradle cannot source a shell +file. Gradle's verification-metadata.xml locks every resolved Maven and plugin +artifact by SHA-256. ci/test.sh fails if direct pins or packaged notices drift. + +After a deliberate dependency change, regenerate the checksum manifest while +running every Gradle path that resolves artifacts, then review its diff: + + gradle --write-verification-metadata sha256 \ + :app:testReleaseUnitTest :app:lintRelease :app:assembleRelease Determinism ----------- - rsh (Go): built with the NDK clang, -trimpath, -ldflags "-s -w -buildid=", - and CGO_CFLAGS=-ffile-prefix-map to drop build paths. + CGO_CFLAGS=-ffile-prefix-map to drop build paths, and 16 KB ELF alignment. - rsync (C): cross-compiled with -ffile-prefix-map and stripped; optional dependencies are disabled and zlib and popt come from rsync's bundled copies, - so the binary needs nothing outside Bionic. + so the binary needs nothing outside Bionic. Stack protection, fortified libc + calls, RELRO, immediate binding, and a non-executable stack are explicit. + Configure's host-dependent nobody group is pinned explicitly. Linker flags + give it 16 KB ELF alignment too. - APK: no code shrinking (minifyEnabled false), the Google dependency-metadata block is dropped (dependenciesInfo off), and native libs use legacy packaging. -Verify locally by building the complete unsigned APK twice and comparing it +Verify locally from a clean commit. The script extracts that commit twice at +different absolute paths, builds both complete unsigned APKs, and compares them byte for byte: make verify-repro +A local gitignored keystore.properties is harmless: git archive cannot copy it +into either clean build tree. + F-Droid ------- @@ -39,11 +57,17 @@ rsync/build.sh and rsh/build.sh; Gradle then assembles the APK. The release build is unsigned unless a local keystore.properties is present, so F-Droid signs the published APK with its own key. +Each release recipe must set the go srclib ref to the exact GO_VERSION in +./versions. In particular, the published 0.1.3 recipe's go1.25.0 ref must not +be reused for a release built with a newer Go patch version. + All-files access ---------------- rsend requests MANAGE_EXTERNAL_STORAGE. It is required: backup targets such as OEM call-recording directories and app-specific media folders live outside the scoped-media buckets, and rsync needs real filesystem paths rather than SAF -content URIs. The app makes no network access other than the SSH connection to -the host the user configures. +content URIs. At runtime the app makes no network access other than SSH +connections to hosts the user configures. Android backup and device-transfer +rules exclude all app data, including the encrypted identity, host pins, paths, +and logs. diff --git a/versions b/versions index 3c2d837..d52e433 100644 --- a/versions +++ b/versions @@ -5,8 +5,8 @@ # App release identity. Bump both together; the release's changelog file is # fastlane/metadata/android/en-US/changelogs/${APP_VERSION_CODE}.txt. -APP_VERSION=0.1.3 -APP_VERSION_CODE=4 +APP_VERSION=0.2.0 +APP_VERSION_CODE=5 # Native rsync, built from source via the Android NDK. The tarball sha256 lives # in rsync/rsync-${RSYNC_VERSION}.tar.gz.sha256, which the build verifies. -- cgit v1.2.3