feat(android): TOTP codes and custom fields in the detail screen

The detail screen showed site, username and password and silently dropped
everything else - custom fields, notes, 2FA. It is now built per entry
rather than from a fixed template: every row is a caption, a value, and
whether it is secret, so reveal and copy exist once instead of once per
kind of field.

Two rows stay special on purpose. A note's body is stored in the password
column, so it gets its own caption and no mask. The 2FA code is the only
live row - refreshed every second, placed first, since it is what you pull
a phone out for.

PMA.Totp takes bare base32 or an otpauth:// URI; both travel in snapshots
depending on how the entry was created. Checked against all six RFC 6238
vectors, including T=20000000000, which overflows 32 bits.

The rows hold plaintext, so clearing them is part of locking, not of
navigating away.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
r-zakarya
2026-08-15 10:35:09 +01:00
parent 3232f6f163
commit 20ba2438df
6 changed files with 431 additions and 63 deletions
+35 -1
View File
@@ -22,7 +22,8 @@ program selftest;
uses
System.SysUtils, System.Hash, System.NetEncoding, System.Diagnostics,
PMA.Crypto in 'PMA.Crypto.pas',
PMA.TestVector in 'PMA.TestVector.pas';
PMA.TestVector in 'PMA.TestVector.pas',
PMA.Totp in 'PMA.Totp.pas';
var
GFailures: Integer = 0;
@@ -40,6 +41,19 @@ begin
end;
end;
// URI plutot que base32 nu : elle porte les 8 chiffres de la RFC, et fait
// passer l'analyseur d'otpauth:// dans le meme mouvement.
const
RFC6238_URI = 'otpauth://totp/rfc?secret=' +
'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ&digits=8';
procedure CheckTotp(AUnix: Int64; const AWant: string);
var
LLeft, LPeriod: Integer;
begin
Check('T=' + AUnix.ToString, TotpAt(RFC6238_URI, AUnix, LLeft, LPeriod), AWant);
end;
var
LSalt, LKey: TBytes;
LPlain: string;
@@ -107,6 +121,26 @@ begin
end;
end;
// ---- TOTP, vecteurs de la RFC 6238 --------------------------------------
// Secret "12345678901234567890" en base32, 8 chiffres, aux instants publies
// par la RFC. C'est le seul moyen de verifier a la fois la base32, le HMAC
// et la troncature - un code plausible peut etre faux.
Writeln('TOTP (RFC 6238)...');
try
CheckTotp(59, '94287082');
CheckTotp(1111111109, '07081804');
CheckTotp(1111111111, '14050471');
CheckTotp(1234567890, '89005924');
CheckTotp(2000000000, '69279037');
CheckTotp(20000000000, '65353130');
except
on E: Exception do
begin
Writeln(' ERREUR ', E.ClassName, ' : ', E.Message);
Inc(GFailures);
end;
end;
Writeln;
if GFailures = 0 then
Writeln('Tout est bon.')