on avance un peu pmoqobuz

This commit is contained in:
2025-12-08 22:32:42 +01:00
parent 31ee25c7f2
commit 76132086b6
31 changed files with 9047 additions and 309 deletions

View File

@@ -16,6 +16,9 @@ log = "0.4.20"
anyhow = "1.0.75"
uuid = { version = "1.18.1", features = ["v4"] }
tracing = "0.1.41"
aes-gcm = "0.10"
sha2 = "0.10"
base64 = "0.22"
# Async
tokio = { version = "1.0", features = ["full"], optional = true }

View File

@@ -0,0 +1,232 @@
# Chiffrement des mots de passe dans la configuration
## Vue d'ensemble
`pmoconfig` fournit un système de chiffrement transparent des mots de passe basé sur l'**UUID matériel de la machine**. Cette approche offre un bon compromis entre sécurité et simplicité d'utilisation.
## Principe de fonctionnement
### Clé de chiffrement dérivée de la machine
- La clé de chiffrement AES-256 est dérivée de l'UUID matériel de votre machine
- Sur macOS : utilise `IOPlatformUUID` (via `ioreg`)
- Sur Linux : utilise `/etc/machine-id` ou `/var/lib/dbus/machine-id`
- Sur Windows : utilise l'UUID du BIOS (via `wmic`)
### Algorithme
- **Chiffrement** : AES-256-GCM (Authenticated Encryption)
- **Dérivation de clé** : SHA-256 sur UUID machine + salt
- **Nonce** : Dérivé du mot de passe (chiffrement déterministe)
- **Format** : `encrypted:BASE64(nonce + ciphertext)`
### Avantages
**Pas de keyring** - Aucune dépendance système complexe
**Transparent** - Pas de clé maître à gérer
**Machine-specific** - Le fichier config chiffré ne fonctionne que sur cette machine
**Déchiffrement automatique** - Détection automatique du format
**Déterministe** - Même password = même ciphertext (évite les modifications inutiles du fichier)
### Inconvénients
⚠️ **Non portable** - Le fichier config ne fonctionne pas sur une autre machine
⚠️ **Sécurité limitée** - Un utilisateur avec accès physique peut déchiffrer
⚠️ **Pas de rotation** - Si l'UUID change, les mots de passe deviennent inaccessibles
## Utilisation
### 1. Chiffrer un mot de passe
```bash
cd pmoconfig
cargo run --example encrypt_password -- encrypt "MonMotDePasse123"
```
**Sortie** :
```
Original: MonMotDePasse123
Encrypted: encrypted:yRyu/jNlJRSdVz0eE+JX56UC2Tk016TmESDoLT6npLBJB3ZuhJ0XTqNOQjiXkkcB
Add this to your config.yaml:
password: "encrypted:yRyu/jNlJRSdVz0eE+JX56UC2Tk016TmESDoLT6npLBJB3ZuhJ0XTqNOQjiXkkcB"
```
### 2. Mettre à jour le fichier config.yaml
Remplacez le mot de passe en clair par la version chiffrée :
**Avant** :
```yaml
accounts:
qobuz:
username: user@example.com
password: MonMotDePasse123
```
**Après** :
```yaml
accounts:
qobuz:
username: user@example.com
password: encrypted:yRyu/jNlJRSdVz0eE+JX56UC2Tk016TmESDoLT6npLBJB3ZuhJ0XTqNOQjiXkkcB
```
### 3. Déchiffrement automatique
Le code de l'application déchiffre automatiquement les mots de passe :
```rust
use pmoconfig::get_config;
use pmoqobuz::QobuzConfigExt;
let config = get_config();
// Déchiffrement automatique si le password commence par "encrypted:"
let password = config.get_qobuz_password()?;
// password contient le mot de passe en clair
```
### 4. Tester le chiffrement
```bash
cargo run --example encrypt_password -- test
```
Cette commande teste le chiffrement/déchiffrement avec différents mots de passe.
### 5. Déchiffrer un mot de passe manuellement
```bash
cargo run --example encrypt_password -- decrypt "encrypted:ABC123..."
```
**Note** : Cela ne fonctionnera que sur la machine où le mot de passe a été chiffré.
## Format du mot de passe chiffré
```
encrypted:BASE64(nonce || ciphertext)
│ │ │ └─ Données chiffrées (longueur variable)
│ │ └─ Nonce de 12 bytes (96 bits)
│ └─ Encodage Base64
└─ Préfixe pour identifier les passwords chiffrés
```
## API de chiffrement
### Fonctions principales
```rust
use pmoconfig::encryption;
// Chiffrer un mot de passe
let encrypted = encryption::encrypt_password("secret")?;
// encrypted = "encrypted:ABC123..."
// Déchiffrer un mot de passe
let password = encryption::decrypt_password(&encrypted)?;
// password = "secret"
// Déchiffrement automatique (gère plaintext et encrypted)
let password = encryption::get_password("encrypted:ABC123...")?;
let password = encryption::get_password("plaintext")?; // Retourne tel quel
// Vérifier si un mot de passe est chiffré
if encryption::is_encrypted(&value) {
// C'est un mot de passe chiffré
}
```
## Migration progressive
Le système supporte à la fois les mots de passe en clair et chiffrés. Vous pouvez migrer progressivement :
1. **Phase 1** : Le système fonctionne avec des mots de passe en clair
2. **Phase 2** : Chiffrez les mots de passe avec l'outil
3. **Phase 3** : Mettez à jour config.yaml avec les versions chiffrées
4. **Phase 4** : L'application déchiffre automatiquement
Le code fonctionne avec les deux formats, vous n'avez donc pas besoin de tout migrer en même temps.
## Sécurité
### Protection offerte
- ✅ Protection contre la lecture directe du fichier config.yaml
- ✅ Protection si le fichier config est accidentellement partagé/commité
- ✅ Protection contre l'inspection casual du système de fichiers
### Limitations
-**Pas de protection contre un utilisateur root** - root peut lire l'UUID et déchiffrer
-**Pas de protection physique** - Quelqu'un avec accès physique peut extraire l'UUID
-**Pas de protection contre les malwares** - Un malware peut lire l'UUID et déchiffrer
### Recommandations
Pour une sécurité maximale, utilisez plutôt :
- **macOS** : Keychain (`security add-generic-password`)
- **Linux** : Secret Service API (GNOME Keyring, KWallet)
- **Windows** : Credential Manager
Cette implémentation est un **compromis pragmatique** pour :
- Éviter les dépendances lourdes (keyring, etc.)
- Fonctionner sur tous les OS
- Être simple et transparent
- Offrir une protection de base
## Dépannage
### "Decryption failed (wrong machine or corrupted data)"
Ce message apparaît si :
- Le mot de passe a été chiffré sur une autre machine
- L'UUID de la machine a changé (réinstallation OS, nouvelle carte mère)
- Les données sont corrompues
**Solution** : Rechiffrez le mot de passe sur cette machine.
### "Invalid encrypted password format"
Le mot de passe ne commence pas par `encrypted:` ou le format Base64 est invalide.
**Solution** : Vérifiez le format du mot de passe dans config.yaml.
### "Failed to extract IOPlatformUUID" (macOS)
Impossible de lire l'UUID de la machine.
**Solution** : Vérifiez que vous avez les droits d'exécuter `ioreg`.
## Exemple complet
```rust
// Dans pmoqobuz/src/config_ext.rs
impl QobuzConfigExt for Config {
fn get_qobuz_password(&self) -> Result<String> {
match self.get_value(&["accounts", "qobuz", "password"])? {
Value::String(s) => {
// Déchiffrement automatique si le mot de passe est chiffré
pmoconfig::encryption::get_password(&s)
.map_err(|e| anyhow!("Failed to decrypt password: {}", e))
}
_ => Err(anyhow!("Qobuz password not configured")),
}
}
}
```
## Tests
```bash
# Tester le module de chiffrement
cargo test -p pmoconfig encryption
# Tester l'outil CLI
cargo run --example encrypt_password -- test
# Tester avec un vrai service (Qobuz)
cargo run --example basic_usage
```

