Chuyển tới nội dung chính

Security & Trade-offs

Payments demand defense in depth: no single control is trusted on its own, and money only moves when several independent layers agree. This page lists the layers, then gives the recommended UX-vs-security decisions and a phased rollout.

The security layers​

1. Transport security​

TLS 1.2+ everywhere. mTLS for vendor server-to-server calls; certificate pinning in the user and merchant apps; signed webhooks so vendors can trust callbacks.

2. Authentication​

  • User: reuses the existing xperise session, plus device binding (a per-install key) so a stolen session can't be replayed from a new device. Payment confirmation adds biometrics/PIN.
  • Vendor: an API key + secret; every request is HMAC-signed over method, path, body, and timestamp, with an IP allowlist. Rotate secrets; support two active keys for rotation.

3. Payload & message integrity​

QR payloads and card tokens are signed (HMAC/JWT with a key id) and single-use with a short TTL. A nonce + timestamp on each request blocks replay. Never trust an amount that arrived from the client/QR — resolve the canonical value from the server.

4. Transaction integrity (double-spend prevention)​

  • Idempotency keys on every state-changing call — a retried request is a no-op, not a second charge.
  • Double-entry ledger with row locks or optimistic concurrency on the balance, so two concurrent payments can never spend the same funds.
  • A strict state machine (see Overview) so a transaction can't be captured twice or refunded beyond its amount.

5. Data at rest​

  • Credit-card data is handled by 2c2p, not xperise: PANs are captured/tokenized by the 2c2p gateway (with 3DS), so xperise stores only a 2c2p token + last4 and stays out of scope for that data.
  • xperise-card numbers live only in a tokenization vault; systems hold token + last4 + hash.
  • CVV is never stored; PAN and CVV are never logged.
  • Keys in an HSM / KMS; card cryptogram keys live in the card's secure element and never leave it. PII encrypted; least-privilege access; retention limits.

6. Risk & fraud engine​

Real-time scoring on velocity (count/amount per window), limits (per-transaction, daily), device/geo anomalies, and blocklists. Its output drives the step-up decision below. Users can freeze a card instantly and report lost to kill the token.

7. Audit & reconciliation​

An append-only, immutable audit log of every money movement; scheduled reconciliation of the ledger against transactions and vendor settlements. The ledger is the single source of truth — screens and receipts are derived from it, never the reverse.

Threat → mitigation summary​

ThreatPrimary mitigation
Replayed QR / tapSingle-use intent + short TTL; tap cryptogram counter verified server-side.
Amount tamperingAmount held server-side; app shows the server's value; payload signed.
Card cloningNFC dynamic cryptogram per tap; static token can't authorize alone.
Photographed printed QR / stolen cardToken ≠ credential; a second factor (app confirm / PIN) is always required.
Vendor impersonationAPI key + HMAC + mTLS + IP allowlist.
Account takeoverMFA, device binding, biometric confirmation, anomaly detection.
Credit-card fraud (2c2p rail)3DS / step-up via 2c2p; xperise never holds the PAN; Layer 1 allowance still gates the amount.
Overspend beyond policyTwo-layer authorization — the payment is declined unless both the Layer 1 allowance and Layer 2 funding cover it.
Shared bearer card misuseBearer card (unassigned) is usable by anyone with the card + PIN → CVM is PIN; mitigate with low balance / stored value, tight limits, and monitoring.
Double-spendIdempotency keys + ledger locks + state machine + atomic dual decrement.
Data breachTokenization, no CVV storage, HSM/KMS, encryption, least privilege.

OTP & step-up verification​

OTP appears in several payment moments — card activation, manual-entry (card-not-present) payments, and any step-up the risk engine triggers. The recommendation is not to build a payment-specific OTP system, but to reuse the existing xperise OTP service — the same one that already powers login and password reset over email, SMS, and Zalo — behind one shared verification API. One service, one set of delivery channels, one place to harden.

One shared service, scoped per purpose​

The single OTP service is reused by binding each code to a purpose (scope), so a code can only be spent on the action it was issued for. A login OTP can never approve a payment, and a payment OTP can never approve a different payment.

A verification is requested against a scope + reference, delivered on the account's registered channel, then verified:

POST /v1/verifications (illustrative)
{
"scope": "payment_step_up", // login | password_reset | card_activation | payment_step_up
"ref": "TXN_9f2a...", // the exact action being authorized
"channel": "sms", // email | sms | zalo (from the account's registered channels)
"context": { "amountMinor": 150000, "merchant": "Vendor X" }
}

What-you-see-is-what-you-sign (WYSIWYS)​

For a payment step-up the OTP message states the amount and merchant, and the code is bound to that specific transaction ref. So the user approves this payment, not an abstract code an attacker could redirect to a different charge:

