Partie I : Hash, Signatures et Chiffrement – Le B.A.-BA du Développeur Java
Ah, la cryptographie en Java. Ce doux mélange de concepts mathématiques effrayants et d’abréviations que seuls les dieux de la sécurité semblent comprendre. C'est un domaine mystérieux qui fait souvent fuir les développeurs plus vite qu'un bug en production un vendredi 🥺. Mais pas de panique ! Avec Java Cryptography Architecture (JCA) et Java Cryptography Extension (JCE), signer, chiffrer et hacher des données devient presque… amusant
1. La cryptographie: un aperçu
La cryptographie est l’art de sécuriser des informations en les rendant inaccessibles à des tiers non autorisés. Elle repose sur trois grands principes :
a. Intégrité :
Assurer que l’information n’a pas été altérée pendant son transfert.
b. Authenticité :
Garantir que l’information provient bien de l’expéditeur déclaré.
c. Confidentialité :
Empêcher qu’une information soit lue par quelqu’un d’autre que le destinataire prévu.
En cryptographie moderne, ces principes sont mis en œuvre grâce à plusieurs techniques :
a'. Le hachage
Une fonction de hachage génère une empreinte unique (appelée digest) pour un ensemble de données. Elle est irréversible, ce qui signifie qu’on ne peut pas retrouver les données d’origine à partir du digest.
Une fonction de hachage, c'est comme une empreinte digitale : pour une même personne, l'empreinte est toujours la même, mais il est impossible de reconstruire la personne à partir de son empreinte.
b'. Les signatures numériques
Elles garantissent l’authenticité et l’intégrité d’un message. Une signature est créée à l’aide d’une clé privée et peut être vérifiée à l’aide de la clé publique correspondante.
c'. Le chiffrement
Symétrique : Utilise une même clé pour chiffrer et déchiffrer des données.
Asymétrique : Utilise une paire de clés (publique/privée), l’une pour chiffrer et l’autre pour déchiffrer.
2. JCA et JCE : pourquoi et comment ?
L'API JCA (Java Cryptography Architecture) a pour objectif principal de fournir une base solide pour les fonctionnalités cryptographiques dans la plateforme Java.
Elle a été conçue avec plusieurs principes clés en tête :
- Modularité via les fournisseurs :
L'implémentation des algorithmes cryptographiques est déléguée à des fournisseurs (providers), ce qui permet une flexibilité dans le choix des solutions.
- Extensibilité :
Elle prend en charge différentes implémentations et algorithmes proposés par divers fournisseurs, offrant ainsi une large palette de possibilités.
- Interopérabilité :
JCA garantit que les différentes implémentations fonctionnent harmonieusement entre elles, indépendamment du fournisseur choisi.
📝 Initialement, les algorithmes de chiffrement avancés (comme DES et AES) étaient soumis à des restrictions légales concernant leur exportation. Pour respecter ces lois, Sun Microsystems (créateur de Java) a séparé la cryptographie de base (JCA) des fonctionnalités avancées (JCE).
Aujourd’hui, ces restrictions ont été levées, et JCE est directement intégré à JCA. Cependant, la distinction historique reste utile pour comprendre leur rôle respectif :
JCA : Gestion des clés, signatures numériques, hachage.
JCE : Chiffrement symétrique/asymétrique, protocoles avancés.
a. Les Providers
Les providers sont des modules qui implémentent des algorithmes cryptographiques et des services de sécurité dans Java. Chaque provider offre une implémentation spécifique des services, comme les algorithmes de chiffrement, de hashing ou de signature.
Analogie: Un provider est comme un chef spécialisé dans une cuisine; chaque chef(provider) connaît des recettes(algorithmes) pour préparer les plats(opérations cryptographiques ). Java choisit le chef approprié selon la recette demandée si le client n'a pas choisi un chef(provider).
- Lister les providers enregistrés et leurs informations:
Code:
public static void listAllProviders() {
System.out.println("Provider disponibles :");
Provider[] providers = Security.getProviders();
for (Provider provider : providers) {
System.out.println("Provider: " + provider.getName());
System.out.println("Version: " + provider.getVersionStr());
System.out.println("Info: " + provider.getInfo());
}
}
NB: L'ordre est important car le premier provider supportant un algorithme sera utilisé par défaut si on ne précise pas le provider avec lequel on veut travailler
Vous pouvez trouver l'ordre de configurations de ces providers dans votre fichier java.security de votre jdk($JAVA_HOME/jre/lib/security/java.security ou $JAVA_HOME/conf/security/java.security selon votre version de jdk)
Depuis ce fichier java.security vous pouvez changer de façon statique les ordres, ajouter un provider s'il est installé, retirer un provider etc mais ce n'est pas recommandé.
Il est possible d'enregistrer dynamiquement des fournisseurs(providers) de services. Ces providers apportent de nouveaux algorithmes ou des implémentations différentes pour le chiffrement, le hachage, etc.
Pour ajouter un provider, nous avons deux méthodes principales :
public static void addProvider(Provider provider) {
Security.addProvider(provider);
System.out.println("Provider " + provider.getName() + " ajouté");
}
public static void addProvider(Provider provider, int position) {
Security.insertProviderAt(provider,position);
System.out.println("Provider " + provider.getName() + " ajouté à la position " + position);
}
La première méthode ajoute simplement le provider à la fin de la liste des providers disponibles. La seconde permet de choisir sa position exacte dans la liste. La position est importante car Java utilise le premier provider de la liste capable de fournir le service demandé.
Pour utiliser ces méthodes, il suffit d'appeler:
addProvider(new BouncyCastleProvider(),1);
NB: il faut ajouter la dépendance du provider. Pour mon cas j'utilise gradle et c'est implementation("org.bouncycastle:bcprov-jdk16:1.46")
.
Pour maven, utiliser
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk16</artifactId>
<version>1.46</version>
</dependency>
Si on affiche à nouveau la liste des providers on aura le provider BC(BouncyCastleProvider) à la première position(cf capture ci-dessous) :
Vous pouvez aussi récupérer un provider á partir de son nom. Ex "SUN"
Provider provider = Security.getProvider("SUN")
On peut lister les services offerts par un provider(ex "SUN")
Code:
public static void listAllServicesForProvider(Provider provider) {
System.out.println("Service disponibles pour le provider "+provider.getName()+ " :");
for (Provider.Service service : provider.getServices()) {
System.out.println("Service Type: " + service.getType());
System.out.println("Algorithm: " + service.getAlgorithm());
}
}
listAllServicesForProvider(Security.getProvider("SUN"));
Résultat:
Lister les algorithmes disponibles pour un type de service(ex "Signature")
Imaginez que vous êtes dans un centre commercial avec plusieurs magasins (les providers), et chaque magasin propose des articles similaires (les services).
Par exemple :
Le rayon "Chaussures" est présent dans plusieurs magasins.
Le magasin A propose des baskets et des bottes.
Le magasin B propose des sandales et des baskets.
Si vous demandez à l’accueil : "Quels types de chaussures sont disponibles ?", on vous donnera une liste combinée : [Baskets, Bottes, Sandales].
De la même manière, en Java :
Plusieurs providers peuvent proposer des algorithmes pour un même service (par exemple, "Signature").
Lorsque vous utilisez Security.getAlgorithms("Signature")
==>
Set<String> signatureAlgorithms = Security.getAlgorithms("Signature");
System.out.println("Algorithmes disponibles pour Signature : " + signatureAlgorithms);
, Java regroupe tous les algorithmes disponibles auprès de tous les providers et vous donne le résultat ci-dessous
b. MessageDigest: Intégrité
MessageDigest est une classe de JCA qui implemente les fonctions de hachage. C'est elle qui permet de calculer l'empreinte d'un message à l'aide d'un algorithme comme SHA-256,SHA-1, MD5 etc
On peut avoir la liste des algorithmes disponibles pour ce service en faisant
Set<String> signatureAlgorithms = Security.getAlgorithms("MessageDigest");
System.out.println("Algorithmes disponibles pour MessageDigest : " + signatureAlgorithms);
et le résultat est: Algorithmes disponibles pour MessageDigest : [SHA3-512, SHA-1, SHA-384, SHA3-384, SHA-224, SHA-512/256, SHA-256, MD2, SHA-512/224, SHA3-256, SHA-512, SHA3-224, MD5]
Le code suivant est une fonction qui nous permet de générer l'empreinte d'un message en choisissant l'algorithme à utiliser(ex "SHA-256" et le provider(ex "SUN")
public static String calculHash(String message,String algorithm, String provider) throws Exception {
//creation de l'objet MessageDigest en donnant l'algorithme et le provider
MessageDigest digest = MessageDigest.getInstance(algorithm,provider);
// calcul de l'empreinte
byte[] hash = digest.digest(message.getBytes(StandardCharsets.UTF_8));
// conversion de l'empreinte en hexadecimal pour affichage
StringBuilder stringBuilder = new StringBuilder();
for (byte b : hash) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) stringBuilder.append('0');
stringBuilder.append(hex);
}
return stringBuilder.toString();
}
Pour générer l'empreinte du mot de passe Passer@1234 on peut appeler la méthode String hash = calculHash("Passer@1234","SHA-256","SUN");
System.out.println("L'empreinte est "+hash);
Le résultat est a912b3fa7d4ba7676f5252b26558ea2cd9c6c3f239b2710e5b5e4de80e4abe88
📝 Dans une application réelle, lors de la vérification d'un mot de passe après login, on génère l'empreinte du mot de passe fourni par l'utilisateur(ex "Passer@1234") avec le même algorithme de hachage que celui utilisé pour stocker l'empreinte, puis on compare cette empreinte à celle stockée dans la base de données.
📝 Pour ce qui concerne les flux(fichier, données en streaming, etc), les classes DigestInputStream et DigestOutputStream permettent de lire ou écrire des données tout en calculant automatiquement leur empreinte avec un algorithme de hachage
c. Signature: Authenticité & Intégrité
La classe Signature de JCA est utilisé pour créer et vérifier des signatures numériques. Elle fournit des outils pour garantir l'authenticité et l'intégrité des données en utilisant des algorithmes cryptographiques tels que SHA256withRSA, SHA512withECDSA etc.
📝 Création de la signature: l'expéditeur utilise utilise sa clé privée pour signer les documents. Le résultat est une signature numérique.
📝 Vérification de la signature: le récepteur utilise la clé publique de l'expéditeur pour vérifier la signature et s'assurer que les données sont authentiques.
Classe:
public class SignatureServive {
/**
* Signer un message
*/
public static String signature(PrivateKey privateKey, String message, String algorithm, String provider) throws Exception {
//creation de l'objet Signature en donnant l'algorithme et le provider
Signature signature = Signature.getInstance(algorithm, provider);
// pour signer avec une clé privé
signature.initSign(privateKey);
// ajouter les données á signer
signature.update(message.getBytes(StandardCharsets.UTF_8));
//générer la signature
byte[] digitalSignature = signature.sign();
return Base64.getEncoder().encodeToString(digitalSignature);
}
/**
* Verifier la signature
* @param base64Signature
* @param message
* @param algorithm
* @param provider
* @return
* @throws Exception
*/
public static boolean verifySignature(PublicKey publicKey,String base64Signature, String message, String algorithm, String provider) throws Exception {
Signature signature = Signature.getInstance(algorithm, provider);
signature.initVerify(publicKey);
signature.update(message.getBytes(StandardCharsets.UTF_8));
byte[] decodedSignature = Base64.getDecoder().decode(base64Signature);
return signature.verify(decodedSignature);
}
}
Main:
public class Main {
public static void main(String[] args) throws Exception {
System.out.println("####################### BEGIN JAVA SECURITY DEMO #######################\n");
//Génération des clés
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
KeyPair keyPair = keyPairGenerator.generateKeyPair();
// --- Signer un message: expéditeur "Mamadou Seck"
String message = "Hello Kéba tu as reçu un virement de Mamadou Seck";
String base64Signature = signature(keyPair.getPrivate(),message,"SHA256withRSA","SunRsaSign");
System.out.println("La signature est: "+base64Signature);
// verifier la signature: récepteur "Kéba"
boolean isValid = verifySignature(keyPair.getPublic(),base64Signature,message,"SHA256withRSA","SunRsaSign");
System.out.println("La signature est valide: "+isValid);
System.out.println("####################### END JAVA SECURITY DEMO #######################\n");
}
Résultat:
d. Cipher: confidentialité
Le chiffrement est au cœur de la sécurité des applications modernes, permettant de protéger les données sensibles en les rendant inaccessibles sans la bonne clé. Avec la classe Cipher de l'API Java Cryptography Architecture (JCA), Java offre un outil puissant et flexible pour réaliser ces opérations de manière sécurisée.
Vous pouvez lister les algorithmes disponibles pour la classe Cipher en faisant
Set<String> algorithms = Security.getAlgorithms("Cipher");
System.out.println("Algorithmes disponibles pour cipher : " + algorithms);
et le résultat sera => Algorithmes disponibles pour cipher : [AES_192/GCM/NOPADDING, AES_256/CBC/NOPADDING, AES_128/KW/NOPADDING, AES_128/CBC/NOPADDING, AES_256/KW/NOPADDING, PBEWITHMD5ANDDES, PBEWITHHMACSHA256ANDAES_256, PBEWITHSHA1ANDRC4_128, AES_192/OFB/NOPADDING, DESEDEWRAP, RC2, PBEWITHSHA1ANDRC4_40, RSA, AES_192/CFB/NOPADDING, AES_192/KW/PKCS5PADDING, AES_256/KWP/NOPADDING, AES_128/CFB/NOPADDING, DESEDE, BLOWFISH, AES, AES_128/GCM/NOPADDING, PBEWITHHMACSHA256ANDAES_128, PBEWITHSHA1ANDDESEDE, AES_256/OFB/NOPADDING, PBEWITHHMACSHA384ANDAES_256, PBEWITHHMACSHA1ANDAES_256, PBEWITHHMACSHA224ANDAES_128, PBEWITHHMACSHA384ANDAES_128, AES_256/GCM/NOPADDING, AES/KW/PKCS5PADDING, PBEWITHHMACSHA512ANDAES_128, AES/KWP/NOPADDING, AES_256/ECB/NOPADDING, CHACHA20, PBEWITHHMACSHA224ANDAES_256, AES_192/CBC/NOPADDING, PBEWITHHMACSHA1ANDAES_128, AES_128/ECB/NOPADDING, PBEWITHHMACSHA512ANDAES_256, CHACHA20-POLY1305, AES_256/KW/PKCS5PADDING, AES/GCM/NOPADDING, AES_128/KWP/NOPADDING, AES_192/ECB/NOPADDING, ARCFOUR, AES_256/CFB/NOPADDING, AES_128/OFB/NOPADDING, AES_128/KW/PKCS5PADDING, AES_192/KW/NOPADDING, DES, AES/KW/NOPADDING, PBEWITHSHA1ANDRC2_40, PBEWITHSHA1ANDRC2_128, PBEWITHMD5ANDTRIPLEDES, AES_192/KWP/NOPADDING]
La classe Cipher est le composant central de JCA pour effectuer :
- Chiffrement : Conversion des données en un format illisible sans clé.
- Déchiffrement : Rétablissement des données originales à partir des données chiffrées.
La classe Cipher fonctionne sur le principe des transformations, qui définissent la méthode de chiffrement/déchiffrement à utiliser. Une transformation est une chaîne au format suivant : ALGORITHME/MODE/PADDING
ex. "AES/CBC/PKCS5Padding"
📝 Classes Principales
Cipher : Classe centrale pour le chiffrement/déchiffrement
KeyGenerator : Génération de clés symétriques
KeyPairGenerator : Génération de paires de clés asymétriques
SecretKey : Interface pour les clés symétriques
KeyStore : Stockage sécurisé des clés
- Le Chiffrement Symétrique :
Le chiffrement symétrique, aussi appelé chiffrement à clé secrète, utilise la même clé pour chiffrer et déchiffrer les données. C'est comme avoir une seule clé qui ouvre et ferme un cadenas. Cette méthode est particulièrement efficace pour chiffrer de grandes quantités de données rapidement.
Classe :
public class SymmetricEncryptionService {
public static String encrypt(SecretKey secretKey, byte[] iv, String message, String transformation) throws Exception {
// Initialisation du Cipher
Cipher cipher = Cipher.getInstance(transformation);
cipher.init(Cipher.ENCRYPT_MODE, secretKey,new IvParameterSpec(iv));
// Chiffrement des données
byte[] cipherText = cipher.doFinal(message.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(cipherText);
}
public static String decrypt(SecretKey secretKey, byte[] iv, String cipherMessage, String transformation) throws Exception {
// Initialisation du Cipher
Cipher cipher = Cipher.getInstance(transformation);
cipher.init(Cipher.DECRYPT_MODE, secretKey,new IvParameterSpec(iv));
//déchiffrement des données
byte[] message = cipher.doFinal(Base64.getDecoder().decode(cipherMessage));
return new String(message, StandardCharsets.UTF_8);
}
}
La classe Main:
public class Main {
public static void main(String[] args) throws Exception {
System.out.println("####################### BEGIN JAVA SECURITY DEMO #######################\n");
// Génération de la clé
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
keyGen.init(256, new SecureRandom()); // Taille de clé
SecretKey secretKey = keyGen.generateKey();
// vecteur d'initialisation comme j'utilise le moded CBC
byte[] iv = new byte[16];
SecureRandom random = new SecureRandom();
random.nextBytes(iv);
String plainText = "Données sensibles";
String cipherText = encrypt(secretKey,iv, plainText,"AES/CBC/PKCS5Padding");
System.out.println("Text chiffré : " + cipherText);
String message = decrypt(secretKey,iv, cipherText,"AES/CBC/PKCS5Padding");
System.out.println("Text déchiffré : " + message);
System.out.println("####################### END JAVA SECURITY DEMO #######################\n");
}
}
Résultat:
- Le Chiffrement asymétrique :
Le chiffrement asymétrique est une méthode de cryptographie utilisant une paire de clés : une clé publique et une clé privée. La clé publique sert à chiffrer les données ou à vérifier une signature, tandis que la clé privée est utilisée pour déchiffrer les données ou signer les informations.
RSA est l'un des algorithmes les plus utilisés pour le chiffrement asymétrique, basé sur la difficulté de la factorisation des grands nombres premiers.
Classe:
public class AsymmetricEncryptionService {
public static String encrypt(PublicKey publicKey, String message) throws Exception {
// Initialisation du Cipher en mode chiffrement
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
// Chiffrement des données
byte[] cipherText = cipher.doFinal(message.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(cipherText);
}
public static String decrypt(PrivateKey privateKey, String cipherMessage) throws Exception {
// Initialisation du Cipher en mode déchiffrement
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, privateKey);
// Déchiffrement des données
byte[] message = cipher.doFinal(Base64.getDecoder().decode(cipherMessage));
return new String(message, StandardCharsets.UTF_8);
}
}
La classe Main:
public class Main {
public static void main(String[] args) throws Exception {
System.out.println("####################### BEGIN JAVA SECURITY DEMO #######################\n");
// Génération de la paire de clés RSA
KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA");
keyPairGen.initialize(2048); // Taille de clé en bits
KeyPair keyPair = keyPairGen.generateKeyPair();
String plainText = "Données sensibles";
// Appel de la méthode d'encryption avec la clé publique
String cipherText = encrypt(keyPair.getPublic(), plainText);
System.out.println("Text chiffré : " + cipherText);
// Appel de la méthode de décryption avec la clé privée
String message = decrypt(keyPair.getPrivate(), cipherText);
System.out.println("Text déchiffré : " + message);
System.out.println("####################### END JAVA SECURITY DEMO #######################\n");
}
}
Résultat:
e. MAC(Message Authentication Code): Authenticité & Integrité
Un MAC (Message Authentication Code) est un code cryptographique généré à partir d’un message et d’une clé secrète partagée. Il sert principalement à deux fins :
Vérifier l’intégrité du message : Vous pouvez savoir si le message a été altéré pendant son transport.
Vérifier l’authenticité du message : Vous êtes certain que le message provient bien de l’expéditeur attendu, celui qui possède la clé secrète.
Exemple : Une demande de transfert d'argent qui inclut un MAC, où la banque vérifie le MAC pour s'assurer que la demande n'a pas été falsifiée en transit.
📝 Contrairement à un simple algorithme de hachage comme SHA-256, un MAC nécessite une clé secrète partagée entre l’expéditeur et le récepteur. Ainsi, seuls ceux qui connaissent cette clé peuvent générer ou vérifier le MAC pour un message donné.
Classe:
public class MacService {
public static String calculMAC(String message, String algorithm, String provider, SecretKey secretKey) throws Exception {
// Création de l'objet Mac en spécifiant l'algorithme et le provider
Mac mac = Mac.getInstance(algorithm, provider);
// Initialisation de l'objet Mac avec la clé secrète
mac.init(secretKey);
// Calcul du MAC (code d'authentification)
byte[] macResult = mac.doFinal(message.getBytes(StandardCharsets.UTF_8));
// Conversion du MAC en hexadécimal pour affichage
StringBuilder stringBuilder = new StringBuilder();
for (byte b : macResult) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) stringBuilder.append('0');
stringBuilder.append(hex);
}
return stringBuilder.toString();
}
}
La classe Main:
public class Main {
public static void main(String[] args) throws Exception {
System.out.println("####################### BEGIN JAVA SECURITY DEMO #######################\n");
// Génération de la clé
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
keyGen.init(256, new SecureRandom()); // Taille de clé
SecretKey secretKey = keyGen.generateKey();
String message = "un message sécurisé";
String mac = calculMAC(message, "HmacSHA256", "SunJCE", secretKey);
System.out.println("Le MAC généré est : " + mac);
System.out.println("####################### END JAVA SECURITY DEMO #######################\n");
}
}
Résultat:
Partie II (à venir dans un autre article) : Certificats, Keystores, TrustManagers, Gestion de la Confiance, .... — Les Mécanismes Avancés de Sécurité en Java
Références:
1.https://docs.oracle.com/javase/8/docs/technotes/guides/security/crypto/CryptoSpec.html
Top comments (4)
Article très intéressant, merci pour le partage ! Hâte de te lire à nouveau.
Je vais au travail moins bête.
Merci pour la clarté des détails; définitivement dans l’attente de tes nouvelles publications.
Diadieuf
Awesome, merci pour le partage
Merci pour le partage mister Kéba. Article très intéressant