feat(android): settings live on the device, dark theme, and the app is called Coffre
URL and token are no longer constants. They sit in settings.bin, sealed with the no-auth Keystore AES key - deliberately weaker than the vault: they must be readable to rebuild a vault that no longer exists, and a bearer token only opens ENCRYPTED data, worthless without the sync password that stays in the guarded vault. Retyping 64 hex characters after every cache purge was the real risk: the risk of writing them down elsewhere. They are saved only after a fetch that worked - proven settings, not typed ones. The token and sync password moved to the gitignored local include, as first-run defaults only. Tracked source now carries no secret, and leaving them empty is a viable configuration: the app asks. Theme: Delphi's Dark.style, copied into the repo and linked as a resource rather than deployed as a file - this project's deployment puts nothing beside the app, which is the same hole that forces the manifest and dex staging. A failure to load is never fatal; the default look is ugly but works. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,14 @@
|
||||
// Modele. Copier en PMA.LocalConfig.inc (ignore par git) et remplir.
|
||||
//
|
||||
// Le mot de passe de sync ouvre TOUT le coffre une fois le conteneur
|
||||
// telecharge : il n'a rien a faire dans un depot, meme sans remote.
|
||||
// VALEURS DE PREMIER LANCEMENT SEULEMENT. Une fois que l'appareil a enregistre
|
||||
// ses reglages, elles ne sont plus lues. Laisser vide est parfaitement viable :
|
||||
// l'application demande alors de saisir jeton et mot de passe, ce qui est le
|
||||
// comportement d'une vraie installation.
|
||||
//
|
||||
// Le jeton authentifie l'appareil aupres de l'endpoint ; le mot de passe de
|
||||
// sync ouvre TOUT le coffre une fois le conteneur telecharge. Aucun des deux
|
||||
// n'a sa place dans un depot.
|
||||
|
||||
const
|
||||
FIRSTRUN_TOKEN = '';
|
||||
TEST_PWD = '';
|
||||
|
||||
@@ -107,6 +107,20 @@ procedure ClearVault;
|
||||
/// Chemin du fichier, pour le journal et les tests.
|
||||
function VaultPath: string;
|
||||
|
||||
/// Reglages du serveur : URL et jeton Bearer.
|
||||
///
|
||||
/// FICHIER SEPARE, ET PROTECTION PLUS FAIBLE, DELIBEREMENT. Ils sont chiffres
|
||||
/// par la cle AES du Keystore - liee au materiel, mais lisible SANS
|
||||
/// authentification. Deux raisons :
|
||||
/// - il faut pouvoir les relire pour reconstruire un coffre efface, alors
|
||||
/// que la cle qui protege le coffre ne sert a rien s'il n'existe plus ;
|
||||
/// - le jeton ne donne acces qu'a du CHIFFRE. Sans le mot de passe de sync,
|
||||
/// qui lui reste dans le coffre protege, il ne vaut rien.
|
||||
/// Retaper un jeton de 64 caracteres apres chaque purge de cache serait le
|
||||
/// vrai risque : celui de le noter ailleurs.
|
||||
procedure SaveDeviceSettings(const AUrl, AToken: string);
|
||||
function LoadDeviceSettings(out AUrl, AToken: string): Boolean;
|
||||
|
||||
implementation
|
||||
|
||||
{$IF DEFINED(ANDROID)}
|
||||
@@ -115,7 +129,8 @@ uses
|
||||
{$ENDIF}
|
||||
|
||||
const
|
||||
VAULT_FILE = 'vault.bin';
|
||||
VAULT_FILE = 'vault.bin';
|
||||
SETTINGS_FILE = 'settings.bin';
|
||||
// Un alias par schema : la contrainte d'authentification est figee a la
|
||||
// creation de la cle et ne peut pas etre ajoutee apres coup.
|
||||
KEY_ALIAS = 'pm_vault_key'; // AES, sans authentification
|
||||
@@ -596,6 +611,100 @@ begin
|
||||
end;
|
||||
{$ENDIF}
|
||||
|
||||
{$IF DEFINED(ANDROID)}
|
||||
/// Chiffre sous la cle AES du Keystore, sans contrainte d'authentification.
|
||||
/// Format : [IV][chiffre+tag]. Sert aux reglages, pas au coffre.
|
||||
function AesSeal(const APlain: TBytes): TBytes;
|
||||
var
|
||||
LCipher: JCipherX;
|
||||
LIv, LCt: TBytes;
|
||||
begin
|
||||
LCipher := TJCipherX.JavaClass.getInstance(StringToJString('AES/GCM/NoPadding'));
|
||||
LCipher.init(TJCipherX.JavaClass.ENCRYPT_MODE, EnsureAesKey);
|
||||
LIv := FromJava(LCipher.getIV); // genere par le Keystore, jamais choisi ici
|
||||
LCt := FromJava(LCipher.doFinal(ToJava(APlain)));
|
||||
SetLength(Result, Length(LIv) + Length(LCt));
|
||||
Move(LIv[0], Result[0], Length(LIv));
|
||||
Move(LCt[0], Result[Length(LIv)], Length(LCt));
|
||||
end;
|
||||
|
||||
function AesOpen(const ARaw: TBytes): TBytes;
|
||||
var
|
||||
LCipher: JCipherX;
|
||||
LIv, LCt: TBytes;
|
||||
begin
|
||||
if Length(ARaw) < IV_LEN + 16 then Exit(nil);
|
||||
SetLength(LIv, IV_LEN);
|
||||
Move(ARaw[0], LIv[0], IV_LEN);
|
||||
SetLength(LCt, Length(ARaw) - IV_LEN);
|
||||
Move(ARaw[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)));
|
||||
Result := FromJava(LCipher.doFinal(ToJava(LCt)));
|
||||
end;
|
||||
{$ENDIF}
|
||||
|
||||
procedure SaveDeviceSettings(const AUrl, AToken: string);
|
||||
{$IF DEFINED(ANDROID)}
|
||||
var
|
||||
LObj: TJSONObject;
|
||||
begin
|
||||
LObj := TJSONObject.Create;
|
||||
try
|
||||
LObj.AddPair('url', AUrl);
|
||||
LObj.AddPair('token', AToken);
|
||||
try
|
||||
TFile.WriteAllBytes(TPath.Combine(TPath.GetHomePath, SETTINGS_FILE),
|
||||
AesSeal(TEncoding.UTF8.GetBytes(LObj.ToJSON)));
|
||||
except
|
||||
// Des reglages non enregistres se retapent ; ce n'est pas une raison
|
||||
// d'interrompre une synchronisation qui, elle, a reussi.
|
||||
on E: Exception do FLastStoreError := E.ClassName + ': ' + E.Message;
|
||||
end;
|
||||
finally
|
||||
LObj.Free;
|
||||
end;
|
||||
end;
|
||||
{$ELSE}
|
||||
begin
|
||||
end;
|
||||
{$ENDIF}
|
||||
|
||||
function LoadDeviceSettings(out AUrl, AToken: string): Boolean;
|
||||
{$IF DEFINED(ANDROID)}
|
||||
var
|
||||
LPath: string;
|
||||
LObj: TJSONObject;
|
||||
begin
|
||||
Result := False;
|
||||
AUrl := '';
|
||||
AToken := '';
|
||||
LPath := TPath.Combine(TPath.GetHomePath, SETTINGS_FILE);
|
||||
if not TFile.Exists(LPath) then Exit;
|
||||
try
|
||||
LObj := TJSONObject.ParseJSONValue(
|
||||
TEncoding.UTF8.GetString(AesOpen(TFile.ReadAllBytes(LPath)))) as TJSONObject;
|
||||
if LObj = nil then Exit;
|
||||
try
|
||||
AUrl := LObj.GetValue<string>('url', '');
|
||||
AToken := LObj.GetValue<string>('token', '');
|
||||
Result := AUrl <> '';
|
||||
finally
|
||||
LObj.Free;
|
||||
end;
|
||||
except
|
||||
Result := False;
|
||||
end;
|
||||
end;
|
||||
{$ELSE}
|
||||
begin
|
||||
Result := False;
|
||||
AUrl := '';
|
||||
AToken := '';
|
||||
end;
|
||||
{$ENDIF}
|
||||
|
||||
function VaultNeedsAuth: Boolean;
|
||||
var
|
||||
LFs: TFileStream;
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
unit PMA.Theme;
|
||||
{
|
||||
Thème sombre, chargé depuis une ressource embarquée.
|
||||
|
||||
POURQUOI UNE RESSOURCE ET NON UN FICHIER : le mecanisme de deploiement de ce
|
||||
projet ne pose aucun fichier a cote de l'application - c'est le meme trou qui
|
||||
oblige a mettre en place le manifeste et les .dex a la main. Une ressource
|
||||
liee dans le .so ne depend de rien : elle est la ou n'est pas, et elle est la.
|
||||
|
||||
Le style vient de Dark.style, livre avec Delphi, recopie dans le depot pour
|
||||
que la compilation ne dependre pas d'un chemin d'installation.
|
||||
|
||||
Un echec ici n'est jamais fatal : sans style, l'application garde l'apparence
|
||||
par defaut. Elle est laide, elle fonctionne.
|
||||
}
|
||||
|
||||
interface
|
||||
|
||||
/// A appeler une fois, avant la creation de la forme.
|
||||
procedure ApplyDarkTheme;
|
||||
|
||||
implementation
|
||||
|
||||
uses
|
||||
System.SysUtils, System.Types, System.Classes, FMX.Styles, FMX.Types;
|
||||
|
||||
{$R 'theme\theme.res' 'theme\theme.rc'}
|
||||
|
||||
procedure ApplyDarkTheme;
|
||||
var
|
||||
LStyle: TFmxObject;
|
||||
begin
|
||||
try
|
||||
// La lecture appartient a TStyleStreaming, l'application a TStyleManager.
|
||||
LStyle := TStyleStreaming.LoadFromResource(HInstance, 'PMDARKSTYLE', RT_RCDATA);
|
||||
if LStyle <> nil then
|
||||
TStyleManager.SetStyle(LStyle);
|
||||
except
|
||||
// Rien : l'apparence par defaut reste utilisable.
|
||||
end;
|
||||
end;
|
||||
|
||||
end.
|
||||
@@ -196,6 +196,13 @@ De quoi il a besoin, mis en place à la main sous `Android64\Debug\pmserverApk\`
|
||||
| `classes\*.dex` | `Android64\Debug\pmserverApk.classes\*.dex` |
|
||||
| `library\lib\arm64-v8a\libpmserverApk.so` | `Android64\Debug\libpmserverApk.so` |
|
||||
|
||||
Le thème a besoin de sa ressource, **une fois** (MSBuild ne compile pas les
|
||||
`.rc` référencés par `{$R}` — c'est une commodité de l'EDI, pas du compilateur) :
|
||||
|
||||
```bash
|
||||
cd android-client\theme && brcc32 theme.rc -fotheme.res
|
||||
```
|
||||
|
||||
Ensuite `msbuild pmserverApk.dproj /p:Platform=Android64 /p:Config=Debug
|
||||
/t:Deploy` produit et signe `bin\pmserverApk.apk`, et `adb install -r` l'installe.
|
||||
Le correctif durable serait d'écrire ces entrées une fois dans le `.dproj`.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
PMDARKSTYLE RCDATA "pm-dark.style"
|
||||
Reference in New Issue
Block a user