246
pmoconfig/README.md Normal file
View File

@@ -0,0 +1,246 @@
# pmoconfig - PMOMusic Configuration Module
Module de gestion de configuration pour PMOMusic avec support du chiffrement des mots de passe.
## Fonctionnalités
-**Configuration YAML** avec valeurs par défaut intégrées
-**Fusion automatique** entre config par défaut et config utilisateur
-**Overrides via variables d'environnement** (`PMOMUSIC_CONFIG__`)
-**Getters/setters type-safe** pour les valeurs de configuration
-**Pattern singleton thread-safe** pour l'accès global
-**🔒 Chiffrement des mots de passe** basé sur l'UUID de la machine
-**API REST optionnelle** (feature `api`)
## Utilisation de base
```rust
use pmoconfig::get_config;
// Obtenir la configuration globale
let config = get_config();
// Lire des valeurs
let port = config.get_http_port();
let cache_dir = config.get_cover_cache_dir()?;
// Modifier des valeurs
config.set_http_port(9000)?;
```
## Chiffrement des mots de passe
PMOConfig intègre un système de chiffrement transparent des mots de passe basé sur l'UUID matériel de la machine.
### Chiffrer un mot de passe
```bash
cargo run --example encrypt_password -- encrypt "MonMotDePasse"
```
**Sortie** :
```
Original: MonMotDePasse
Encrypted: encrypted:yRyu/jNlJRSdVz0eE+JX56UC2Tk016TmESDoLT6npLBJB3ZuhJ0XTqNOQjiXkkcB
Add this to your config.yaml:
password: "encrypted:yRyu/jNlJRSdVz0eE+JX56UC2Tk016TmESDoLT6npLBJB3ZuhJ0XTqNOQjiXkkcB"
```
### Configuration
**config.yaml avec mot de passe chiffré** :
```yaml
accounts:
qobuz:
username: user@example.com
password: encrypted:yRyu/jNlJRSdVz0eE+JX56UC2Tk016TmESDoLT6npLBJB3ZuhJ0XTqNOQjiXkkcB
```
### Utilisation dans le code
```rust
use pmoconfig::encryption;
// Déchiffrement automatique (gère plaintext et encrypted)
let password = encryption::get_password(&value)?;
// Chiffrer
let encrypted = encryption::encrypt_password("secret")?;
// Déchiffrer
let decrypted = encryption::decrypt_password(&encrypted)?;
// Tester si chiffré
if encryption::is_encrypted(&value) {
// ...
}
```
### Caractéristiques du chiffrement
- **Algorithme** : AES-256-GCM
- **Clé** : Dérivée de l'UUID matériel (SHA-256)
- **Format** : `encrypted:BASE64(nonce + ciphertext)`
- **Déterministe** : Même password = même ciphertext
### Avantages
✅ Pas de keyring/keychain requis
✅ Pas de clé maître à gérer
✅ Transparent pour l'utilisateur
✅ Migration progressive (supporte plaintext et encrypted)
✅ Déchiffrement automatique
### Limitations
⚠️ Non portable entre machines
⚠️ Sécurité limitée contre accès physique
⚠️ Pas de protection contre root/admin
📖 **Documentation complète** : [PASSWORD_ENCRYPTION.md](PASSWORD_ENCRYPTION.md)
## Structure de la configuration
```yaml
host:
http_port: 8080
base_url: "http://192.168.1.10:8080"
cover_cache:
directory: cache_covers
size: 2000
audio_cache:
directory: cache_audio
size: 500
logger:
buffer_capacity: 200
enable_console: true
min_level: INFO
playlists:
directory: playlists
devices:
mediarenderer:
pmo_mediarenderer:
udn: e4b68fbc-2bd5-4cea-98d8-be843fec0bd4
mediaserver:
pmo_mediaserver:
udn: 17fe2ea6-8908-4e30-bc52-b28ea4cab3e4
accounts:
qobuz:
username: user@example.com
password: encrypted:ABC123... # ← Mot de passe chiffré
appid: '798273057'
secret: 806331c3b0b641da923b890aed01d04a
```
## Répertoires de configuration
La configuration est recherchée dans cet ordre :
1. Répertoire fourni en paramètre
2. Variable d'environnement `PMOMUSIC_CONFIG`
3. `.pmomusic` dans le répertoire courant
4. `.pmomusic` dans le répertoire home (`~/.pmomusic`)
## Overrides via variables d'environnement
```bash
# Format: PMOMUSIC_CONFIG__section__key
export PMOMUSIC_CONFIG__host__http_port=9000
export PMOMUSIC_CONFIG__host__logger__min_level=DEBUG
# Lancer l'application
./pmomusic
```
## API REST (feature `api`)
```toml
[dependencies]
pmoconfig = { path = "../pmoconfig", features = ["api"] }
```
```rust
use pmoconfig::api::create_config_router;
use axum::Router;
let config_router = create_config_router();
let app = Router::new().nest("/api/config", config_router);
```
**Endpoints disponibles** :
- `GET /api/config` - Récupère toute la configuration
- `GET /api/config/{path}` - Récupère une valeur spécifique
- `PUT /api/config/{path}` - Modifie une valeur
- `GET /api/config/docs` - Documentation OpenAPI/Swagger
## Exemples
### Exemple complet
Voir [examples/encrypt_password.rs](examples/encrypt_password.rs) pour un exemple complet de chiffrement/déchiffrement.
### Utilisation dans un projet
```rust
use pmoconfig::{get_config, encryption};
use anyhow::Result;
fn main() -> Result<()> {
let config = get_config();
// Lire la configuration
let port = config.get_http_port();
println!("HTTP port: {}", port);
// Lire un mot de passe (automatiquement déchiffré)
let password_value = config.get_value(&["accounts", "service", "password"])?;
if let serde_yaml::Value::String(s) = password_value {
let password = encryption::get_password(&s)?;
println!("Password loaded successfully");
}
Ok(())
}
```
## Tests
```bash
# Tests unitaires
cargo test
# Tests du module encryption
cargo test encryption
# Tester l'outil de chiffrement
cargo run --example encrypt_password -- test
```
## Dépendances
```toml
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_yaml = "0.9"
anyhow = "1.0"
dirs = "6.0"
uuid = { version = "1.18", features = ["v4"] }
tracing = "0.1"
# Chiffrement
aes-gcm = "0.10"
sha2 = "0.10"
base64 = "0.22"
# Feature API (optionnel)
axum = { version = "0.8", optional = true }
utoipa = { version = "5.3", optional = true }
```
## Licence
Voir LICENSE dans la racine du projet.

