feat(apk): copie de secours au mot de passe, le coffre survit au retrait du schema
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -186,6 +186,19 @@ procedure SetPendingFlag(AOn: Boolean);
|
|||||||
/// Chemin du fichier, pour le journal et les tests.
|
/// Chemin du fichier, pour le journal et les tests.
|
||||||
function VaultPath: string;
|
function VaultPath: string;
|
||||||
|
|
||||||
|
/// COPIE DE SECOURS du cache quand il est scelle par le verrouillage d'ecran.
|
||||||
|
///
|
||||||
|
/// Retirer le schema (ou le reinitialiser) fait DETRUIRE par Android la cle qui
|
||||||
|
/// scelle vault.bin : le coffre devenait illisible et il fallait le
|
||||||
|
/// retelecharger. SaveVault ecrit donc a cote une seconde copie scellee sous le
|
||||||
|
/// mot de passe (meme format que le code d'app, VER_PIN : cle de l'appareil
|
||||||
|
/// sans contrainte + PBKDF2 du mot de passe), que rien dans Android n'invalide.
|
||||||
|
/// Elle n'existe QUE pour un cache VER_WRAP ; tout autre scellement l'efface.
|
||||||
|
function HasRecoveryVault: Boolean;
|
||||||
|
function LoadRecoveryVault(out APlainJson, AETag, ASyncPwd: string;
|
||||||
|
out AKey, ASalt: TBytes; out AIterations: Integer;
|
||||||
|
const APwd: string): Boolean;
|
||||||
|
|
||||||
/// Reglages du serveur : URL et jeton Bearer.
|
/// Reglages du serveur : URL et jeton Bearer.
|
||||||
///
|
///
|
||||||
/// FICHIER SEPARE, ET PROTECTION PLUS FAIBLE, DELIBEREMENT. Ils sont chiffres
|
/// FICHIER SEPARE, ET PROTECTION PLUS FAIBLE, DELIBEREMENT. Ils sont chiffres
|
||||||
@@ -218,6 +231,7 @@ const
|
|||||||
LOCAL_FILE = 'vault-local.json';
|
LOCAL_FILE = 'vault-local.json';
|
||||||
PENDING_FILE = 'pending.flag';
|
PENDING_FILE = 'pending.flag';
|
||||||
SETTINGS_FILE = 'settings.bin';
|
SETTINGS_FILE = 'settings.bin';
|
||||||
|
RECOVER_FILE = 'vault-recover.bin';
|
||||||
// Un alias par schema : la contrainte d'authentification est figee a la
|
// Un alias par schema : la contrainte d'authentification est figee a la
|
||||||
// creation de la cle et ne peut pas etre ajoutee apres coup.
|
// creation de la cle et ne peut pas etre ajoutee apres coup.
|
||||||
KEY_ALIAS = 'pm_vault_key'; // AES, sans authentification
|
KEY_ALIAS = 'pm_vault_key'; // AES, sans authentification
|
||||||
@@ -298,12 +312,18 @@ begin
|
|||||||
TFile.Delete(LocalContainerPath);
|
TFile.Delete(LocalContainerPath);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
function RecoverPath: string;
|
||||||
|
begin
|
||||||
|
Result := TPath.Combine(TPath.GetHomePath, RECOVER_FILE);
|
||||||
|
end;
|
||||||
|
|
||||||
procedure DropLocalVault;
|
procedure DropLocalVault;
|
||||||
begin
|
begin
|
||||||
// Chacun sous son propre try : un fichier absent ou verrouille ne doit pas
|
// Chacun sous son propre try : un fichier absent ou verrouille ne doit pas
|
||||||
// empecher les deux autres de partir - un effacement a moitie fait laisse
|
// empecher les deux autres de partir - un effacement a moitie fait laisse
|
||||||
// l'appareil dans un etat que personne n'a voulu.
|
// l'appareil dans un etat que personne n'a voulu.
|
||||||
try if TFile.Exists(VaultPath) then TFile.Delete(VaultPath); except end;
|
try if TFile.Exists(VaultPath) then TFile.Delete(VaultPath); except end;
|
||||||
|
try if TFile.Exists(RecoverPath) then TFile.Delete(RecoverPath); except end;
|
||||||
try DropLocalContainer; except end;
|
try DropLocalContainer; except end;
|
||||||
try SetPendingFlag(False); except end;
|
try SetPendingFlag(False); except end;
|
||||||
end;
|
end;
|
||||||
@@ -335,6 +355,11 @@ begin
|
|||||||
Result := TPath.Combine(TPath.GetHomePath, VAULT_FILE);
|
Result := TPath.Combine(TPath.GetHomePath, VAULT_FILE);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
function HasRecoveryVault: Boolean;
|
||||||
|
begin
|
||||||
|
Result := TFile.Exists(RecoverPath);
|
||||||
|
end;
|
||||||
|
|
||||||
function HasVault: Boolean;
|
function HasVault: Boolean;
|
||||||
begin
|
begin
|
||||||
Result := TFile.Exists(VaultPath);
|
Result := TFile.Exists(VaultPath);
|
||||||
@@ -344,6 +369,8 @@ procedure ClearVault;
|
|||||||
begin
|
begin
|
||||||
if TFile.Exists(VaultPath) then
|
if TFile.Exists(VaultPath) then
|
||||||
TFile.Delete(VaultPath);
|
TFile.Delete(VaultPath);
|
||||||
|
if TFile.Exists(RecoverPath) then
|
||||||
|
TFile.Delete(RecoverPath);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
{$IF DEFINED(ANDROID)}
|
{$IF DEFINED(ANDROID)}
|
||||||
@@ -919,7 +946,7 @@ begin
|
|||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure SaveVault(const APlainJson, AETag, ASyncPwd: string;
|
procedure SaveVaultAt(const APath, APlainJson, AETag, ASyncPwd: string;
|
||||||
ARequireAuth, AFreshKey: Boolean; const APin: string;
|
ARequireAuth, AFreshKey: Boolean; const APin: string;
|
||||||
const AKey, ASalt: TBytes; AIterations: Integer; AClearPin: Boolean);
|
const AKey, ASalt: TBytes; AIterations: Integer; AClearPin: Boolean);
|
||||||
{$IF DEFINED(ANDROID)}
|
{$IF DEFINED(ANDROID)}
|
||||||
@@ -1010,7 +1037,7 @@ begin
|
|||||||
LOut[0] := VER_PIN;
|
LOut[0] := VER_PIN;
|
||||||
Move(LIv[0], LOut[1], Length(LIv));
|
Move(LIv[0], LOut[1], Length(LIv));
|
||||||
Move(LCt[0], LOut[1 + Length(LIv)], Length(LCt));
|
Move(LCt[0], LOut[1 + Length(LIv)], Length(LCt));
|
||||||
TFile.WriteAllBytes(VaultPath, LOut);
|
TFile.WriteAllBytes(APath, LOut);
|
||||||
Exit;
|
Exit;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
@@ -1087,7 +1114,7 @@ begin
|
|||||||
Move(LCt[0], LOut[1 + Length(LIv)], Length(LCt));
|
Move(LCt[0], LOut[1 + Length(LIv)], Length(LCt));
|
||||||
end;
|
end;
|
||||||
|
|
||||||
TFile.WriteAllBytes(VaultPath, LOut);
|
TFile.WriteAllBytes(APath, LOut);
|
||||||
end;
|
end;
|
||||||
{$ELSE}
|
{$ELSE}
|
||||||
begin
|
begin
|
||||||
@@ -1095,9 +1122,30 @@ begin
|
|||||||
end;
|
end;
|
||||||
{$ENDIF}
|
{$ENDIF}
|
||||||
|
|
||||||
function LoadVault(out APlainJson, AETag, ASyncPwd: string;
|
procedure SaveVault(const APlainJson, AETag, ASyncPwd: string;
|
||||||
|
ARequireAuth, AFreshKey: Boolean; const APin: string;
|
||||||
|
const AKey, ASalt: TBytes; AIterations: Integer; AClearPin: Boolean);
|
||||||
|
begin
|
||||||
|
SaveVaultAt(VaultPath, APlainJson, AETag, ASyncPwd, ARequireAuth, AFreshKey,
|
||||||
|
APin, AKey, ASalt, AIterations, AClearPin);
|
||||||
|
// Suit le scellement REEL (lu dans l'en-tete), pas celui demande : un
|
||||||
|
// appareil sans verrouillage retombe sur VER_PLAIN, et une ecriture refusee
|
||||||
|
// laisse l'ancien en place.
|
||||||
|
try
|
||||||
|
if VaultNeedsAuth and (ASyncPwd <> '') then
|
||||||
|
SaveVaultAt(RecoverPath, APlainJson, AETag, ASyncPwd, False, False,
|
||||||
|
ASyncPwd, AKey, ASalt, AIterations, False)
|
||||||
|
else if TFile.Exists(RecoverPath) then
|
||||||
|
TFile.Delete(RecoverPath);
|
||||||
|
except
|
||||||
|
on E: Exception do
|
||||||
|
FLastStoreError := 'recovery copy not written: ' + E.Message;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function LoadVaultAt(const APath: string; out APlainJson, AETag, ASyncPwd: string;
|
||||||
out AKey, ASalt: TBytes; out AIterations: Integer;
|
out AKey, ASalt: TBytes; out AIterations: Integer;
|
||||||
const APin: string = ''): Boolean;
|
const APin: string): Boolean;
|
||||||
{$IF DEFINED(ANDROID)}
|
{$IF DEFINED(ANDROID)}
|
||||||
var
|
var
|
||||||
LRaw, LIv, LCt, LPlain, LWrapped, LAesRaw, LInner, LSalt, LPinKey: TBytes;
|
LRaw, LIv, LCt, LPlain, LWrapped, LAesRaw, LInner, LSalt, LPinKey: TBytes;
|
||||||
@@ -1114,10 +1162,10 @@ begin
|
|||||||
ASalt := nil;
|
ASalt := nil;
|
||||||
AIterations := 0;
|
AIterations := 0;
|
||||||
FLastStoreError := '';
|
FLastStoreError := '';
|
||||||
if not TFile.Exists(VaultPath) then Exit;
|
if not TFile.Exists(APath) then Exit;
|
||||||
|
|
||||||
try
|
try
|
||||||
LRaw := TFile.ReadAllBytes(VaultPath);
|
LRaw := TFile.ReadAllBytes(APath);
|
||||||
if Length(LRaw) < 1 then Exit;
|
if Length(LRaw) < 1 then Exit;
|
||||||
|
|
||||||
if LRaw[0] = VER_PIN then
|
if LRaw[0] = VER_PIN then
|
||||||
@@ -1268,4 +1316,21 @@ begin
|
|||||||
end;
|
end;
|
||||||
{$ENDIF}
|
{$ENDIF}
|
||||||
|
|
||||||
|
function LoadVault(out APlainJson, AETag, ASyncPwd: string;
|
||||||
|
out AKey, ASalt: TBytes; out AIterations: Integer;
|
||||||
|
const APin: string = ''): Boolean;
|
||||||
|
begin
|
||||||
|
Result := LoadVaultAt(VaultPath, APlainJson, AETag, ASyncPwd, AKey, ASalt,
|
||||||
|
AIterations, APin);
|
||||||
|
end;
|
||||||
|
|
||||||
|
function LoadRecoveryVault(out APlainJson, AETag, ASyncPwd: string;
|
||||||
|
out AKey, ASalt: TBytes; out AIterations: Integer;
|
||||||
|
const APwd: string): Boolean;
|
||||||
|
begin
|
||||||
|
Result := (APwd <> '') and
|
||||||
|
LoadVaultAt(RecoverPath, APlainJson, AETag, ASyncPwd, AKey, ASalt,
|
||||||
|
AIterations, APwd);
|
||||||
|
end;
|
||||||
|
|
||||||
end.
|
end.
|
||||||
|
|||||||
+72
-4
@@ -130,6 +130,9 @@ type
|
|||||||
/// ouverture qui ne peut pas aboutir, et nomme le telechargement pour ce
|
/// ouverture qui ne peut pas aboutir, et nomme le telechargement pour ce
|
||||||
/// qu'il est : un remplacement.
|
/// qu'il est : un remplacement.
|
||||||
FVaultUnreadable: Boolean;
|
FVaultUnreadable: Boolean;
|
||||||
|
// La cle du verrouillage d'ecran est morte (schema retire ou reinitialise) :
|
||||||
|
// on ouvre la copie de secours, scellee sous le mot de passe.
|
||||||
|
FRecover: Boolean;
|
||||||
// Mot de passe de sync tel qu'il a ete range avec le coffre. Il rend le
|
// Mot de passe de sync tel qu'il a ete range avec le coffre. Il rend le
|
||||||
// rafraichissement de fond silencieux : sans lui, chaque modification
|
// rafraichissement de fond silencieux : sans lui, chaque modification
|
||||||
// faite sur le poste ferait apparaitre une invite au lancement.
|
// faite sur le poste ferait apparaitre une invite au lancement.
|
||||||
@@ -508,7 +511,8 @@ begin
|
|||||||
// coffre scelle sous un code s'ouvrait donc sans code, echouait, et l'echec
|
// coffre scelle sous un code s'ouvrait donc sans code, echouait, et l'echec
|
||||||
// etait pris pour une corruption - l'appareil proposait de retelecharger un
|
// etait pris pour une corruption - l'appareil proposait de retelecharger un
|
||||||
// coffre qu'il avait parfaitement en main.
|
// coffre qu'il avait parfaitement en main.
|
||||||
WebCall(Format('PM.setPinNeeded(%s)', [LowerCase(BoolToStr(VaultNeedsPin, True))]));
|
WebCall(Format('PM.setPinNeeded(%s)',
|
||||||
|
[LowerCase(BoolToStr(VaultNeedsPin or FRecover, True))]));
|
||||||
|
|
||||||
// LES TEMOINS SE REPOSENT ICI, A CHAQUE RETOUR SUR L'ACCUEIL.
|
// LES TEMOINS SE REPOSENT ICI, A CHAQUE RETOUR SUR L'ACCUEIL.
|
||||||
//
|
//
|
||||||
@@ -540,7 +544,9 @@ begin
|
|||||||
LCanOpen := HasVault and not FVaultUnreadable;
|
LCanOpen := HasVault and not FVaultUnreadable;
|
||||||
if LCanOpen then
|
if LCanOpen then
|
||||||
begin
|
begin
|
||||||
if VaultNeedsPin then
|
if FRecover then
|
||||||
|
Status('Screen lock changed - type your vault password')
|
||||||
|
else if VaultNeedsPin then
|
||||||
Status('Vault on this device - unlock code required')
|
Status('Vault on this device - unlock code required')
|
||||||
else if VaultNeedsAuth then
|
else if VaultNeedsAuth then
|
||||||
Status('Vault on this device - protected')
|
Status('Vault on this device - protected')
|
||||||
@@ -1787,6 +1793,7 @@ begin
|
|||||||
FPending := False;
|
FPending := False;
|
||||||
FETag := '';
|
FETag := '';
|
||||||
FVaultUnreadable := False;
|
FVaultUnreadable := False;
|
||||||
|
FRecover := False;
|
||||||
Log('DEBUG: local vault dropped');
|
Log('DEBUG: local vault dropped');
|
||||||
ShowUnlock;
|
ShowUnlock;
|
||||||
WebCall('PM.toast("Local vault deleted")');
|
WebCall('PM.toast("Local vault deleted")');
|
||||||
@@ -1904,6 +1911,7 @@ begin
|
|||||||
Log(' ' + FVault.LiveCount.ToString + ' entries, ' +
|
Log(' ' + FVault.LiveCount.ToString + ' entries, ' +
|
||||||
FVault.TrashCount.ToString + ' in trash');
|
FVault.TrashCount.ToString + ' in trash');
|
||||||
FVaultUnreadable := False;
|
FVaultUnreadable := False;
|
||||||
|
FRecover := False;
|
||||||
Touch;
|
Touch;
|
||||||
FIdleTimer.Enabled := True;
|
FIdleTimer.Enabled := True;
|
||||||
// Deja dans l'application : reconstruire la liste SANS y ramener
|
// Deja dans l'application : reconstruire la liste SANS y ramener
|
||||||
@@ -2125,7 +2133,7 @@ procedure TForm8.OpenLocalNow;
|
|||||||
var
|
var
|
||||||
LPlain, LETag, LPwd: string;
|
LPlain, LETag, LPwd: string;
|
||||||
LSw: TStopwatch;
|
LSw: TStopwatch;
|
||||||
LChainSync: Boolean;
|
LChainSync, LOk: Boolean;
|
||||||
begin
|
begin
|
||||||
// LU ET CONSOMME ICI, une fois pour toutes : un drapeau qui survivrait a un
|
// LU ET CONSOMME ICI, une fois pour toutes : un drapeau qui survivrait a un
|
||||||
// code faux ferait synchroniser la prochaine ouverture, sans rapport.
|
// code faux ferait synchroniser la prochaine ouverture, sans rapport.
|
||||||
@@ -2140,8 +2148,28 @@ begin
|
|||||||
// LA CLE REVIENT AVEC LE CACHE, et c'est tout l'objet de l'y avoir mise :
|
// LA CLE REVIENT AVEC LE CACHE, et c'est tout l'objet de l'y avoir mise :
|
||||||
// ouvrir depuis le cache ne derive rien, donc sans cela le PREMIER
|
// ouvrir depuis le cache ne derive rien, donc sans cela le PREMIER
|
||||||
// enregistrement de chaque session repayait les quatre secondes et demie.
|
// enregistrement de chaque session repayait les quatre secondes et demie.
|
||||||
if LoadVault(LPlain, LETag, LPwd, FKey, FKeySalt, FKeyIters, FPin) then
|
if FRecover then
|
||||||
|
LOk := LoadRecoveryVault(LPlain, LETag, LPwd, FKey, FKeySalt, FKeyIters, FPin)
|
||||||
|
else
|
||||||
|
LOk := LoadVault(LPlain, LETag, LPwd, FKey, FKeySalt, FKeyIters, FPin);
|
||||||
|
if LOk then
|
||||||
begin
|
begin
|
||||||
|
if FRecover then
|
||||||
|
begin
|
||||||
|
FRecover := False;
|
||||||
|
// RESCELLER TOUT DE SUITE, sous ce qui existe maintenant : un nouveau
|
||||||
|
// verrouillage d'ecran s'il y en a un (cle neuve, l'ancienne est morte),
|
||||||
|
// sinon le mot de passe qu'on vient de taper - jamais en clair.
|
||||||
|
try
|
||||||
|
if BiometricAvailable then
|
||||||
|
SaveVault(LPlain, LETag, LPwd, True, True, '', FKey, FKeySalt, FKeyIters)
|
||||||
|
else
|
||||||
|
SaveVault(LPlain, LETag, LPwd, False, False, LPwd, FKey, FKeySalt, FKeyIters);
|
||||||
|
Log('Opened with the vault password after a screen lock change; cache re-sealed');
|
||||||
|
except
|
||||||
|
on E: Exception do Log('Re-seal after recovery failed: ' + E.Message);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
Log('Vault opened from cache in ' + LSw.ElapsedMilliseconds.ToString + ' ms');
|
Log('Vault opened from cache in ' + LSw.ElapsedMilliseconds.ToString + ' ms');
|
||||||
FETag := LETag;
|
FETag := LETag;
|
||||||
if LETag = '' then
|
if LETag = '' then
|
||||||
@@ -2272,6 +2300,25 @@ begin
|
|||||||
// l'appareil detient, et ecraserait donc l'explication.
|
// l'appareil detient, et ecraserait donc l'explication.
|
||||||
// Un code faux se retape ; le cache, lui, n'a rien perdu. Le marquer
|
// Un code faux se retape ; le cache, lui, n'a rien perdu. Le marquer
|
||||||
// illisible retirerait le bouton d'ouverture pour le reste de la session.
|
// illisible retirerait le bouton d'ouverture pour le reste de la session.
|
||||||
|
if FRecover then
|
||||||
|
begin
|
||||||
|
Log('Recovery copy did not open under this password');
|
||||||
|
ShowUnlock;
|
||||||
|
StatusErr('Wrong vault password');
|
||||||
|
WebCall('PM.focusPin()');
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
|
// LA CLE VIENT DE MOURIR SOUS NOS YEUX, mais la copie de secours est la :
|
||||||
|
// on demande le mot de passe au lieu de declarer le coffre illisible.
|
||||||
|
if LastStoreKeyInvalidated and HasRecoveryVault then
|
||||||
|
begin
|
||||||
|
FRecover := True;
|
||||||
|
Log('Screen lock key destroyed - recovery copy available');
|
||||||
|
ShowUnlock;
|
||||||
|
StatusErr('Screen lock changed - type your vault password');
|
||||||
|
WebCall('PM.focusPin()');
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
if VaultNeedsPin then
|
if VaultNeedsPin then
|
||||||
begin
|
begin
|
||||||
Log('Vault did not open under this unlock code');
|
Log('Vault did not open under this unlock code');
|
||||||
@@ -2319,6 +2366,27 @@ begin
|
|||||||
WebCall('PM.focusPin()');
|
WebCall('PM.focusPin()');
|
||||||
Exit;
|
Exit;
|
||||||
end;
|
end;
|
||||||
|
// PLUS DE VERROUILLAGE D'ECRAN : la cle qui scelle le cache est morte, et
|
||||||
|
// demander l'empreinte ne pourrait que repondre "indisponible". La copie de
|
||||||
|
// secours s'ouvre au mot de passe.
|
||||||
|
if (not FRecover) and VaultNeedsAuth and HasRecoveryVault and
|
||||||
|
(not BiometricAvailable) then
|
||||||
|
begin
|
||||||
|
FRecover := True;
|
||||||
|
Log('No screen lock any more - recovery copy available');
|
||||||
|
ShowUnlock;
|
||||||
|
end;
|
||||||
|
if FRecover then
|
||||||
|
begin
|
||||||
|
if FPin = '' then
|
||||||
|
begin
|
||||||
|
StatusErr('Screen lock changed - type your vault password');
|
||||||
|
WebCall('PM.focusPin()');
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
|
OpenLocalNow;
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
// L'authentification affiche un dialogue systeme : elle doit rester sur le
|
// L'authentification affiche un dialogue systeme : elle doit rester sur le
|
||||||
// thread principal, et PRECEDER toute lecture du coffre.
|
// thread principal, et PRECEDER toute lecture du coffre.
|
||||||
if not VaultNeedsAuth then
|
if not VaultNeedsAuth then
|
||||||
|
|||||||
Reference in New Issue
Block a user