Skip to content

feat(android): config import/export — clipboard, QR, deep link, share#266

Merged
therealaleph merged 2 commits intotherealaleph:mainfrom
yyoyoian-pixel:feat/config-import-export
Apr 26, 2026
Merged

feat(android): config import/export — clipboard, QR, deep link, share#266
therealaleph merged 2 commits intotherealaleph:mainfrom
yyoyoian-pixel:feat/config-import-export

Conversation

@yyoyoian-pixel
Copy link
Copy Markdown
Contributor

Summary

One-tap config sharing between devices. Export your config as a QR code or compressed link, import by pasting, scanning, or tapping a deep link.

Features

Import:

  • Clipboard paste — banner appears at the top when mhrv://... or raw JSON is detected in clipboard. One tap to import, clipboard cleared after.
  • QR scanner — opens camera in portrait, scans and imports.
  • Deep link — tapping mhrv://... in any app/chat auto-opens mhrv-rs and imports.

Export:

  • Unified dialog — QR code + compressed hash + copy button in one view.
  • Share sheet — sends QR image + text hash together via Android share (Telegram, WhatsApp, email, etc.).
  • Compact encoding — only non-default fields, DEFLATE compressed before base64. Typical config is ~200 chars vs ~800 raw.

Security:

  • Warning: "This includes your auth_key. Only share with people you trust."

What's shared vs not

Included Not included (device-specific)
mode, script_ids, auth_key listen_host, listen_port, socks5_port
google_ip (if changed), front_domain (if changed) connectionMode (VPN/proxy)
sni_hosts, verify_ssl, log_level, parallel_relay splitMode, splitApps
upstream_socks5, passthrough_hosts uiLang

Changes

