🔝 Retour au Sommaire
Sur les systèmes Linux, en particulier avec l'environnement de bureau GNOME, le stockage sécurisé des secrets est géré par le GNOME Keyring (ou son équivalent dans d'autres environnements). C'est l'équivalent Linux du DPAPI Windows ou du Keychain macOS.
Dans cette section, nous allons explorer comment utiliser ces systèmes de stockage sécurisé avec FreePascal et Lazarus sur Ubuntu et autres distributions Linux.
GNOME Keyring est un daemon (service système) qui stocke de manière sécurisée les mots de passe, clés et certificats. Il fait partie de l'environnement GNOME mais peut être utilisé par d'autres environnements de bureau.
Application
↓
Secret Service API (D-Bus)
↓
GNOME Keyring Daemon
↓
Fichiers chiffrés (~/.local/share/keyrings/)
↓
Chiffrement avec mot de passe de session utilisateur
1. GNOME Keyring Daemon :
- Service qui tourne en arrière-plan
- Déverrouillé automatiquement au login
- Gère plusieurs "keyrings" (trousseaux)
2. Secret Service API :
- API standardisée (freedesktop.org)
- Communication via D-Bus
- Utilisée par de nombreuses applications (Firefox, Chrome, etc.)
3. Keyrings (trousseaux) :
- login : Keyring principal, déverrouillé au login
- session : Temporaire, perdu au logout
- Keyrings personnalisés possibles
✅ Sécurité :
- Chiffrement avec le mot de passe de session
- Protection automatique au verrouillage de session
- Intégration avec PAM (Pluggable Authentication Modules)
- Possibilité d'utiliser TPM ou SELinux
✅ Standardisation :
- Secret Service API standard freedesktop.org
- Compatible avec différents backends (KWallet, etc.)
- Utilisé par de nombreuses applications
✅ Simplicité :
- Déverrouillage automatique au login
- Interface utilisateur intégrée
- Gestion centralisée
| Solution | Environnement | Compatibilité |
|---|---|---|
| GNOME Keyring | GNOME | Secret Service API |
| KWallet | KDE | Secret Service API |
| pass | Ligne de commande | GPG |
| Seahorse | Interface graphique | GNOME Keyring |
# Vérifier si le daemon est actif
ps aux | grep gnome-keyring
# Vérifier le package
dpkg -l | grep gnome-keyring
# Sur Ubuntu/Debian
apt list --installed | grep gnome-keyring# Sur Ubuntu/Debian
sudo apt update
sudo apt install gnome-keyring libsecret-1-0 libsecret-1-dev
# Sur Fedora
sudo dnf install gnome-keyring libsecret libsecret-devel
# Sur Arch Linux
sudo pacman -S gnome-keyring libsecret# Vérifier que le service D-Bus est accessible
dbus-send --session --print-reply \
--dest=org.freedesktop.secrets \
/org/freedesktop/secrets \
org.freedesktop.DBus.Introspectable.Introspect# Sur Ubuntu/Debian
sudo apt install libsecret-tools
# Test
secret-tool --versionStocker un secret :
# Format: secret-tool store --label="Description" attribut1 valeur1 attribut2 valeur2
secret-tool store --label="Ma clé API" \
application MonApp \
service api_key
# Taper le secret (ne s'affiche pas)
# Appuyer sur Ctrl+D pour terminerRécupérer un secret :
secret-tool lookup application MonApp service api_keySupprimer un secret :
secret-tool clear application MonApp service api_keyRechercher des secrets :
secret-tool search application MonAppuses
Process, SysUtils;
function StoreSecret(const Service, Account, Secret: string): Boolean;
var
Proc: TProcess;
begin
Result := False;
Proc := TProcess.Create(nil);
try
Proc.Executable := 'secret-tool';
Proc.Parameters.Add('store');
Proc.Parameters.Add('--label=' + Service + ':' + Account);
Proc.Parameters.Add('service');
Proc.Parameters.Add(Service);
Proc.Parameters.Add('account');
Proc.Parameters.Add(Account);
Proc.Options := [poWaitOnExit, poUsePipes];
Proc.Execute;
// Envoyer le secret via stdin
Proc.Input.Write(Secret[1], Length(Secret));
Proc.CloseInput;
Result := (Proc.ExitStatus = 0);
finally
Proc.Free;
end;
end;
function RetrieveSecret(const Service, Account: string): string;
var
Proc: TProcess;
Buffer: array[0..4095] of Char;
BytesRead: Integer;
begin
Result := '';
Proc := TProcess.Create(nil);
try
Proc.Executable := 'secret-tool';
Proc.Parameters.Add('lookup');
Proc.Parameters.Add('service');
Proc.Parameters.Add(Service);
Proc.Parameters.Add('account');
Proc.Parameters.Add(Account);
Proc.Options := [poWaitOnExit, poUsePipes];
Proc.Execute;
// Lire la sortie
BytesRead := Proc.Output.Read(Buffer, SizeOf(Buffer));
if BytesRead > 0 then
SetString(Result, Buffer, BytesRead);
Result := Trim(Result);
finally
Proc.Free;
end;
end;
function DeleteSecret(const Service, Account: string): Boolean;
var
Proc: TProcess;
begin
Result := False;
Proc := TProcess.Create(nil);
try
Proc.Executable := 'secret-tool';
Proc.Parameters.Add('clear');
Proc.Parameters.Add('service');
Proc.Parameters.Add(Service);
Proc.Parameters.Add('account');
Proc.Parameters.Add(Account);
Proc.Options := [poWaitOnExit];
Proc.Execute;
Result := (Proc.ExitStatus = 0);
finally
Proc.Free;
end;
end;
// Utilisation
var
APIKey: string;
begin
// Stocker
if StoreSecret('MonApplication', 'api_key', 'sk_live_abc123...') then
WriteLn('✓ Secret stocké dans le keyring');
// Récupérer
APIKey := RetrieveSecret('MonApplication', 'api_key');
WriteLn('API Key: ', APIKey);
// Supprimer
if DeleteSecret('MonApplication', 'api_key') then
WriteLn('✓ Secret supprimé');
end.- ✅ API native (pas besoin d'appeler secret-tool)
- ✅ Plus rapide et plus fiable
- ✅ Gestion d'erreurs plus fine
- ✅ Support des attributs personnalisés
{$IFDEF UNIX}
unit libsecret;
interface
uses
ctypes;
const
libsecret_name = 'libsecret-1.so.0';
type
PSecretSchema = ^TSecretSchema;
TSecretSchema = record
name: PChar;
flags: cint;
attributes: array[0..31] of record
name: PChar;
type_: cint;
end;
end;
PGError = ^TGError;
TGError = record
domain: cuint32;
code: cint;
message: PChar;
end;
const
SECRET_SCHEMA_NONE = 0;
SECRET_SCHEMA_DONT_MATCH_NAME = 1;
SECRET_SCHEMA_ATTRIBUTE_BOOLEAN = 0;
SECRET_SCHEMA_ATTRIBUTE_STRING = 1;
// Fonctions libsecret
function secret_password_store_sync(
schema: PSecretSchema;
collection: PChar;
label_: PChar;
password: PChar;
cancellable: Pointer;
error: PPGError;
// Suivi des attributs (paires clé-valeur, terminé par nil)
args: array of const
): cbool; cdecl; external libsecret_name;
function secret_password_lookup_sync(
schema: PSecretSchema;
cancellable: Pointer;
error: PPGError;
// Attributs de recherche
args: array of const
): PChar; cdecl; external libsecret_name;
function secret_password_clear_sync(
schema: PSecretSchema;
cancellable: Pointer;
error: PPGError;
// Attributs
args: array of const
): cbool; cdecl; external libsecret_name;
procedure g_error_free(error: PGError); cdecl; external 'libglib-2.0.so.0';
implementation
end.
{$ENDIF}{$IFDEF UNIX}
uses
libsecret;
type
TLinuxKeyring = class
private
FSchema: TSecretSchema;
FApplicationName: string;
procedure InitSchema;
public
constructor Create(const ApplicationName: string);
function Store(const Key, Value: string): Boolean;
function Retrieve(const Key: string): string;
function Delete(const Key: string): Boolean;
end;
constructor TLinuxKeyring.Create(const ApplicationName: string);
begin
inherited Create;
FApplicationName := ApplicationName;
InitSchema;
end;
procedure TLinuxKeyring.InitSchema;
begin
FillChar(FSchema, SizeOf(FSchema), 0);
FSchema.name := PChar(FApplicationName + '.Secret');
FSchema.flags := SECRET_SCHEMA_NONE;
// Définir les attributs
FSchema.attributes[0].name := 'application';
FSchema.attributes[0].type_ := SECRET_SCHEMA_ATTRIBUTE_STRING;
FSchema.attributes[1].name := 'key';
FSchema.attributes[1].type_ := SECRET_SCHEMA_ATTRIBUTE_STRING;
// Terminer avec nil
FSchema.attributes[2].name := nil;
end;
function TLinuxKeyring.Store(const Key, Value: string): Boolean;
var
Error: PGError;
begin
Error := nil;
Result := secret_password_store_sync(
@FSchema,
'default', // Collection (nil = default)
PChar(FApplicationName + ':' + Key), // Label
PChar(Value), // Password/Secret
nil, // Cancellable
@Error,
// Attributs
['application', PChar(FApplicationName),
'key', PChar(Key),
nil]
);
if Error <> nil then
begin
WriteLn('Erreur stockage: ', Error^.message);
g_error_free(Error);
end;
end;
function TLinuxKeyring.Retrieve(const Key: string): string;
var
Error: PGError;
Password: PChar;
begin
Result := '';
Error := nil;
Password := secret_password_lookup_sync(
@FSchema,
nil,
@Error,
['application', PChar(FApplicationName),
'key', PChar(Key),
nil]
);
if Error <> nil then
begin
WriteLn('Erreur récupération: ', Error^.message);
g_error_free(Error);
end
else if Password <> nil then
begin
Result := string(Password);
// Libérer la mémoire allouée par libsecret
// (utiliser g_free de glib)
end;
end;
function TLinuxKeyring.Delete(const Key: string): Boolean;
var
Error: PGError;
begin
Error := nil;
Result := secret_password_clear_sync(
@FSchema,
nil,
@Error,
['application', PChar(FApplicationName),
'key', PChar(Key),
nil]
);
if Error <> nil then
begin
WriteLn('Erreur suppression: ', Error^.message);
g_error_free(Error);
end;
end;
// Utilisation
var
Keyring: TLinuxKeyring;
APIKey: string;
begin
Keyring := TLinuxKeyring.Create('MonApplication');
try
// Stocker
if Keyring.Store('api_key', 'sk_live_abc123...') then
WriteLn('✓ Secret stocké');
// Récupérer
APIKey := Keyring.Retrieve('api_key');
WriteLn('API Key: ', APIKey);
// Supprimer
if Keyring.Delete('api_key') then
WriteLn('✓ Secret supprimé');
finally
Keyring.Free;
end;
end;
{$ENDIF}Seahorse est l'application graphique de gestion de GNOME Keyring. Elle permet de :
- Visualiser tous les secrets stockés
- Créer/modifier/supprimer des secrets manuellement
- Gérer les keyrings
- Importer/exporter des clés
sudo apt install seahorseseahorse
# Ou depuis le menu : Applications → Accessoires → Mots de passe et clés- Voir les secrets : Onglet "Mots de passe"
- Créer un secret : Clic droit → Nouveau mot de passe
- Exporter : Clic droit → Exporter
# Avec secret-tool (stocke dans un keyring spécifique)
secret-tool store --label="Mon secret" \
xdg:schema "org.freedesktop.Secret.Generic" \
service MonApp \
key testfunction TLinuxKeyring.StoreInCollection(const Collection, Key, Value: string): Boolean;
var
Error: PGError;
begin
Error := nil;
Result := secret_password_store_sync(
@FSchema,
PChar(Collection), // Nom du keyring/collection
PChar(FApplicationName + ':' + Key),
PChar(Value),
nil,
@Error,
['application', PChar(FApplicationName),
'key', PChar(Key),
nil]
);
if Error <> nil then
begin
WriteLn('Erreur: ', Error^.message);
g_error_free(Error);
end;
end;
// Utilisation
begin
// Stocker dans le keyring par défaut
Keyring.Store('api_key', 'secret123');
// Stocker dans un keyring personnalisé
Keyring.StoreInCollection('MonAppSecrets', 'api_key', 'secret123');
end;type
TSecretAttributes = record
Application: string;
Category: string;
Name: string;
Version: string;
end;
function StoreSecretWithAttributes(const Attrs: TSecretAttributes;
const Value: string): Boolean;
var
Proc: TProcess;
Params: TStringList;
begin
Proc := TProcess.Create(nil);
Params := TStringList.Create;
try
Proc.Executable := 'secret-tool';
Proc.Parameters.Add('store');
Proc.Parameters.Add('--label=' + Attrs.Application + ':' + Attrs.Name);
// Ajouter les attributs
Proc.Parameters.Add('application');
Proc.Parameters.Add(Attrs.Application);
Proc.Parameters.Add('category');
Proc.Parameters.Add(Attrs.Category);
Proc.Parameters.Add('name');
Proc.Parameters.Add(Attrs.Name);
Proc.Parameters.Add('version');
Proc.Parameters.Add(Attrs.Version);
Proc.Options := [poWaitOnExit, poUsePipes];
Proc.Execute;
Proc.Input.Write(Value[1], Length(Value));
Proc.CloseInput;
Result := (Proc.ExitStatus = 0);
finally
Params.Free;
Proc.Free;
end;
end;
// Recherche par attributs
function FindSecretsByCategory(const Application, Category: string): TStringList;
var
Proc: TProcess;
Output: string;
Buffer: array[0..4095] of Char;
BytesRead: Integer;
begin
Result := TStringList.Create;
Proc := TProcess.Create(nil);
try
Proc.Executable := 'secret-tool';
Proc.Parameters.Add('search');
Proc.Parameters.Add('application');
Proc.Parameters.Add(Application);
Proc.Parameters.Add('category');
Proc.Parameters.Add(Category);
Proc.Options := [poWaitOnExit, poUsePipes];
Proc.Execute;
// Lire la sortie
repeat
BytesRead := Proc.Output.Read(Buffer, SizeOf(Buffer));
if BytesRead > 0 then
begin
SetString(Output, Buffer, BytesRead);
Result.Text := Result.Text + Output;
end;
until BytesRead = 0;
finally
Proc.Free;
end;
end;
// Utilisation
var
Attrs: TSecretAttributes;
Secrets: TStringList;
begin
Attrs.Application := 'MonApp';
Attrs.Category := 'database';
Attrs.Name := 'production';
Attrs.Version := '1.0';
StoreSecretWithAttributes(Attrs, 'MyDatabasePassword');
// Rechercher tous les secrets de catégorie "database"
Secrets := FindSecretsByCategory('MonApp', 'database');
try
WriteLn('Secrets trouvés: ', Secrets.Count);
finally
Secrets.Free;
end;
end;Lorsqu'un keyring est verrouillé, l'accès aux secrets nécessite le mot de passe de déverrouillage.
Comportement par défaut :
- Le keyring "login" est déverrouillé automatiquement au login
- Les autres keyrings peuvent nécessiter un mot de passe
- Une boîte de dialogue s'affiche pour demander le mot de passe
// Note: Le déverrouillage programmatique est généralement déconseillé
// Il est préférable de laisser GNOME Keyring gérer cela
function UnlockKeyring(const KeyringName: string): Boolean;
var
Proc: TProcess;
begin
// Utiliser gnome-keyring-daemon pour déverrouiller
// (nécessite généralement une interaction utilisateur)
Proc := TProcess.Create(nil);
try
Proc.Executable := 'gnome-keyring-daemon';
Proc.Parameters.Add('--unlock');
Proc.Options := [poWaitOnExit];
Proc.Execute;
Result := (Proc.ExitStatus = 0);
finally
Proc.Free;
end;
end;# Configuration du temps avant verrouillage automatique
# Via dconf-editor :
# org.gnome.desktop.screensaver > lock-delay
# Ou en ligne de commande :
gsettings set org.gnome.desktop.screensaver lock-delay 300 # 5 minutesLes keyrings sont stockés dans :
~/.local/share/keyrings/
Permissions :
ls -la ~/.local/share/keyrings/
# drwx------ (700) - Seul le propriétaire peut accéder
# -rw------- (600) - Fichiers keyringsLes keyrings sont chiffrés avec :
- Le mot de passe de session utilisateur (keyring "login")
- Un mot de passe personnalisé (autres keyrings)
- Algorithme : AES-128 ou AES-256
function VerifyKeyringIntegrity: Boolean;
var
KeyringPath: string;
begin
KeyringPath := GetEnvironmentVariable('HOME') + '/.local/share/keyrings/';
// Vérifier les permissions
if not CheckDirectoryPermissions(KeyringPath, '700') then
begin
WriteLn('⚠️ Permissions incorrectes sur ', KeyringPath);
Result := False;
Exit;
end;
Result := True;
end;
function CheckDirectoryPermissions(const Path: string;
const ExpectedMode: string): Boolean;
var
Info: stat;
begin
if fpStat(PChar(Path), Info) = 0 then
begin
// Vérifier les permissions (en octal)
Result := (Info.st_mode and $1FF) = OctToDec(ExpectedMode);
end
else
Result := False;
end;pass est un gestionnaire de mots de passe en ligne de commande utilisant GPG pour le chiffrement.
Avantages :
- Simple et basé sur des fichiers texte
- Chiffrement GPG robuste
- Synchronisation facile (Git)
- Portable et universel
sudo apt install pass# Initialiser avec une clé GPG
pass init your-gpg-id
# Stocker un secret
pass insert MonApp/api_key
# Récupérer un secret
pass show MonApp/api_key
# Lister les secrets
pass ls
# Générer un mot de passe aléatoire
pass generate MonApp/password 32function PassStore(const Path, Value: string): Boolean;
var
Proc: TProcess;
begin
Proc := TProcess.Create(nil);
try
Proc.Executable := 'pass';
Proc.Parameters.Add('insert');
Proc.Parameters.Add('-m'); // Multiline
Proc.Parameters.Add(Path);
Proc.Options := [poWaitOnExit, poUsePipes];
Proc.Execute;
Proc.Input.Write(Value[1], Length(Value));
Proc.CloseInput;
Result := (Proc.ExitStatus = 0);
finally
Proc.Free;
end;
end;
function PassRetrieve(const Path: string): string;
var
Proc: TProcess;
Buffer: array[0..4095] of Char;
BytesRead: Integer;
begin
Result := '';
Proc := TProcess.Create(nil);
try
Proc.Executable := 'pass';
Proc.Parameters.Add('show');
Proc.Parameters.Add(Path);
Proc.Options := [poWaitOnExit, poUsePipes];
Proc.Execute;
BytesRead := Proc.Output.Read(Buffer, SizeOf(Buffer));
if BytesRead > 0 then
SetString(Result, Buffer, BytesRead);
Result := Trim(Result);
finally
Proc.Free;
end;
end;
// Utilisation
begin
if PassStore('MonApp/api_key', 'sk_live_abc123...') then
WriteLn('✓ Secret stocké dans pass');
APIKey := PassRetrieve('MonApp/api_key');
WriteLn('API Key: ', APIKey);
end;Pour les environnements KDE, KWallet est l'équivalent de GNOME Keyring.
# Installation
sudo apt install kwalletmanager
# Stocker avec kwalletcli
echo "MySecret" | kwallet-query -w MyApp kdewallettype
TKeyringBackend = (kbGNOME, kbKDE, kbPass);
function DetectKeyringBackend: TKeyringBackend;
var
Session: string;
begin
// Détecter l'environnement de bureau
Session := GetEnvironmentVariable('DESKTOP_SESSION');
if Pos('gnome', Session) > 0 then
Result := kbGNOME
else if Pos('kde', Session) > 0 then
Result := kbKDE
else if FileExists('/usr/bin/pass') then
Result := kbPass
else
Result := kbGNOME; // Défaut
end;
function StoreSecretUniversal(const Key, Value: string): Boolean;
begin
case DetectKeyringBackend of
kbGNOME: Result := StoreSecret('MonApp', Key, Value);
kbKDE: Result := KWalletStore(Key, Value);
kbPass: Result := PassStore('MonApp/' + Key, Value);
end;
end;// ❌ Mauvais
StoreSecret('app', 'key1', 'value');
// ✅ Bon
secret-tool store --label="MonApp - Clé API Production" \
application MonApp \
environment production \
type api_key// Structurer les secrets par catégorie
type
TSecretCategory = (scDatabase, scAPI, scCertificate, scOAuth);
procedure StoreOrganizedSecret(const Category: TSecretCategory;
const Name, Value: string);
var
CategoryStr: string;
begin
case Category of
scDatabase: CategoryStr := 'database';
scAPI: CategoryStr := 'api';
scCertificate: CategoryStr := 'certificate';
scOAuth: CategoryStr := 'oauth';
end;
StoreSecretWithAttributes(
'MonApp',
CategoryStr,
Name,
Value
);
end;procedure CleanupOldSecrets;
var
AllSecrets: TStringList;
i: Integer;
begin
AllSecrets := FindSecretsByCategory('MonApp', '');
try
for i := 0 to AllSecrets.Count - 1 do
begin
// Vérifier la date de dernière utilisation
if IsSecretOld(AllSecrets[i]) then
DeleteSecret('MonApp', AllSecrets[i]);
end;
finally
AllSecrets.Free;
end;
end;function SafeRetrieveSecret(const Key: string;
out Value: string): Boolean;
begin
Result := False;
Value := '';
try
Value := RetrieveSecret('MonApp', Key);
Result := (Value <> '');
except
on E: Exception do
begin
WriteLn('Erreur récupération secret: ', E.Message);
// Fallback : demander à l'utilisateur
Value := InputBox('Secret requis',
'Veuillez entrer ' + Key + ':', '');
// Stocker pour la prochaine fois
if Value <> '' then
StoreSecret('MonApp', Key, Value);
end;
end;
end;procedure LogSecretAccess(const Operation, Key: string; Success: Boolean);
begin
WriteLnToLog(Format(
'[KEYRING] %s - Op: %s, Key: %s, Success: %s, User: %s',
[DateTimeToStr(Now), Operation, Key, BoolToStr(Success),
GetEnvironmentVariable('USER')]
));
end;
// Utilisation
function LoggedRetrieveSecret(const Key: string): string;
begin
try
Result := RetrieveSecret('MonApp', Key);
LogSecretAccess('RETRIEVE', Key, True);
except
on E: Exception do
begin
LogSecretAccess('RETRIEVE', Key, False);
raise;
end;
end;
end;program SecureConfigManager;
{$mode objfpc}{$H+}
{$IFDEF UNIX}
uses
SysUtils, Classes, Process, IniFiles;
type
TSecureConfig = class
private
FAppName: string;
FConfigFile: string;
function GetSecretKey(const Section, Key: string): string;
public
constructor Create(const AppName: string);
// Méthodes publiques
function GetString(const Section, Key, Default: string): string;
procedure SetString(const Section, Key, Value: string);
function GetSecure(const Section, Key: string): string;
procedure SetSecure(const Section, Key, Value: string);
// Utilitaires
procedure SaveToFile;
procedure LoadFromFile;
end;
constructor TSecureConfig.Create(const AppName: string);
begin
inherited Create;
FAppName := AppName;
FConfigFile := GetEnvironmentVariable('HOME') +
'/.config/' + AppName + '/config.ini';
ForceDirectories(ExtractFilePath(FConfigFile));
end;
function TSecureConfig.GetSecretKey(const Section, Key: string): string;
begin
Result := FAppName + ':' + Section + ':' + Key;
end;
function TSecureConfig.GetString(const Section, Key, Default: string): string;
var
Ini: TIniFile;
begin
Ini := TIniFile.Create(FConfigFile);
try
Result := Ini.ReadString(Section, Key, Default);
finally
Ini.Free;
end;
end;
procedure TSecureConfig.SetString(const Section, Key, Value: string);
var
Ini: TIniFile;
begin
Ini := TIniFile.Create(FConfigFile);
try
Ini.WriteString(Section, Key, Value);
finally
Ini.Free;
end;
end;
function TSecureConfig.GetSecure(const Section, Key: string): string;
var
SecretKey: string;
begin
SecretKey := GetSecretKey(Section, Key);
Result := RetrieveSecret(FAppName, SecretKey);
end;
procedure TSecureConfig.SetSecure(const Section, Key, Value: string);
var
SecretKey: string;
begin
SecretKey := GetSecretKey(Section, Key);
StoreSecret(FAppName, SecretKey, Value);
end;
procedure TSecureConfig.SaveToFile;
begin
// Les données sécurisées sont déjà dans le keyring
// Rien à faire
end;
procedure TSecureConfig.LoadFromFile;
begin
// Les données sécurisées sont chargées à la demande depuis le keyring
// Rien à faire
end;
// Programme principal
var
Config: TSecureConfig;
DBHost, DBUser, DBPassword: string;
begin
WriteLn('=== Gestionnaire de Configuration Sécurisé ===');
WriteLn;
Config := TSecureConfig.Create('MonApplication');
try
// Configuration non sensible (fichier INI normal)
Config.SetString('Database', 'Host', 'localhost');
Config.SetString('Database', 'Port', '5432');
Config.SetString('Application', 'LogLevel', 'INFO');
// Configuration sensible (GNOME Keyring)
Config.SetSecure('Database', 'Username', 'admin');
Config.SetSecure('Database', 'Password', 'MySecurePassword123');
Config.SetSecure('API', 'Key', 'sk_live_abc123...');
WriteLn('Configuration sauvegardée');
WriteLn;
// Lecture
DBHost := Config.GetString('Database', 'Host', '');
DBUser := Config.GetSecure('Database', 'Username');
DBPassword := Config.GetSecure('Database', 'Password');
WriteLn('Configuration chargée:');
WriteLn(' Host: ', DBHost);
WriteLn(' User: ', DBUser);
WriteLn(' Password: ', StringOfChar('*', Length(DBPassword)));
finally
Config.Free;
end;
WriteLn;
WriteLn('Appuyez sur Entrée pour quitter...');
ReadLn;
end.
{$ENDIF}type
TCredentialManager = class
private
FBackend: TKeyringBackend;
procedure DetectBackend;
public
constructor Create;
function Store(const Service, Account, Password: string): Boolean;
function Retrieve(const Service, Account: string; out Password: string): Boolean;
function Delete(const Service, Account: string): Boolean;
function List(const Service: string): TStringList;
end;
constructor TCredentialManager.Create;
begin
inherited Create;
DetectBackend;
end;
procedure TCredentialManager.DetectBackend;
var
DesktopSession: string;
begin
DesktopSession := LowerCase(GetEnvironmentVariable('DESKTOP_SESSION'));
if Pos('gnome', DesktopSession) > 0 then
FBackend := kbGNOME
else if Pos('kde', DesktopSession) > 0 then
FBackend := kbKDE
else if Pos('xfce', DesktopSession) > 0 then
FBackend := kbGNOME // XFCE peut utiliser GNOME Keyring
else if FileExists('/usr/bin/pass') then
FBackend := kbPass
else
FBackend := kbGNOME; // Défaut
WriteLn('Backend détecté: ', GetEnumName(TypeInfo(TKeyringBackend), Ord(FBackend)));
end;
function TCredentialManager.Store(const Service, Account, Password: string): Boolean;
begin
case FBackend of
kbGNOME: Result := StoreSecret(Service, Account, Password);
kbKDE: Result := KWalletStore(Service + ':' + Account, Password);
kbPass: Result := PassStore(Service + '/' + Account, Password);
else Result := False;
end;
end;
function TCredentialManager.Retrieve(const Service, Account: string;
out Password: string): Boolean;
begin
Result := False;
Password := '';
try
case FBackend of
kbGNOME: Password := RetrieveSecret(Service, Account);
kbKDE: Password := KWalletRetrieve(Service + ':' + Account);
kbPass: Password := PassRetrieve(Service + '/' + Account);
end;
Result := (Password <> '');
except
on E: Exception do
WriteLn('Erreur récupération: ', E.Message);
end;
end;
function TCredentialManager.Delete(const Service, Account: string): Boolean;
begin
case FBackend of
kbGNOME: Result := DeleteSecret(Service, Account);
kbKDE: Result := KWalletDelete(Service + ':' + Account);
kbPass: Result := PassDelete(Service + '/' + Account);
else Result := False;
end;
end;
function TCredentialManager.List(const Service: string): TStringList;
begin
Result := TStringList.Create;
case FBackend of
kbGNOME: Result := FindSecretsByCategory(Service, '');
kbKDE: Result := KWalletList(Service);
kbPass: Result := PassList(Service);
end;
end;
// Utilisation
var
CM: TCredentialManager;
Password: string;
Services: TStringList;
begin
CM := TCredentialManager.Create;
try
// Stocker (fonctionne sur GNOME, KDE ou avec pass)
CM.Store('MonApp', 'database', 'MyPassword123');
// Récupérer
if CM.Retrieve('MonApp', 'database', Password) then
WriteLn('Password: ', Password);
// Lister
Services := CM.List('MonApp');
try
WriteLn('Services trouvés: ', Services.Count);
finally
Services.Free;
end;
finally
CM.Free;
end;
end;type
TSecretExport = record
Service: string;
Account: string;
EncryptedValue: string; // Chiffré avec mot de passe export
Timestamp: TDateTime;
end;
function ExportSecrets(const Service: string;
const ExportPassword: string): TArray<TSecretExport>;
var
Secrets: TStringList;
i: Integer;
PlainValue, EncryptedValue: string;
Export: TSecretExport;
Results: TList<TSecretExport>;
begin
Results := TList<TSecretExport>.Create;
try
// Lister tous les secrets du service
Secrets := FindSecretsByCategory(Service, '');
try
for i := 0 to Secrets.Count - 1 do
begin
// Récupérer la valeur
PlainValue := RetrieveSecret(Service, Secrets[i]);
// Chiffrer avec le mot de passe d'export
EncryptedValue := EncryptAES256(PlainValue, ExportPassword);
// Créer l'entrée d'export
Export.Service := Service;
Export.Account := Secrets[i];
Export.EncryptedValue := EncryptedValue;
Export.Timestamp := Now;
Results.Add(Export);
end;
finally
Secrets.Free;
end;
Result := Results.ToArray;
finally
Results.Free;
end;
end;
procedure SaveExportToFile(const Exports: TArray<TSecretExport>;
const FileName: string);
var
F: TextFile;
i: Integer;
begin
AssignFile(F, FileName);
Rewrite(F);
try
WriteLn(F, 'SECRET_EXPORT_V1');
WriteLn(F, 'COUNT:', Length(Exports));
WriteLn(F, '---');
for i := 0 to High(Exports) do
begin
WriteLn(F, 'SERVICE:', Exports[i].Service);
WriteLn(F, 'ACCOUNT:', Exports[i].Account);
WriteLn(F, 'VALUE:', Exports[i].EncryptedValue);
WriteLn(F, 'TIMESTAMP:', DateTimeToStr(Exports[i].Timestamp));
WriteLn(F, '---');
end;
finally
CloseFile(F);
end;
end;
// Utilisation
var
Exports: TArray<TSecretExport>;
begin
Exports := ExportSecrets('MonApp', 'ExportPassword123');
SaveExportToFile(Exports, 'secrets_export.txt');
WriteLn('✓ ', Length(Exports), ' secrets exportés');
end;function ImportSecrets(const FileName: string;
const ImportPassword: string): Integer;
var
F: TextFile;
Line, Version: string;
Count: Integer;
Current: TSecretExport;
DecryptedValue: string;
begin
Result := 0;
AssignFile(F, FileName);
Reset(F);
try
// Lire l'en-tête
ReadLn(F, Version);
if Version <> 'SECRET_EXPORT_V1' then
raise Exception.Create('Format d''export invalide');
ReadLn(F, Line); // COUNT:x
Count := StrToInt(Copy(Line, 7, MaxInt));
ReadLn(F, Line); // ---
// Lire les secrets
while not Eof(F) do
begin
ReadLn(F, Line);
if Line = '---' then Continue;
if Copy(Line, 1, 8) = 'SERVICE:' then
Current.Service := Copy(Line, 9, MaxInt)
else if Copy(Line, 1, 8) = 'ACCOUNT:' then
Current.Account := Copy(Line, 9, MaxInt)
else if Copy(Line, 1, 6) = 'VALUE:' then
begin
Current.EncryptedValue := Copy(Line, 7, MaxInt);
// Déchiffrer et importer
try
DecryptedValue := DecryptAES256(Current.EncryptedValue, ImportPassword);
if StoreSecret(Current.Service, Current.Account, DecryptedValue) then
Inc(Result);
except
on E: Exception do
WriteLn('Erreur import ', Current.Account, ': ', E.Message);
end;
end;
end;
finally
CloseFile(F);
end;
end;
// Utilisation
var
Imported: Integer;
begin
Imported := ImportSecrets('secrets_export.txt', 'ExportPassword123');
WriteLn('✓ ', Imported, ' secrets importés');
end;function CheckKeyringHealth: Boolean;
var
DaemonRunning: Boolean;
KeyringDir: string;
PermissionsOK: Boolean;
TestValue: string;
begin
Result := True;
WriteLn('=== Vérification du Keyring ===');
// 1. Vérifier que le daemon tourne
DaemonRunning := IsProcessRunning('gnome-keyring-daemon');
WriteLn('Daemon actif: ', BoolToStr(DaemonRunning, True));
if not DaemonRunning then
begin
WriteLn('⚠️ Le daemon GNOME Keyring n''est pas actif');
Result := False;
end;
// 2. Vérifier les permissions du répertoire
KeyringDir := GetEnvironmentVariable('HOME') + '/.local/share/keyrings/';
PermissionsOK := CheckDirectoryPermissions(KeyringDir, '700');
WriteLn('Permissions OK: ', BoolToStr(PermissionsOK, True));
if not PermissionsOK then
begin
WriteLn('⚠️ Permissions incorrectes sur ', KeyringDir);
Result := False;
end;
// 3. Tester l'accès
try
StoreSecret('_test_', '_health_check_', 'test');
TestValue := RetrieveSecret('_test_', '_health_check_');
DeleteSecret('_test_', '_health_check_');
WriteLn('Accès fonctionnel: Oui');
except
on E: Exception do
begin
WriteLn('Accès fonctionnel: Non');
WriteLn('Erreur: ', E.Message);
Result := False;
end;
end;
end;
function IsProcessRunning(const ProcessName: string): Boolean;
var
Proc: TProcess;
Output: string;
Buffer: array[0..255] of Char;
BytesRead: Integer;
begin
Proc := TProcess.Create(nil);
try
Proc.Executable := 'pgrep';
Proc.Parameters.Add(ProcessName);
Proc.Options := [poWaitOnExit, poUsePipes];
Proc.Execute;
BytesRead := Proc.Output.Read(Buffer, SizeOf(Buffer));
if BytesRead > 0 then
SetString(Output, Buffer, BytesRead);
Result := (Trim(Output) <> '');
finally
Proc.Free;
end;
end;procedure CleanupOrphanSecrets(const AppName: string; DryRun: Boolean = True);
var
AllSecrets: TStringList;
i: Integer;
SecretAge: Integer;
begin
WriteLn('=== Nettoyage des secrets orphelins ===');
if DryRun then
WriteLn('Mode simulation (aucune suppression réelle)');
WriteLn;
AllSecrets := FindSecretsByCategory(AppName, '');
try
WriteLn('Secrets trouvés: ', AllSecrets.Count);
for i := 0 to AllSecrets.Count - 1 do
begin
// Vérifier si le secret est encore utilisé
if IsSecretOrphan(AppName, AllSecrets[i]) then
begin
WriteLn('Orphelin détecté: ', AllSecrets[i]);
if not DryRun then
begin
if DeleteSecret(AppName, AllSecrets[i]) then
WriteLn(' ✓ Supprimé')
else
WriteLn(' ✗ Erreur de suppression');
end;
end;
end;
finally
AllSecrets.Free;
end;
end;
function IsSecretOrphan(const AppName, SecretName: string): Boolean;
begin
// Logique personnalisée pour déterminer si un secret est orphelin
// Par exemple : secret non accédé depuis X jours, ou référence obsolète
// Exemple simple : vérifier si le nom contient "old_" ou "deprecated_"
Result := (Copy(SecretName, 1, 4) = 'old_') or
(Copy(SecretName, 1, 11) = 'deprecated_') or
(Pos('_backup_', SecretName) > 0);
end;type
TSecretAudit = record
Timestamp: TDateTime;
Operation: string; // STORE, RETRIEVE, DELETE
Service: string;
Account: string;
Success: Boolean;
User: string;
ProcessName: string;
end;
var
AuditLog: TList<TSecretAudit>;
procedure LogSecretOperation(const Operation, Service, Account: string;
Success: Boolean);
var
Audit: TSecretAudit;
F: TextFile;
AuditFile: string;
begin
Audit.Timestamp := Now;
Audit.Operation := Operation;
Audit.Service := Service;
Audit.Account := Account;
Audit.Success := Success;
Audit.User := GetEnvironmentVariable('USER');
Audit.ProcessName := ParamStr(0);
// Ajouter à la liste en mémoire
if AuditLog = nil then
AuditLog := TList<TSecretAudit>.Create;
AuditLog.Add(Audit);
// Écrire dans le fichier de log
AuditFile := GetEnvironmentVariable('HOME') + '/.config/MonApp/secret_audit.log';
AssignFile(F, AuditFile);
if FileExists(AuditFile) then
Append(F)
else
Rewrite(F);
try
WriteLn(F, Format('%s|%s|%s|%s|%s|%s|%s',
[DateTimeToStr(Audit.Timestamp),
Audit.Operation,
Audit.Service,
Audit.Account,
BoolToStr(Audit.Success),
Audit.User,
Audit.ProcessName]));
finally
CloseFile(F);
end;
end;
// Wrapper avec audit
function AuditedRetrieveSecret(const Service, Account: string): string;
begin
try
Result := RetrieveSecret(Service, Account);
LogSecretOperation('RETRIEVE', Service, Account, True);
except
on E: Exception do
begin
LogSecretOperation('RETRIEVE', Service, Account, False);
raise;
end;
end;
end;function SecureCompareSecrets(const Secret1, Secret2: string): Boolean;
var
i: Integer;
Diff: Byte;
begin
Diff := Byte(Length(Secret1) <> Length(Secret2));
// Toujours comparer tous les caractères (temps constant)
for i := 1 to Min(Length(Secret1), Length(Secret2)) do
Diff := Diff or Byte(Secret1[i] <> Secret2[i]);
Result := (Diff = 0);
end;| Critère | GNOME Keyring | KWallet | pass | secret-tool |
|---|---|---|---|---|
| Environnement | GNOME | KDE | CLI | Universel |
| Interface | D-Bus | D-Bus | GPG | CLI wrapper |
| Chiffrement | AES | Blowfish | GPG | Via backend |
| Déverrouillage auto | ✅ | ✅ | ❌ | ✅ |
| Synchronisation | ❌ | ❌ | ✅ (Git) | ❌ |
| Complexité | Moyenne | Moyenne | Faible | Faible |
| Portable | Linux | Linux | Multi-OS | Linux |
✅ À faire :
- Utiliser des labels descriptifs
- Organiser avec des attributs clairs
- Logger les accès (sans les valeurs)
- Nettoyer les secrets inutilisés
- Vérifier régulièrement la santé du keyring
- Utiliser le backend approprié à l'environnement
- Implémenter un système de fallback
- Créer des exports réguliers pour sauvegarde
❌ À éviter :
- Ne pas stocker de secrets dans le code
- Ne pas logger les valeurs des secrets
- Ne pas partager le répertoire ~/.local/share/keyrings/
- Ne pas désactiver le verrouillage automatique
- Ne pas ignorer les erreurs d'accès au keyring
GNOME Keyring / libsecret :
- ✅ Applications desktop GNOME/GTK
- ✅ Besoin d'intégration système
- ✅ Multiples secrets à gérer
- ✅ Interface utilisateur souhaitée
secret-tool :
- ✅ Scripts shell
- ✅ Prototypage rapide
- ✅ Applications simples
- ✅ Pas besoin de libsecret
pass :
- ✅ Ligne de commande
- ✅ Besoin de synchronisation (Git)
- ✅ Environnement sans GUI
- ✅ Préférence pour GPG
- Secret Service API : https://specifications.freedesktop.org/secret-service/
- GNOME Keyring : https://wiki.gnome.org/Projects/GnomeKeyring
- libsecret : https://gnome.pages.gitlab.gnome.org/libsecret/
- pass : https://www.passwordstore.org/
# Installer tous les outils de gestion de keyring
sudo apt install \
gnome-keyring \
libsecret-1-0 \
libsecret-1-dev \
libsecret-tools \
seahorse \
pass- FreePascal D-Bus : https://wiki.freepascal.org/D-Bus
- GNOME GitLab : https://gitlab.gnome.org/GNOME/libsecret
GNOME Keyring et le Secret Service API offrent une solution robuste et intégrée pour le stockage sécurisé de secrets sur Linux. Bien que plus fragmenté que sur Windows (avec différents backends possibles), le standard Secret Service permet une relative portabilité entre environnements de bureau.
Points clés :
- 🔒 Chiffrement automatique avec le mot de passe de session
- 🖥️ Intégration native avec l'environnement de bureau
- 🔓 Déverrouillage automatique au login
- 🔄 Support de plusieurs backends (GNOME, KDE, pass)
- 🛠️ Accessible via secret-tool ou libsecret
- 📦 Standard freedesktop.org (Secret Service API)
Avec FreePascal, vous pouvez facilement intégrer ces systèmes de stockage sécurisé dans vos applications Linux, que ce soit via secret-tool pour la simplicité ou libsecret pour plus de contrôle et de performance.
Prochaine section : Autres méthodes de stockage sécurisé multi-plateformes