feat(android): mark the clipboard sensitive, and lock after 3 min idle

Since Android 13 the system shows a preview of whatever was just copied, at the bottom of the screen. For a password that is precisely what we are trying to avoid: it appears in clear, outside the app, for several seconds. The flag that suppresses it is a ClipDescription extra, which IFMXClipboardService does not expose - hence going straight to ClipboardManager. Note Jcontent_ClipboardManager, not JClipboardManager: the latter is the deprecated android.text one, which knows only bare text and has no ClipData to carry an extra.

Backgrounding already locks; the idle timer covers the phone left face-up on a table with the app open. It samples timestamps every 15 s rather than counting seconds - a wake-up per second would cost battery for no precision that matters.

Attachments dropped from the plan: this vault has none.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
r-zakarya
2026-08-15 14:58:27 +01:00
parent 0e39cf7e56
commit 7939d268f3
4 changed files with 199 additions and 23 deletions
+54 -15
View File
@@ -24,7 +24,7 @@ interface
uses
System.SysUtils, System.Types, System.UITypes, System.Classes,
System.Diagnostics, System.Rtti, System.NetEncoding,
System.Diagnostics, System.Rtti, System.NetEncoding, System.DateUtils,
System.Generics.Collections,
FMX.Types, FMX.Controls, FMX.Forms, FMX.Graphics, FMX.Dialogs, FMX.Platform,
FMX.Controls.Presentation, FMX.StdCtrls, FMX.Memo, FMX.Memo.Types,
@@ -51,6 +51,11 @@ const
// mot de passe traine dans l'historique du presse-papiers.
CLIPBOARD_CLEAR_SECONDS = 45;
// Verrouillage sur inactivite au premier plan. Le passage en arriere-plan
// verrouille deja ; ceci couvre le telephone pose sur une table, ecran
// allume, application ouverte.
AUTOLOCK_MINUTES = 3;
// TEST_PWD vient d'un fichier LOCAL, hors depot : il ouvre le coffre entier
// une fois le conteneur telecharge. Absent apres un clone -> recopier
// PMA.LocalConfig.template.inc.
@@ -121,6 +126,12 @@ type
FClipTimer: TTimer;
FCopied: string;
// --- verrouillage sur inactivite
FIdleTimer: TTimer;
FLastActivity: TDateTime;
procedure Touch;
procedure OnIdleTimer(Sender: TObject);
procedure BuildUnlock;
procedure BuildList;
procedure BuildDetail;
@@ -176,7 +187,7 @@ implementation
uses
PMA.Crypto, PMA.SyncClient, PMA.Store, PMA.Biometric, PMA.Totp,
PMA.ScreenGuard;
PMA.ScreenGuard, PMA.Clipboard;
{$R *.fmx}
@@ -215,6 +226,13 @@ begin
FTotpTimer.Interval := 1000;
FTotpTimer.OnTimer := OnTotpTick;
// Tick grossier : on compare des horodatages, il ne sert qu'a echantillonner.
// Une seconde de precision n'apporterait rien et reveillerait l'appareil.
FIdleTimer := TTimer.Create(Self);
FIdleTimer.Enabled := False;
FIdleTimer.Interval := 15000;
FIdleTimer.OnTimer := OnIdleTimer;
// Le bouton retour materiel arrive comme une touche : sans ce gestionnaire
// il ferme l'application depuis le detail, au lieu de revenir a la liste.
OnKeyUp := OnHardwareBack;
@@ -532,6 +550,7 @@ var
end;
begin
Touch;
ClearDetailRows;
FCurrent := FVault.Entry(AVaultIndex);
FDetTitle.Text := FCurrent.Display;
@@ -673,6 +692,7 @@ begin
// afficher - et sur un poste sans cache local (hors Android), plus aucun
// moyen de rouvrir le coffre sans relancer l'application.
FETag := '';
FIdleTimer.Enabled := False;
FVault.Clear;
FList.Clear;
FIcons.Clear;
@@ -690,6 +710,8 @@ begin
FVault.TrashCount.ToString + ' en corbeille');
RebuildIcons;
PopulateFolders;
Touch;
FIdleTimer.Enabled := True;
// Deja dans l'application : reconstruire la liste SANS y ramener
// l'utilisateur. Obligatoire meme s'il est dans le detail - les lignes de
// la liste portent des indices vers l'ancien coffre, et un appui ouvrirait
@@ -1060,6 +1082,7 @@ begin
// ponytail: refonte complete de la liste a chaque frappe. Sur un coffre de
// quelques centaines d'entrees c'est imperceptible ; passer a un modele
// virtuel (TListView) le jour ou ca rame.
Touch;
LFolder := '';
if FFolderBox.ItemIndex > 0 then // 0 = tous les dossiers
LFolder := FFolderBox.Items[FFolderBox.ItemIndex];
@@ -1105,6 +1128,7 @@ procedure TForm8.OnRevealTap(Sender: TObject);
var
I: Integer;
begin
Touch;
if not (Sender is TButton) then Exit;
I := TButton(Sender).Tag;
if (I < 0) or (I > High(FRows)) then Exit;
@@ -1126,6 +1150,7 @@ procedure TForm8.OnCopyTap(Sender: TObject);
var
I: Integer;
begin
Touch;
if not (Sender is TButton) then Exit;
I := TButton(Sender).Tag;
// -1 : la ligne du TOTP, qui n'est pas dans FRows - sa valeur change chaque
@@ -1137,13 +1162,12 @@ begin
end;
procedure TForm8.CopyToClipboard(const AText, ALabel: string);
var
LClip: IFMXClipboardService;
begin
if AText = '' then Exit;
if not TPlatformServices.Current.SupportsPlatformService(
IFMXClipboardService, LClip) then Exit;
LClip.SetClipboard(AText);
// Marque le contenu sensible : sans cela Android 13+ affiche un apercu de ce
// qui vient d'etre copie, mot de passe compris, en clair et hors de
// l'application.
if not CopySensitive(AText) then Exit;
FCopied := AText;
// Reprogramme depuis zero : deux copies successives donnent au second
// contenu son delai complet, pas le reste de celui du premier.
@@ -1157,17 +1181,12 @@ begin
end;
procedure TForm8.OnClipTimer(Sender: TObject);
var
LClip: IFMXClipboardService;
begin
FClipTimer.Enabled := False;
if FCopied = '' then Exit;
if TPlatformServices.Current.SupportsPlatformService(
IFMXClipboardService, LClip) then
// Seulement si c'est TOUJOURS notre texte : l'utilisateur a pu copier
// autre chose entre-temps, et le lui effacer serait une surprise.
if LClip.GetClipboard.ToString = FCopied then
LClip.SetClipboard('');
// Seulement si c'est TOUJOURS notre texte : l'utilisateur a pu copier autre
// chose entre-temps, et le lui effacer serait une surprise.
if CurrentClipboard = FCopied then ClearClipboard;
FCopied := '';
end;
@@ -1211,6 +1230,26 @@ begin
end);
end;
procedure TForm8.Touch;
begin
FLastActivity := Now;
end;
procedure TForm8.OnIdleTimer(Sender: TObject);
begin
// Rien d'ouvert : rien a verrouiller.
if FVault.Count = 0 then
begin
FIdleTimer.Enabled := False;
Exit;
end;
if MinutesBetween(Now, FLastActivity) >= AUTOLOCK_MINUTES then
begin
Log('Verrouille apres ' + AUTOLOCK_MINUTES.ToString + ' min d''inactivite');
LockVault;
end;
end;
procedure TForm8.Status(const AMsg: string);
begin
FStatus.Text := AMsg;