580 lines
28 KiB
Markdown
580 lines
28 KiB
Markdown
# 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`) — confirmed identical to upstream `master` (§6.5) |
|
|
| **Report date** | 17 August 2026 |
|
|
| **Revised** | 17 August 2026 — §3.1, §3.6, O-3 corrected; §6.5 resolved. See "Revisions" below |
|
|
| **Companion report** | [APKANALYSIS.md](./APKANALYSIS.md) — binary comparison against Tangem's official v6.1.2 release APK |
|
|
| **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.
|
|
|
|
### Revisions (17 August 2026)
|
|
|
|
This report was amended after a build of the source was produced and compared against Tangem's
|
|
official release APK. **Three corrections narrow claims that were stated too broadly**; one closes a
|
|
limitation. All should be read before relying on the original text:
|
|
|
|
| Section | Change |
|
|
|---|---|
|
|
| §3.1 | "No native code" holds for the repository, **not** for the built application, which bundles 14 native libraries — including those performing key derivation, signing, and at-rest encryption |
|
|
| §3.6 | The nine permissions listed are the *source manifest* set. The **merged** manifest contains 23. "Permissions minimised" overstated the position |
|
|
| O-3 | A clean checkout **does not build at all** — the `ds-tokens` submodule is unresolvable outside Tangem. The prior text said it compiles |
|
|
| §6.5 | Commit provenance is now **verified** against upstream, closing a limitation rather than adding one |
|
|
|
|
None of these alters the §1 verdict on the source. §3.1 and §3.6 do mean the shipped application has
|
|
a larger native-code and permission surface than the original report conveyed.
|
|
|
|
---
|
|
|
|
## 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 the *repository* contain native binaries or opaque blobs? | **No** — but the built APK bundles 14 native libraries from binary dependencies; see §3.1 |
|
|
| 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 — repository.** No `.so` libraries, no JNI, and no NDK build inputs are committed to
|
|
the tree. Only two `.jar` files exist:
|
|
|
|
| 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) |
|
|
|
|
**Native code — built application.** *(Correction, 17 Aug 2026.)* The statement above describes the
|
|
repository, not the artifact. A build of this source bundles **14 native libraries**, several of
|
|
them security-critical:
|
|
|
|
```
|
|
libTrustWalletCore.so · libsecp256k1-jni.so · libTrezorCrypto.so · libargon2.so
|
|
libblst.so · libsqlcipher.so · libuniffi_yttrium.so · libtoolChecker.so
|
|
libtensorflowlite_jni.so · libbarhopper_v3.so · libjnidispatch.so
|
|
libimage_processing_util_jni.so · libandroidx.graphics.path.so · libsurface_util_jni.so
|
|
```
|
|
|
|
Key derivation and signing (`libTrustWalletCore`, `libsecp256k1-jni`, `libTrezorCrypto`), key
|
|
stretching (`libargon2`), and at-rest database encryption (`libsqlcipher`) are all performed in
|
|
native code. These arrive through the pre-built Maven dependencies excluded from this review by §6.1
|
|
and were therefore **not** examined. Tangem's own release build adds `libdexprotector.so` and
|
|
`libalice.so` on top. See [APKANALYSIS.md](./APKANALYSIS.md) §4.2.
|
|
|
|
**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
|
|
<network-security-config>
|
|
<base-config cleartextTrafficPermitted="false">
|
|
<trust-anchors>
|
|
<certificates src="system" />
|
|
</trust-anchors>
|
|
</base-config>
|
|
</network-security-config>
|
|
```
|
|
|
|
- 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 — RESTRAINED IN SOURCE, WIDER AS SHIPPED
|
|
|
|
Permission set declared in the **hand-written manifests** (`app/src/main/`, `app/src/huawei/`):
|
|
|
|
`INTERNET` · `ACCESS_NETWORK_STATE` · `CAMERA` (QR scanning) · `NFC` (card scanning) ·
|
|
`USE_BIOMETRIC` · `VIBRATE` · `WAKE_LOCK` · `HIDE_OVERLAY_WINDOWS` · `AD_ID`
|
|
|
|
Nothing there exceeds what a wallet requires.
|
|
|
|
**Correction (17 Aug 2026).** An earlier revision presented the nine above as the complete set and
|
|
listed external storage and `RECEIVE_BOOT_COMPLETED` among permissions that were *absent*. That was
|
|
wrong. The nine describe the source manifests; the **merged** manifest in the built APK contains
|
|
**23**. The additional fourteen, contributed by manifest merging from bundled third-party SDKs:
|
|
|
|
```
|
|
READ_EXTERNAL_STORAGE · WRITE_EXTERNAL_STORAGE · WRITE_SETTINGS
|
|
RECEIVE_BOOT_COMPLETED · FOREGROUND_SERVICE · USE_FINGERPRINT · POST_NOTIFICATIONS
|
|
BIND_GET_INSTALL_REFERRER_SERVICE · ACCESS_ADSERVICES_ATTRIBUTION
|
|
ACCESS_ADSERVICES_AD_ID · c2dm.RECEIVE · DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION
|
|
com.samsung.android.mapsagent.permission.READ_APP_INFO
|
|
com.huawei.appmarket.service.commondata.permission.GET_COMMON_DATA
|
|
```
|
|
|
|
`WRITE_SETTINGS` and `RECEIVE_BOOT_COMPLETED` are broader than a wallet needs and are not explained
|
|
by first-party code; they warrant enquiry with the SDK vendors that introduce them. This merged set
|
|
is **identical** in Tangem's official release APK and in a build from this source — see
|
|
[APKANALYSIS.md](./APKANALYSIS.md) §4.1 and D-1.
|
|
|
|
**Still genuinely absent** from the merged manifest: SMS, contacts, call log, location, microphone,
|
|
accessibility services, `SYSTEM_ALERT_WINDOW`, and `REQUEST_INSTALL_PACKAGES`.
|
|
|
|
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
|
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" tools:node="remove" />
|
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" tools:node="remove" />
|
|
<uses-permission android:name="android.permission.RECORD_AUDIO" tools:node="remove" />
|
|
```
|
|
|
|
Denying location and microphone access to advertising and analytics SDKs is a privacy measure that
|
|
argues directly against hostile intent. **This control was verified in the shipped artifact:**
|
|
neither location nor `RECORD_AUDIO` appears in the merged manifest of Tangem's official release APK
|
|
(APKANALYSIS.md §4.1). The stripping is effective, not merely declared.
|
|
|
|
**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 does not build, and does not fully function once it does
|
|
|
|
*(Revised 17 Aug 2026 after an actual build was attempted. The previous text stated a clean checkout
|
|
compiles; it does not.)*
|
|
|
|
**The build fails outright.** `core/ui/ds-tokens` is registered in the tree as a submodule gitlink
|
|
(commit `a59c6a36`), but **no `.gitmodules` file exists anywhere in the repository's 16,087-commit
|
|
history**. There is no URL from which the submodule can be resolved. `:core:ui:verifyDesignTokens`
|
|
runs during `preBuild` and aborts before any module compiles, emitting the instruction
|
|
`git submodule update --init --recursive` — which cannot succeed outside Tangem's own environment.
|
|
|
|
The task only *verifies*: it hashes the submodule's JSON and SVG sources and compares the digest
|
|
against a committed `.tokens-hash`. It generates nothing, and the 380 generated files it guards are
|
|
all committed. Building with `-x :core:ui:verifyDesignTokens` therefore produces a functionally
|
|
complete APK, at the cost of losing the assurance that the committed generated sources still
|
|
correspond to the design tokens they derive from.
|
|
|
|
**Once building, integrations remain partly inert.** All configuration values are `PLACEHOLDER`
|
|
(§3.10) and are compiled into the APK at build time by `GenerateEnvironmentConfigTask`. No runtime
|
|
check detects the sentinel, so the application transmits `PLACEHOLDER` as a live credential and
|
|
receives authentication failures.
|
|
|
|
The practical effect is uneven. `providers_order.json` lists RPC providers per chain as either
|
|
`public` (no key) or `private` (keyed), and the SDK falls through the list in order. Of 97 chains,
|
|
**87 retain at least one public provider and will synchronise without any key**. The 10 that do not
|
|
are:
|
|
|
|
```
|
|
bitcoin · ethereum · solana · cardano · litecoin
|
|
dogecoin · bitcoin-cash · dash · the-open-network · chia
|
|
```
|
|
|
|
— that is, the chains most users actually hold. Fiat prices and the swap aggregator additionally
|
|
depend on Tangem-issued credentials that cannot be self-provisioned.
|
|
|
|
**Consequence for downstream forks:** a `PLACEHOLDER` build presents as a working wallet while
|
|
silently failing to report balances on the major chains. A user scanning a funded card may see a
|
|
zero balance and conclude their funds are lost. Such a build should not be distributed to
|
|
non-technical users, and any release of one should say so explicitly.
|
|
|
|
Publishing a source tree without private keys or internal submodules is correct practice, not a
|
|
defect — but the resulting checkout is neither buildable nor safely usable as-is, and that should be
|
|
stated plainly rather than left to be discovered.
|
|
|
|
---
|
|
|
|
## 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 — resolved.** *(Updated 17 Aug 2026.)* An earlier revision recorded this as
|
|
an open limitation. It has since been closed: `git ls-remote` confirms that `master` at
|
|
`github.com/tangem/tangem-app-android` is `358144ee61a9f65e0f39578e8c943226abb5d996` — the exact
|
|
commit reviewed — and `git diff` against it is empty. The reviewed tree is the published upstream
|
|
tree, unmodified.
|
|
|
|
The `Updated on 2026-08-14` subject line appears on **all 16,087 commits**, not merely recent
|
|
ones, so it is an artefact of how the upstream repository is published rather than evidence of
|
|
rewriting. Per-change authorship and history remain unavailable for inspection, so the *content*
|
|
of individual upstream changes could not be reviewed; the *identity* of the tree is now
|
|
established.
|
|
|
|
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 and the native
|
|
libraries they contribute (§3.1) — **nothing in the reviewed first-party source is an obstacle to
|
|
building and distributing this application.**
|
|
|
|
That is a narrower statement than "safe to distribute", and deliberately so. A build produced from
|
|
this source alone carries `PLACEHOLDER` credentials and will not report balances on the ten most
|
|
widely held chains while otherwise presenting as a functioning wallet (O-3). Anyone distributing
|
|
such a build must say so explicitly. Assurance over the binary that Tangem itself ships is a
|
|
separate question, addressed — and largely left open — in [APKANALYSIS.md](./APKANALYSIS.md).
|
|
|
|
---
|
|
|
|
*Static source review of commit `358144ee61a`. Findings reflect that commit only; re-audit after
|
|
merging upstream changes.*
|