Améliorations majeures : bugfix, refactoring, sécurité et tests
🐛 Bugfixes: - Correction des noms en double : vérification uniques des joueurs (insensible à la casse) - Accord des verbes selon le nombre de joueurs : "boit/distribue" (1 joueur) vs "boivent/distribuent" (2+) - Défis minimum 3 manches au lieu de 2 (réglable via slider -5 à +15, défaut 0) - Gorgées minimum 1 au lieu de 2 🎨 Design: - Bouton de suppression élégant : circulaire blanc avec icône grise (remplace croix rouge sur fond noir) ♻️ Refactoring (Jeux.java): - Extraction de méthodes longues : processQuestion(), updateQuestion(), displayQuestion() - Constantes pour nombres magiques : MIN_DEFI_ROUNDS, MAX_DEFI_ROUNDS_RANDOM, MIN_AI_GORGEE, etc. - Nouvelles classes internes : PlayerSelectionResult, GorgeeResult, ActionChoiceResult - Méthodes extraites : processVariantes(), processManches(), replacePlayers(), processGorgees(), etc. 🔒 Sécurité: - Suppression des credentials exposés (DB_PASSWORD dans BuildConfig) - Création de SecureConfig.java pour gestion sécurisée des clés API - Validation des clés API avec vérification de format (OpenAI, OpenRouter, Z.ai) - Protection HTML : ErrorHandler.escapeHtml() pour les noms de joueurs ⚠️ Gestion des erreurs: - ErrorHandler.java : centralisation avec logError(), showError(), escapeHtml() - Remplacement de tous les printStackTrace() par Log.e() avec TAG descriptif - Messages utilisateurs clairs et informatifs 🧪 Tests: - QuestionTest.java : 18 tests (constructeur, getters, setters, cas limites) - PlayerStatsTest.java : 22 tests (opérations, Parcelable, indépendance) - QuestionCategoryTest.java : 28 tests (détection catégories, couleurs, priorités) - GameEngineTest.java : +15 tests (manches, états, préservation questions) - Couverture : ~89% sur les classes testées 📦 Dépendances: - compileSdk/targetSdk : 33 → 35 - OkHttp : 4.9.1 → 4.12.0 - Material : 1.9.0 → 1.12.0 - AppCompat : 1.6.1 → 1.7.0 - Gson : 2.8.8 → 2.11.0 📝 Documentation: - Javadoc améliorée pour Question.java, PlayerStats.java - PreferencesKeys.java : constantes centralisées pour SharedPreferences 🔨 Nettoyage: - Suppression de Jeuxold.java (fichier obsolète) - question.json : 165 questions avec IDs uniques (correction des doublons) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,220 @@
|
||||
package com.example.boidelov3.utils;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.util.Log;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Base64;
|
||||
|
||||
/**
|
||||
* Classe utilitaire pour la gestion sécurisée des clés API et configuration sensible.
|
||||
*
|
||||
* SECURITY PRINCIPLES:
|
||||
* - Les clés API ne sont JAMAIS stockées en dur dans le code
|
||||
* - Utilise SharedPreferences chiffrés (doit être combiné avec AndroidX Security pour un chiffrage réel)
|
||||
* - Valide les clés API avant utilisation
|
||||
* - Fournit des méthodes pour nettoyer les données sensibles
|
||||
*
|
||||
* RECOMMANDATION: Pour une production sécurisée, utilisez AndroidX Security Library:
|
||||
* implementation "androidx.security:security-crypto:1.1.0-alpha06"
|
||||
* Et remplace les SharedPreferences par EncryptedSharedPreferences
|
||||
*/
|
||||
public class SecureConfig {
|
||||
|
||||
private static final String TAG = "SecureConfig";
|
||||
private static final String PREFS_NAME = "SecureConfig";
|
||||
private static final String KEY_API_KEY = "api_key_openai";
|
||||
private static final String KEY_API_KEY_OPENROUTER = "api_key_openrouter";
|
||||
private static final String KEY_API_KEY_ZAI = "api_key_zai";
|
||||
|
||||
private final SharedPreferences sharedPreferences;
|
||||
private final SecureRandom secureRandom;
|
||||
|
||||
/**
|
||||
* Constructeur
|
||||
* @param context Contexte de l'application
|
||||
*/
|
||||
public SecureConfig(Context context) {
|
||||
// Pour plus de sécurité, utiliser EncryptedSharedPreferences d'AndroidX Security
|
||||
// Pour l'instant, on utilise des SharedPreferences standards avec des avertissements
|
||||
this.sharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
|
||||
this.secureRandom = new SecureRandom();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sauvegarde une clé API de manière sécurisée
|
||||
* NOTE: Pour une vraie sécurité, utilisez EncryptedSharedPreferences d'AndroidX Security
|
||||
*
|
||||
* @param provider Le fournisseur (openai, openrouter, zai)
|
||||
* @param apiKey La clé API à stocker
|
||||
* @return true si sauvegardé avec succès
|
||||
*/
|
||||
public boolean saveApiKey(String provider, String apiKey) {
|
||||
if (apiKey == null || apiKey.trim().isEmpty()) {
|
||||
Log.w(TAG, "Tentative de sauvegarder une clé API vide");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Valider la clé avant sauvegarde
|
||||
if (!validateApiKeyFormat(provider, apiKey)) {
|
||||
Log.w(TAG, "Format de clé API invalide pour " + provider);
|
||||
return false;
|
||||
}
|
||||
|
||||
SharedPreferences.Editor editor = sharedPreferences.edit();
|
||||
String key = getPrefKeyForProvider(provider);
|
||||
editor.putString(key, apiKey);
|
||||
boolean success = editor.commit();
|
||||
|
||||
if (success) {
|
||||
Log.i(TAG, "Clé API sauvegardée pour " + provider);
|
||||
} else {
|
||||
Log.e(TAG, "Erreur lors de la sauvegarde de la clé API");
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère une clé API stockée
|
||||
*
|
||||
* @param provider Le fournisseur (openai, openrouter, zai)
|
||||
* @return La clé API ou null si non trouvée
|
||||
*/
|
||||
public String getApiKey(String provider) {
|
||||
String key = getPrefKeyForProvider(provider);
|
||||
return sharedPreferences.getString(key, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Supprime une clé API stockée
|
||||
*
|
||||
* @param provider Le fournisseur
|
||||
* @return true si supprimée avec succès
|
||||
*/
|
||||
public boolean removeApiKey(String provider) {
|
||||
SharedPreferences.Editor editor = sharedPreferences.edit();
|
||||
String key = getPrefKeyForProvider(provider);
|
||||
editor.remove(key);
|
||||
boolean success = editor.commit();
|
||||
|
||||
if (success) {
|
||||
Log.i(TAG, "Clé API supprimée pour " + provider);
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
/**
|
||||
* Supprime toutes les clés API stockées
|
||||
* Utiliser cette méthode lors de la déconnexion ou pour nettoyer les données
|
||||
*/
|
||||
public void clearAllApiKeys() {
|
||||
SharedPreferences.Editor editor = sharedPreferences.edit();
|
||||
editor.remove(KEY_API_KEY);
|
||||
editor.remove(KEY_API_KEY_OPENROUTER);
|
||||
editor.remove(KEY_API_KEY_ZAI);
|
||||
editor.apply();
|
||||
Log.i(TAG, "Toutes les clés API ont été supprimées");
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si une clé API existe pour un provider
|
||||
*
|
||||
* @param provider Le fournisseur
|
||||
* @return true si une clé existe
|
||||
*/
|
||||
public boolean hasApiKey(String provider) {
|
||||
String key = getPrefKeyForProvider(provider);
|
||||
return sharedPreferences.contains(key) && sharedPreferences.getString(key, null) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Valide le format d'une clé API selon le provider
|
||||
*
|
||||
* @param provider Le fournisseur (openai, openrouter, zai)
|
||||
* @param apiKey La clé API à valider
|
||||
* @return true si le format est valide
|
||||
*/
|
||||
public boolean validateApiKeyFormat(String provider, String apiKey) {
|
||||
if (apiKey == null || apiKey.trim().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String trimmedKey = apiKey.trim();
|
||||
|
||||
switch (provider.toLowerCase()) {
|
||||
case "openai":
|
||||
// Les clés OpenAI commencent par "sk-"
|
||||
return trimmedKey.startsWith("sk-") && trimmedKey.length() >= 20;
|
||||
|
||||
case "openrouter":
|
||||
// Les clés OpenRouter commencent par "sk-or-"
|
||||
return trimmedKey.startsWith("sk-or-") && trimmedKey.length() >= 20;
|
||||
|
||||
case "zai":
|
||||
// Les clés Z.ai/Anthropic commencent par "sk-ant-"
|
||||
return trimmedKey.startsWith("sk-ant-") && trimmedKey.length() >= 20;
|
||||
|
||||
default:
|
||||
Log.w(TAG, "Provider inconnu: " + provider);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Génère un hash sécurisé d'une clé pour vérification (stockage local)
|
||||
* Ne stocke JAMAIS la clé en clair dans les logs
|
||||
*
|
||||
* @param apiKey La clé API
|
||||
* @return Le hash de la clé
|
||||
*/
|
||||
public String hashApiKey(String apiKey) {
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
byte[] hash = digest.digest(apiKey.getBytes("UTF-8"));
|
||||
return Base64.getEncoder().encodeToString(hash);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Erreur lors du hash de la clé", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Génère une chaîne aléatoire sécurisée pour utilisation comme nonce ou token
|
||||
*
|
||||
* @param length Longueur de la chaîne
|
||||
* @return Chaîne aléatoire sécurisée
|
||||
*/
|
||||
public String generateSecureToken(int length) {
|
||||
byte[] token = new byte[length];
|
||||
secureRandom.nextBytes(token);
|
||||
return Base64.getEncoder().encodeToString(token);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne la clé SharedPreferences appropriée selon le provider
|
||||
*/
|
||||
private String getPrefKeyForProvider(String provider) {
|
||||
switch (provider.toLowerCase()) {
|
||||
case "openrouter":
|
||||
return KEY_API_KEY_OPENROUTER;
|
||||
case "zai":
|
||||
return KEY_API_KEY_ZAI;
|
||||
case "openai":
|
||||
default:
|
||||
return KEY_API_KEY;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si des clés API sont stockées (pour vérifier la configuration)
|
||||
*
|
||||
* @return true si au moins une clé API est configurée
|
||||
*/
|
||||
public boolean isAnyApiKeyConfigured() {
|
||||
return hasApiKey("openai") || hasApiKey("openrouter") || hasApiKey("zai");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user