feat(android): config import/export — clipboard, QR, deep link, share#266
Conversation
…, 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>
therealaleph
left a comment
There was a problem hiding this comment.
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:
- Attacker crafts
mhrv://<base64-of-attacker-config>— their ownscript_ids+auth_key - Posts the link in a Telegram channel claiming "free fast deployment"
- User taps → mhrv-rs opens → config silently overwrites → traffic now flows through attacker's Apps Script
- 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>
|
@therealaleph fixed, please check if all is fine. |
- 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>
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:
mhrv://...or raw JSON is detected in clipboard. One tap to import, clipboard cleared after.mhrv://...in any app/chat auto-opens mhrv-rs and imports.Export:
Security:
What's shared vs not
Changes
ui/ConfigSharing.kt(new)ConfigStore.ktencode(),decode(),looksLikeConfig(),loadFromJson()(deduplicated fromload())MainActivity.ktmhrv://scheme)AndroidManifest.xmlbuild.gradle.ktsHomeScreen.ktConfigSharingBarwired in above Mode dropdownstrings.xmlxml/file_paths.xml(new)Test plan
mhrv://...opens app and imports🤖 Generated with Claude Code