feat(android): a local account, so the app serves without a repository
Until now the phone could only read a vault that came from elsewhere: without a URL, a token and a sync password it showed nothing. It now signs in like the desktop does - username and master password - and can create its own vault, with no network at all. The local container carries exactly what the repository would carry: same format, same encryption under the password. Two reasons, not one. It is the desktop's model, where the master password protects the vault at rest rather than the hardware alone; and the format staying the repository's means sync keeps working with nothing to convert. No Keystore on that file - it is already encrypted under the password, and binding it to the device would make it unrecoverable after a reinstall. vault.bin keeps that role: it is the fast-open cache, and only it needs sealing. The password opens the container - the decryption itself is the check, no secret is ever compared - and the username, read from inside once open, says whose vault it is. The home screen now knows which of three situations it is in: nothing here yet, a container without a cache, or a cache ready to open. Verified on the device: with the cache set aside, the screen offers username, master password and Create vault, alongside the download path. Creating an account could not be exercised to the end - the device locked its screen mid-run - and the vault that was set aside is back in place, same size, still sealed under its code.
This commit is contained in:
@@ -117,6 +117,23 @@ function LastStoreError: string;
|
|||||||
/// certitude, devrait l'appeler : il se retelecharge, mais pas gratuitement.
|
/// certitude, devrait l'appeler : il se retelecharge, mais pas gratuitement.
|
||||||
procedure ClearVault;
|
procedure ClearVault;
|
||||||
|
|
||||||
|
/// LE CONTENEUR LOCAL - l'autorite quand l'appareil se sert seul.
|
||||||
|
///
|
||||||
|
/// Il porte EXACTEMENT ce que porterait le depot : meme format, meme
|
||||||
|
/// chiffrement sous mot de passe (`pm-encrypted-export-v1`, PBKDF2 600k). Deux
|
||||||
|
/// raisons, pas une. C'est le modele du poste - le mot de passe maitre protege
|
||||||
|
/// le coffre au repos, et pas seulement le materiel. Et le format restant celui
|
||||||
|
/// du depot, la synchronisation continue de marcher sans rien convertir.
|
||||||
|
///
|
||||||
|
/// PAS DE KEYSTORE ICI, volontairement : ce fichier est deja chiffre sous le
|
||||||
|
/// mot de passe, comme sa copie distante. Le lier en plus a l'appareil le
|
||||||
|
/// rendrait irrecuperable apres une reinstallation, alors que le mot de passe,
|
||||||
|
/// lui, se retient. `vault.bin` garde ce role - c'est le cache d'ouverture
|
||||||
|
/// rapide, et lui seul a besoin d'etre scelle.
|
||||||
|
function HasLocalContainer: Boolean;
|
||||||
|
function LoadLocalContainer: string;
|
||||||
|
procedure SaveLocalContainer(const AContainer: string);
|
||||||
|
|
||||||
/// Chemin du fichier, pour le journal et les tests.
|
/// Chemin du fichier, pour le journal et les tests.
|
||||||
function VaultPath: string;
|
function VaultPath: string;
|
||||||
|
|
||||||
@@ -147,6 +164,7 @@ uses
|
|||||||
|
|
||||||
const
|
const
|
||||||
VAULT_FILE = 'vault.bin';
|
VAULT_FILE = 'vault.bin';
|
||||||
|
LOCAL_FILE = 'vault-local.json';
|
||||||
SETTINGS_FILE = 'settings.bin';
|
SETTINGS_FILE = 'settings.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.
|
||||||
@@ -194,6 +212,28 @@ begin
|
|||||||
Result := FLastStoreError;
|
Result := FLastStoreError;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
function LocalContainerPath: string;
|
||||||
|
begin
|
||||||
|
Result := TPath.Combine(TPath.GetHomePath, LOCAL_FILE);
|
||||||
|
end;
|
||||||
|
|
||||||
|
function HasLocalContainer: Boolean;
|
||||||
|
begin
|
||||||
|
Result := TFile.Exists(LocalContainerPath);
|
||||||
|
end;
|
||||||
|
|
||||||
|
function LoadLocalContainer: string;
|
||||||
|
begin
|
||||||
|
Result := '';
|
||||||
|
if TFile.Exists(LocalContainerPath) then
|
||||||
|
Result := TFile.ReadAllText(LocalContainerPath, TEncoding.UTF8);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure SaveLocalContainer(const AContainer: string);
|
||||||
|
begin
|
||||||
|
TFile.WriteAllText(LocalContainerPath, AContainer, TEncoding.UTF8);
|
||||||
|
end;
|
||||||
|
|
||||||
function VaultPath: string;
|
function VaultPath: string;
|
||||||
begin
|
begin
|
||||||
// GetHomePath sur Android = /data/data/<paquet>/files, prive a l'app.
|
// GetHomePath sur Android = /data/data/<paquet>/files, prive a l'app.
|
||||||
|
|||||||
@@ -35,6 +35,11 @@ type
|
|||||||
/// ecrire quand meme la ferait reapparaitre.
|
/// ecrire quand meme la ferait reapparaitre.
|
||||||
function PatchEntryField(const APlainJson, AUuid, AField, AValue: string): string;
|
function PatchEntryField(const APlainJson, AUuid, AField, AValue: string): string;
|
||||||
|
|
||||||
|
/// Un instantane vide, au nom de ce compte. Sert a l'inscription depuis le
|
||||||
|
/// telephone : meme forme que ce qu'ecrit le poste, pour qu'un coffre ne
|
||||||
|
/// naisse pas dans un dialecte a part.
|
||||||
|
function NewEmptySnapshot(const AUsername: string): string;
|
||||||
|
|
||||||
/// Horodatage UTC au format que tout le reste emploie (toISOString cote JS).
|
/// Horodatage UTC au format que tout le reste emploie (toISOString cote JS).
|
||||||
/// MELANGER heure locale et UTC a deja decale l'arbitrage de la fusion une
|
/// MELANGER heure locale et UTC a deja decale l'arbitrage de la fusion une
|
||||||
/// fois : il n'y a qu'une source d'heure ici, et elle est en UTC.
|
/// fois : il n'y a qu'une source d'heure ici, et elle est en UTC.
|
||||||
@@ -48,6 +53,28 @@ begin
|
|||||||
TTimeZone.Local.ToUniversalTime(Now));
|
TTimeZone.Local.ToUniversalTime(Now));
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
function NewEmptySnapshot(const AUsername: string): string;
|
||||||
|
var
|
||||||
|
LRoot: TJSONObject;
|
||||||
|
begin
|
||||||
|
LRoot := TJSONObject.Create;
|
||||||
|
try
|
||||||
|
// Les memes cles que buildSyncSnapshot, tableaux vides compris : un lecteur
|
||||||
|
// qui trouve la cle absente et un lecteur qui la trouve vide ne se
|
||||||
|
// comportent pas forcement pareil, et ce coffre sera lu par le poste.
|
||||||
|
LRoot.AddPair('version', TJSONNumber.Create(1));
|
||||||
|
LRoot.AddPair('snapshot_at', NowUtcIso);
|
||||||
|
LRoot.AddPair('username', AUsername);
|
||||||
|
LRoot.AddPair('folders', TJSONArray.Create);
|
||||||
|
LRoot.AddPair('avatar_b64', '');
|
||||||
|
LRoot.AddPair('entries', TJSONArray.Create);
|
||||||
|
LRoot.AddPair('tombstones', TJSONArray.Create);
|
||||||
|
Result := LRoot.ToJSON;
|
||||||
|
finally
|
||||||
|
LRoot.Free;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
function PatchEntryField(const APlainJson, AUuid, AField, AValue: string): string;
|
function PatchEntryField(const APlainJson, AUuid, AField, AValue: string): string;
|
||||||
var
|
var
|
||||||
LRoot, LEntry: TJSONObject;
|
LRoot, LEntry: TJSONObject;
|
||||||
|
|||||||
@@ -227,6 +227,27 @@
|
|||||||
<div class="screen" id="unlock">
|
<div class="screen" id="unlock">
|
||||||
<div class="scroll">
|
<div class="scroll">
|
||||||
<div class="status" id="status"></div>
|
<div class="status" id="status"></div>
|
||||||
|
<!-- SE CONNECTER, comme sur le poste. Le mot de passe maitre ouvre le
|
||||||
|
conteneur local - c'est le dechiffrement lui-meme qui verifie, aucun
|
||||||
|
secret n'est compare - et le nom dit a qui appartient ce coffre.
|
||||||
|
Cache des qu'un cache existe : redemander six secondes de derivation
|
||||||
|
a chaque lancement alors que le cache est la n'apporterait rien, et
|
||||||
|
c'est le code de deverrouillage qui garde cette porte-la. -->
|
||||||
|
<div id="authbox" class="stack" hidden>
|
||||||
|
<div class="in-wrap">
|
||||||
|
<input class="field-in" id="user" placeholder="username"
|
||||||
|
autocomplete="off" autocorrect="off" autocapitalize="none"
|
||||||
|
spellcheck="false">
|
||||||
|
<div class="in-x" data-clear="user">×</div>
|
||||||
|
</div>
|
||||||
|
<div class="in-wrap">
|
||||||
|
<input class="field-in" id="pass" placeholder="master password"
|
||||||
|
type="password" autocomplete="off">
|
||||||
|
<div class="in-x" data-clear="pass">×</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn wide main" id="btnAuth">Sign in</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="stack">
|
<div class="stack">
|
||||||
<button class="btn wide main" id="btnOpen">➜ Open vault</button>
|
<button class="btn wide main" id="btnOpen">➜ Open vault</button>
|
||||||
<button class="btn wide" id="btnSync">↻ Sync</button>
|
<button class="btn wide" id="btnSync">↻ Sync</button>
|
||||||
@@ -385,6 +406,19 @@ function pushConfig() {
|
|||||||
el.addEventListener('blur', pushConfig);
|
el.addEventListener('blur', pushConfig);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Le meme bouton dans les deux sens : creer un coffre et en ouvrir un ne
|
||||||
|
// different que par ce qui existe deja sur l'appareil, et Delphi le sait.
|
||||||
|
var authMode = 'open';
|
||||||
|
document.getElementById('btnAuth').addEventListener('click', function () {
|
||||||
|
var u = document.getElementById('user').value.trim();
|
||||||
|
var p = document.getElementById('pass').value;
|
||||||
|
// Le mot de passe part, puis le champ se vide : il n'a plus rien a faire
|
||||||
|
// dans le document une fois la commande envoyee.
|
||||||
|
send((authMode === 'register' ? 'register' : 'login')
|
||||||
|
+ '?u=' + encodeURIComponent(u) + '&p=' + encodeURIComponent(p));
|
||||||
|
document.getElementById('pass').value = '';
|
||||||
|
});
|
||||||
|
|
||||||
document.getElementById('btnOpen').addEventListener('click', function () {
|
document.getElementById('btnOpen').addEventListener('click', function () {
|
||||||
pushConfig(); // le code vient peut-etre d'etre tape
|
pushConfig(); // le code vient peut-etre d'etre tape
|
||||||
send('openvault');
|
send('openvault');
|
||||||
@@ -720,6 +754,17 @@ var PM = {
|
|||||||
/// n'est pose supprimerait le seul endroit ou en poser un ; le montrer sans
|
/// n'est pose supprimerait le seul endroit ou en poser un ; le montrer sans
|
||||||
/// rien dire laisserait croire qu'il en faut un pour ouvrir. C'est donc le
|
/// rien dire laisserait croire qu'il en faut un pour ouvrir. C'est donc le
|
||||||
/// texte d'aide qui porte la difference.
|
/// texte d'aide qui porte la difference.
|
||||||
|
/// Dans lequel des trois cas se trouve cet appareil : rien du tout,
|
||||||
|
/// un conteneur sans cache, ou un cache pret a s'ouvrir.
|
||||||
|
setAuthMode: function (mode) {
|
||||||
|
authMode = mode;
|
||||||
|
var box = document.getElementById('authbox');
|
||||||
|
box.hidden = (mode === 'open');
|
||||||
|
document.getElementById('pinbox').hidden = (mode !== 'open');
|
||||||
|
document.getElementById('btnAuth').textContent =
|
||||||
|
(mode === 'register') ? 'Create vault' : 'Sign in';
|
||||||
|
},
|
||||||
|
|
||||||
setPinNeeded: function (on) {
|
setPinNeeded: function (on) {
|
||||||
document.getElementById('pin').placeholder =
|
document.getElementById('pin').placeholder =
|
||||||
on ? 'unlock code' : 'set an unlock code (optional)';
|
on ? 'unlock code' : 'set an unlock code (optional)';
|
||||||
|
|||||||
+176
-1
@@ -114,6 +114,8 @@ 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;
|
||||||
|
/// Le compte de cet appareil. Vide tant qu'aucun coffre local n'existe.
|
||||||
|
FUser: string;
|
||||||
/// Code d'ouverture du cache local, choisi dans l'application. JAMAIS
|
/// Code d'ouverture du cache local, choisi dans l'application. JAMAIS
|
||||||
/// ENREGISTRE : il est vide a chaque lancement et doit etre retape. C'est
|
/// 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
|
/// ce qui le distingue du jeton, dont la persistance est un confort sans
|
||||||
@@ -162,6 +164,10 @@ type
|
|||||||
procedure OnIdleTimer(Sender: TObject);
|
procedure OnIdleTimer(Sender: TObject);
|
||||||
|
|
||||||
procedure ShowUnlock;
|
procedure ShowUnlock;
|
||||||
|
/// Cree un coffre local vide au nom donne, chiffre sous ce mot de passe.
|
||||||
|
procedure RegisterLocal(const AUser, APwd: string);
|
||||||
|
/// Ouvre le conteneur local avec ce mot de passe, et verifie le nom.
|
||||||
|
procedure LoginLocal(const AUser, APwd: string);
|
||||||
procedure ShowList;
|
procedure ShowList;
|
||||||
procedure OnTotpTick(Sender: TObject);
|
procedure OnTotpTick(Sender: TObject);
|
||||||
|
|
||||||
@@ -341,6 +347,20 @@ begin
|
|||||||
// 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, True))]));
|
||||||
|
|
||||||
|
// TROIS SITUATIONS, ET ELLES NE SE DEVINENT PAS DE L'EXTERIEUR :
|
||||||
|
// 'register' - rien sur cet appareil : il faut creer un coffre (ou en
|
||||||
|
// telecharger un, ce que les reglages du serveur permettent) ;
|
||||||
|
// 'login' - un conteneur local existe mais aucun cache utilisable : le
|
||||||
|
// mot de passe maitre est necessaire, et il coute six secondes ;
|
||||||
|
// 'open' - un cache est la : l'ouverture est immediate, eventuellement
|
||||||
|
// derriere le code de deverrouillage.
|
||||||
|
if HasVault and not FVaultUnreadable then
|
||||||
|
WebCall('PM.setAuthMode("open")')
|
||||||
|
else if HasLocalContainer then
|
||||||
|
WebCall('PM.setAuthMode("login")')
|
||||||
|
else
|
||||||
|
WebCall('PM.setAuthMode("register")');
|
||||||
|
|
||||||
LCanOpen := HasVault and not FVaultUnreadable;
|
LCanOpen := HasVault and not FVaultUnreadable;
|
||||||
if LCanOpen then
|
if LCanOpen then
|
||||||
begin
|
begin
|
||||||
@@ -354,7 +374,10 @@ begin
|
|||||||
end
|
end
|
||||||
else
|
else
|
||||||
begin
|
begin
|
||||||
Status('No local vault - first download');
|
if HasLocalContainer then
|
||||||
|
Status('Sign in')
|
||||||
|
else
|
||||||
|
Status('No vault on this device yet');
|
||||||
WebCall('PM.setActions(false, "Download vault", false)');
|
WebCall('PM.setActions(false, "Download vault", false)');
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
@@ -849,6 +872,17 @@ begin
|
|||||||
else if LAction = 'sync' then
|
else if LAction = 'sync' then
|
||||||
DoSync(nil)
|
DoSync(nil)
|
||||||
|
|
||||||
|
// AVEC 'setpwd', LES SEULES COMMANDES QUI TRANSPORTENT UN SECRET. Meme
|
||||||
|
// regle qu'elle : la valeur ne quitte pas le processus, l'iframe qui la
|
||||||
|
// porte est detruite, et seule l'action est journalisee - jamais le mot de
|
||||||
|
// passe, ni sa longueur.
|
||||||
|
else if LAction = 'register' then
|
||||||
|
RegisterLocal(TNetEncoding.URL.Decode(Param('u')),
|
||||||
|
TNetEncoding.URL.Decode(Param('p')))
|
||||||
|
else if LAction = 'login' then
|
||||||
|
LoginLocal(TNetEncoding.URL.Decode(Param('u')),
|
||||||
|
TNetEncoding.URL.Decode(Param('p')))
|
||||||
|
|
||||||
// La NOUVELLE valeur remonte par le pont - c'est la seule commande qui
|
// La NOUVELLE valeur remonte par le pont - c'est la seule commande qui
|
||||||
// transporte un secret. Elle ne quitte pas le processus, l'iframe qui la
|
// transporte un secret. Elle ne quitte pas le processus, l'iframe qui la
|
||||||
// porte est detruite, et HandleWebCommand ne journalise que l'action.
|
// porte est detruite, et HandleWebCommand ne journalise que l'action.
|
||||||
@@ -933,6 +967,147 @@ begin
|
|||||||
ShowList;
|
ShowList;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
/// INSCRIPTION - un coffre qui nait ici, sans reseau.
|
||||||
|
///
|
||||||
|
/// Ce que le poste fait dans SQLite, l'appareil le fait dans un fichier : le
|
||||||
|
/// conteneur local porte exactement ce que porterait le depot, chiffre sous ce
|
||||||
|
/// mot de passe. Le poste ne connaitra pas ce compte pour autant - il cree les
|
||||||
|
/// siens dans sa base, et rien ne les rapproche aujourd'hui.
|
||||||
|
///
|
||||||
|
/// LA DERIVATION COUTE QUATRE A SIX SECONDES sur ce telephone : elle ne peut
|
||||||
|
/// pas rester sur le thread d'interface, sinon l'ecran gele sans rien dire.
|
||||||
|
procedure TForm8.RegisterLocal(const AUser, APwd: string);
|
||||||
|
begin
|
||||||
|
if FBusy then Exit;
|
||||||
|
if AUser = '' then
|
||||||
|
begin
|
||||||
|
Status('Pick a username');
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
|
// Huit caracteres, comme le poste. Ce mot de passe est la SEULE chose qui
|
||||||
|
// protege le conteneur - il n'y a pas de serveur derriere pour limiter les
|
||||||
|
// tentatives, et le fichier est un fichier.
|
||||||
|
if Length(APwd) < 8 then
|
||||||
|
begin
|
||||||
|
Status('Master password: 8 characters at least');
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
|
// Un coffre existant ne se remplace pas par accident : ce serait perdre tout
|
||||||
|
// ce qu'il contient, sans retour possible.
|
||||||
|
if HasLocalContainer or HasVault then
|
||||||
|
begin
|
||||||
|
Status('This device already holds a vault');
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
|
|
||||||
|
SetBusy(True);
|
||||||
|
Status('Creating the vault...');
|
||||||
|
TThread.CreateAnonymousThread(
|
||||||
|
procedure
|
||||||
|
var
|
||||||
|
LPlain, LContainer: string;
|
||||||
|
begin
|
||||||
|
try
|
||||||
|
LPlain := NewEmptySnapshot(AUser);
|
||||||
|
LContainer := EncryptContainer(LPlain, APwd);
|
||||||
|
SaveLocalContainer(LContainer);
|
||||||
|
// Pas d'ETag : rien n'a ete publie. Le cache s'ecrit quand meme, sinon
|
||||||
|
// la prochaine ouverture redemanderait six secondes de derivation pour
|
||||||
|
// un coffre vide.
|
||||||
|
SaveVault(LPlain, '', APwd, BiometricAvailable, False, FPin);
|
||||||
|
TThread.Queue(nil,
|
||||||
|
procedure
|
||||||
|
begin
|
||||||
|
SetBusy(False);
|
||||||
|
FUser := AUser;
|
||||||
|
FSyncPwd := APwd;
|
||||||
|
Log('Vault created locally for ' + AUser);
|
||||||
|
AdoptVault(LPlain);
|
||||||
|
end);
|
||||||
|
except
|
||||||
|
on E: Exception do
|
||||||
|
TThread.Queue(nil,
|
||||||
|
procedure
|
||||||
|
begin
|
||||||
|
SetBusy(False);
|
||||||
|
Log('Register failed: ' + E.ClassName + ' - ' + E.Message);
|
||||||
|
Status('Could not create the vault - see log');
|
||||||
|
end);
|
||||||
|
end;
|
||||||
|
end).Start;
|
||||||
|
end;
|
||||||
|
|
||||||
|
/// CONNEXION - le mot de passe ouvre le conteneur, le nom identifie.
|
||||||
|
///
|
||||||
|
/// C'est le dechiffrement qui fait office de verification : un mot de passe
|
||||||
|
/// faux echoue sur la marque d'authenticite de GCM, sans qu'aucun secret n'ait
|
||||||
|
/// eu besoin d'etre compare. Le nom, lui, se lit DANS le conteneur une fois
|
||||||
|
/// ouvert - il ne protege rien, il dit a qui appartient ce coffre.
|
||||||
|
procedure TForm8.LoginLocal(const AUser, APwd: string);
|
||||||
|
begin
|
||||||
|
if FBusy then Exit;
|
||||||
|
if not HasLocalContainer then
|
||||||
|
begin
|
||||||
|
Status('No vault on this device');
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
|
if APwd = '' then
|
||||||
|
begin
|
||||||
|
Status('Master password required');
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
|
|
||||||
|
SetBusy(True);
|
||||||
|
Status('Opening...');
|
||||||
|
TThread.CreateAnonymousThread(
|
||||||
|
procedure
|
||||||
|
var
|
||||||
|
LPlain, LOwner: string;
|
||||||
|
LKey, LSalt: TBytes;
|
||||||
|
LIters: Integer;
|
||||||
|
LDoc: TJSONObject;
|
||||||
|
begin
|
||||||
|
try
|
||||||
|
LPlain := DecryptContainerEx(LoadLocalContainer, APwd, LKey, LSalt, LIters);
|
||||||
|
LOwner := '';
|
||||||
|
LDoc := TJSONObject.ParseJSONValue(LPlain) as TJSONObject;
|
||||||
|
if LDoc <> nil then
|
||||||
|
try
|
||||||
|
LOwner := LDoc.GetValue<string>('username', '');
|
||||||
|
finally
|
||||||
|
LDoc.Free;
|
||||||
|
end;
|
||||||
|
// Un nom qui ne correspond pas n'est PAS un refus d'acces - le mot de
|
||||||
|
// passe a deja ouvert le coffre, le mal serait fait. C'est une faute de
|
||||||
|
// frappe qu'on signale, et le coffre s'ouvre au nom qu'il porte.
|
||||||
|
if (AUser <> '') and (LOwner <> '') and not SameText(AUser, LOwner) then
|
||||||
|
TThread.Queue(nil, procedure
|
||||||
|
begin Log('Vault belongs to ' + LOwner + ', not ' + AUser); end);
|
||||||
|
|
||||||
|
SaveVault(LPlain, FETag, APwd, BiometricAvailable, False, FPin);
|
||||||
|
TThread.Queue(nil,
|
||||||
|
procedure
|
||||||
|
begin
|
||||||
|
SetBusy(False);
|
||||||
|
if LOwner <> '' then FUser := LOwner else FUser := AUser;
|
||||||
|
FSyncPwd := APwd;
|
||||||
|
AdoptVault(LPlain);
|
||||||
|
end);
|
||||||
|
except
|
||||||
|
on E: Exception do
|
||||||
|
TThread.Queue(nil,
|
||||||
|
procedure
|
||||||
|
begin
|
||||||
|
SetBusy(False);
|
||||||
|
Log('Login failed: ' + E.ClassName + ' - ' + E.Message);
|
||||||
|
// GCM echoue a sa verification quand la cle est fausse, et son
|
||||||
|
// message ne le dit pas. Le cas de loin le plus frequent.
|
||||||
|
Status('Wrong master password');
|
||||||
|
end);
|
||||||
|
end;
|
||||||
|
end).Start;
|
||||||
|
end;
|
||||||
|
|
||||||
procedure TForm8.OpenLocalNow;
|
procedure TForm8.OpenLocalNow;
|
||||||
var
|
var
|
||||||
LPlain, LETag, LPwd: string;
|
LPlain, LETag, LPwd: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user