SMS
xperise: 482913 approves paying 150,000₫ to Vendor X. Never share this code.

Hardening rules (applied once, for every caller)​

RuleValue
Scope-boundA code is valid only for its scope + ref; wrong-scope submission is rejected.
Single-useConsumed on first successful verify; cannot be replayed.
Short TTL~5 minutes; expired codes are dead.
Attempt lockoute.g. 5 wrong tries → invalidate + require re-request.
Request throttlingRate-limit re-sends per account/channel to stop OTP-bombing.
Channel = registered onlySent to the account's known email/phone, never a caller-supplied one.

How it combines with the other factors​

OTP is one rung on the step-up ladder — not the only one. Prefer the lowest-friction factor that meets the risk, and fall back to OTP when a stronger one isn't available:

SituationPreferred factorOTP's role
User has the app + biometricsIn-app push confirm (biometric) — WYSIWYS, one tapFallback if push fails / no biometrics
Card activationSession + OTP to prove identityPrimary identity factor
Manual entry (card-not-present)3DS-style in-app confirmFallback step-up when the user isn't in-app
Corporate adminMFA on the admin accountOne MFA factor (with an authenticator preferred for admins)
Combine, don't duplicate

Treat OTP as a shared, scope-aware verification primitive reused across the platform. This keeps one delivery infrastructure and one hardening surface instead of two.

Card transaction authentication (CVM)​

For xperise card transactions the recorded Cardholder Verification Method (CVM) is a PIN or an OTP — one of the two, never both (configured per card/company; see Transaction authentication). Biometrics, where used, only unlock the app to enter the PIN or approve — they are not the recorded CVM. Low-value taps below the contactless floor limit may skip the CVM entirely.

UX vs security trade-offs​

The right lever is risk-based (adaptive) authentication: keep friction low when the transaction is low-risk, and step up only when risk rises. The recommendation:

ScenarioLower frictionHigher securityRecommended default
Small tap (under floor limit)Tap-and-go, no CVMCVM every tapTap-and-go under the contactless floor limit (e.g. 200,000₫); the card's CVM (PIN or OTP) above it.
QR pay in appAuto-confirmConfirm every timePIN/OTP on the confirm screen (biometric may unlock the app); it shows the amount.
Printed-QR card scanApprove on tokenApp confirm + CVMAlways the card's CVM (PIN or OTP); static token alone never authorizes.
Manual entry (CNP)Amount onlyFull 3DS-style step-upAlways step-up (app confirm or OTP); restrict to trusted vendors + low limits.
New device / new vendor / odd geoTrust itRe-verifyStep-up on anomaly, frictionless when the risk score is low.
Where the amount livesIn the QR (offline-friendly)Server-sideServer-side, resolved by the app — tamper-proof.

Guiding principles:

  • Scale friction to risk and amount. Most payments are small and low-risk — make those effortless (a single tap, one biometric) and reserve PIN/OTP/3DS for high amounts, anomalies, and card-not-present.
  • Tiered limits (per-transaction, daily, per-mode) cap the blast radius of any single compromise without adding friction to everyday spend.
  • Prefer dynamic over static. Dynamic QR on a screen and NFC cryptograms beat static printed codes; offer static QR for convenience but keep it behind a second factor.
  • Give users instant control. One-tap freeze and report-lost in the app is both a security control and a trust feature.
PhaseScope
1 — QR on the walletMerchant-presented dynamic QR funded by the company wallet, decrementing monthly limit or cost center; the two-layer authorize→commit; in-app biometric confirm; idempotency; core risk limits; audit log.
2 — xperise cardIssue cards with a card balance; tap (NFC cryptogram) and printed-QR + app-confirm; freeze/lost-card; tap-and-go threshold.
3 — AdvancedManual entry for trusted vendors with 3DS-style step-up; extend funding combinations (e.g. QR via 2c2p); richer fraud scoring; vendor self-serve settlement.

The existing company wallet and credit card (2c2p) methods already run; starting the new work with QR on the wallet delivers value fast while exercising the two-layer authorization the xperise card then builds on.

Compliance note​

The architecture is hybrid: xperise's own instruments (company wallet, QR, xperise card) are closed-loop, while credit-card payments run through 2c2p. Routing card data through 2c2p keeps the sensitive PAN/CVV out of xperise, minimizing PCI-DSS scope for the credit-card rail (2c2p carries the bulk of that burden; xperise stores only 2c2p tokens + last4). The closed-group xperise card still relies on tokenization, no-CVV-storage, and HSM/KMS key management as above. Depending on jurisdiction, holding company balances/credit may trigger KYC/AML and e-money regulations; confirm with legal before launch.

See Overview, QR-Code Payment, and Card Payment for the flows these layers protect.