Static source review of commit 358144ee61 covering malware, backdoors, information leaks, obfuscated code, and remote code-download behaviour. Verdict: clean - 0 critical/high/medium/low findings, 3 informational observations.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
21 KiB
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:
// 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.javaTangemBridgeProcessor.javaTangemPaymentAccount.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:
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 backupfeatures/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:
<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-overridesblock.
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:
<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.
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):
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) andrestrictedForLogHosts(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:
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:
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 Decentralised EXchange — 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.ktdata/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.
-
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, andweb3j— 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. -
Third-party Maven dependencies were not individually reviewed, although the repository set from which they are drawn is constrained and official (§3.9).
-
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.
-
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.
-
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. -
Point-in-time. Findings apply to commit
358144ee61aonly.
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_REPOSconstraining 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.