Bonjour, Habr! Je vous présente la traduction de l'article "Java Cryptography" de Jakob Jenkov.
Cette publication est une traduction du premier article sur la cryptographie Java d'une série d'articles pour les débutants qui souhaitent apprendre les bases de la cryptographie en Java.
Table des matières:
- Cryptographie Java
- Chiffre
- Messagedigest
- Mac
- Signature
- Keypair
- Générateur de clés
- KeyPairGenerator
- Keystore
- Keytool
- Attestation
- CertificateFactory
- CertPath
Cryptographie Java
L'API de cryptographie Java offre la possibilité de crypter et de décrypter des données en java, ainsi que de gérer les clés, les signatures et d'authentifier (authentifier) ​​les messages, de calculer des hachages cryptographiques et bien plus encore.
Cet article explique les bases de l'utilisation de l'API Java Cryptography pour effectuer diverses tâches qui nécessitent un chiffrement sécurisé.
Cet article n'explique pas les bases de la théorie cryptographique. Vous devrez voir ces informations ailleurs.
Extension de cryptographie Java
L'API de cryptographie Java est fournie par ce que l'on appelle l'extension de cryptographie Java (JCE). JCE fait depuis longtemps partie de la plateforme Java. Initialement, JCE a été séparé de Java en raison des restrictions à l'exportation sur la technologie de chiffrement aux États-Unis. Par conséquent, les algorithmes de chiffrement les plus puissants n'étaient pas inclus dans la plate-forme Java standard. Ces algorithmes de chiffrement plus robustes peuvent être appliqués si votre entreprise est située aux États-Unis, mais dans d'autres cas, vous devrez utiliser des algorithmes plus faibles ou implémenter vos propres algorithmes de chiffrement et les connecter à JCE.
Depuis 2017, les règles d'exportation des algorithmes de chiffrement aux États-Unis ont été considérablement assouplies et, dans la plupart des régions du monde, vous pouvez utiliser les normes de chiffrement internationales via Java JCE.
Architecture de cryptographie Java
Java Cryptography Architecture (JCA) est le nom de la conception de l'API de cryptographie interne en Java. JCA est structuré autour de plusieurs classes de base et interfaces à usage général. La fonctionnalité réelle de ces interfaces est fournie par les fournisseurs. Ainsi, vous pouvez utiliser la classe Cipher pour chiffrer et déchiffrer certaines données, mais l'implémentation spécifique du chiffrement (algorithme de chiffrement) dépend du fournisseur particulier utilisé.
Vous pouvez également implémenter et connecter vos propres fournisseurs, mais vous devez être prudent avec cela. Il est difficile de mettre en œuvre correctement le chiffrement sans failles de sécurité! Si vous ne savez pas ce que vous faites, vous feriez probablement mieux d’utiliser le fournisseur Java intégré ou un fournisseur de confiance tel que Bouncy Castle.
Classes principales et interfaces
L'API Java Cryptography se compose des packages Java suivants:
- java.security
- java.security.cert
- java.security.spec
- java.security.interfaces
- javax.crypto
- javax.crypto.spec
- javax.crypto.interfaces
Les principales classes et interfaces de ces packages:
- Fournisseur
- SecureRandom
- Chiffre
- Messagedigest
- Signature
- Mac
- AlgorithmParameters
- AlgorithmParameterGenerator
- Keyfactory
- SecretKeyFactory
- KeyPairGenerator
- Générateur de clés
- Accord de clé
- Keystore
- CertificateFactory
- CertPathBuilder
- CertPathValidator
- CertStore
Fournisseur
La classe Provider (java.security.Provider) est la classe centrale de l'API de chiffrement Java. Pour utiliser l'API de cryptographie Java, vous devez installer un fournisseur de cryptographie. Le SDK Java est livré avec son propre fournisseur de cryptographie. Sauf si vous définissez explicitement le fournisseur de cryptographie, le fournisseur par défaut sera utilisé. Cependant, ce fournisseur de chiffrement peut ne pas prendre en charge les algorithmes de chiffrement que vous souhaitez utiliser. Par conséquent, vous devrez peut-être installer votre propre fournisseur de cryptographie.
L'un des fournisseurs de cryptographie les plus populaires pour l'API de cryptographie Java est appelé Bouncy Castle. Voici un exemple où BouncyCastleProvider est défini comme fournisseur cryptographique:
import org.bouncycastle.jce.provider.BouncyCastleProvider; import java.security.Security; public class ProviderExample { public static void main(String[] args) { Security.addProvider(new BouncyCastleProvider()); } }
Chiffre
La classe Cipher (javax.crypto.Cipher) représente un algorithme cryptographique. Un chiffrement peut être utilisé à la fois pour le chiffrement et pour le déchiffrement des données. La classe Cipher est expliquée plus en détail dans les sections suivantes, avec une brève description ci-dessous.
Création d'une instance de la classe de chiffrement qui utilise l'algorithme de chiffrement AES pour un usage interne:
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
La méthode Cipher.getInstance (...) accepte une chaîne qui détermine l'algorithme de chiffrement à utiliser, ainsi que d'autres paramètres d'algorithme.
Dans l'exemple ci-dessus:
- AES - algorithme de chiffrement
- CBC est un mode dans lequel l'algorithme AES peut fonctionner.
- PKCS5Padding est la façon dont l'algorithme AES doit traiter les derniers octets de données pour le chiffrement. Qu'est-ce que cela signifie exactement, regardez dans le manuel de cryptographie dans son ensemble, et non dans cet article.
Initialisation du chiffrement
Avant d'utiliser une instance de chiffrement, vous devez l'initialiser. L'instance de chiffrement est initialisée en appelant la méthode init () . La méthode init () prend deux paramètres:
- Mode - Cryptage / décryptage
- Clé
Le premier paramètre indique le mode de fonctionnement de l'instance de chiffrement: pour chiffrer ou déchiffrer des données. Le deuxième paramètre indique quelle clé ils utilisent pour crypter ou décrypter les données.
Un exemple:
byte[] keyBytes = new byte[]{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}; String algorithm = "RawBytes"; SecretKeySpec key = new SecretKeySpec(keyBytes, algorithm); cipher.init(Cipher.ENCRYPT_MODE, key);
Veuillez noter que la méthode de création de clé dans cet exemple n'est pas sûre et ne doit pas être utilisée dans la pratique. Cet article dans les sections suivantes explique comment créer des clés de manière plus sécurisée.
Pour initialiser une instance de chiffrement pour déchiffrer des données, vous devez utiliser Cipher.DECRYPT_MODE, par exemple:
cipher.init(Cipher.DECRYPT_MODE, key);
Cryptage ou décryptage des données
Après avoir initialisé le chiffrement, vous pouvez commencer à chiffrer ou déchiffrer les données en appelant les méthodes update () ou doFinal () . La méthode update () est utilisée si vous chiffrez ou déchiffrez une donnée. La méthode doFinal () est appelée lorsque vous chiffrez la dernière donnée ou si le bloc de données que vous passez à doFinal () est un ensemble unique de données pour le chiffrement.
Un exemple de chiffrement des données à l'aide de la méthode doFinal () :
byte[] plainText = "abcdefghijklmnopqrstuvwxyz".getBytes("UTF-8"); byte[] cipherText = cipher.doFinal(plainText);
Pour décrypter les données, vous devez passer le texte chiffré (données) à la méthode doFinal () ou doUpdate () .
Clés
Pour crypter ou décrypter des données, vous avez besoin d'une clé. Il existe deux types de clés, selon le type d'algorithme de chiffrement utilisé:
- Clés symétriques
- Clés asymétriques
Les clés symétriques sont utilisées pour les algorithmes de chiffrement symétriques. Les algorithmes de chiffrement symétrique utilisent la même clé pour le chiffrement et le déchiffrement.
Les clés asymétriques sont utilisées pour les algorithmes de chiffrement asymétriques. Les algorithmes de chiffrement asymétriques utilisent une clé pour le chiffrement et une autre pour le déchiffrement. Les algorithmes de chiffrement à clé publique et privée sont des exemples d'algorithmes de chiffrement asymétriques.
D'une manière ou d'une autre, la partie qui doit déchiffrer les données doit connaître la clé nécessaire pour déchiffrer les données. Si le déchiffreur n'est pas partie au chiffrement des données, les deux parties doivent se mettre d'accord sur une clé ou échanger une clé. C'est ce qu'on appelle l'échange de clés.
Sécurité des clés
Les clés doivent être difficiles à deviner afin qu'un attaquant ne puisse pas facilement récupérer une clé de chiffrement. Dans l'exemple de la section précédente sur la classe Cipher, une clé très simple et codée en dur a été utilisée. En pratique, cela ne vaut pas la peine. Si la clé des parties est facile à deviner, il sera facile pour un attaquant de déchiffrer les données chiffrées et, éventuellement, de créer lui-même de faux messages. Il est important de créer une clé difficile à deviner. Ainsi, la clé doit être constituée d'octets aléatoires. Plus il y a d'octets aléatoires, plus il est difficile de deviner, car il y a plus de combinaisons possibles.
Génération de clés
Pour générer des clés de chiffrement aléatoires, vous pouvez utiliser la classe Java KeyGenerator. KeyGenerator sera décrit plus en détail dans les chapitres suivants, voici un petit exemple de son utilisation ici:
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES"); SecureRandom secureRandom = new SecureRandom(); int keyBitSize = 256; keyGenerator.init(keyBitSize, secureRandom); SecretKey secretKey = keyGenerator.generateKey();
L'instance SecretKey résultante peut être transmise à la méthode Cipher.init () , par exemple comme ceci:
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
Génération de paires de clés
Les algorithmes de chiffrement asymétriques utilisent une paire de clés composée d'une clé publique et d'une clé privée pour chiffrer et déchiffrer les données. Pour créer une paire de clés asymétriques, vous pouvez utiliser KeyPairGenerator (java.security.KeyPairGenerator). KeyPairGenerator sera décrit plus en détail dans les chapitres suivants, ci-dessous est un exemple simple d'utilisation de Java KeyPairGenerator:
SecureRandom secureRandom = new SecureRandom(); KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("DSA"); KeyPair keyPair = keyPairGenerator.generateKeyPair();
Magasin de clés
Java KeyStore est une base de données qui peut contenir des clés. Java KeyStore est représenté par la classe KeyStore (java.security.KeyStore). Un magasin de clés peut contenir des clés des types suivants:
- Clés privées
- Clés publiques et certificats (Clés publiques + certificats)
- Clés secrètes
Les clés privées et publiques sont utilisées dans le chiffrement asymétrique. La clé publique peut avoir un certificat associé. Un certificat est un document prouvant l'identité d'une personne, d'une organisation ou d'un appareil prétendant posséder une clé publique.
Le certificat est généralement signé numériquement par la partie utilisatrice comme preuve.
Les clés privées sont utilisées dans le chiffrement symétrique. La classe KeyStore est assez complexe, c'est pourquoi elle est décrite plus en détail plus loin dans un chapitre séparé sur Java KeyStore.
Java Keytool est un outil en ligne de commande qui peut fonctionner avec des fichiers Java KeyStore. Keytool peut générer des paires de clés dans un fichier KeyStore, exporter des certificats et importer des certificats dans KeyStore et certaines autres fonctions. Keytool est livré avec une installation Java. Keytool est décrit plus en détail plus loin dans un chapitre séparé sur Java Keytool.
Résumé des messages
Lorsque vous recevez des données cryptées de l'autre côté, pouvez-vous être sûr que personne n'a modifié les données cryptées en route vers vous?
En règle générale, la solution consiste à calculer le résumé de message à partir des données avant de le chiffrer, puis à chiffrer à la fois les données et le résumé de message, et à l'envoyer sur le réseau. Un résumé de message est une valeur de hachage calculée sur la base des données de message. Si au moins un octet est modifié dans les données chiffrées, le résumé de message calculé à partir des données changera également.
Lorsque vous recevez des données chiffrées, vous les déchiffrez, calculez le résumé de message à partir de celles-ci et comparez le résumé de message calculé avec le résumé du message envoyé avec les données chiffrées. Si les deux résumés de messages sont identiques, il y a une forte probabilité (mais pas 100%) que les données n'aient pas été modifiées.
Java MessageDigest (java.security.MessageDigest) peut être utilisé pour calculer les résumés de messages. Pour créer une instance de MessageDigest, la méthode MessageDigest.getInstance () est appelée. Il existe plusieurs algorithmes différents de résumé des messages. Vous devez spécifier l'algorithme que vous souhaitez utiliser lors de la création de l'instance MessageDigest. L'utilisation de MessageDigest sera décrite plus en détail dans le chapitre Java MessageDigest.
Une brève introduction à la classe MessageDigest:
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
Cet exemple crée une instance de MessageDigest qui utilise l'algorithme de hachage cryptographique interne SHA-256 pour calculer les résumés de messages.
Pour calculer le résumé de message de certaines données, vous appelez la méthode update () ou digest () . La méthode update () peut être appelée plusieurs fois et le résumé du message est mis à jour à l'intérieur de l'objet. Lorsque vous avez transmis toutes les données que vous souhaitez inclure dans le résumé de message, vous appelez digest () et récupérez le résumé du résumé de message.
Un exemple d'appel multiple de update () , suivi d'un appel Ă digest () :
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); byte[] data1 = "0123456789".getBytes("UTF-8"); byte[] data2 = "abcdefghijklmnopqrstuvxyz".getBytes("UTF-8"); messageDigest.update(data1); messageDigest.update(data2); byte[] digest = messageDigest.digest();
Vous pouvez également appeler digest () une fois, en transmettant toutes les données pour calculer le résumé du message. Un exemple:
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); byte[] data1 = "0123456789".getBytes("UTF-8"); byte[] digest = messageDigest.digest(data1);
Code d'authentification de message (MAC)
La classe Java Mac est utilisée pour créer un MAC (Message Authentication Code) à partir d'un message. Le MAC est similaire à un résumé de message, mais utilise une clé supplémentaire pour crypter le résumé de message. N'ayant que les données source et la clé, vous pouvez vérifier le MAC. Ainsi, un MAC est un moyen plus sûr de protéger un bloc de données contre la modification qu'un résumé de message. La classe Mac est décrite plus en détail dans le chapitre Java Mac, suivie d'une brève introduction.
Une instance Java Mac est créée en appelant la méthode Mac.getInstance () , en passant le nom de l'algorithme à utiliser comme paramètre. Voici à quoi ça ressemble:
Mac mac = Mac.getInstance("HmacSHA256");
Avant de créer un MAC à partir de données, vous devez initialiser l'instance Mac avec la clé. Voici un exemple d'initialisation d'une instance Mac avec une clé:
byte[] keyBytes = new byte[]{0,1,2,3,4,5,6,7,8 ,9,10,11,12,13,14,15}; String algorithm = "RawBytes"; SecretKeySpec key = new SecretKeySpec(keyBytes, algorithm); mac.init(key);
Après avoir initialisé l'instance Mac, vous pouvez calculer le MAC à partir des données en appelant les méthodes update () et doFinal () . Si vous disposez de toutes les données pour calculer le MAC, vous pouvez immédiatement appeler la méthode doFinal () . Voici à quoi ça ressemble:
byte[] data = "abcdefghijklmnopqrstuvxyz".getBytes("UTF-8"); byte[] data2 = "0123456789".getBytes("UTF-8"); mac.update(data); mac.update(data2); byte[] macBytes = mac.doFinal();
Signature
La classe Signature (java.security.Signature) est utilisée pour signer numériquement les données. Lorsque des données sont signées, une signature numérique est créée à partir de ces données. Ainsi, la signature est séparée des données.
Une signature numérique est créée en créant un résumé de message (hachage) à partir des données et en chiffrant ce résumé de message avec la clé privée de l'appareil, de la personne ou de l'organisation qui doit signer les données. Le résumé du message crypté est appelé signature numérique.
Pour créer une instance de Signature, la méthode Signature.getInstance (...) est appelée:
Signature signature = Signature.getInstance("SHA256WithDSA");
Signature des données
Pour signer des données, vous devez initialiser l'instance de signature en mode signature en appelant la méthode initSign (...), en passant la clé privée pour signer les données. Un exemple d'initialisation d'une instance de signature en mode signature:
signature.initSign(keyPair.getPrivate(), secureRandom);
Après avoir initialisé l'instance de signature, elle peut être utilisée pour signer les données. Cela se fait en appelant la méthode update (), en passant les données de signature en tant que paramètre. Vous pouvez appeler la méthode update () plusieurs fois pour compléter les données de création de la signature. Une fois toutes les données transmises à la méthode update (), la méthode sign () est appelée pour obtenir une signature numérique. Voici à quoi ça ressemble:
byte[] data = "abcdefghijklmnopqrstuvxyz".getBytes("UTF-8"); signature.update(data); byte[] digitalSignature = signature.sign();
Vérification de signature
Pour vérifier la signature, vous devez initialiser l'instance de signature en mode vérification en appelant la méthode initVerify (...) , en transmettant la clé publique en tant que paramètre, qui est utilisée pour vérifier la signature. Un exemple d'initialisation d'une instance de signature en mode de vérification ressemble à ceci:
Signature signature = Signature.getInstance("SHA256WithDSA"); signature.initVerify(keyPair.getPublic());
Après l'initialisation en mode vérification, les données signées sont transmises à la méthode update () . Un appel à la méthode verify () renvoie vrai ou faux selon que la signature peut être vérifiée ou non. Voici la vérification de signature:
byte[] data2 = "abcdefghijklmnopqrstuvxyz".getBytes("UTF-8"); signature2.update(data2); boolean verified = signature2.verify(digitalSignature);
Exemple complet de signature et de vérification
SecureRandom secureRandom = new SecureRandom(); KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("DSA"); KeyPair keyPair = keyPairGenerator.generateKeyPair(); Signature signature = Signature.getInstance("SHA256WithDSA"); signature.initSign(keyPair.getPrivate(), secureRandom); byte[] data = "abcdefghijklmnopqrstuvxyz".getBytes("UTF-8"); signature.update(data); byte[] digitalSignature = signature.sign(); Signature signature2 = Signature.getInstance("SHA256WithDSA"); signature2.initVerify(keyPair.getPublic()); signature2.update(data); boolean verified = signature2.verify(digitalSignature); System.out.println("verified = " + verified);