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:
@@ -16,9 +16,20 @@ uses
|
||||
System.SysUtils, System.Classes, System.JSON, System.Generics.Collections;
|
||||
|
||||
type
|
||||
/// Un champ personnalise. Le poste Windows nomme la cle "is_secret" - c'est
|
||||
/// lui qui decide si la valeur s'affiche masquee.
|
||||
TVaultField = record
|
||||
Caption, Value: string;
|
||||
Secret: Boolean;
|
||||
end;
|
||||
|
||||
TVaultEntry = record
|
||||
Uuid, Title, Site, Username, Password, Folder, Tags, Kind, Totp: string;
|
||||
Fields: TArray<TVaultField>;
|
||||
Deleted: Boolean;
|
||||
/// Le corps d'une note EST son mot de passe cote stockage : il n'y a pas
|
||||
/// de colonne separee. L'affichage doit donc l'intituler autrement.
|
||||
function IsNote: Boolean;
|
||||
/// Ce qu'on affiche en tete de ligne : le nom donne par l'utilisateur,
|
||||
/// sinon le site, sinon l'identifiant. Une entree sans aucun des trois
|
||||
/// existe (note vide) et ne doit pas apparaitre comme une ligne muette.
|
||||
@@ -59,6 +70,11 @@ uses
|
||||
|
||||
{ TVaultEntry }
|
||||
|
||||
function TVaultEntry.IsNote: Boolean;
|
||||
begin
|
||||
Result := SameText(Kind, 'note');
|
||||
end;
|
||||
|
||||
function TVaultEntry.Display: string;
|
||||
begin
|
||||
Result := Title.Trim;
|
||||
@@ -98,6 +114,32 @@ begin
|
||||
Result := AVal.Value;
|
||||
end;
|
||||
|
||||
/// Les champs personnalises : tableau d'objets {label, value, is_secret}.
|
||||
/// Un champ sans intitule ET sans valeur ne vaut pas une ligne a l'ecran.
|
||||
function ParseFields(const AVal: TJSONValue): TArray<TVaultField>;
|
||||
var
|
||||
LArr: TJSONArray;
|
||||
LObj: TJSONObject;
|
||||
I, N: Integer;
|
||||
begin
|
||||
Result := nil;
|
||||
if not (AVal is TJSONArray) then Exit;
|
||||
LArr := AVal as TJSONArray;
|
||||
SetLength(Result, LArr.Count);
|
||||
N := 0;
|
||||
for I := 0 to LArr.Count - 1 do
|
||||
begin
|
||||
if not (LArr.Items[I] is TJSONObject) then Continue;
|
||||
LObj := LArr.Items[I] as TJSONObject;
|
||||
Result[N].Caption := LObj.GetValue<string>('label', '');
|
||||
Result[N].Value := LObj.GetValue<string>('value', '');
|
||||
Result[N].Secret := LObj.GetValue<Boolean>('is_secret', False);
|
||||
if (Result[N].Caption = '') and (Result[N].Value = '') then Continue;
|
||||
Inc(N);
|
||||
end;
|
||||
SetLength(Result, N);
|
||||
end;
|
||||
|
||||
procedure TVault.LoadFromJson(const APlainJson: string);
|
||||
var
|
||||
LRoot, LObj: TJSONObject;
|
||||
@@ -131,6 +173,7 @@ begin
|
||||
FEntries[N].Kind := LObj.GetValue<string>('kind', 'login');
|
||||
FEntries[N].Totp := LObj.GetValue<string>('totp_secret', '');
|
||||
FEntries[N].Tags := TagsToString(LObj.GetValue('tags'));
|
||||
FEntries[N].Fields := ParseFields(LObj.GetValue('custom_fields'));
|
||||
// deleted arrive en 0/1 depuis nos conteneurs ; absent = vivant.
|
||||
FEntries[N].Deleted := LObj.GetValue<Integer>('deleted', 0) = 1;
|
||||
Inc(N);
|
||||
|
||||
Reference in New Issue
Block a user