Files
Application/app/src/main/java/com/example/boidelov3/utils/SecureConfig.java
T
feldenr c803469643 feat: Ajout du jeu Papelito, améliorations UX et corrections de bugs
Nouveau jeu:
- Ajout du jeu Papelito (Undercover) avec flow complet
- Configuration des joueurs, temps de discussion, votes
- Système d'élimination et gestion des égalités
- Interface Material Design avec cartes et dialogues

Corrections de bugs critiques:
- Fix crash Papelito au lancement (MaterialSwitch vs Switch)
- Fix crash lors des votes nuls (égalité entre joueurs)
- Fix crash fin de partie lors du retour (navigation vers hub)
- Fix visibilité texte questions (couleur dynamique)
- Fix compteur tours défis invisible (blanc sur blanc)
- Fix icone question manquante pendant défis

Améliorations UX Boidelo Classic:
- Harmonisation des couleurs dynamiques (toolbar, bouton)
- Bouton de réglages maintenant visible (MaterialButton)
- Conteneur IA se rétracte quand désactivé
- Meilleure gestion des couleurs selon catégorie
- Fix délai entre manches pour affichage message fin

Améliorations techniques:
- Mise à jour CLAUDE.md avec architecture Papelito
- Amélioration tests unitaires (GameEngine, PlayerStats, QuestionCategory)
- Standardisation des clés Intent entre activités
- Nettoyage code mort (méthodes non utilisées)

Tests:
- 302 tests unitaires passants
- Couverture GameEngine, PlayerStats, QuestionCategory
- Tests Papelito (game logic, player management)
- Tests Game89 (challenges, players)

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-16 12:05:29 +01:00

240 lines
8.0 KiB
Java

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) {
if (provider == null || provider.trim().isEmpty()) {
return null;
}
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) {
if (provider == null || provider.trim().isEmpty()) {
Log.w(TAG, "Provider null ou vide pour removeApiKey");
return false;
}
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) {
if (provider == null || provider.trim().isEmpty()) {
return false;
}
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;
}
if (provider == null || provider.trim().isEmpty()) {
Log.w(TAG, "Provider null ou vide");
return false;
}
String trimmedKey = apiKey.trim();
switch (provider.toLowerCase().trim()) {
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) {
if (provider == null) {
return KEY_API_KEY; // Default to OpenAI key
}
switch (provider.toLowerCase().trim()) {
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");
}
}