Pin arti dependency and reconcile Tor activation docs

Address two follow-ups from the Tor (arti) MR review.

Supply-chain hardening for the arti-mobile AAR, a native artifact with
network-proxy privileges:

- Pin the gpmaven Maven source to an immutable commit SHA
  (guardianproject/gpmaven@b3ee2a6) instead of the mutable `master`
  branch, so a force-push or new commit can't silently change what we
  resolve.
- Verify the resolved AAR's SHA-256 at build time
  (verifyArtiChecksum, wired ahead of assemble/bundle). A mismatch fails
  the build before any APK is produced. Scoped to the one privileged
  artifact rather than enabling global dependency verification, which
  would force-verify every transitive dep.

Reconcile stale "apply on relaunch" / "next app launch" doc comments in
AppContext.ts, tor.ts, useTor.ts, TorController.java, and TorPlugin.java
with the actual behavior: the Advanced Settings toggle activates Tor
live via start/stop (arti starts/stops immediately, relay layer
remounts); the persisted flag only governs cold-launch auto-start.
This commit is contained in:
Alex Gleason
2026-06-13 14:32:28 -05:00
parent 0d334e89e7
commit 7ce1ca87e9
7 changed files with 95 additions and 17 deletions
+46 -1
View File
@@ -63,8 +63,10 @@ dependencies {
implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion" implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion"
implementation project(':capacitor-android') implementation project(':capacitor-android')
implementation 'com.squareup.okhttp3:okhttp:4.12.0' 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. // 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' implementation 'org.torproject:arti-mobile:1.7.0.1'
// arti pulls androidx.webkit in transitively but only at runtime; we // arti pulls androidx.webkit in transitively but only at runtime; we
// compile against ProxyController/WebViewFeature in TorController, so // compile against ProxyController/WebViewFeature in TorController, so
@@ -78,6 +80,49 @@ dependencies {
apply from: 'capacitor.build.gradle' 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 { try {
def servicesJSON = file('google-services.json') def servicesJSON = file('google-services.json')
if (servicesJSON.text) { if (servicesJSON.text) {
@@ -36,10 +36,12 @@ import okhttp3.Response;
* Capacitor WebView traffic (every {@code fetch} and relay {@code WebSocket}) * Capacitor WebView traffic (every {@code fetch} and relay {@code WebSocket})
* is routed through Tor. No changes to the TypeScript HTTP layer are needed. * is routed through Tor. No changes to the TypeScript HTTP layer are needed.
* *
* <p>Activation is "apply on relaunch": the enabled flag is persisted to * <p>The enabled flag is persisted to {@link SharedPreferences} by
* {@link SharedPreferences} by {@link TorPlugin} and read here at startup from * {@link TorPlugin} and read here at startup from {@link MainActivity}, so arti
* {@link MainActivity}. arti is started <em>before</em> the WebView loads so * auto-starts on a cold launch <em>before</em> the WebView loads — there is no
* there is no pre-bootstrap leak window. * 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.
* *
* <p>Pluggable transports (obfs4 via IPtProxy) are intentionally not wired up * <p>Pluggable transports (obfs4 via IPtProxy) are intentionally not wired up
* yet — the builder already exposes {@code setObfs4Port}/{@code setBridgeLines} * yet — the builder already exposes {@code setObfs4Port}/{@code setBridgeLines}
@@ -104,13 +106,17 @@ public class TorController {
return instance; 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) { public static boolean isEnabled(Context context) {
SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
return prefs.getBoolean(KEY_ENABLED, false); 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) { public static void setEnabled(Context context, boolean enabled) {
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
.edit() .edit()
@@ -10,9 +10,12 @@ import com.getcapacitor.annotation.CapacitorPlugin;
* Capacitor bridge for the Tor (arti) mode. * Capacitor bridge for the Tor (arti) mode.
* *
* <p>Mirrors {@link DittoNotificationPlugin}'s pattern: JS configures native * <p>Mirrors {@link DittoNotificationPlugin}'s pattern: JS configures native
* state, native owns the work. The enabled flag is persisted only — arti is * state, native owns the work. On a cold launch arti auto-starts from
* actually started at launch from {@link MainActivity} (apply on relaunch). * {@link MainActivity} based on the persisted flag. At runtime the settings
* Live bootstrap status is pushed to JS via the {@code torStatus} event. * 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.
* *
* <p>JS interface: see {@code src/lib/tor.ts}. * <p>JS interface: see {@code src/lib/tor.ts}.
*/ */
@@ -43,7 +46,11 @@ public class TorPlugin extends Plugin {
call.resolve(ret); 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 @PluginMethod
public void setEnabled(PluginCall call) { public void setEnabled(PluginCall call) {
Boolean enabled = call.getBoolean("enabled"); Boolean enabled = call.getBoolean("enabled");
+10 -1
View File
@@ -23,7 +23,16 @@ allprojects {
mavenCentral() mavenCentral()
// Guardian Project's experimental Maven repo, hosting the prebuilt // Guardian Project's experimental Maven repo, hosting the prebuilt
// org.torproject:arti-mobile AAR (Tor in Rust) used for the optional Tor mode. // 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" }
} }
} }
+5 -3
View File
@@ -290,9 +290,11 @@ export interface AppConfig {
lowBandwidthMode: boolean; lowBandwidthMode: boolean;
/** /**
* Route all app traffic through the Tor network (arti). **Android only** — * Route all app traffic through the Tor network (arti). **Android only** —
* ignored on web and iOS. The actual proxy is installed natively at app * ignored on web and iOS. The Advanced Settings toggle applies changes live
* startup, so changes take effect on the next launch (see `src/lib/tor.ts` * via the native `start`/`stop` bridge (arti starts/stops immediately and the
* and the native `TorController`). * 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. * Default: false.
*/ */
+4 -1
View File
@@ -21,7 +21,10 @@ export interface UseTor {
error: string | null; error: string | null;
/** Tor exit-node IP from the last successful check, when connected. */ /** Tor exit-node IP from the last successful check, when connected. */
exitIp: string | null; 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<void>; setEnabled: (enabled: boolean) => Promise<void>;
} }
+7 -1
View File
@@ -15,7 +15,13 @@ export interface TorStatusEvent {
export interface TorPlugin { export interface TorPlugin {
/** Whether Tor is enabled in persisted native preferences. */ /** Whether Tor is enabled in persisted native preferences. */
isEnabled(): Promise<{ enabled: boolean }>; 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<void>; setEnabled(options: { enabled: boolean }): Promise<void>;
/** Start arti now (live) and persist enabled=true. */ /** Start arti now (live) and persist enabled=true. */
start(): Promise<void>; start(): Promise<void>;