← U App download

U App security posture

What protects the wallet, what we deliberately accept, and what we will not claim.

Nothing here is a promise of impossibility: no software is "unhackable." This is the

honest map as of v0.9.6 (internal security review 2026-08-29; prior passes 2026-08-26

and 2026-08-27 cited inline).

The custody core (strong by construction)

AES-256-GCM envelope in app-private storage. Plaintext is never written to disk or logs.

and process death wipe them. storeMnemonic refuses to overwrite an existing seed -

checking BOTH vault generations (a committed v2 vault deletes its v1 blob; a v1-only

guard would let a second phrase be sealed beside the live seed - 8/29).

derivation path, which is structurally paired to the address shown. Watch-only imports

can never be a from.

prices cannot produce a wrong-amount send.

one-decision guard prevents double-tap double-broadcasts.

executor, the dapp bridge) serializes its estimate -> nonce -> sign -> broadcast span

through one cross-sender lock, so no two senders can interleave a nonce read. The dapp

bridge re-verifies the nonce UNDER the lock before signing and consults the offline

queue's reserved-nonce ceilings, as do the online senders (8/29 - a dapp sheet sitting

open for minutes previously raced the outbox pump).

cashaddr), XRP/XLM/XTZ/TON/XMR, DOT (SS58 blake2b-512 "SS58PRE" domain-separated),

ALGO (strict base32 + SHA-512/256 checksum), and EVM EIP-55 - a mixed-case address

whose checksum does not verify is rejected at scan, at compose, and at send; the app's

own EVM receive address is displayed and QR'd in checksummed form (audit 8/27).

(BiometricPrompt.CryptoObject) per unlock; enrollment changes invalidate it.

emulator holes live only in debug overrides - 8/29). ALL first-party hosts the app

opens a frame on (pay, accounts, api, mobile, buy, send, swap, bridge, portal, una,

ai, agents, names, pos, verify, sell.u.cash) are CERTIFICATE-PINNED to their CA

chains (widened from 4 hosts - the shared SSO cookie makes every one of them an

authenticated surface - 8/29). allowBackup=false + full cloud-backup AND

device-transfer extraction exclusion (schema-correct wrappers - 8/29); FLAG_SECURE

in release.

(8/29): fee quotes are taken from TWO independent public endpoints and the cheaper

is kept, with an absolute per-gas ceiling; UTXO sends refuse to sign when the fee

exceeds the amount; XRPL fee quotes are ceilinged; the offline chain-state cache

(nonces, UTXO sets, fee shapes) validates every network answer with the same

predicates persistence uses before caching it.

authenticated per message with a per-mount token only the injected provider can

know (iframes inside an allowed page, and documents that escape the navigation gate

via POST navigation or its 250ms fail-open, cannot forge bridge traffic - 8/29),

re-verified per message against the document actually loaded, and every sign goes

through the SignSheet with decoded calldata, origin, and network labels.

eth_sendTransaction fills nonce/gas/fees BEFORE the sheet (the user approves the

exact numbers that will be signed), surfaces page-supplied gas/fee fields verbatim,

applies the same 2x drift re-gate as the first-party send, and signs+broadcasts in

one lock span (audit 8/27 parity fix, hardened 8/29).

on-screen (selectable, with its length) - no truncation (audit 8/27). Calldata

decodes sourced from public signature registries are only trusted when

keccak256(signature) provably equals the call selector - a registry entry anyone

can submit can no longer dress a drainer up with friendly labels (8/29).

boot with no vault) wipes every on-device store derived from the wallet - signed

outbox transactions, eTransfer claim records, the receive-address pool, address

dossiers, diagnostics - not just the sealed seed.

Native-enforced unlock (2026-08-26 pass, hardened 8/29)

Previously the native layer would mint a session token for unlock/openVault with NO

credential - the JS unlock screen was the only gate, which a compromised bundle defeats.

That structural hole is closed in the native layer itself:

last 60 seconds (the verification is recorded natively by the PIN/pattern check or the

biometric prompt). The window is single-shot: minting a token (or revealing the seed)

consumes EVERY verification window that could have admitted it - including the

non-crypto biometric step-up (8/29; one biometric prompt previously admitted

unlimited v1 mints plus a reveal).

(UX unchanged); the token mint requires it.

is the only path, enforced natively. The mid-migration state (v2 sealed, v1 blob still

present) can no longer fall back to the zero-auth legacy decrypt.

the device. Every other configured lock is enforced in native code.

the window. For method none the reveal stays available (a none bundle can already

sign-and-drain, so blocking it adds nothing while holding recovery hostage); the screen

nudges such users to set a lock.

method, replacing a PIN/pattern, or resetting the vault requires the same fresh

verification as a seed reveal, and the stored method is enum-validated - a compromised

bundle can no longer flip the method to none, store a lock state no verification can

ever satisfy, or disable erase-after-N (8/29).

bridge once, outward for the mandatory onboard reveal (audit 8/27); entropy is floored

at 128 bits natively. Failed phrase imports return a generic error (a wallet-library

exception can echo the offending word).

nudged to upgrade); wrong-entry lockout doubles from 30s to a 24h cap; an OPTIONAL

"erase wallet after 10 failed attempts" (default OFF) wipes the vault artifacts -

including every AndroidKeyStore seal key (uapp_seed_v1/_v2, uapp_pin_v1) and

the wallet-derived keychain stores - funds stay recoverable with the 12-word phrase.

bridges) are bounds-checked: the BLE holder role only accepts frames from the

