Add TPIN reset via OTP, with server-clock countdowns and redacted API logs

- Reset TPIN: empty-body initiate -> OTP -> encrypted new TPIN via
  /tpin/reset/*, from a Services tile, search, and "Forgot your TPIN?"
  on the transfer's TPIN sheet (returns to the same transfer review)
- Handle every documented code: SESSION_INVALID signs out, TPIN_NOT_SET
  offers Create TPIN, CBS customer/account/org refusals stop the flow,
  outages retry from the start, expired or spent OTP/token restarts,
  policy VALIDATION_ERROR stays on the new-TPIN step
- Secrets live in screen state only and are dropped as each step ends
- Countdowns use the envelope timestamp, kept aside by the interceptor,
  so a wrong device clock cannot skew OTP or token expiry
- New TPINs refuse any three-digit run (1235, 5987), for create too
- Dev API logger redacts the session token, OTPs, PINs, encrypted
  payloads and one-time tokens
- Tests: request bodies, RSA-OAEP SHA-256 payload decrypted with Node,
  fresh nonce on key retry, server-clock deadlines, log redaction

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Abin Toppo
2026-09-30 18:41:03 +05:30
co-authored by Claude Opus 5.5
parent f1def81eb7
commit 5fbd13ba12
24 changed files with 1571 additions and 29 deletions
+25 -4
View File
@@ -3,6 +3,26 @@ import { isRepeatedDigits, isSequentialDigits } from '../../../utils/validation'
/** A TPIN is always exactly four digits. */
export const TPIN_LENGTH = 4;
/** The shortest run up or down the backend's TPIN policy refuses. */
const SEQUENCE_RUN_LENGTH = 3;
/**
* True when any three neighbouring digits run straight up or down — 1235 (123),
* 5987 (987) and 1234 alike. The TPIN policy refuses "a sequence of three
* digits" and answers "TPIN cannot contain sequential digits", so a run
* anywhere counts, not only one spanning all four. No wrap-around: 8901 is not
* a run.
*/
export const containsSequentialRun = (value: string) => {
for (let start = 0; start + SEQUENCE_RUN_LENGTH <= value.length; start += 1) {
if (isSequentialDigits(value.slice(start, start + SEQUENCE_RUN_LENGTH))) {
return true;
}
}
return false;
};
/**
* Any four digits — for ENTERING the TPIN on a transfer. Deliberately loose:
* whether they are the right four is the server's to say, with INVALID_TPIN.
@@ -19,8 +39,9 @@ export const isValidTpinEntry = (value: string) =>
* than after one.
*
* The backend stays the final authority. It also refuses a TPIN matching the
* customer's date of birth, which the app never holds — so that one can only
* arrive as a VALIDATION_ERROR on confirm.
* customer's date of birth, which the app never holds, and — on a reset — the
* current TPIN or any of the previous three, which only the server's history
* knows. Those can only arrive as a VALIDATION_ERROR on confirm.
*/
export const describeTpinProblem = (value: string, mobileNumber?: string): string | undefined => {
if (value.length === 0) {
@@ -39,8 +60,8 @@ export const describeTpinProblem = (value: string, mobileNumber?: string): strin
return 'Pick a TPIN that is not the same digit repeated';
}
if (isSequentialDigits(value)) {
return 'Pick a TPIN that is not a run of digits like 1234 or 4321';
if (containsSequentialRun(value)) {
return 'Pick a TPIN without a run of digits like 123 or 987';
}
const mobileDigits = mobileNumber?.replace(/\D/g, '') ?? '';