View File

@@ -0,0 +1,126 @@
//! Outil CLI pour chiffrer/déchiffrer des mots de passe
//!
//! Usage:
//! cargo run --example encrypt_password -- encrypt "mon_mot_de_passe"
//! cargo run --example encrypt_password -- decrypt "encrypted:ABC123..."
//! cargo run --example encrypt_password -- test
use anyhow::Result;
use pmoconfig::encryption::{decrypt_password, encrypt_password, get_password, is_encrypted};
fn main() -> Result<()> {
let args: Vec<String> = std::env::args().collect();
if args.len() < 2 {
print_usage();
return Ok(());
}
match args[1].as_str() {
"encrypt" => {
if args.len() < 3 {
eprintln!("Error: Missing password to encrypt");
print_usage();
return Ok(());
}
let password = &args[2];
let encrypted = encrypt_password(password)?;
println!("Original: {}", password);
println!("Encrypted: {}", encrypted);
println!("\nAdd this to your config.yaml:");
println!("password: \"{}\"", encrypted);
}
"decrypt" => {
if args.len() < 3 {
eprintln!("Error: Missing encrypted password");
print_usage();
return Ok(());
}
let encrypted = &args[2];
if !is_encrypted(encrypted) {
eprintln!("Error: Value does not start with 'encrypted:'");
return Ok(());
}
match decrypt_password(encrypted) {
Ok(password) => {
println!("Encrypted: {}", encrypted);
println!("Decrypted: {}", password);
}
Err(e) => {
eprintln!("Error: Failed to decrypt password");
eprintln!("This encrypted password was created on a different machine.");
eprintln!("Details: {}", e);
}
}
}
"test" => {
println!("=== Password Encryption Test ===\n");
// Test avec différents mots de passe
let test_passwords = vec![
"simple",
"Complex_P@ssw0rd!",
"très long mot de passe avec des caractères spéciaux: é à ç ê",
"12345",
];
for password in test_passwords {
println!("Testing: {}", password);
let encrypted = encrypt_password(password)?;
println!(" Encrypted: {}", encrypted);
let decrypted = decrypt_password(&encrypted)?;
println!(" Decrypted: {}", decrypted);
if password == decrypted {
println!(" ✓ Success!\n");
} else {
println!(" ✗ FAILED! Passwords don't match!\n");
return Err(anyhow::anyhow!("Test failed"));
}
}
// Test de la fonction get_password
println!("=== Testing get_password() ===\n");
let plaintext = get_password("plaintext_password")?;
println!("Plaintext input: {}", plaintext);
assert_eq!(plaintext, "plaintext_password");
let encrypted_input = encrypt_password("secret123")?;
let decrypted = get_password(&encrypted_input)?;
println!("Encrypted input: {}", encrypted_input);
println!("Auto-decrypted: {}", decrypted);
assert_eq!(decrypted, "secret123");
println!("\n✓ All tests passed!");
}
_ => {
eprintln!("Error: Unknown command '{}'", args[1]);
print_usage();
}
}
Ok(())
}
fn print_usage() {
println!("Usage:");
println!(" cargo run --example encrypt_password -- encrypt <password>");
println!(" cargo run --example encrypt_password -- decrypt <encrypted>");
println!(" cargo run --example encrypt_password -- test");
println!("\nExamples:");
println!(" cargo run --example encrypt_password -- encrypt \"MySecretPassword\"");
println!(
" cargo run --example encrypt_password -- decrypt \"encrypted:SGVsbG8gV29ybGQh...\""
);
}

290
pmoconfig/src/encryption.rs Normal file
View File

@@ -0,0 +1,290 @@
//! Module de chiffrement des mots de passe basé sur l'UUID de la machine
//!
//! Ce module fournit un chiffrement transparent des mots de passe dans la
//! configuration. La clé de chiffrement est dérivée de l'UUID matériel de
//! la machine, ce qui rend le fichier config non-portable mais protégé.
use aes_gcm::{
aead::{Aead, KeyInit},
Aes256Gcm, Nonce,
};
use anyhow::{anyhow, Result};
use base64::Engine;
use sha2::{Digest, Sha256};
use std::process::Command;
/// Préfixe pour identifier les mots de passe chiffrés
const ENCRYPTED_PREFIX: &str = "encrypted:";
/// Récupère l'UUID matériel de la machine
///
/// Sur macOS, utilise `ioreg -d2 -c IOPlatformExpertDevice`
/// Sur Linux, utilise `/etc/machine-id` ou `/var/lib/dbus/machine-id`
/// Sur Windows, utilise `wmic csproduct get UUID`
fn get_machine_uuid() -> Result<String> {
#[cfg(target_os = "macos")]
{
let output = Command::new("ioreg")
.args(["-d2", "-c", "IOPlatformExpertDevice"])
.output()?;
let output_str = String::from_utf8_lossy(&output.stdout);
// Chercher la ligne contenant IOPlatformUUID
for line in output_str.lines() {
if line.contains("IOPlatformUUID") {
// Format: "IOPlatformUUID" = "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
if let Some(uuid) = line.split('"').nth(3) {
return Ok(uuid.to_string());
}
}
}
Err(anyhow!("Failed to extract IOPlatformUUID from ioreg"))
}
#[cfg(target_os = "linux")]
{
use std::fs;
// Essayer /etc/machine-id en premier
if let Ok(uuid) = fs::read_to_string("/etc/machine-id") {
return Ok(uuid.trim().to_string());
}
// Fallback sur /var/lib/dbus/machine-id
if let Ok(uuid) = fs::read_to_string("/var/lib/dbus/machine-id") {
return Ok(uuid.trim().to_string());
}
Err(anyhow!("Failed to read machine-id"))
}
#[cfg(target_os = "windows")]
{
let output = Command::new("wmic")
.args(["csproduct", "get", "UUID"])
.output()?;
let output_str = String::from_utf8_lossy(&output.stdout);
// La deuxième ligne contient l'UUID
if let Some(uuid) = output_str.lines().nth(1) {
return Ok(uuid.trim().to_string());
}
Err(anyhow!("Failed to extract UUID from wmic"))
}
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
{
Err(anyhow!("Unsupported platform for machine UUID extraction"))
}
}
/// Dérive une clé de chiffrement AES-256 à partir de l'UUID de la machine
fn derive_key() -> Result<[u8; 32]> {
let machine_uuid = get_machine_uuid()?;
// Utiliser SHA-256 pour dériver une clé de 256 bits
let mut hasher = Sha256::new();
hasher.update(machine_uuid.as_bytes());
hasher.update(b"pmomusic-config-encryption-v1"); // Salt pour différencier
let result = hasher.finalize();
let mut key = [0u8; 32];
key.copy_from_slice(&result);
Ok(key)
}
/// Chiffre un mot de passe avec la clé dérivée de la machine
///
/// # Arguments
///
/// * `password` - Le mot de passe en clair
///
/// # Returns
///
/// Le mot de passe chiffré au format "encrypted:BASE64"
/// Le format encodé est : nonce(12 bytes) + ciphertext
///
/// # Example
///
/// ```rust,ignore
/// let encrypted = encrypt_password("my_password")?;
/// // encrypted = "encrypted:SGVsbG8gV29ybGQh..."
/// ```
pub fn encrypt_password(password: &str) -> Result<String> {
let key = derive_key()?;
let cipher = Aes256Gcm::new_from_slice(&key)
.map_err(|e| anyhow!("Failed to create cipher: {}", e))?;
// Nonce de 96 bits (12 bytes) - dérivé du mot de passe pour avoir
// un chiffrement déterministe (même password = même ciphertext)
// Cela permet d'éviter de modifier le fichier config si le password n'a pas changé
let mut nonce_bytes = [0u8; 12];
let mut hasher = Sha256::new();
hasher.update(password.as_bytes());
hasher.update(b"pmomusic-nonce-v1");
let nonce_hash = hasher.finalize();
nonce_bytes.copy_from_slice(&nonce_hash[..12]);
let nonce = Nonce::from_slice(&nonce_bytes);
let ciphertext = cipher
.encrypt(nonce, password.as_bytes())
.map_err(|e| anyhow!("Encryption failed: {}", e))?;
// Stocker nonce + ciphertext ensemble
let mut combined = Vec::with_capacity(12 + ciphertext.len());
combined.extend_from_slice(&nonce_bytes);
combined.extend_from_slice(&ciphertext);
Ok(format!(
"{}{}",
ENCRYPTED_PREFIX,
base64::engine::general_purpose::STANDARD.encode(&combined)
))
}
/// Déchiffre un mot de passe avec la clé dérivée de la machine
///
/// # Arguments
///
/// * `encrypted` - Le mot de passe chiffré au format "encrypted:BASE64"
///
/// # Returns
///
/// Le mot de passe en clair
///
/// # Errors
///
/// Retourne une erreur si le format est invalide ou si le déchiffrement échoue
///
/// # Example
///
/// ```rust,ignore
/// let password = decrypt_password("encrypted:SGVsbG8gV29ybGQh...")?;
/// ```
pub fn decrypt_password(encrypted: &str) -> Result<String> {
// Vérifier le préfixe
let base64_data = encrypted
.strip_prefix(ENCRYPTED_PREFIX)
.ok_or_else(|| anyhow!("Invalid encrypted password format (missing prefix)"))?;
let key = derive_key()?;
let cipher = Aes256Gcm::new_from_slice(&key)
.map_err(|e| anyhow!("Failed to create cipher: {}", e))?;
let ciphertext = base64::engine::general_purpose::STANDARD
.decode(base64_data)
.map_err(|e| anyhow!("Invalid base64: {}", e))?;
// Dériver le même nonce (on ne peut pas le stocker car on veut un chiffrement déterministe)
// On va essayer de déchiffrer avec tous les nonces possibles... non, ça ne marche pas.
// Problème : on ne peut pas dériver le nonce du mot de passe chiffré car on ne connaît pas le plaintext.
// Solution : stocker le nonce avec le ciphertext
// Format: nonce(12 bytes) + ciphertext
if ciphertext.len() < 12 {
return Err(anyhow!("Invalid ciphertext (too short)"));
}
let nonce = Nonce::from_slice(&ciphertext[..12]);
let actual_ciphertext = &ciphertext[12..];
let plaintext = cipher
.decrypt(nonce, actual_ciphertext)
.map_err(|e| anyhow!("Decryption failed (wrong machine or corrupted data): {}", e))?;
String::from_utf8(plaintext).map_err(|e| anyhow!("Invalid UTF-8: {}", e))
}
/// Vérifie si une valeur est un mot de passe chiffré
///
/// # Arguments
///
/// * `value` - La valeur à tester
///
/// # Returns
///
/// `true` si la valeur commence par "encrypted:", `false` sinon
pub fn is_encrypted(value: &str) -> bool {
value.starts_with(ENCRYPTED_PREFIX)
}
/// Obtient le mot de passe en clair, qu'il soit chiffré ou non
///
/// Cette fonction gère automatiquement la détection du format :
/// - Si le mot de passe commence par "encrypted:", il est déchiffré
/// - Sinon, il est retourné tel quel (plaintext)
///
/// # Arguments
///
/// * `value` - Le mot de passe (chiffré ou non)
///
/// # Returns
///
/// Le mot de passe en clair
///
/// # Example
///
/// ```rust,ignore
/// // Plaintext
/// let password = get_password("my_password")?;
/// // password = "my_password"
///
/// // Encrypted
/// let password = get_password("encrypted:SGVsbG8...")?;
/// // password = "decrypted_password"
/// ```
pub fn get_password(value: &str) -> Result<String> {
if is_encrypted(value) {
decrypt_password(value)
} else {
Ok(value.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_machine_uuid() {
let uuid = get_machine_uuid();
assert!(uuid.is_ok(), "Should be able to get machine UUID");
println!("Machine UUID: {}", uuid.unwrap());
}
#[test]
fn test_encrypt_decrypt() {
let password = "SuperSecret123!";
let encrypted = encrypt_password(password).unwrap();
assert!(encrypted.starts_with(ENCRYPTED_PREFIX));
assert_ne!(encrypted, password);
let decrypted = decrypt_password(&encrypted).unwrap();
assert_eq!(decrypted, password);
}
#[test]
fn test_is_encrypted() {
assert!(is_encrypted("encrypted:SGVsbG8="));
assert!(!is_encrypted("plaintext"));
assert!(!is_encrypted(""));
}
#[test]
fn test_get_password() {
// Plaintext
let password = get_password("plaintext").unwrap();
assert_eq!(password, "plaintext");
// Encrypted
let encrypted = encrypt_password("secret").unwrap();
let password = get_password(&encrypted).unwrap();
assert_eq!(password, "secret");
}
}

View File

@@ -37,6 +37,9 @@ use std::{
use tracing::info;
use uuid::Uuid;
// Module de chiffrement des mots de passe
pub mod encryption;
// Modules conditionnels pour l'API REST
#[cfg(feature = "api")]
pub mod api;
@@ -63,24 +66,6 @@ const DEFAULT_LOG_BUFFER_CAPACITY: usize = 1000;
const DEFAULT_LOG_MIN_LEVEL: &str = "TRACE";
const DEFAULT_LOG_ENABLE_CONSOLE: bool = true;
/// Macro to generate getter/setter for String values
macro_rules! impl_string_config {
($(#[$meta:meta])* $getter:ident, $setter:ident, $path:expr, $default:expr) => {
$(#[$meta])*
pub fn $getter(&self) -> Result<String> {
match self.get_value($path)? {
Value::String(s) => Ok(s),
_ => Err(anyhow!(concat!(stringify!($getter), " not configured"))),
}
}
$(#[$meta])*
pub fn $setter(&self, value: &str) -> Result<()> {
self.set_value($path, Value::String(value.to_string()))
}
};
}
/// Macro to generate getter/setter for usize values with default
macro_rules! impl_usize_config {
($getter:ident, $setter:ident, $path:expr, $default:expr) => {
@@ -606,37 +591,6 @@ impl Config {
self.set_value(&["devices", devtype, name, "udn"], Value::String(sanitized))
}
impl_string_config!(
/// Gets the Qobuz username from configuration
get_qobuz_username,
set_qobuz_username,
&["accounts", "qobuz", "username"],
""
);
impl_string_config!(
/// Gets the Qobuz password from configuration
get_qobuz_password,
set_qobuz_password,
&["accounts", "qobuz", "password"],
""
);
/// Gets the Qobuz credentials (username and password) from configuration
///
/// # Returns
///
/// Returns a `Result` containing a tuple of (username, password)
///
/// # Errors
///
/// Returns an error if either username or password is not configured
pub fn get_qobuz_credentials(&self) -> Result<(String, String)> {
let username = self.get_qobuz_username()?;
let password = self.get_qobuz_password()?;
Ok((username, password))
}
impl_usize_config!(
get_log_cache_size,
set_log_cache_size,