session-handshake peer (an unauthenticated MTU request can no longer install itself

as the frame target or inject inbound frames - 8/29), frame buffers are capped

everywhere, and the NFC length encoding is spec-correct.

Offline / cold-wallet trust model (v0.9.5-0.9.6)

approved payment. Received packets are shape-validated (family allowlist, whole-byte

hex, chainId must be served, length caps) and deduped/routed by RAW bytes, never the

display txid. The app does not verify a received packet's signature - it broadcasts

as-is, which can only ever lose the sender's money.

truth: a stale cache still signs (the chain verifies at broadcast; balances/UTXOs the

network rejects are parked with honest copy). Every persisted row is hydrated through

the same predicates as fresh entry, future-timestamped rows are rejected, and the

network writers validate what they cache (8/29).

same envelope; the relay URL rides the URL fragment so no server ever sees the payment

from the sender's side.

The receive-address rotation (xpub push, v0.9.6)

Linking "fresh receiving address for every payment" exports the wallet's account-level

PUBLIC extended keys (7 families) to the user's pay.u.cash account. This is a permanent

privacy grant by design: whoever holds those keys can derive every receive address that

account will ever use, including after the link is turned off (unlink removes pay's

derivations, but the keys were seen). The app discloses this at the moment of sharing,

never exports private material, only the device's own locally-generated keys can be

pushed, and every address pay claims back is re-derived from the device seed before it

is shown - a lying server's claims render as a warning, never as addresses. The server

engine serializes link/unlink under a database lock so a concurrent sync cannot

resurrect a turned-off link (8/29).

Supply chain (2026-08-26 pass, widened 8/29)

fails the build before it can ship.

commit SHAs (8/29); the CI signs nothing and holds no release material.

records, verified receive pool) assert Android Keystore-backed storage

(SECURE_HARDWARE) with a never-lose-the-write fallback; no service uses auth-bound

reads (they would prompt at every cold boot).

test trust anchors for live devnet exercise - resource-level isolation keeps them out

of release (verified 8/29).

Accepted risks (deliberate, reviewed)

enforce pin-sets: traffic from embedded first-party pages relies on the system CA

store plus the bridge-layer origin and message-authentication laws above. All

first-party hosts are pinned on the native path; public chain-data endpoints (a

multi-party API system of public RPC and explorer providers) are NOT pinned - they

rotate across providers and the standard system-CA posture applies, with the

hostile-endpoint bounds above capping what a lying endpoint can achieve. Pin expiry

(2027-08-31) converts an abandoned install from bricked to unpinned; bump every

release.

seed is recoverable from app-private storage regardless of the soft lock; only the

biometric v2 vault is hardware-bound. The lock-method screen says this in plain

language. This is the Android KeyStore trade-off (setUserAuthenticationRequired would

prompt per signing operation).

shown). It lives in React state briefly, is cleared on background/leave, and clipboard

copies are swept after 45 seconds. The reveal is natively gated (above) but a fresh

verification window still exists for ~60 seconds after each unlock. Mnemonics are not

zeroized in native memory between operations (deferred).

swap.u.cash aggregated quotes. The SignSheet decode and the slippage floor bound what

those servers can do silently, but a compromised fleet host remains the major remaining

trust edge. (First-party hosts are pinned; the dapp allowlist is the *.u.cash

registry - the surface is as wide as the fleet.)

the user's pay.u.cash account. By design, for reinstall restore. The receive-address

rotation above is the wider grant of the same kind.

app signs out of pay only on explicit user sign-out. On a rooted device the cookie is

replayable while the wallet is locked; the vault itself stays sealed.

only, so a drained-background outbox can finish while unlocked. Auto-lock is a UX

bound, not a security boundary; the native gate is.

stranger can consume relay-queue slots; never funds).

RN Image never executes content).

data, part of the same multi-party API system (quota abuse only).

anyone with the app can read mesh traffic; the packets are bearer payment data, never

keys).

arbitrates at broadcast); account-1+ keys are send-gated but not

balance-hidden-by-default; the JavaScript fallback engine flavor is a dynamic

dependency (dormant while the default engine is enabled); first-party broadcast

proxies are open, rate-limited relays by design; the iOS reference signer is a stub

(parity runs on the Android engine).

"internally reviewed," never "audited." See the claims law below.

App Links

https://pay.u.cash/transfer/<token> is the single verified App Link (Android

autoVerify; the server's assetlinks.json lists exactly the release-signer fingerprint,

verified 2026-08-26). It routes only to the eTransfer claim screen and only while a

wallet is unlocked; it never signs anything.

What we claim - and what we do not

WE SAY: self-custodial by design (keys generated and sealed on device, StrongBox/TEE);

hardware-bound Class-3 biometric lock with NATIVE enforcement; PIN/pattern locks with

6-digit minimum and escalating lockout; optional erase-after-10; offline/cold signing

for every supported coin; certificate pinning across every first-party host on the

app's own network stack; supply-chain-pinned signing library; signer parity executed

on-device in continuous integration against independently derived goldens; internally

reviewed with the findings published here; responsible disclosure program.

WE DO NOT SAY (until separately earned): "audited" or "independently audited" (no

third-party engagement exists), "pentested," "bug bounty" (rewards are discretionary),

"open source" (the repository is private).

Reporting

Security issues: security@u.cash (the fleet's responsible-disclosure channel,

u.cash/legal/disclosure.html; safe harbor applies). Please do not open public issues for

vulnerabilities.