File What
ui/ConfigSharing.kt (new) Import/export composables, QR generation, scanner launcher
ConfigStore.kt encode(), decode(), looksLikeConfig(), loadFromJson() (deduplicated from load())
MainActivity.kt Deep link intent handling (mhrv:// scheme)
AndroidManifest.xml Deep link intent filter, FileProvider for QR sharing, ZXing portrait lock
build.gradle.kts ZXing core + zxing-android-embedded dependencies
HomeScreen.kt ConfigSharingBar wired in above Mode dropdown
strings.xml 14 new string resources
xml/file_paths.xml (new) FileProvider paths for cache dir

Test plan

  • Export → copy → paste on another device → import works
  • Export → share via Telegram → recipient pastes → import works
  • QR code scans successfully from another phone's screen
  • Deep link mhrv://... opens app and imports
  • Clipboard banner disappears after import (clipboard cleared)
  • Raw JSON in clipboard also detected and importable
  • QR scanner opens in portrait

🤖 Generated with Claude Code

…, and share sheet

- Clipboard paste: banner auto-detects mhrv:// or raw JSON in clipboard,
  one tap to import. Clipboard cleared after successful import.
- Export dialog: QR code + compressed hash + copy button + Android share
  sheet (sends QR image + text together).
- QR scanner: ZXing embedded scanner in portrait orientation.
- Deep link: mhrv:// URIs auto-open the app and import the config.
- Compact encoding: only non-default fields included, DEFLATE compressed
  before base64. Accepts both compressed and raw JSON on import.
- ConfigStore.loadFromJson() deduplicated — shared by file load + import.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added the type: feature feat: PR — auto-applied by release-drafter label Apr 26, 2026
Copy link
Copy Markdown
Owner

@therealaleph therealaleph left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @yyoyoian-pixel — feature is well-built (clipboard banner + QR + DEFLATE compression + careful field-by-field include/exclude list is exactly right). One blocker on security grounds before this can merge:

🚨 Blocker — handleDeepLink auto-imports without confirmation. The QR scanner path is safe; the deep link path is the inconsistency:

MainActivity.kt:

private fun handleDeepLink(intent: Intent?) {
    val data = intent?.data ?: return
    if (data.scheme != "mhrv") return
    val encoded = data.toString()
    val cfg = ConfigStore.decode(encoded) ?: return
    ConfigStore.save(this, cfg)                    // ← overwrites existing config
    android.widget.Toast.makeText(this, "Config imported", ...).show()
}

vs. ConfigSharing.kt scanner path (correct):

val scanLauncher = rememberLauncherForActivityResult(ScanContract()) { result ->
    val decoded = ConfigStore.decode(scanned)
    if (decoded != null) {
        pendingImport = decoded
        showImportConfirm = true   // ← user has to confirm
    }
}

Why this matters for our threat model:

This project's user base is exactly the population an attacker would want to MITM — Iranian users who can't use a regular VPN, who often discover tools via Telegram/forum links, who tap things without scrutiny. The attack flow:

  1. Attacker crafts mhrv://<base64-of-attacker-config> — their own script_ids + auth_key
  2. Posts the link in a Telegram channel claiming "free fast deployment"
  3. User taps → mhrv-rs opens → config silently overwrites → traffic now flows through attacker's Apps Script
  4. Attacker has full visibility into URLs visited and can MITM HTTPS (mhrv-rs's MITM CA is already trusted by the device, and the CA private key is in the attacker-controlled deployment indirectly via the relay path — wait, actually the CA key stays local; but the attacker doesn't need to break TLS, they just see all the destination URLs and can return arbitrary responses)

The Toast only appears AFTER the config has already been overwritten. By the time the user reads it, they're already routing traffic through the attacker.

The fix is small — make handleDeepLink use the same showImportConfirm modal the scanner path does:

private fun handleDeepLink(intent: Intent?) {
    val data = intent?.data ?: return
    if (data.scheme != "mhrv") return
    val encoded = data.toString()
    val cfg = ConfigStore.decode(encoded) ?: return
    // Don't auto-save. Stash for the UI to confirm.
    pendingDeepLinkImport = cfg
}

And then in AppRoot() / HomeScreen, surface a pendingDeepLinkImport-driven dialog using the same showImportConfirm UX — show the new script_ids[0..3], the mode, and the tunnel_node_url (if Full mode), with "Import (replaces current config)" and "Cancel" buttons. The dialog should explicitly note that imported deployment IDs and auth_key will route the user's traffic.

The clipboard banner path is fine because the user has to tap "Import" — that's already explicit consent.

Other points (non-blocking, suggestions):

  • The export warning is good ("This includes your auth_key. Only share with people you trust.") — same warning text on the import side would be appropriate, with the inverse phrasing: "Importing routes your traffic through the deployment IDs in this config. Only import from trusted sources."
  • Consider adding a fingerprint display on the import-confirm dialog: hash of (script_ids[0], auth_key[..8]) shown as 4-byte hex, so users can verify with the sender out-of-band that they got the right config (not a swapped one).
  • The mhrv:// scheme is fairly generic — collision risk with any future app using the same scheme. mhrv-rs:// would be slightly more specific. Not blocking but worth thinking about.

The clipboard / QR / share / compact-encoding parts are excellent. Once the deep-link auto-import becomes confirm-first, this is mergeable. Happy to re-review on push.


[reply via Anthropic Claude | reviewed by @therealaleph]

…s:// scheme

Security fix: deep link (mhrv-rs://) no longer auto-imports config.
Stashes decoded config for UI confirmation dialog — same flow as
clipboard paste and QR scan.

Import confirmation dialog now shows:
- Trust warning: "Importing routes your traffic through the deployment
  IDs in this config. Only import from trusted sources."
- Mode and deployment ID count with first 3 IDs previewed
- Explicit Import / Cancel buttons

Also:
- Renamed scheme from mhrv:// to mhrv-rs:// (less collision risk)
- Deduplicated import dialog into shared ImportConfirmDialog composable

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@yyoyoian-pixel
Copy link
Copy Markdown
Contributor Author

@therealaleph fixed, please check if all is fine.

@therealaleph therealaleph merged commit 1c9d288 into therealaleph:main Apr 26, 2026
1 check passed
therealaleph added a commit that referenced this pull request Apr 26, 2026
- mhrv-rs:// deep links, QR scanner, clipboard banner, share sheet
- DEFLATE-compressed base64 encoding (~200 chars vs ~800 raw)
- Every import path requires explicit user confirmation; the dialog
  shows the new deployment IDs and a trust warning so an attacker
  posting a malicious mhrv-rs:// link in a public channel can't
  silently overwrite a user's auth_key + script_ids
- ZXing for QR generation/scanning (no Google Play Services)

Closes #266. Thanks @yyoyoian-pixel — the rebase from auto-import
to confirmation-gated import is exactly the right shape.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type: feature feat: PR — auto-applied by release-drafter

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants