feat(android): an unlock code that survives a screen-lock change
The reported pain: changing the pattern destroys the local cache every time. That is Android doing exactly what it promises - a key created with setUserAuthenticationRequired is invalidated the moment the screen lock changes, so a new lock cannot open data sealed under the old one. No storage format changes that, which is why SQLite would not have helped: the problem is what the key is tied to, not what the file looks like. So the cache gains a third sealing format, VER_PIN, with two layers: inner - AES-GCM under PBKDF2(code, 100k) : it takes the code outer - the Keystore AES key WITHOUT the auth constraint : it takes the device The outer key is never invalidated, so the file survives any change to the screen lock; the code restores the protection that removing the constraint would otherwise cost. 100k iterations rather than 600k because a four to six digit code carries little entropy - more rounds mostly slow the honest user, and the real barrier is the outer layer requiring this phone. The code lives in a fourth settings field and is NEVER stored: empty at every launch, typed to open, and used to re-seal on every write. That is what separates it from the token, whose persistence is a convenience with no consequence since it only opens ciphertext. Built but NOT installed - the phone went off the network. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -83,8 +83,16 @@ function HasVault: Boolean;
|
|||||||
/// protection vient de reapparaitre sur l'appareil : le changement qui l'a
|
/// protection vient de reapparaitre sur l'appareil : le changement qui l'a
|
||||||
/// ramenee a tres probablement invalide l'ancienne cle, et ca ne se verrait
|
/// ramenee a tres probablement invalide l'ancienne cle, et ca ne se verrait
|
||||||
/// qu'a la prochaine lecture - trop tard, le fichier serait deja scelle.
|
/// qu'a la prochaine lecture - trop tard, le fichier serait deja scelle.
|
||||||
|
/// APin non vide : le cache est scelle sous ce code (VER_PIN), et ARequireAuth
|
||||||
|
/// est alors ignore - les deux protections ne se cumulent pas, et celle du code
|
||||||
|
/// est la seule qui survive a un changement de verrouillage d'ecran.
|
||||||
procedure SaveVault(const APlainJson, AETag, ASyncPwd: string;
|
procedure SaveVault(const APlainJson, AETag, ASyncPwd: string;
|
||||||
ARequireAuth: Boolean = False; AFreshKey: Boolean = False);
|
ARequireAuth: Boolean = False; AFreshKey: Boolean = False;
|
||||||
|
const APin: string = '');
|
||||||
|
|
||||||
|
/// Le fichier local est-il scelle sous un code d'application ? Lu dans
|
||||||
|
/// l'en-tete, sans rien dechiffrer.
|
||||||
|
function VaultNeedsPin: Boolean;
|
||||||
|
|
||||||
/// Le coffre local exige-t-il une authentification pour etre lu ? Lu dans
|
/// Le coffre local exige-t-il une authentification pour etre lu ? Lu dans
|
||||||
/// l'en-tete du fichier, sans dechiffrer quoi que ce soit.
|
/// l'en-tete du fichier, sans dechiffrer quoi que ce soit.
|
||||||
@@ -94,7 +102,8 @@ function VaultNeedsAuth: Boolean;
|
|||||||
/// reinstallation, fichier altere) - dans ce cas il faut retelecharger.
|
/// reinstallation, fichier altere) - dans ce cas il faut retelecharger.
|
||||||
/// ASyncPwd est vide pour un fichier ecrit avant que le mot de passe y soit
|
/// ASyncPwd est vide pour un fichier ecrit avant que le mot de passe y soit
|
||||||
/// range : l'appelant redemande alors, et le prochain enregistrement le pose.
|
/// range : l'appelant redemande alors, et le prochain enregistrement le pose.
|
||||||
function LoadVault(out APlainJson, AETag, ASyncPwd: string): Boolean;
|
function LoadVault(out APlainJson, AETag, ASyncPwd: string;
|
||||||
|
const APin: string = ''): Boolean;
|
||||||
|
|
||||||
/// Pourquoi la derniere lecture a echoue. Vide apres un succes.
|
/// Pourquoi la derniere lecture a echoue. Vide apres un succes.
|
||||||
///
|
///
|
||||||
@@ -129,7 +138,11 @@ implementation
|
|||||||
|
|
||||||
{$IF DEFINED(ANDROID)}
|
{$IF DEFINED(ANDROID)}
|
||||||
uses
|
uses
|
||||||
Androidapi.Jni, Androidapi.JNIBridge, Androidapi.JNI.JavaTypes, Androidapi.Helpers;
|
// PMA.Crypto pour PBKDF2 et SecureRandomBytes : le code de l'application
|
||||||
|
// derive sa cle exactement comme le conteneur de sync, avec la meme
|
||||||
|
// implementation eprouvee contre le vecteur de test.
|
||||||
|
Androidapi.Jni, Androidapi.JNIBridge, Androidapi.JNI.JavaTypes, Androidapi.Helpers,
|
||||||
|
PMA.Crypto;
|
||||||
{$ENDIF}
|
{$ENDIF}
|
||||||
|
|
||||||
const
|
const
|
||||||
@@ -155,6 +168,22 @@ const
|
|||||||
// et s'il faut authentifier d'abord.
|
// et s'il faut authentifier d'abord.
|
||||||
VER_PLAIN = 1; // AES Keystore sans contrainte
|
VER_PLAIN = 1; // AES Keystore sans contrainte
|
||||||
VER_WRAP = 3; // AES aleatoire enveloppee par la cle publique RSA
|
VER_WRAP = 3; // AES aleatoire enveloppee par la cle publique RSA
|
||||||
|
// CODE CHOISI DANS L'APPLICATION, deux couches superposees :
|
||||||
|
// interieur : AES-GCM sous PBKDF2(code) -> il faut le code
|
||||||
|
// exterieur : cle AES du Keystore SANS contrainte -> il faut l'appareil
|
||||||
|
//
|
||||||
|
// C'est le seul schema qui survive a un changement de verrouillage d'ecran.
|
||||||
|
// VER_WRAP y meurt par construction : Android invalide une cle exigeant
|
||||||
|
// l'authentification des que le code de l'ecran change, precisement pour
|
||||||
|
// qu'un nouveau code n'ouvre pas d'anciennes donnees. La cle sans contrainte,
|
||||||
|
// elle, n'est pas invalidee - et le code de l'application rend la protection
|
||||||
|
// que cela retire.
|
||||||
|
VER_PIN = 4;
|
||||||
|
// 100k et non 600k : l'entropie d'un code a quatre ou six chiffres est
|
||||||
|
// faible, et multiplier les tours ralentit surtout l'usage honnete. La vraie
|
||||||
|
// barriere est la couche exterieure, qui exige l'appareil.
|
||||||
|
PIN_ITERS = 100000;
|
||||||
|
SALT_LEN = 16;
|
||||||
RSA_TRANSFORM = 'RSA/ECB/OAEPWithSHA-256AndMGF1Padding';
|
RSA_TRANSFORM = 'RSA/ECB/OAEPWithSHA-256AndMGF1Padding';
|
||||||
|
|
||||||
var
|
var
|
||||||
@@ -718,6 +747,26 @@ begin
|
|||||||
end;
|
end;
|
||||||
{$ENDIF}
|
{$ENDIF}
|
||||||
|
|
||||||
|
function VaultNeedsPin: Boolean;
|
||||||
|
{$IF DEFINED(ANDROID)}
|
||||||
|
var
|
||||||
|
LRaw: TBytes;
|
||||||
|
begin
|
||||||
|
Result := False;
|
||||||
|
if not TFile.Exists(VaultPath) then Exit;
|
||||||
|
try
|
||||||
|
LRaw := TFile.ReadAllBytes(VaultPath);
|
||||||
|
Result := (Length(LRaw) > 0) and (LRaw[0] = VER_PIN);
|
||||||
|
except
|
||||||
|
// Illisible : l'appelant le decouvrira en tentant de l'ouvrir.
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
{$ELSE}
|
||||||
|
begin
|
||||||
|
Result := False;
|
||||||
|
end;
|
||||||
|
{$ENDIF}
|
||||||
|
|
||||||
function VaultNeedsAuth: Boolean;
|
function VaultNeedsAuth: Boolean;
|
||||||
var
|
var
|
||||||
LFs: TFileStream;
|
LFs: TFileStream;
|
||||||
@@ -736,11 +785,11 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
procedure SaveVault(const APlainJson, AETag, ASyncPwd: string;
|
procedure SaveVault(const APlainJson, AETag, ASyncPwd: string;
|
||||||
ARequireAuth, AFreshKey: Boolean);
|
ARequireAuth, AFreshKey: Boolean; const APin: string);
|
||||||
{$IF DEFINED(ANDROID)}
|
{$IF DEFINED(ANDROID)}
|
||||||
var
|
var
|
||||||
LWrap: TJSONObject;
|
LWrap: TJSONObject;
|
||||||
LPlain, LIv, LCt, LOut, LAesRaw, LWrapped: TBytes;
|
LPlain, LIv, LCt, LOut, LAesRaw, LWrapped, LSalt, LPinKey, LInner: TBytes;
|
||||||
LCipher: JCipherX;
|
LCipher: JCipherX;
|
||||||
LGotAuth: Boolean;
|
LGotAuth: Boolean;
|
||||||
LOfs: Integer;
|
LOfs: Integer;
|
||||||
@@ -758,6 +807,48 @@ begin
|
|||||||
LWrap.Free;
|
LWrap.Free;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
if APin <> '' then
|
||||||
|
begin
|
||||||
|
// COUCHE INTERIEURE : le code. Sel neuf a chaque ecriture - il ne coute
|
||||||
|
// rien et evite qu'un meme code produise deux fois la meme cle.
|
||||||
|
LSalt := SecureRandomBytes(SALT_LEN);
|
||||||
|
LPinKey := DeriveKeyPBKDF2Native(APin, LSalt, PIN_ITERS, 32);
|
||||||
|
if LPinKey = nil then
|
||||||
|
LPinKey := DeriveKeyPBKDF2(APin, LSalt, PIN_ITERS, 32);
|
||||||
|
LIv := RandomBytes(IV_LEN);
|
||||||
|
LCipher := TJCipherX.JavaClass.getInstance(StringToJString('AES/GCM/NoPadding'));
|
||||||
|
LCipher.init(TJCipherX.JavaClass.ENCRYPT_MODE,
|
||||||
|
TJSecretKeySpecX.JavaClass.init(ToJava(LPinKey), StringToJString('AES')),
|
||||||
|
TJGCMParameterSpecX.JavaClass.init(128, ToJava(LIv)));
|
||||||
|
LCt := FromJava(LCipher.doFinal(ToJava(LPlain)));
|
||||||
|
|
||||||
|
// Sel et iterations voyagent EN CLAIR avec la charge : ils ne sont pas
|
||||||
|
// secrets, et sans eux le code ne redonnerait pas la meme cle.
|
||||||
|
SetLength(LInner, SALT_LEN + 4 + IV_LEN + Length(LCt));
|
||||||
|
LOfs := 0;
|
||||||
|
Move(LSalt[0], LInner[LOfs], SALT_LEN); Inc(LOfs, SALT_LEN);
|
||||||
|
LInner[LOfs] := Byte(PIN_ITERS shr 24); LInner[LOfs+1] := Byte(PIN_ITERS shr 16);
|
||||||
|
LInner[LOfs+2] := Byte(PIN_ITERS shr 8); LInner[LOfs+3] := Byte(PIN_ITERS);
|
||||||
|
Inc(LOfs, 4);
|
||||||
|
Move(LIv[0], LInner[LOfs], IV_LEN); Inc(LOfs, IV_LEN);
|
||||||
|
Move(LCt[0], LInner[LOfs], Length(LCt));
|
||||||
|
|
||||||
|
// COUCHE EXTERIEURE : la cle AES du Keystore, sans contrainte. Elle lie le
|
||||||
|
// fichier a l'appareil sans jamais etre invalidee par un changement de
|
||||||
|
// verrouillage d'ecran - c'est tout l'objet de ce format.
|
||||||
|
LCipher := TJCipherX.JavaClass.getInstance(StringToJString('AES/GCM/NoPadding'));
|
||||||
|
LCipher.init(TJCipherX.JavaClass.ENCRYPT_MODE, EnsureAesKey);
|
||||||
|
LIv := FromJava(LCipher.getIV);
|
||||||
|
LCt := FromJava(LCipher.doFinal(ToJava(LInner)));
|
||||||
|
|
||||||
|
SetLength(LOut, 1 + Length(LIv) + Length(LCt));
|
||||||
|
LOut[0] := VER_PIN;
|
||||||
|
Move(LIv[0], LOut[1], Length(LIv));
|
||||||
|
Move(LCt[0], LOut[1 + Length(LIv)], Length(LCt));
|
||||||
|
TFile.WriteAllBytes(VaultPath, LOut);
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
|
|
||||||
LGotAuth := False;
|
LGotAuth := False;
|
||||||
LWrapped := nil;
|
LWrapped := nil;
|
||||||
if ARequireAuth then
|
if ARequireAuth then
|
||||||
@@ -839,14 +930,15 @@ begin
|
|||||||
end;
|
end;
|
||||||
{$ENDIF}
|
{$ENDIF}
|
||||||
|
|
||||||
function LoadVault(out APlainJson, AETag, ASyncPwd: string): Boolean;
|
function LoadVault(out APlainJson, AETag, ASyncPwd: string;
|
||||||
|
const APin: string = ''): Boolean;
|
||||||
{$IF DEFINED(ANDROID)}
|
{$IF DEFINED(ANDROID)}
|
||||||
var
|
var
|
||||||
LRaw, LIv, LCt, LPlain, LWrapped, LAesRaw: TBytes;
|
LRaw, LIv, LCt, LPlain, LWrapped, LAesRaw, LInner, LSalt, LPinKey: TBytes;
|
||||||
LCipher: JCipherX;
|
LCipher: JCipherX;
|
||||||
LWrap: TJSONObject;
|
LWrap: TJSONObject;
|
||||||
LStore: JKeyStoreX;
|
LStore: JKeyStoreX;
|
||||||
LLen, LOfs: Integer;
|
LLen, LOfs, LIter: Integer;
|
||||||
begin
|
begin
|
||||||
Result := False;
|
Result := False;
|
||||||
APlainJson := '';
|
APlainJson := '';
|
||||||
@@ -859,7 +951,47 @@ begin
|
|||||||
LRaw := TFile.ReadAllBytes(VaultPath);
|
LRaw := TFile.ReadAllBytes(VaultPath);
|
||||||
if Length(LRaw) < 1 then Exit;
|
if Length(LRaw) < 1 then Exit;
|
||||||
|
|
||||||
if LRaw[0] = VER_WRAP then
|
if LRaw[0] = VER_PIN then
|
||||||
|
begin
|
||||||
|
if APin = '' then
|
||||||
|
begin
|
||||||
|
FLastStoreError := 'Code requis';
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
|
// Couche exterieure d'abord : la cle de l'appareil, sans authentification.
|
||||||
|
SetLength(LIv, IV_LEN);
|
||||||
|
Move(LRaw[1], LIv[0], IV_LEN);
|
||||||
|
SetLength(LCt, Length(LRaw) - 1 - IV_LEN);
|
||||||
|
Move(LRaw[1 + IV_LEN], LCt[0], Length(LCt));
|
||||||
|
LCipher := TJCipherX.JavaClass.getInstance(StringToJString('AES/GCM/NoPadding'));
|
||||||
|
LCipher.init(TJCipherX.JavaClass.DECRYPT_MODE, EnsureAesKey,
|
||||||
|
TJGCMParameterSpecX.JavaClass.init(128, ToJava(LIv)));
|
||||||
|
LInner := FromJava(LCipher.doFinal(ToJava(LCt)));
|
||||||
|
|
||||||
|
if Length(LInner) < SALT_LEN + 4 + IV_LEN + 16 then Exit;
|
||||||
|
LOfs := 0;
|
||||||
|
SetLength(LSalt, SALT_LEN);
|
||||||
|
Move(LInner[LOfs], LSalt[0], SALT_LEN); Inc(LOfs, SALT_LEN);
|
||||||
|
LIter := (LInner[LOfs] shl 24) or (LInner[LOfs+1] shl 16)
|
||||||
|
or (LInner[LOfs+2] shl 8) or LInner[LOfs+3];
|
||||||
|
Inc(LOfs, 4);
|
||||||
|
SetLength(LIv, IV_LEN);
|
||||||
|
Move(LInner[LOfs], LIv[0], IV_LEN); Inc(LOfs, IV_LEN);
|
||||||
|
SetLength(LCt, Length(LInner) - LOfs);
|
||||||
|
Move(LInner[LOfs], LCt[0], Length(LCt));
|
||||||
|
|
||||||
|
// Couche interieure : le code. Un code faux echoue ICI, sur le tag GCM,
|
||||||
|
// et se lit donc comme une alteration - c'est le meme evenement.
|
||||||
|
LPinKey := DeriveKeyPBKDF2Native(APin, LSalt, LIter, 32);
|
||||||
|
if LPinKey = nil then
|
||||||
|
LPinKey := DeriveKeyPBKDF2(APin, LSalt, LIter, 32);
|
||||||
|
LCipher := TJCipherX.JavaClass.getInstance(StringToJString('AES/GCM/NoPadding'));
|
||||||
|
LCipher.init(TJCipherX.JavaClass.DECRYPT_MODE,
|
||||||
|
TJSecretKeySpecX.JavaClass.init(ToJava(LPinKey), StringToJString('AES')),
|
||||||
|
TJGCMParameterSpecX.JavaClass.init(128, ToJava(LIv)));
|
||||||
|
LPlain := FromJava(LCipher.doFinal(ToJava(LCt)));
|
||||||
|
end
|
||||||
|
else if LRaw[0] = VER_WRAP then
|
||||||
begin
|
begin
|
||||||
if Length(LRaw) < 3 then Exit;
|
if Length(LRaw) < 3 then Exit;
|
||||||
LLen := (LRaw[1] shl 8) or LRaw[2];
|
LLen := (LRaw[1] shl 8) or LRaw[2];
|
||||||
|
|||||||
@@ -261,6 +261,14 @@
|
|||||||
type="password" autocomplete="off">
|
type="password" autocomplete="off">
|
||||||
<div class="in-x" data-clear="pwd">×</div>
|
<div class="in-x" data-clear="pwd">×</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- JAMAIS PRE-REMPLI, contrairement aux trois autres : ce code protege
|
||||||
|
le cache local, et le retenir reviendrait a ne rien proteger. Il se
|
||||||
|
retape a chaque lancement. -->
|
||||||
|
<div class="in-wrap">
|
||||||
|
<input class="field-in" id="pin" placeholder="unlock code (local cache)"
|
||||||
|
type="password" inputmode="numeric" autocomplete="off">
|
||||||
|
<div class="in-x" data-clear="pin">×</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<pre id="log" hidden></pre>
|
<pre id="log" hidden></pre>
|
||||||
</div>
|
</div>
|
||||||
@@ -345,7 +353,8 @@ var entries = [], folders = [], folder = '', query = '', view = 'unlock';
|
|||||||
// --- deverrouillage --------------------------------------------------------
|
// --- deverrouillage --------------------------------------------------------
|
||||||
var elUrl = document.getElementById('url'),
|
var elUrl = document.getElementById('url'),
|
||||||
elTok = document.getElementById('tok'),
|
elTok = document.getElementById('tok'),
|
||||||
elPwd = document.getElementById('pwd');
|
elPwd = document.getElementById('pwd'),
|
||||||
|
elPin = document.getElementById('pin');
|
||||||
|
|
||||||
// Les trois valeurs remontent A LA PERTE DU FOCUS, pas a chaque touche : c'est
|
// Les trois valeurs remontent A LA PERTE DU FOCUS, pas a chaque touche : c'est
|
||||||
// une navigation par champ modifie au lieu d'une par caractere. Delphi les
|
// une navigation par champ modifie au lieu d'une par caractere. Delphi les
|
||||||
@@ -353,14 +362,16 @@ var elUrl = document.getElementById('url'),
|
|||||||
function pushConfig() {
|
function pushConfig() {
|
||||||
send('cfg?u=' + encodeURIComponent(elUrl.value.trim())
|
send('cfg?u=' + encodeURIComponent(elUrl.value.trim())
|
||||||
+ '&t=' + encodeURIComponent(elTok.value.trim())
|
+ '&t=' + encodeURIComponent(elTok.value.trim())
|
||||||
+ '&p=' + encodeURIComponent(elPwd.value));
|
+ '&p=' + encodeURIComponent(elPwd.value)
|
||||||
|
+ '&c=' + encodeURIComponent(elPin.value));
|
||||||
}
|
}
|
||||||
[elUrl, elTok, elPwd].forEach(function (el) {
|
[elUrl, elTok, elPwd, elPin].forEach(function (el) {
|
||||||
el.addEventListener('change', pushConfig);
|
el.addEventListener('change', pushConfig);
|
||||||
el.addEventListener('blur', pushConfig);
|
el.addEventListener('blur', pushConfig);
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('btnOpen').addEventListener('click', function () {
|
document.getElementById('btnOpen').addEventListener('click', function () {
|
||||||
|
pushConfig(); // le code vient peut-etre d'etre tape
|
||||||
send('openvault');
|
send('openvault');
|
||||||
});
|
});
|
||||||
document.getElementById('btnSync').addEventListener('click', function () {
|
document.getElementById('btnSync').addEventListener('click', function () {
|
||||||
@@ -640,6 +651,7 @@ var PM = {
|
|||||||
s.style.opacity = busy ? .4 : 1;
|
s.style.opacity = busy ? .4 : 1;
|
||||||
},
|
},
|
||||||
setConfig: function (url, tok, pwd, show) {
|
setConfig: function (url, tok, pwd, show) {
|
||||||
|
// elPin volontairement absent : rien ne le restaure, jamais.
|
||||||
elUrl.value = url; elTok.value = tok; elPwd.value = pwd;
|
elUrl.value = url; elTok.value = tok; elPwd.value = pwd;
|
||||||
// Replier des champs vides revient a cacher la seule chose a faire.
|
// Replier des champs vides revient a cacher la seule chose a faire.
|
||||||
document.getElementById('cfg').hidden = !show;
|
document.getElementById('cfg').hidden = !show;
|
||||||
|
|||||||
+14
-5
@@ -114,6 +114,14 @@ type
|
|||||||
/// Coordonnees du serveur. Delphi en est le detenteur ; la page ne fait
|
/// Coordonnees du serveur. Delphi en est le detenteur ; la page ne fait
|
||||||
/// que les editer et les lui renvoyer quand un champ est quitte.
|
/// que les editer et les lui renvoyer quand un champ est quitte.
|
||||||
FUrl, FToken, FPwd: string;
|
FUrl, FToken, FPwd: string;
|
||||||
|
/// Code d'ouverture du cache local, choisi dans l'application. JAMAIS
|
||||||
|
/// ENREGISTRE : il est vide a chaque lancement et doit etre retape. C'est
|
||||||
|
/// ce qui le distingue du jeton, dont la persistance est un confort sans
|
||||||
|
/// consequence puisqu'il n'ouvre que du chiffre.
|
||||||
|
///
|
||||||
|
/// Il remplace la protection par verrouillage d'ecran, qu'Android detruit
|
||||||
|
/// des qu'on change le schema - par conception, et sans recours.
|
||||||
|
FPin: string;
|
||||||
|
|
||||||
// --- l'interface entiere, rendue par le moteur du systeme
|
// --- l'interface entiere, rendue par le moteur du systeme
|
||||||
FLayWeb: TLayout;
|
FLayWeb: TLayout;
|
||||||
@@ -639,7 +647,7 @@ begin
|
|||||||
// ouverture pour retrouver ce qu'on vient d'ecrire.
|
// ouverture pour retrouver ce qu'on vient d'ecrire.
|
||||||
FETag := LPut.ETag;
|
FETag := LPut.ETag;
|
||||||
try
|
try
|
||||||
SaveVault(LNew, FETag, FSyncPwd, BiometricAvailable);
|
SaveVault(LNew, FETag, FSyncPwd, BiometricAvailable, False, FPin);
|
||||||
except
|
except
|
||||||
on E: Exception do Log(' cache not written: ' + E.Message);
|
on E: Exception do Log(' cache not written: ' + E.Message);
|
||||||
end;
|
end;
|
||||||
@@ -869,6 +877,7 @@ begin
|
|||||||
FUrl := TNetEncoding.URL.Decode(Param('u')).Trim;
|
FUrl := TNetEncoding.URL.Decode(Param('u')).Trim;
|
||||||
FToken := TNetEncoding.URL.Decode(Param('t')).Trim;
|
FToken := TNetEncoding.URL.Decode(Param('t')).Trim;
|
||||||
FPwd := TNetEncoding.URL.Decode(Param('p'));
|
FPwd := TNetEncoding.URL.Decode(Param('p'));
|
||||||
|
FPin := TNetEncoding.URL.Decode(Param('c'));
|
||||||
// L'empreinte du mot de passe AUSSI : si une saisie destinee au jeton
|
// L'empreinte du mot de passe AUSSI : si une saisie destinee au jeton
|
||||||
// atterrit dans le champ voisin - les deux sont masques - c'est celle-ci
|
// atterrit dans le champ voisin - les deux sont masques - c'est celle-ci
|
||||||
// qui bouge, et le jeton reste inchange sans que rien ne l'explique.
|
// qui bouge, et le jeton reste inchange sans que rien ne l'explique.
|
||||||
@@ -981,7 +990,7 @@ begin
|
|||||||
// scelle, la mise en forme vers la page, ou le rafraichissement de fond qui
|
// scelle, la mise en forme vers la page, ou le rafraichissement de fond qui
|
||||||
// suit - sinon on cherche au hasard.
|
// suit - sinon on cherche au hasard.
|
||||||
LSw := TStopwatch.StartNew;
|
LSw := TStopwatch.StartNew;
|
||||||
if LoadVault(LPlain, LETag, LPwd) then
|
if LoadVault(LPlain, LETag, LPwd, FPin) then
|
||||||
begin
|
begin
|
||||||
Log('Vault opened from cache in ' + LSw.ElapsedMilliseconds.ToString + ' ms');
|
Log('Vault opened from cache in ' + LSw.ElapsedMilliseconds.ToString + ' ms');
|
||||||
FETag := LETag;
|
FETag := LETag;
|
||||||
@@ -1011,7 +1020,7 @@ begin
|
|||||||
// protection a tres probablement invalide l'ancienne, et le
|
// protection a tres probablement invalide l'ancienne, et le
|
||||||
// chiffrement ne saurait pas le voir - on scellerait un fichier
|
// chiffrement ne saurait pas le voir - on scellerait un fichier
|
||||||
// impossible a rouvrir.
|
// impossible a rouvrir.
|
||||||
SaveVault(LPlain, LETag, LPwd, True, True);
|
SaveVault(LPlain, LETag, LPwd, True, True, FPin);
|
||||||
if VaultNeedsAuth then
|
if VaultNeedsAuth then
|
||||||
Log('Cache re-sealed under authentication')
|
Log('Cache re-sealed under authentication')
|
||||||
else
|
else
|
||||||
@@ -1194,7 +1203,7 @@ begin
|
|||||||
begin
|
begin
|
||||||
// Le coffre local d'abord : s'il est la, on connait deja l'ETag et on
|
// Le coffre local d'abord : s'il est la, on connait deja l'ETag et on
|
||||||
// evite les 4 secondes de derivation quand rien n'a change.
|
// evite les 4 secondes de derivation quand rien n'a change.
|
||||||
if (FETag = '') and LoadVault(LCached, LCachedETag, LCachedPwd) then
|
if (FETag = '') and LoadVault(LCached, LCachedETag, LCachedPwd, FPin) then
|
||||||
FETag := LCachedETag;
|
FETag := LCachedETag;
|
||||||
|
|
||||||
LClient := TSyncClient.Create(AUrl, ATok);
|
LClient := TSyncClient.Create(AUrl, ATok);
|
||||||
@@ -1341,7 +1350,7 @@ begin
|
|||||||
try
|
try
|
||||||
// Le mot de passe part avec le coffre : c'est lui qui permettra au
|
// Le mot de passe part avec le coffre : c'est lui qui permettra au
|
||||||
// prochain lancement de rafraichir sans rien demander.
|
// prochain lancement de rafraichir sans rien demander.
|
||||||
SaveVault(LPlain, FETag, APwd, LCanAuth);
|
SaveVault(LPlain, FETag, APwd, LCanAuth, False, FPin);
|
||||||
Log(' cache now holds sha: ' + FETag);
|
Log(' cache now holds sha: ' + FETag);
|
||||||
if VaultNeedsAuth then
|
if VaultNeedsAuth then
|
||||||
Log(' cache written - protected by biometrics/PIN')
|
Log(' cache written - protected by biometrics/PIN')
|
||||||
|
|||||||
Reference in New Issue
Block a user