Skip to content

Latest commit

 

History

History
1796 lines (1474 loc) · 41.8 KB

File metadata and controls

1796 lines (1474 loc) · 41.8 KB

🔝 Retour au Sommaire

17.6.2 Keyring Linux/GNOME

Introduction

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.

Qu'est-ce que GNOME Keyring ?

Définition

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.

Architecture

Application
    ↓
Secret Service API (D-Bus)
    ↓
GNOME Keyring Daemon
    ↓
Fichiers chiffrés (~/.local/share/keyrings/)
    ↓
Chiffrement avec mot de passe de session utilisateur

Composants principaux

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

Avantages

✅ 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

Alternatives sur Linux

Solution Environnement Compatibilité
GNOME Keyring GNOME Secret Service API
KWallet KDE Secret Service API
pass Ligne de commande GPG
Seahorse Interface graphique GNOME Keyring

Installation et vérification

Vérifier la présence de 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

Installation si nécessaire

# 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 Secret Service

# 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

Utilisation avec secret-tool (ligne de commande)

Installation de secret-tool

# Sur Ubuntu/Debian
sudo apt install libsecret-tools

# Test
secret-tool --version

Commandes de base

Stocker 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 terminer

Récupérer un secret :

secret-tool lookup application MonApp service api_key

Supprimer un secret :

secret-tool clear application MonApp service api_key

Rechercher des secrets :

secret-tool search application MonApp

Exemple d'utilisation depuis FreePascal

uses
  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.

Utilisation avec libsecret (API native)

Avantages de libsecret

  • ✅ API native (pas besoin d'appeler secret-tool)
  • ✅ Plus rapide et plus fiable
  • ✅ Gestion d'erreurs plus fine
  • ✅ Support des attributs personnalisés

Bindings libsecret pour FreePascal

{$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}

Classe wrapper pour libsecret

{$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}

Interface graphique avec Seahorse

Qu'est-ce que Seahorse ?

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

Installation

sudo apt install seahorse

Lancement

seahorse
# Ou depuis le menu : Applications → Accessoires → Mots de passe et clés

Utilisation

  1. Voir les secrets : Onglet "Mots de passe"
  2. Créer un secret : Clic droit → Nouveau mot de passe
  3. Exporter : Clic droit → Exporter

Gestion des keyrings multiples

Créer un keyring personnalisé

# Avec secret-tool (stocke dans un keyring spécifique)
secret-tool store --label="Mon secret" \
  xdg:schema "org.freedesktop.Secret.Generic" \
  service MonApp \
  key test

Avec libsecret

function 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;

Stockage de données structurées

Utiliser les attributs pour organiser

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;

Gestion du déverrouillage

Keyring verrouillé

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

Déverrouillage programmatique

// 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;

Gérer le verrouillage automatique

# 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 minutes

Sécurité et permissions

Permissions des fichiers keyring

Les 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 keyrings

Chiffrement

Les 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

Vérification d'intégrité

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;

Alternatives : pass (Password Store)

Qu'est-ce que pass ?

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

Installation

sudo apt install pass

Utilisation de base

# 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 32

Intégration avec FreePascal

function 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;

KWallet (KDE)

Utilisation avec KWallet

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 kdewallet

Wrapper universel

type
  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;

Bonnes pratiques

1. Utiliser des labels descriptifs

// ❌ Mauvais
StoreSecret('app', 'key1', 'value');

// ✅ Bon
secret-tool store --label="MonApp - Clé API Production" \
  application MonApp \
  environment production \
  type api_key

2. Organiser avec des attributs

// 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;

3. Nettoyer les secrets inutilisés

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;

4. Gérer les erreurs gracieusement

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;

5. Logger les accès

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;

Exemple d'application complète

Gestionnaire de configuration sécurisé

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}

Gestionnaire de credentials multi-backend

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;

Migration et export/import

Export de secrets

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;

Import de secrets

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;

Surveillance et maintenance

Vérifier l'état du keyring

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;

Nettoyage des secrets orphelins

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;

Sécurité avancée

Audit des accès

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;

Protection contre les attaques par timing

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;

Résumé et comparaison

Tableau comparatif des solutions

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

Checklist de sécurité

✅ À 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

Quand utiliser quoi ?

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

Ressources

Documentation

Outils

# Installer tous les outils de gestion de keyring
sudo apt install \
  gnome-keyring \
  libsecret-1-0 \
  libsecret-1-dev \
  libsecret-tools \
  seahorse \
  pass

Exemples de code

Conclusion

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

⏭️ Analyse de vulnérabilités