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