diff --git a/android/app/build.gradle b/android/app/build.gradle index fbbd4f3e..59002384 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -63,8 +63,10 @@ dependencies { implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion" implementation project(':capacitor-android') implementation 'com.squareup.okhttp3:okhttp:4.12.0' - // Tor in Rust (arti) — prebuilt AAR from Guardian Project's gpmaven repo. + // Tor in Rust (arti) — prebuilt AAR from Guardian Project's gpmaven repo + // (source pinned to an immutable commit in the root build.gradle). // Provides org.torproject.arti.ArtiProxy used by TorController. + // The resolved AAR is checksum-verified below (verifyArtiChecksum). implementation 'org.torproject:arti-mobile:1.7.0.1' // arti pulls androidx.webkit in transitively but only at runtime; we // compile against ProxyController/WebViewFeature in TorController, so @@ -78,6 +80,49 @@ dependencies { apply from: 'capacitor.build.gradle' +// Supply-chain pin for the arti-mobile AAR. It carries a native library with +// network-proxy privileges and is sourced from Guardian Project's gpmaven repo, +// so we verify the resolved artifact's SHA-256 against a value pinned here. +// A mismatch fails the build before any APK is assembled. To bump arti, update +// the version + repo commit (root build.gradle) and replace this checksum after +// re-verifying a fresh download. +ext.artiMobileSha256 = 'cbdb34ce3cdb32f755f25f6dd05a2d1eb9a44025a17ec9202729816e2a3af05b' + +task verifyArtiChecksum { + doLast { + def artifact = configurations.releaseRuntimeClasspath + .resolvedConfiguration + .resolvedArtifacts + .find { it.moduleVersion.id.group == 'org.torproject' && it.moduleVersion.id.name == 'arti-mobile' } + + if (artifact == null) { + throw new GradleException("arti-mobile artifact not found on the runtime classpath; cannot verify Tor native library.") + } + + def actual = java.security.MessageDigest.getInstance("SHA-256") + .digest(artifact.file.bytes) + .collect { String.format('%02x', it) } + .join('') + + if (actual != project.ext.artiMobileSha256) { + throw new GradleException( + "arti-mobile AAR checksum mismatch!\n" + + " expected: ${project.ext.artiMobileSha256}\n" + + " actual: ${actual}\n" + + " file: ${artifact.file}\n" + + "Refusing to build a Tor proxy from an unverified native artifact." + ) + } + logger.lifecycle("Verified arti-mobile AAR checksum (${actual}).") + } +} + +afterEvaluate { + tasks.matching { it.name.startsWith('assemble') || it.name.startsWith('bundle') }.configureEach { + dependsOn verifyArtiChecksum + } +} + try { def servicesJSON = file('google-services.json') if (servicesJSON.text) { diff --git a/android/app/src/main/java/spot/agora/app/TorController.java b/android/app/src/main/java/spot/agora/app/TorController.java index e5f4173d..c20d452e 100644 --- a/android/app/src/main/java/spot/agora/app/TorController.java +++ b/android/app/src/main/java/spot/agora/app/TorController.java @@ -36,10 +36,12 @@ import okhttp3.Response; * Capacitor WebView traffic (every {@code fetch} and relay {@code WebSocket}) * is routed through Tor. No changes to the TypeScript HTTP layer are needed. * - *

Activation is "apply on relaunch": the enabled flag is persisted to - * {@link SharedPreferences} by {@link TorPlugin} and read here at startup from - * {@link MainActivity}. arti is started before the WebView loads so - * there is no pre-bootstrap leak window. + *

The enabled flag is persisted to {@link SharedPreferences} by + * {@link TorPlugin} and read here at startup from {@link MainActivity}, so arti + * auto-starts on a cold launch before the WebView loads — there is no + * pre-bootstrap leak window. Beyond that, activation is live: the settings + * toggle calls {@link #start}/{@link #stop} (bridged through {@link TorPlugin}), + * which start or stop arti immediately while also updating the persisted flag. * *

Pluggable transports (obfs4 via IPtProxy) are intentionally not wired up * yet — the builder already exposes {@code setObfs4Port}/{@code setBridgeLines} @@ -104,13 +106,17 @@ public class TorController { return instance; } - /** Whether Tor is enabled in persisted preferences (read at next launch). */ + /** Whether Tor is enabled in persisted preferences (read at cold-launch startup). */ public static boolean isEnabled(Context context) { SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); return prefs.getBoolean(KEY_ENABLED, false); } - /** Persist the enabled flag. Takes effect on the next app launch. */ + /** + * Persist the enabled flag only. This controls whether arti auto-starts on + * the next cold launch; it does not start or stop arti now. For live + * activation call {@link #start}/{@link #stop}, which also persist the flag. + */ public static void setEnabled(Context context, boolean enabled) { context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) .edit() diff --git a/android/app/src/main/java/spot/agora/app/TorPlugin.java b/android/app/src/main/java/spot/agora/app/TorPlugin.java index 124b3d2d..9ed57f40 100644 --- a/android/app/src/main/java/spot/agora/app/TorPlugin.java +++ b/android/app/src/main/java/spot/agora/app/TorPlugin.java @@ -10,9 +10,12 @@ import com.getcapacitor.annotation.CapacitorPlugin; * Capacitor bridge for the Tor (arti) mode. * *

Mirrors {@link DittoNotificationPlugin}'s pattern: JS configures native - * state, native owns the work. The enabled flag is persisted only — arti is - * actually started at launch from {@link MainActivity} (apply on relaunch). - * Live bootstrap status is pushed to JS via the {@code torStatus} event. + * state, native owns the work. On a cold launch arti auto-starts from + * {@link MainActivity} based on the persisted flag. At runtime the settings + * toggle activates Tor live via {@link #start}/{@link #stop}, which start or + * stop arti immediately and update the persisted flag. ({@link #setEnabled} + * persists the flag only, without touching the running proxy.) Live bootstrap + * status is pushed to JS via the {@code torStatus} event. * *

JS interface: see {@code src/lib/tor.ts}. */ @@ -43,7 +46,11 @@ public class TorPlugin extends Plugin { call.resolve(ret); } - /** Persist the enabled flag. Applied on the next app launch. */ + /** + * Persist the enabled flag only, without starting or stopping arti now. + * Controls whether arti auto-starts on the next cold launch. For live + * activation use {@link #start}/{@link #stop}. + */ @PluginMethod public void setEnabled(PluginCall call) { Boolean enabled = call.getBoolean("enabled"); diff --git a/android/build.gradle b/android/build.gradle index ef049aaf..d1cf3dae 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -23,7 +23,16 @@ allprojects { mavenCentral() // Guardian Project's experimental Maven repo, hosting the prebuilt // org.torproject:arti-mobile AAR (Tor in Rust) used for the optional Tor mode. - maven { url "https://raw.githubusercontent.com/guardianproject/gpmaven/master" } + // + // Pinned to an immutable commit SHA rather than the mutable `master` + // branch: this artifact ships a native library with network-proxy + // privileges, so we don't want a force-push or new commit to gpmaven + // silently changing what we resolve. To bump arti, update both the + // commit below and the checksum pin in `app/build.gradle`, and re-verify + // the SHA-256 against a fresh download. + // + // Commit: guardianproject/gpmaven@b3ee2a63eec4ce37ea22fcc6b1ff009f406f2b13 + maven { url "https://raw.githubusercontent.com/guardianproject/gpmaven/b3ee2a63eec4ce37ea22fcc6b1ff009f406f2b13" } } } diff --git a/src/contexts/AppContext.ts b/src/contexts/AppContext.ts index 97d5f90e..baceb8bd 100644 --- a/src/contexts/AppContext.ts +++ b/src/contexts/AppContext.ts @@ -290,9 +290,11 @@ export interface AppConfig { lowBandwidthMode: boolean; /** * Route all app traffic through the Tor network (arti). **Android only** — - * ignored on web and iOS. The actual proxy is installed natively at app - * startup, so changes take effect on the next launch (see `src/lib/tor.ts` - * and the native `TorController`). + * ignored on web and iOS. The Advanced Settings toggle applies changes live + * via the native `start`/`stop` bridge (arti starts/stops immediately and the + * relay layer remounts). The flag is also persisted natively so arti + * auto-starts on the next cold launch (see `src/lib/tor.ts` and the native + * `TorController`). * * Default: false. */ diff --git a/src/hooks/useTor.ts b/src/hooks/useTor.ts index de4e837b..045d24ab 100644 --- a/src/hooks/useTor.ts +++ b/src/hooks/useTor.ts @@ -21,7 +21,10 @@ export interface UseTor { error: string | null; /** Tor exit-node IP from the last successful check, when connected. */ exitIp: string | null; - /** Persist the enabled flag natively. Takes effect on next app launch. */ + /** + * Toggle Tor live: starts/stops arti immediately and persists the flag + * natively (so it auto-starts again on the next cold launch). + */ setEnabled: (enabled: boolean) => Promise; } diff --git a/src/lib/tor.ts b/src/lib/tor.ts index f2198f8f..2578555d 100644 --- a/src/lib/tor.ts +++ b/src/lib/tor.ts @@ -15,7 +15,13 @@ export interface TorStatusEvent { export interface TorPlugin { /** Whether Tor is enabled in persisted native preferences. */ isEnabled(): Promise<{ enabled: boolean }>; - /** Persist the enabled flag. Applied on the next app launch. */ + /** + * Persist the enabled flag only, without starting or stopping arti now. + * The persisted value controls whether arti auto-starts on the next cold + * launch. For live activation use {@link TorPlugin.start} / + * {@link TorPlugin.stop} (what the settings toggle calls), which both + * change state immediately *and* persist the flag. + */ setEnabled(options: { enabled: boolean }): Promise; /** Start arti now (live) and persist enabled=true. */ start(): Promise;