diff --git a/AUDIT.md b/AUDIT.md new file mode 100644 index 0000000000..c81491e0e5 --- /dev/null +++ b/AUDIT.md @@ -0,0 +1,473 @@ +# Security Audit Report + +## Tangem Android Wallet — Source Code Review + +--- + +| | | +|---|---| +| **Subject** | `tangem-app-android` — Tangem cryptocurrency wallet for Android | +| **Upstream** | `https://github.com/tangem/tangem-app-android` | +| **Commit reviewed** | `358144ee61a` (branch `master`) | +| **Report date** | 17 August 2026 | +| **Review type** | Static source code review (manual, targeted) | +| **Codebase size** | ~292 Gradle modules · 7,719 Kotlin files · 641 XML · 5 Java | +| **Objective** | Detect malware, backdoors, information leaks, obfuscated code, and remote code-download behaviour | + +--- + +## 1. Executive Summary + +**The reviewed source code is clean.** No malicious code, backdoor, data-exfiltration channel, +obfuscated logic, or remote code-download ("dropper") capability was identified. + +Beyond the absence of malicious code, the codebase demonstrates **deliberate, consistent defensive +engineering** of a standard appropriate to software that custodies private keys. Several controls +found here are ones that trojanised or negligent applications characteristically lack: JavaScript +is disabled in WebViews with no JavaScript bridge present at all, certificate validation is left +entirely intact, analytics identifiers are cryptographically hashed rather than transmitted raw, +and the application manifest actively *removes* location and microphone permissions that bundled +third-party SDKs attempt to introduce. + +Ten categories of malicious or leak-prone behaviour were examined. All ten returned negative. +Three informational observations are recorded in §5 — none is a vulnerability, and none affects +the verdict. + +### Findings Summary + +| Severity | Count | Detail | +|---|---|---| +| **Critical** | **0** | — | +| **High** | **0** | — | +| **Medium** | **0** | — | +| **Low** | **0** | — | +| Informational | 3 | Operational notes for downstream forks (§5) | + +### Verdict + +> **CLEAN.** Nothing in the reviewed first-party source behaves maliciously or leaks sensitive +> data. The security posture is materially better than typical for the category. + +Readers should note the scope boundaries in §6 — most importantly that Tangem's own SDKs are +consumed as **pre-built binary artifacts** and were therefore outside the reach of this source +review. + +--- + +## 2. Methodology + +The review targeted the behaviours that distinguish malicious software from ordinary application +code, rather than attempting an exhaustive line-by-line reading of 7,719 files. Each question +below was resolved by exhaustive pattern analysis across the full tree, followed by manual reading +of every candidate hit in context. + +| # | Question put to the code | Result | +|---|---|---| +| 1 | Can it load or execute code obtained at runtime? | **No** | +| 2 | Can it execute shell commands or spawn processes? | **No** | +| 3 | Does it ship native binaries or opaque blobs? | **No** | +| 4 | Does key material reach logs, storage, or the network? | **No** | +| 5 | Does it contact hidden or hostile endpoints? | **No** | +| 6 | Are live credentials committed to the repository? | **No** | +| 7 | Can the build system pull hostile artifacts? | **No** | +| 8 | Does telemetry leak personally identifying data? | **No** — pseudonymised | +| 9 | Is transport security weakened anywhere? | **No** — hardened | +| 10 | Are permissions over-broad or components exposed? | **No** — minimised | + +A finding was only cleared once the underlying code was read and understood in context. Two +pattern classes produce **misleading automated results** and are documented explicitly in §4 so +that any repeat or automated review does not raise a false alarm. + +--- + +## 3. Detailed Findings + +### 3.1 Runtime Code Loading and Dropper Behaviour — CLEAN + +No occurrence of `DexClassLoader`, `PathClassLoader`, `BaseDexClassLoader`, +`InMemoryDexClassLoader`, or any `dalvik.system` API exists in the repository. The application +cannot load code it did not ship with. + +**Process execution.** Exactly one match tree-wide: + +```kotlin +// app/src/main/java/com/tangem/tap/common/finisher/AndroidAppFinisher.kt:25 +Runtime.getRuntime().exit(0) +``` + +This terminates the application's own process. It executes nothing. + +**Native code.** No `.so` libraries, no JNI, no NDK build inputs. Only two `.jar` files exist in +the tree: + +| File | Size | Assessment | +|---|---|---| +| `gradle/wrapper/gradle-wrapper.jar` | 55.6 KB | Standard Gradle 8.14.1 wrapper | +| `app/libs/dexprotector-annotations.jar` | 3.5 KB | Compile-time annotations only (§3.9) | + +**Reflection.** Searches for `Class.forName`, `getDeclaredMethod`, `getDeclaredField`, and +`setAccessible` returned **zero** genuine results. There is no reflective dispatch to hide +behaviour behind. + +### 3.2 Obfuscation and Encoded Payloads — CLEAN + +A scan for long opaque literals (≥200 characters of Base64-like text) returned three hits, all in +`libs/visa/`: + +- `TangemPaymentAccountRegistry.java` +- `TangemBridgeProcessor.java` +- `TangemPaymentAccount.java` + +Each declares `public static final String BINARY = "6080604052…"`. The `6080604052` prefix is the +standard **EVM (Solidity) contract constructor prologue**. These are Web3j-generated smart-contract +wrappers carrying on-chain deployment bytecode — an expected artifact of the Visa payment-account +integration, not concealed program logic. + +All eight `Base64.decode` call sites serve declared cryptographic purposes — AES/RSA key and IV +decoding in `RainCryptoUtil`, `DefaultAuthNonceDecryptor`, and `SetVisaPinCodeUseCase`, plus swap +transaction decoding. None reconstructs an executable payload. + +### 3.3 Private Key and Seed Phrase Handling — CLEAN + +This is the highest-consequence area for a wallet. It is handled correctly. + +**Key material never reaches logs.** Every logging call (`Log.*`, `Timber.*`, `println`) was +cross-referenced against *mnemonic, seed, privateKey, passphrase, secret, accessCode, passcode*. +**No true positives.** The only matches were UI-state fields on onboarding screens +(`dialog.dismissButtonText`, `dialog.dismissWarningColor`) — incidental matches on the substring +"dismiss", carrying no secret data. + +**Key material never reaches the network.** The sole endpoint whose name suggests otherwise is +`v1/seedphrase-notification/{wallet_id}`. Its complete payload is: + +```kotlin +data class SeedPhraseNotificationDTO(val status: Status) + +enum class Status { NOT_NEEDED, NOTIFIED, DECLINED, CONFIRMED, REJECTED, ACCEPTED } +``` + +A single enum recording whether the user was reminded to back up their phrase. No phrase material +is transmitted. + +**Export is authentication-gated.** `ExportSeedPhraseUseCase` delegates to +`DefaultHotWalletAccessor.exportSeedPhrase()`, which routes through `hotSdkRequest()` — a path +requiring password or biometric unlock (`HotAuth.Password` / `HotAuth.Biometry`) before the SDK +releases the mnemonic. Wrong-password retry and biometric lockout are handled explicitly. + +**Exported phrases have exactly two consumers**, both user-initiated display flows: + +- `features/hot-wallet/impl/…/viewphrase/model/ViewPhraseModel.kt` — user views their own backup +- `features/onboarding-v2/impl/…/upgradewallet/model/MultiWalletUpgradeWalletModel.kt` + +Neither transmits, persists, nor logs the value. + +**Supporting controls.** `clearContextualUnlock()` and `clearAllContextualUnlock()` purge unlocked +wallet handles from memory; `LockUserWalletsTimer` auto-locks wallets on a timer via WorkManager. + +### 3.4 Network Endpoints — CLEAN + +Every hardcoded host in the tree was enumerated and classified. All fall into legitimate +categories: + +| Category | Examples | +|---|---| +| Public blockchain RPC | Ankr, publicnode, dRPC, thirdweb, Infura, QuikNode, GetBlock, Arbitrum, Base, Solana, Tron, Kaspa, Polkadot | +| Tangem infrastructure | `api.tangem.com`, `express.tangem.com`, `buy.tangem.com` | +| Payment / staking partners | MoonPay, Mercuryo, P2P.org, StakeKit, Paera | +| Documentation / schema | `schemas.android.com`, `developer.android.com`, EIP references | + +No unexplained endpoint, dead-drop host, dynamic-DNS domain, or hardcoded IP address was found. + +### 3.5 Transport Security — HARDENED + +`app/src/main/res/xml/network_security_config.xml`: + +```xml + + + + + + + +``` + +- Cleartext HTTP disabled application-wide. +- **System trust anchors only** — user-installed CAs are excluded, resisting casual interception + proxies. +- No `debug-overrides` block. + +Searches for `X509TrustManager`, `checkServerTrusted`, `HostnameVerifier`, `ALLOW_ALL`, and +`SSLContext` returned **zero** results. No certificate-validation bypass exists anywhere in the +repository — a common weakness that is simply absent here. + +**WebView.** `core/ui/…/webview/WebViewExt.kt` sets `javaScriptEnabled = false`. There is no +`addJavascriptInterface` or `@JavascriptInterface` anywhere in the tree. The JavaScript-bridge +attack surface does not exist by construction. + +### 3.6 Permissions and Exported Components — MINIMISED + +Complete permission set across all manifests: + +`INTERNET` · `ACCESS_NETWORK_STATE` · `CAMERA` (QR scanning) · `NFC` (card scanning) · +`USE_BIOMETRIC` · `VIBRATE` · `WAKE_LOCK` · `HIDE_OVERLAY_WINDOWS` · `AD_ID` + +**Absent:** SMS, contacts, call log, location, microphone, external storage, accessibility +services, `SYSTEM_ALERT_WINDOW`, `REQUEST_INSTALL_PACKAGES`, `RECEIVE_BOOT_COMPLETED`. The set +contains nothing a wallet does not require. + +Two measures are worth drawing out, as both are affirmative hardening rather than mere restraint: + +**1. Overlay defence.** `HIDE_OVERLAY_WINDOWS` is requested to defeat tapjacking — a known vector +for hijacking wallet transaction-confirmation dialogs. + +**2. Permission stripping.** The manifest actively removes permissions that bundled third-party +SDKs would otherwise introduce through manifest merging: + +```xml + + + +``` + +Denying location and microphone access to advertising and analytics SDKs is a privacy measure that +argues directly against hostile intent. + +**Exported components.** Exactly one: `MainActivity`, necessarily exported as the launcher. Its +intent filters are tightly scoped — the `LAUNCHER` category, NFC NDEF discovery restricted to +`tangem.com` / `www.tangem.com` / `app.tangem.com` under `/ndef` path prefixes, and the `wc:` and +`tangem://wc` WalletConnect schemes. + +The single declared service, `TangemPushNotificationService` (Firebase Cloud Messaging), is +`exported="false"`. There are **no broadcast receivers, no boot receivers, and no content +providers**. The only `WorkManager` job in the application is `LockUserWalletsTimer` — which +*locks* wallets. + +### 3.7 Telemetry and Personal Data — CLEAN (pseudonymised) + +Analytics and marketing SDKs present: Firebase Analytics/Crashlytics, Amplitude, AppsFlyer, +CustomerIO, SurveySparrow. This is conventional commercial telemetry, declared openly in the build +configuration. + +The user identifier transmitted to these services is **hashed, never raw** +(`app/…/analytics/DefaultTrackingContextProxy.kt`): + +```kotlin +private fun calculateUserIdHash(userWalletId: UserWalletId?): String? { + return userWalletId?.value + ?.calculateSha256() + ?.toHexString() +} +``` + +Every `setUserProperties` call site — cold wallet, hot wallet, and scan response — routes through +this function. Accompanying properties are non-identifying product facts (`batchId`, `productType`, +`firmwareVersion`). + +No device-identifier harvesting was found in any analytics path: no `Settings.Secure.ANDROID_ID`, +no IMEI, no MAC address, no subscriber ID. + +### 3.8 Local Diagnostic Logging — CLEAN + +`NetworkLogsSaveInterceptor` records HTTP traffic to a local `AppLogsStore`, supporting the in-app +"contact support with logs" feature. The implementation shows care: + +- URLs pass through `SensitiveUrlMasker`, replacing configured secrets with `******`. The masker + sorts candidates by **descending length**, so a shorter value that is a prefix of a longer one + cannot mask first and leave a partial secret exposed — a considered detail. +- Sensitive endpoints are excluded from body logging entirely, via `restrictedForLogURLs` + (`api.stakek.it/v1/yields/enabled`) and `restrictedForLogHosts` (`us.paera.com`). +- Response bodies beyond ~2 MB are skipped; binary bodies are omitted. + +**Egress is user-initiated only.** `DefaultFeedbackRepository` exposes `getLogFile()` / +`getZipLogFile()` and passes the file to `EmailSender` as an attachment, opening the platform email +intent. Logs are **never** uploaded automatically or silently. See Observation **O-1**. + +### 3.9 Build System and Supply Chain — CLEAN + +**Repositories** — all official, no typosquats or unexpected mirrors: + +`gradlePluginPortal()` · `google()` (content-filtered to `androidx` / `com.android` / `com.google`) +· `mavenCentral()` · `developer.huawei.com/repo` (official, for the Huawei flavour) · +`maven.pkg.github.com/tangem/*` (first-party) + +`settings.gradle.kts` enforces: + +```kotlin +repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) +``` + +This **prevents any individual module from silently introducing its own repository** — a +meaningful supply-chain control, and one many projects omit. + +**No build-time code execution or fetching.** All `*.gradle.kts` and `*.gradle` files were searched +for `exec {`, `URL(`, `download`, `curl`, `wget`, and `HttpURLConnection`. **Zero results.** The +build does not download or run anything beyond declared Maven dependencies. + +**Gradle wrapper** — `distributionUrl` resolves to the official +`https://services.gradle.org/distributions/gradle-8.14.1-bin.zip`. + +``` +gradle-wrapper.jar SHA-256: 3dc39ad650d40f6c029bd8ff605c6d95865d657dbfdeacdb079db0ddfffedf9f + Size: 55,616 bytes +``` + +Verify this against the official Gradle 8.14.1 wrapper when reproducing the build. + +**DexProtector.** The `com.dexprotector.rtc.RtcStatus` reference in +`DefaultDeviceSecurityInfoProvider` is Licel's commercial application-hardening product, integrated +for **runtime root, emulator, and tamper detection**. Its presence is a defensive control, not +concealment: the mock build flavour transparently substitutes +`MockAwareDeviceSecurityInfoProvider`, which reports a clean device. + +### 3.10 Committed Credentials — CLEAN + +`app/src/main/assets/tangem-app-config/config_prod.json` and `config_dev.json` declare the full key +schema — Amplitude, AppsFlyer, Blockchair, BlockCypher, Infura, QuikNode, GetBlock, MoonPay, +Mercuryo, NowNodes, TonCenter and others — with **every value set to the literal string +`PLACEHOLDER`**. No live credentials are committed. + +The only committed keystore is a dummy debug keystore: + +```properties +store_password=android +key_alias=dummy +key_password=android +``` + +These are the well-known Android debug defaults. The keystore cannot sign a release build. See +Observation **O-3**. + +--- + +## 4. Expected False Positives + +Two pattern classes in this repository will alarm automated scanners and reviewers working from +grep output alone. Both are benign. They are documented here so that any subsequent review reaches +the same conclusion without re-investigating. + +### FP-1 — `loadDex*` is *Decentralised Exchange*, not Android DEX + +A search for `loadDex` returns numerous hits across `features/swap/`: + +``` +loadDexSwapData · loadDexSwapDataNoFee · loadDexSwapFee · DexSwapFeeCalculator · ResolvedFlow.DexLike +``` + +**DEX** here means **D**ecentralised **EX**change — token-swap business logic. These functions load +swap quotes and fee data from exchange providers. They have no relationship to Android DEX +bytecode or class loading. Confirmed by reading `SwapInteractorImpl.kt` and the surrounding swap +domain. + +### FP-2 — Hostile hostnames are security *test fixtures* + +The following appear as string literals in the tree: + +``` +faketangem.com · fake.tangem.com · tangem.com.attacker.com +buy.tangem.com.attacker.com · evil-spoofed.example · fake.surveysparrow.com +``` + +Every one is a **negative test case** asserting `expected = false`, located in: + +- `common/src/testDebug/…/uri/ExternalUrlValidatorTest.kt` +- `data/wallet-connect/src/test/…/DefaultWcPairUseCaseTest.kt` + +Their presence is **evidence of a deliberate anti-spoofing URL validator** and a WalletConnect +proposal-spoofing defence, verified against realistic attack strings. These are security controls +under test, not indicators of compromise. + +--- + +## 5. Observations + +Operational notes for anyone forking or deploying this application. **None is a vulnerability**, +and none affects the verdict in §1. + +### O-1 — Support logs are financially sensitive + +When diagnostic logging is active, full JSON request and response bodies are written to local +application storage. URLs are masked and specific hosts excluded (§3.8), but bodies may still +contain wallet addresses, balances, and transaction details. The data never leaves the device +unless the user deliberately emails it to support. + +**Recommendation:** treat a user-submitted support-log attachment as sensitive financial data in +your support process — restrict access, and set a retention limit. + +### O-2 — Third-party telemetry ships enabled by default + +Five analytics and marketing SDKs (Firebase, Amplitude, AppsFlyer, CustomerIO, SurveySparrow) and +the `AD_ID` permission are present in the Google flavour. This is disclosed and the user identifier +is pseudonymised (§3.7). + +**Recommendation:** a privacy-focused fork may wish to remove Amplitude, AppsFlyer, CustomerIO, and +SurveySparrow, and drop the `AD_ID` permission. The Huawei flavour already differs in SDK +composition. + +### O-3 — A clean checkout builds but does not fully function + +Because all configuration values are `PLACEHOLDER` (§3.10) and only a dummy debug keystore is +committed, a build from a clean checkout compiles but its third-party integrations — price quotes, +buy/sell providers, and some RPC endpoints — will not operate until real API keys are supplied. +Release signing requires a keystore that is deliberately not in the repository. + +This is correct and expected practice for a public source release, not a defect. + +--- + +## 6. Scope and Limitations + +These boundaries are stated plainly so that no broader assurance is inferred than was actually +obtained. + +1. **First-party source only.** Tangem's own SDKs — `com.tangem:blockchain` (Blockchain SDK), + `tangem-sdk-kotlin` (Card SDK), `tangem-hot-sdk-kotlin` (Hot Wallet SDK), `wallet-core`, `vico`, + and `web3j` — are resolved as **pre-built binary artifacts** from GitHub Packages at build time + and were **not** reviewed. This is a material limitation: the hot-wallet SDK performs the actual + mnemonic encryption and at-rest storage. Assurance over those components requires a separate + review of their own repositories. + +2. **Third-party Maven dependencies were not individually reviewed**, although the repository set + from which they are drawn is constrained and official (§3.9). + +3. **No dynamic analysis was performed.** This is a static review. No APK was executed, + instrumented, decompiled, or observed on a network. Runtime behaviour was inferred from source. + +4. **No cryptographic correctness review.** This review establishes that key material is not + *leaked*. It does not assess whether the encryption schemes protecting that material at rest are + correctly chosen or parameterised. + +5. **Commit provenance could not be verified.** The reviewed checkout carries squashed commit + messages (`Updated on 2026-08-14`), so upstream authorship and per-change history were not + available for inspection. + +6. **Point-in-time.** Findings apply to commit `358144ee61a` only. + +--- + +## 7. Conclusion + +The reviewed source is **clean**. No malware, backdoor, dropper, exfiltration channel, or +intentionally obfuscated logic was identified across ten categories of examination. + +The codebase further exhibits positive security engineering that would be unusual in a trojanised +or negligent application: + +- JavaScript disabled in WebViews, with no JavaScript bridge present at all +- Certificate validation fully intact; cleartext traffic disabled; user-installed CAs excluded +- Location and microphone permissions actively stripped from third-party SDK manifests +- Analytics identifiers SHA-256 hashed rather than transmitted raw +- A dedicated external-URL anti-spoofing validator, tested against realistic attack strings +- Tapjacking/overlay protection via `HIDE_OVERLAY_WINDOWS` +- Seed-phrase export gated behind biometric or password authentication +- Automatic wallet locking on an inactivity timer +- `FAIL_ON_PROJECT_REPOS` constraining the build supply chain +- Commercial runtime tamper and root detection + +Subject to the scope boundaries in §6 — in particular the unreviewed binary SDKs — the application +is assessed as safe to build and distribute from this source. + +--- + +*Static source review of commit `358144ee61a`. Findings reflect that commit only; re-audit after +merging upstream changes.*