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:
+176
-1
@@ -114,6 +114,8 @@ type
|
||||
/// Coordonnees du serveur. Delphi en est le detenteur ; la page ne fait
|
||||
/// que les editer et les lui renvoyer quand un champ est quitte.
|
||||
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
|
||||
/// 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
|
||||
@@ -162,6 +164,10 @@ type
|
||||
procedure OnIdleTimer(Sender: TObject);
|
||||
|
||||
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 OnTotpTick(Sender: TObject);
|
||||
|
||||
@@ -341,6 +347,20 @@ begin
|
||||
// coffre qu'il avait parfaitement en main.
|
||||
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;
|
||||
if LCanOpen then
|
||||
begin
|
||||
@@ -354,7 +374,10 @@ begin
|
||||
end
|
||||
else
|
||||
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)');
|
||||
end;
|
||||
end;
|
||||
@@ -849,6 +872,17 @@ begin
|
||||
else if LAction = 'sync' then
|
||||
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
|
||||
// transporte un secret. Elle ne quitte pas le processus, l'iframe qui la
|
||||
// porte est detruite, et HandleWebCommand ne journalise que l'action.
|
||||
@@ -933,6 +967,147 @@ begin
|
||||
ShowList;
|
||||
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;
|
||||
var
|
||||
LPlain, LETag, LPwd: string;
|
||||
|
||||
Reference in New Issue
Block a user