IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Développons en Java v 2.20   Copyright (C) 1999-2021 Jean-Michel DOUDOUX.   
[ Précédent ] [ Sommaire ] [ Suivant ] [ Télécharger ]      [ Accueil ] [ Commentez ]


 

25. JCE (Java Cryptography Extension)

 

chapitre 2 5

 

Niveau : niveau 4 Supérieur 

 

L'API JCE (Java Cryptography Extension) est une extension de JCA qui lui ajoute des API pour l'encryptage et le décryptage, la génération de clés et l'authentification de messages avec des algorithmes de type MAC.

A l'origine, JCE a été développée comme une extension du JDK 1.2. Avant Java 1.4, JCE était diffusée de manière séparée. JCE est intégré à Java SE à partir de la version 1.4.

Depuis que le JCE est fourni avec le JDK, les deux API peuvent sembler moins distinctes d'autant que JCE s'appuie sur JCA. JCE repose sur un principe de conception senblable à celui de JCA : sa mise en oeuvre requiert l'utilisation d'une implémentation d'un fournisseur.

L'implémentation de Sun/Oracle fournie par défaut avec Java est nommée «SunJCE».

Le framework JCE (Java Cryptography Extension) sert de base pour l'implémentation de certaines fonctionnalités notamment :

  • des algorithmes de chiffrement/déchiffrement symétrique et asymétrique travaillant par flux ou par blocs
  • la génération de clés,
  • des algorithmes de type MAC (Message Authentification Code).
  • la gestion de dépôts de clés
  • les objets scellés
  • les signatures digitales
  • Password Based Encryption (PBE)

Les classes de l'API JCE sont contenues dans le package javax.crypto qui contient plusieurs classes et interfaces :

Classe/Interface

Rôle

SecretKey

Interface qui définit les fonctionnalités d'une clé secrète

Cipher

Proposer des fonctionnalités de chiffrement/déchiffrement

CipherInputStream

Implémenter le concept de flux sécurisé qui combine un objet de type InputStream et un objet de type Cipher pour décrypter des données en les lisant

CipherOutputStream

Implémenter le concept de flux sécurisé qui combine un objet de type OutputStream et un objet de type Cipher pour crypter des données en les écrivant

EncryptedPrivateKeyInfo

Implémenter le type correspondant définit dans PKCS #8.

KeyAgreement

Implémenter des fonctionnalités d'échanges de clés (key agreement)

KeyGenerator

Générer des clés secrètes pour algorithmes symétriques

Mac

Proposer des fonctionnalités de type "Message Authentication Code" (MAC)

SealedObject

Encapsuler de manière cryptée le résultat de la sérialisation d'un objet

SecretKeyFactory

C'est une fabrique qui permet de convertir des clés opaques (instance de type java.security.Key) en clés transparentes (de type KeySpec) et vice versa


L'API JCE doit être implémentée par des fournisseurs (Cryptographic Service Providers). Chaque fournisseur doit implémenter le SPI (Service Provider Interface) qui définit les fonctionnalités à proposer. L'architecture de JCE permet d'enregistrer des implémentations en vu de leur utilisation.

Le JDK 7.0 est fourni avec plusieurs implémentations de différents fournisseurs essentiellement pour des raisons historiques et par type de services proposés :

  • Sun fournie à partir de Java 1.1
  • SunRsaSign fournie à partir de Java 1.3
  • SunJSSE fournie à partir de Java 1.4
  • SunJCE fournie à partir Java 5
  • SunPKCS11 fournie à partir Java 5
  • SunMSCAPI fournie à partir de Java 6
  • SunPCSC fournie à partir de Java 6
  • SunEC fournie à partir de Java 7
  • SunSASL

En raison de la législation de certains pays, quelques algorithmes ne disposent que d'une implémentation limitée.

La première implémentation nommée « Sun » fournie avec le JDK propose différentes fonctionnalités :

Fonctionnalités

Implémentation

Fabrique de certificats

X.509

Dépôt de clés

JKS

Message Digest

MD2, MD5, SHA-1, SHA-256, SHA-384, SHA-512

(SecureRandom)

SHA1PRNG

Signature

NONEwithDSA, SHA1withDSA


L'implémentation « SunJCE » fournie avec le JDK propose différentes fonctionnalités :

Fonctionnalités

Implémentation

Algorithme de chiffrement symétrique

DES (56 bits), AES, RC2, RC4 and RC5, IDEA, Triple DES (112 bits), Blowfish (56 bits), PBEWithMD5AndDES, PBEWithHmacSHA1AndDESede, DES ede

Mode d'encryption

Electronic Code Book (ECB), Cipher Block Chaining (CBC), Cipher Feedback (CFB), Output Feedback (OFB), et Propagating Cipher Block Chaining (PCBC)

Algorithme de chiffrement asymétrique

RSA

Algorithme d'échange de clés

Diffie-Hellman (1024 bits)

Mac

HmacMD5, HmacSHA1, HmacSHA256, HmacSHA384, HmacSHA512

Dépôt de clés (KeyStore)

JCEKS


Ce chapitre contient plusieurs sections :

 

25.1. La classe javax.crypto.KeyGenerator

La classe javax.crypto.KeyGenerator, fournie à partir de Java 1.4, permet de générer des clés utilisables par des algorithmes symétriques.

Pour obtenir une instance de type KeyGenerator, il faut invoquer la méthode getInstance() qui est une fabrique attendant en paramètre le nom de l'algorithme. Deux autres surcharges, permettent aussi de préciser le fournisseur de l'algorithme à utiliser.

  • static KeyGenerator getInstance(String algorithm)
  • static KeyGenerator getInstance(String algorithm, String provider)
  • static KeyGenerator getInstance(String algorithm, Provider provider)

La clé générée est utilisable par l'algorithme dont le nom est fourni en paramètre. Les noms des algorithmes fournis en standard peuvent être : AES, Blowfish, DES, DESede, HmacMD5 et HmacSHA1

Avant d'obtenir une clé, l'instance de type KeyGenerator doit être initialisée en invoquant la méthode init().

La clé peut être générée de deux manières dont la principale différence est l'initialisation de l'objet :

  • de manière indépendante de l'algorithme
  • de manière dépendante et spécifique à l'algorithme

Quelque soit l'algorithme cible, ils ont toujours au moins deux paramètres :

  • la taille de la clé (keysize)
  • une source pour la génération de nombres aléatoires sous la forme d'un objet de type SecureRandom

Plusieurs surcharges de la méthode init() permettent de fournir l'un, l'autre ou ces deux paramètres.

  • public void init(int keysize);
  • public void init(SecureRandom random);
  • public void init(int keysize, SecureRandom random);

Si l'algorithme nécessite d'autres paramètres, il faut utiliser deux autres surcharges qui attendent un paramètre de type AlgorithmParameterSpec

  • public void init(AlgorithmParameterSpec params);
  • public void init(AlgorithmParameterSpec params, SecureRandom random);

Si l'instance de type KeyGenerator n'est pas initialisée, chaque implémentation des fournisseurs doit obligatoirement offrir une initialisation par défaut.

Une fois initialisée, il est possible d'invoquer la méthode generateKey() qui permet de demander la génération d'une clé en utilisant l'algorithme configuré :

  • public SecretKey generateKey();

La première surcharge permet de définir la taille de la clé : cette taille est dépendante de l'algorithme. Chaque implémentation de la plate-forme Java doit obligatoirement fournir une implémentation pour les algorithmes suivants :

  • AES taille de la clé 128
  • DES taille de la clé 56
  • DESede taille de la clé 168
  • HmacSHA1
  • HmacSHA256
Exemple ( code Java 1.4 ) :
package com.jmdoudoux.test.securite;

import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;

public class TestKeyGeneratorDESede {

  public static void main(String[] args) {

    KeyGenerator keyGen;
    try {
      keyGen = KeyGenerator.getInstance("DESede");
      keyGen.init(168);
      SecretKey cle = keyGen.generateKey();
      System.out.println("cle (" + cle.getAlgorithm() + "," + cle.getFormat()
          + ") : " + new String(cle.getEncoded()));
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}

Si la taille fournie n'est pas supportée par l'implémentation de l'algorithme alors une exception de type InvalidParameterException est levée.

Résultat :
java.security.InvalidParameterException: Wrong keysize: must be equal to 56
        at com.sun.crypto.provider.DESKeyGenerator.engineInit(DESKeyGenerator.java:90)
        at javax.crypto.KeyGenerator.init(KeyGenerator.java:501)
        at com.jmdoudoux.test.securite.TestKeyGeneratorDES.main(TestKeyGeneratorDES.java:15)

La seconde surcharge attend une instance de type SecureRandom qui permet de fournir la source pour la génération de nombres aléatoires.

Exemple ( code Java 1.4 ) :
package com.jmdoudoux.test.securite;

import java.security.SecureRandom;

import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;

public class TestKeyGeneratorDES {

  public static void main(String[] args) {

    KeyGenerator keyGen;
    try {
      keyGen = KeyGenerator.getInstance("DES");
      keyGen.init(new SecureRandom());
      SecretKey cle = keyGen.generateKey();
      System.out.println("cle (" + cle.getAlgorithm() + "," + cle.getFormat()
          + ") : " + new String(cle.getEncoded()));
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}

La troisième surcharge attend en paramètre la taille de la clé et une instance de type SecureRandom.

Exemple ( code Java 1.4 ) :
package com.jmdoudoux.test.securite;

import java.security.SecureRandom;

import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;

public class TestKeyGeneratorDES {

  public static void main(String[] args) {

    KeyGenerator keyGen;
    try {
      keyGen = KeyGenerator.getInstance("DES");
      keyGen.init(56, new SecureRandom());
      SecretKey cle = keyGen.generateKey();
      System.out.println("cle (" + cle.getAlgorithm() + "," + cle.getFormat()
          + ") : " + new String(cle.getEncoded()));
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}

L'initialisation du générateur de manière indépendante de l'algorithme utilise une des deux surcharges de la méthode init() qui attend en paramètre un objet de type AlgorithmParameterSpec.

Si le KeyGenerator n'est pas initialisé, le fournisseur de l'algorithme utilisé doit proposer des valeurs par défaut pour chaque paramètre.

Exemple ( code Java 1.4 ) :
package com.jmdoudoux.test.securite;

import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;

public class TestKeyGeneratorDES {

  public static void main(String[] args) {

    KeyGenerator keyGen;
    try {
      keyGen = KeyGenerator.getInstance("DES");
      SecretKey cle = keyGen.generateKey();
      System.out.println("cle (" + cle.getAlgorithm() + "," + cle.getFormat()
          + ") : " + new String(cle.getEncoded()));
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}

La classe KeyGenerator est utilisable pour les différents algorithmes proposés par les implémentations du ou des fournisseurs enregistrées.

Exemple ( code Java 1.4 ) :
package com.jmdoudoux.test.securite;

import java.security.NoSuchAlgorithmException;

import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;

public class TestKeyGenerator {

  public static void main(String[] args) {
    KeyGenerator keyGen;
    try {
      System.out.println("Generation d'une cle pour DES");
      keyGen = KeyGenerator.getInstance("DES");
      SecretKey key = keyGen.generateKey();
      System.out
          .println("cle=" + ConvertionHelper.bytesToHex(key.getEncoded()));

      System.out.println("Generation d'une cle pour Blowfish");
      keyGen = KeyGenerator.getInstance("Blowfish");
      key = keyGen.generateKey();
      System.out
          .println("cle=" + ConvertionHelper.bytesToHex(key.getEncoded()));

      System.out.println("Generation d'une cle pour Triple DES");
      keyGen = KeyGenerator.getInstance("DESede");
      key = keyGen.generateKey();
      System.out
          .println("cle=" + ConvertionHelper.bytesToHex(key.getEncoded()));

      System.out.println("Generation d'une cle pour AES");
      keyGen = KeyGenerator.getInstance("AES");
      key = keyGen.generateKey();
      System.out
          .println("cle=" + ConvertionHelper.bytesToHex(key.getEncoded()));

    } catch (NoSuchAlgorithmException e) {
      e.printStackTrace();
    }
  }
}

Résultat :
Generation d'une cle pour DES
cle=FE3731A82052A27A
Generation d'une cle pour Blowfish
cle=83F75B5BBB336632C1A27E9A7211966D
Generation d'une cle pour Triple DES
cle=E6169E9249B0F746801AADE9D6AD85BFCBE05E4AC19EC716
Generation d'une cle pour AES
cle=B71822FA122A3EA57281B2EB9517D518

 

25.2. La classe javax.crypto.SecretKeyFactory

La classe javax.crypto.SecretKeyFactory, ajoutée à Java 1.4, est une fabrique qui permet de convertir des clés opaques (instance de type java.security.Key) en clés transparentes (de type KeySpec) et vice versa.

Une clé opaque (java.security.Key et ses classes filles java.security.PublicKey, java.security.PrivateKey et javax.crypto.SecretKey) ne permet pas de connaitre son implémentation.

A la différence de la classe KeyFactory qui est utilisable avec des paires de clés publiques et privées, la classe SecretFactory n'est utilisable qu'avec des clés privées pour algorithmes symétriques.

Il est nécessaire de consulter la documentation du fournisseur de l'implémentation pour connaître les clés transparentes supportées par les méthodes generateSecret() et getKeySpec(). Par exemple, l'implémentation SunJCE propose un support de la classe DESKeySpec pour les clés de l'algorithme DES et DESedeKeySpec pour les clés de l'algorithme Triple DES.

Chaque implémentation du JDK doit obligatoirement fournir un support des algorithmes DES et DESede par la SecretKeyFactory.

La méthode static getInstance() permet d'obtenir une instance de type SecretFactory.

La méthode generateSecret() permet d'obtenir une clé opaque correspondant à la clé transparente fournie en paramètre :

  • SecretKey generateSecret(KeySpec keySpec)

La méthode getKeySpec() permet d'obtenir une clé transparente à partir de la clé opaque fournie en paramètre.

  • KeySpec getKeySpec(Key key, Class keySpec)

Le paramètre keySpec permet d'indiquer le type de la classe de retour, par exemple la classe DESKeySpec.

Exemple :
package com.jmdoudoux.test.securite;

import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;

import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESKeySpec;

public class TestSecretKeyFactory {

  public static void main(String[] args) {
    byte[] desKeyData = { (byte) 0x04, (byte) 0x01, (byte) 0x07, (byte) 0x04,
        (byte) 0x02, (byte) 0x08, (byte) 0x02, (byte) 0x01 };
    DESKeySpec desKeySpec;
    try {
      desKeySpec = new DESKeySpec(desKeyData);
      SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES");
      SecretKey secretKey = keyFactory.generateSecret(desKeySpec);
      System.out.println("cle secrete : "
          + ConvertionHelper.bytesToHex(secretKey.getEncoded()));
      System.out.println("algorithme  : " + secretKey.getAlgorithm());
      System.out.println("format      : " + secretKey.getFormat());
    } catch (InvalidKeyException e) {
      e.printStackTrace();
    } catch (NoSuchAlgorithmException e) {
      e.printStackTrace();
    } catch (InvalidKeySpecException e) {
      e.printStackTrace();
    }
  }
}

Résultat :
cle secrete : 0401070402080201
algorithme  : DES
format      : RAW

La classe SecretFactory renvoie des implémentations dépendantes du fournisseur utilisé.

 

25.3. La classe javax.crypto.Cipher

La classe Cipher permet d'utiliser des fonctionnalités de chiffrement et de déchiffrement de données selon un algorithme donné parmi ceux proposés par les fournisseurs. Le chiffrement utilise une clé pour transformer des données en clair en données chiffrées. Le déchiffrement est l'opération inverse.

Elle possède plusieurs méthodes :

Méthode

Rôle

getInstance()

Renvoyer une instance de l'objet pour un algorithme particulier dont l'implémentation est celle fournie par le fournisseur précisé

init()

Initialiser la classe pour le mode de fonctionnement précisé (Cipher.ENCRYPT_MODE et Cipher.DECRYPT_MODE)

update()

Ajouter des données partielles à traiter par la classe

doFinal()

Ajouter la dernière partie des données à traiter et générer le résultat

getBlockSize()

Retourner la taille du bloc utilisé par la classe pour ses traitements

getAlgorithm()

Retourner l'algorithme utilisé par la classe pour ses traitements

getProvider()

Retourner le fournisseur de l'algorithme utilisé par la classe pour ses traitements

 

25.3.1. La création d'une instance de type Cipher

Pour créer une instance de la classe javax.crypto.Cipher, il faut invoquer sa méthode getInstance() qui est une fabrique qui possède trois surcharges : les trois attendent en paramètre le nom de la transformation à utiliser. Les deux dernières attendent aussi le fournisseur. La première va tenter de trouver une implémentation dans l'ordre de préférence d'enregistrement.

  • public static Cipher getInstance(String transformation);
  • public static Cipher getInstance(String transformation, String provider);
  • public static Cipher getInstance(String transformation, Provider provider);

Quelque soit la version surchargée de la méthode getInstance() invoquée, il faut lui passer en paramètre la transformation à utiliser. La transformation est une chaîne de caractères qui décrit une ou plusieurs opérations à exécuter pour chiffrer ou déchiffrer les données. Le nom de la transformation peut prendre plusieurs formes :

  • Algorithme_de_chiffrement
  • Algorithme_de_chiffrement/mode/padding_scheme

Exemple :

"DES"

"DES/CBC/PKCS5Padding"

Exemple ( code Java 1.4 ) :
Cipher cipher = Cipher.getInstance("DES/ECB/PKCS5Padding");

L'exemple ci-dessus demande l'utilisation de l'algorithme DES, en utilisant le mode ECB (Electronic CodeBook) et le style de padding PKCS-5.

Plusieurs algorithmes sont utilisables avec les implémentations fournies dans le JDK 7 de Sun/Oracle : AES, AESWrap, ARCFOUR, Blowfish, CCM, DES, DESede, DESedeWrap, ECIES, GCM, PBEWith<digest>And<encryption>, RC2, RC4, RC5, RSA.

Si la transformation contient simplement un nom, JCE va tenter de trouver une implémentation correspondant au nom et prendre celle qui est préférée dans la configuration.

Le mode permet de préciser comment les données vont être chiffrées. Il existe différents modes :

  • CBC : Cipher Block Chaining (défini dans le FIPS PUB 81)
  • CFB, CFBn : Cipher FeedBack (défini dans le FIPS PUB 81). n est la taille optionnelle en bits du bloc
  • CTR : version simplifiée du mode OFB
  • CTS : Cipher Text Stealing
  • ECB : Electronic CookBook (défini dans le FIPS PUB 81)
  • NONE : aucun mode
  • OFB, OFBn : Output FeedBack (défini dans FIPS PUB 81). n est la taille optionnelle en bits du bloc
  • PCBC : Propagating Cipher Block Chaining (défini dans Kerberos V4)

Lorsque la transformation implique une utilisation par bloc (par exemple avec le mode CFB ou OFB), il est possible de préciser la taille du bloc en accolant le nombre de bits au mode.

Exemple :

DES/CFB8/NoPadding

DES/OFB32/PKCS5Padding

Si la taille du bloc n'est pas précisée alors la taille par défaut définie par le fournisseur est utilisée (par exemple avec SunJCE, la taille par défaut est de 64 bits).

Le padding permet de préciser comment sera rempli le dernier bloc de données à chiffrer. Plusieurs styles de padding existent :

  • NoPadding : pas de padding
  • ISO10126Padding : défini par le W3C dans le document "XML Encryption Syntax and Processing"
  • OAEPPadding
  • OAEPWith<digest>And<mgf>Padding : Optimal Asymmetric Encryption Padding avec digest qui est le nom de l'algorithme de type message digest
  • PKCS1Padding : PKCS#1
  • PKCS5Padding : PKCS#5 défini par les laboratoires RSA en 1993
  • SSL3Padding : défini par le protocole SSL version 3.0
Exemple ( code Java 1.4 ) :
Cipher cipher = Cipher.getInstance("DES");
Cipher cipher = Cipher.getInstance("DES/CBC/PKCS5Padding");
Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");

Si aucun mode ni padding ne sont précisés dans la transformation, alors ce sont les valeurs par défaut du fournisseur qui sont utilisées. Par exemple, le fournisseur SunJCE utilise le mode ECB par défaut et le padding PKCS5Padding par défaut pour les algorithmes DES, DESede et Blowfish. Ainsi avec le fournisseur SunJCE, les deux exemples ci-dessous sont identiques :

Exemple ( code Java 1.4 ) :
Cipher cipher1 = Cipher.getInstance("DES/ECB/PKCS5Padding");
Cipher cipher2 = Cipher.getInstance("DES");

 

25.3.2. L'initialisation de l'instance de type Cipher

L'instance retournée par la fabrique n'est pas directement utilisable sans être initialisée en invoquant la méthode init(). Cette méthode possède de nombreuses surcharges utilisables selon les besoins de l'algorithme et permettant de fournir les informations pour initialiser l'instance :

Plusieurs surcharges de la méthode init() existent :

  • public void init(int opmode, Key key)
  • public void init(int opmode, Certificate certificate)
  • public void init(int opmode, Key key, SecureRandom random)
  • public void init(int opmode, Certificate certificate, SecureRandom random)
  • public void init(int opmode, Key key, AlgorithmParameterSpec params)
  • public void init(int opmode, Key key, AlgorithmParameterSpec params, SecureRandom random);
  • public void init(int opmode, Key key, AlgorithmParameters params)
  • public void init(int opmode, Key key, AlgorithmParameters params, SecureRandom random)

Le premier paramètre (opmode) est toujours une valeur entière qui précise le mode d'utilisation de l'instance de la classe Cipher. Cette dernière définit quatre constantes pour spécifier le mode d'utilisation :

  • ENCRYPT_MODE : chiffrer des données
  • DECRYPT_MODE : déchiffrer des données
  • WRAP_MODE : chiffrer une clé pour permettre son échange de manière sécurisée
  • UNWRAP_MODE : déchiffrer une clé reçue pour obtenir une instance de type java.security.Key

Les autres paramètres (Key, Certificate, AlgorithmParameters, AlgorithmParametersSpec et SecureRandom) permettent de fournir des informations pour initialiser l'instance.

Les implémentations de la classe Cipher doivent proposer des valeurs par défaut pour certains paramètres d'initialisation s'ils ne sont pas explicitement fournis.

Si un objet de type Cipher initialisé pour le chiffrement a besoin de paramètres et qu'aucun d'eux n'est fournis par la méthode init() invoquée alors l'implémentation de la classe Cipher doit fournir des valeurs par défaut libres ou générées aléatoirement.

Si un objet de type Cipher initialisé pour le déchiffrement a besoin de paramètres et qu'aucun d'eux n'est fournis par la méthode init() invoquée alors une exception de type InvalidKeyException ou InvalidAlgorithmParameterException est levée selon l'implémentation.

Certaines valeurs d'initialisation doivent être obligatoirement fournies et cohérentes sous peine qu'une exception de type InvalidKeyException ou InvalidAlgorithmParameterException soit levée.

Les mêmes paramètres d'initialisation que ceux utilisés pour le chiffrement doivent être fournis à l'objet Cipher pour décrypter des données.

L'initialisation d'une instance de type Cipher réinitialise toute son éventuelle configuration.

 

25.3.3. Le chiffrement et le déchiffrement de données

L'encryptage et le décryptage de données peuvent se faire en une seule opération ou plusieurs opérations : les données peuvent être traitées par une instance de type Cipher en une ou plusieurs fois. Il est par exemple utile de fractionner le traitement des données si l'on ne connaît pas le volume à traiter ou que ce volume est trop important pour tenir en mémoire.

Pour chiffrer ou déchiffrer des données en une seule fois, il faut utiliser une des surcharges de la méthode doFinal() :

  • public byte[] doFinal(byte[] input);
  • public byte[] doFinal(byte[] input, int inputOffset, int inputLen);
  • public int doFinal(byte[] input, int inputOffset, int inputLen, byte[] output);
  • public int doFinal(byte[] input, int inputOffset, int inputLen, byte[] output, int outputOffset)

Pour chiffrer ou déchiffrer des données en plusieurs fois, il faut utiliser une des surcharges de la méthode update() pour fournir les données :

  • public byte[] update(byte[] input);
  • public byte[] update(byte[] input, int inputOffset, int inputLen);
  • public int update(byte[] input, int inputOffset, int inputLen, byte[] output);
  • public int update(byte[] input, int inputOffset, int inputLen, byte[] output, int outputOffset)

Une fois toutes les données traitées par des appels multiples à la méthode update(), il faut utiliser une des surcharges de la méthode doFinal() pour finaliser le traitement et obtenir le résultat de l'opération :

  • public byte[] doFinal();
  • public int doFinal(byte[] output, int outputOffset);

La seconde surcharge permet de fournir les dernières données si nécessaire.

La méthode doFinal() prend en compte le padding si nécessaire.

Une invocation de la méthode doFinal() réinitialise l'état de l'instance de type Cipher comme il l'était après l'invocation de la méthode init(). L'instance de type Cipher peut alors être de nouveau utilisée pour chiffrer ou déchiffrer des données avec la même configuration.

Exemple ( code Java 1.4 ) :
package com.jmdoudoux.test.securite;

import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;

public class TestCipherDESede {

  public static void main(String[] args) {

    final String message = "Mon message a traiter";

    KeyGenerator keyGen;
    try {
      keyGen = KeyGenerator.getInstance("DESede");
      keyGen.init(168);
      SecretKey cle = keyGen.generateKey();
      System.out.println("cle : " + new String(cle.getEncoded()));

      byte[] enc = encrypter(message, cle);
      System.out.println("texte encrypte : " + new String(enc));

      String dec = decrypter(enc, cle);
      System.out.println("texte decrypte : " + dec);

    } catch (Exception e) {
      e.printStackTrace();
    }
  }

  public static byte[] encrypter(final String message, SecretKey cle)
      throws NoSuchAlgorithmException, NoSuchPaddingException,
      InvalidKeyException, IllegalBlockSizeException, BadPaddingException {
    Cipher cipher = Cipher.getInstance("DESede");
    cipher.init(Cipher.ENCRYPT_MODE, cle);
    byte[] donnees = message.getBytes();

    return cipher.doFinal(donnees);
  }

  public static String decrypter(final byte[] donnees, SecretKey cle)
      throws NoSuchAlgorithmException, NoSuchPaddingException,
      InvalidKeyException, IllegalBlockSizeException, BadPaddingException {
    Cipher cipher = Cipher.getInstance("DESede");
    cipher.init(Cipher.DECRYPT_MODE, cle);

    return new String(cipher.doFinal(donnees));
  }

}

Résultat :
cle : ›_QþÐ)_ãÖ_Úh†...Jý@ÇÈkµûÚJ
texte encrypte : texte encrypte : ³Ù_ó"ú‰f0Z&õ¯?¤_yö×_Ù¿ÛÂ
texte decrypte : Mon message a traiter

La méthode getOutputSize(int taille) retourne la taille des données encryptées quand on lui fournit en paramètre celle des données non chiffrées. L'utilisation de cette méthode est pratique pour permettre de définir la taille du buffer qui contiendra les données chiffrées ou déchiffrées.

 

25.3.4. Les modes wrap et unwrap

La classe Cipher permet d'envelopper une clé pour permettre son transfert ou son stockage de manière sécurisée.

Le mode wrap permet de chiffrer le format encodé d'une clé : la clé ainsi chiffrée peut être stockée en étant à l'abri des regards indiscrets. Le mode unwrap permet le déchiffrement de données préalablement obtenues en utilisant le mode wrap. Ces modes permettent de sécuriser les échanges de clés.

Pour encrypter une clé, il faut initialiser l'objet Cipher avec le mode WRAP_MODE et invoquer la méthode wrap() en lui passant en paramètre un objet de type Key qui est la clé à traiter.

  • public final byte[] wrap(Key key)

Pour permettre d'extraire une clé d'une enveloppe, il est nécessaire d'avoir le nom de l'algorithme utilisé et le type de clé enveloppée.

Il faut initialiser l'instance de type Cipher avec le mode UNWRAP_MODE et invoquer la méthode unwrap().

  • public final Key unwrap(byte[] wrappedKey, String wrappedKeyAlgorithm, int wrappedKeyType))

Le paramètre wrappedKey correspond à l'enveloppe de la clé (créée en invoquant la méthode wrap()).

Le paramètre wrappedKeyAlgorithm est le nom de l'algorithme.

Le paramètre wrappedKeytype est le type de l'enveloppe (Cipher.SECRET_KEY, Cipher.PRIVATE_KEY ou Cipher.PUBLIC_KEY).

Exemple :
package com.jmdoudoux.test.securite;

import java.security.Key;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;

public class TestCipherWrap {

  public static void main(String[] args) {

    try {
      // génération de la clé à envelopper
      KeyGenerator generator = KeyGenerator.getInstance("AES");
      generator.init(128);
      SecretKey cleAEnvelopper = generator.generateKey();
      System.out.println("cle          : "
          + ConvertionHelper.bytesToHex(cleAEnvelopper.getEncoded()));

      // wrap de la clé
      Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding");
      KeyGenerator KeyGen = KeyGenerator.getInstance("AES");
      KeyGen.init(128);
      Key clePourChiffrer = KeyGen.generateKey();
      cipher.init(Cipher.WRAP_MODE, clePourChiffrer);
      byte[] cleEnveloppee = cipher.wrap(cleAEnvelopper);
      System.out.println("cle wrapped  : "
          + ConvertionHelper.bytesToHex(cleEnveloppee));

      // unwrap de la clé
      cipher.init(Cipher.UNWRAP_MODE, clePourChiffrer);
      Key key = cipher.unwrap(cleEnveloppee, "AES", Cipher.SECRET_KEY);
      System.out.println("cle unwrapped: "
          + ConvertionHelper.bytesToHex(key.getEncoded()));

    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}

Résultat :
cle          : 88694775945182C05A7BAA7ED90C0708
cle wrapped  : 3CF655ABEE9135F4D766CDDE0A80622C
cle unwrapped: 88694775945182C05A7BAA7ED90C0708

Dans l'exemple ci-dessus, il est possible de ne pas utiliser de padding car la taille de la clé est un multiple de la taille du bloc.

 

25.3.5. Les paramètres des algorithmes de chiffrement

Le chiffrement peut se faire en utilisant deux modes :

  • block : le chiffrement se fait par bloc. Chaque bloc est traité dans son intégralité : s'il n'y a pas assez de données pour remplir le dernier bloc, les octets manquants doivent être complétés. Cette opération est nommée padding : plusieurs types de padding existent
  • stream : le chiffrement se fait octet par octet. Les données peuvent être de taille arbitraire sans avoir à réaliser un padding

Lors d'un chiffrement simple, deux blocs de données identiques donneront les mêmes données une fois chiffrés. Ceci facilite le travail des crypto-analystes lorsque des blocs de données se répètent. Pour l'éviter et améliorer la complexité d'un déchiffrement forcé, des modes ont été définis pour utiliser les données du bloc précédent pour modifier le bloc en cours de traitement avant son chiffrement. Le premier bloc a alors besoin d'une valeur initiale qui est appelée Initialisation Vector (IV). Les données de l'IV sont simplement utilisées pour modifier les données du premier bloc : cette valeur peut être obtenue aléatoirement et ne doit pas nécessairement être gardée de manière secrète.

Certains algorithmes comme RSA ou AES peuvent utiliser des clés de tailles différentes et d'autres algorithmes comme DES ou Triple DES utilisent des clés de tailles fixes.

Généralement, plus la taille de la clé est importante, plus la résistance aux tentatives de déchiffrement forcé sera grande. Mais plus la taille est importante, plus le temps nécessaire aux algorithmes est important.

La plupart des algorithmes utilisent des clés binaires qui sont difficiles à retenir par des humains : il est plus facile de retenir des mots de passes composés de caractères alphanumériques. C'est la raison pour laquelle le protocole Password Based Encryption (PBE) a été développé pour générer une clé binaire forte à partir d'un mot de passe et de plusieurs paramètres dépendants de l'implémentation (nombre aléatoire, nombre d'itérations, salt, ...). L'utilisation de ces différents paramètres par l'algorithme de l'implémentation permet d'améliorer la génération aléatoire de la clé binaire.

L'API propose plusieurs classes pour encapsuler les différentes valeurs pour les paramètres.

La classe javax.crypto.spec.PBEParameterSpec permet d'encapsuler les paramètres (salt et nombre d'itérations) pour une transformation de type PBE, par exemple PBEWithMD5AndDES.

La classe javax.crypto.spec.IVParameterSpec encapsule un Initialisation Vector (IV) pour les transformations qui en ont besoin. C'est par exemple le cas des algorithmes DES, DESede et Blowfish qui utilisent le mode CBC, CFB, OFB ou PCBC. La méthode getIV() permet d'obtenir le vecteur d'initialisation (initialization vector ou IV).

La méthode getParameters() permet d'obtenir les paramètres utilisés par l'implémentation de l'algorithme utilisé par la classe Cipher. Elle renvoie un objet de type AlgorithmParameters. Elle renvoie null si aucun paramètre n'est utilisé. Par exemple, une transformation de type PBE requiert plusieurs paramètres dont un salt et un nombre d'itérations.

La méthode getEncoded() permet d'obtenir ces paramètres pour les réutiliser pour le chiffrement. Il suffit de fournir les paramètres encodés à la méthode init() de la classe AlgorithmParameters.

Exemple :
package com.jmdoudoux.test.securite;

import java.security.AlgorithmParameters;

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.PBEParameterSpec;

public class TestCipherGetParameters {

  private static final String TRANSFORMATION = "PBEWithMD5AndDES";

  static char[] motDePasse = { 'M', 'o', 't', 'D', 'e', 'P',
      'a', 's', 's', 'e' };

  static byte[] salt = new byte[] { 0x7e, (byte) 0xe0,
      0x41, (byte) 0xf9, 0x4e, (byte) 0xa0, 0x60, 0x02 };

  public static void main(String[] args) {

    try {
      SecretKeyFactory kf = SecretKeyFactory.getInstance(TRANSFORMATION);
      PBEKeySpec keySpec = new PBEKeySpec(motDePasse);
      SecretKey key = kf.generateSecret(keySpec);
      PBEParameterSpec params = new PBEParameterSpec(salt, 1000);

      Cipher cipherEnc = Cipher.getInstance(TRANSFORMATION);
      cipherEnc.init(Cipher.ENCRYPT_MODE, key, params);

      byte[] texteChiffre = cipherEnc.doFinal("mon message".getBytes());
      System.out.println("texte chiffre="
          + ConvertionHelper.bytesToHex(texteChiffre));

      AlgorithmParameters algParams = cipherEnc.getParameters();
      byte[] encodedAlgParams = algParams.getEncoded();

      String texteClair = dechiffrer(key, encodedAlgParams, texteChiffre);
      System.out.println(texteClair);

    } catch (Exception e) {
      e.printStackTrace();
    }
  }

  public static String dechiffrer(SecretKey key, byte[] encodedAlgParams,
      byte[] texteChiffre) throws Exception {
    AlgorithmParameters algParamsDec;
    algParamsDec = AlgorithmParameters.getInstance(TRANSFORMATION);
    algParamsDec.init(encodedAlgParams);

    Cipher cipherDec = Cipher.getInstance(TRANSFORMATION);

    cipherDec.init(Cipher.DECRYPT_MODE, key, algParamsDec);
    byte[] texteClair = cipherDec.doFinal(texteChiffre);

    return new String(texteClair);
  }
}

Résultat :
texte chiffre=9709B1F1310C655F0D75FE20148CECAF
mon message

 

25.3.6. Des exemples d'utilisation d'algorithmes de chiffrement

L'exemple ci-dessous met en oeuvre l'algorithme DES.

Exemple :
package com.jmdoudoux.test.securite;

import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;

public class TestCipherDES {

  public static void main(String[] args) {

    try {
      KeyGenerator keyGen = KeyGenerator.getInstance("DES");
      SecretKey secretKey = keyGen.generateKey();
      String message = "Mon message à chiffer";

      utiliserCipher(secretKey, "DES", message);
      utiliserCipher(secretKey, "DES/ECB/PKCS5Padding", message);
      utiliserCipher(secretKey, "DES/CBC/PKCS5Padding", message);
      utiliserCipher(secretKey, "DES/PCBC/PKCS5Padding", message);
      utiliserCipher(secretKey, "DES/CFB/PKCS5Padding", message);
      utiliserCipher(secretKey, "DES/OFB/PKCS5Padding", message);

    } catch (Exception e) {
      System.out.println("Erreur " + e);
    }
  }

  public static void utiliserCipher(SecretKey secretKey, String transformation,
      String message) throws NoSuchAlgorithmException, NoSuchPaddingException,
      InvalidKeyException, IllegalBlockSizeException, BadPaddingException,
      InvalidAlgorithmParameterException {
    Cipher desCipher = Cipher.getInstance(transformation);
    desCipher.init(Cipher.ENCRYPT_MODE, secretKey);
    byte[] byteCipherText = desCipher.doFinal(message.getBytes());
    System.out.println(ConvertionHelper.bytesToHex(byteCipherText));

    desCipher.init(Cipher.DECRYPT_MODE, secretKey, desCipher.getParameters());
    byte[] byteDecryptedText = desCipher.doFinal(byteCipherText);
    System.out.println(new String(byteDecryptedText));
  }
}

Résultat :
2A856E495FCC686DB21FCD677CA7F7081BD39FCBD8053913
Mon message à chiffer
2A856E495FCC686DB21FCD677CA7F7081BD39FCBD8053913
Mon message à chiffer
B05B556E44401B60BA7CB43B6BC3307FD33B99D782FFBB4A
Mon message à chiffer
9864AE319B96C2EAF7149CC2E5D196268FD17321D20D93E6
Mon message à chiffer
C3F682D4577CE3EAAA7F2532983CCBDBEEF1A7BC1A3EC6F1
Mon message à chiffer
49375D703EE141E3DD30747531C312D583E14551B00A180D
Mon message à chiffer

L'exemple ci-dessous met en oeuvre l'algorithme AES.

Exemple :
package com.jmdoudoux.test.securite;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;

public class TestCipherAES {

  public static void main(String[] args) {

    try {
      KeyGenerator keyGen = KeyGenerator.getInstance("AES");
      keyGen.init(128);
      SecretKey secretKey = keyGen.generateKey();

      Cipher aesCipher = Cipher.getInstance("AES");
      aesCipher.init(Cipher.ENCRYPT_MODE, secretKey);
      byte[] byteCipherText = aesCipher.doFinal("Mon message".getBytes());
      System.out.println(ConvertionHelper.bytesToHex(byteCipherText));

      aesCipher.init(Cipher.DECRYPT_MODE, secretKey, aesCipher.getParameters());
      byte[] byteDecryptedText = aesCipher.doFinal(byteCipherText);
      System.out.println(new String(byteDecryptedText));
    } catch (Exception e) {
      System.out.println("Erreur " + e);
    }
  }
}

Résultat :
BF1FFEECF2C58AB9CB295D7DB95A18E7
Mon message

 

25.4. Les classes javax.crypto.CipherInputStream et javax.crypto.CipherOutputStream

Le JCE propose le concept de flux sécurisé qui combine :

  • un objet de type InputStream ou OutputStream
  • un objet de type Cipher

Ces flux sont encapsulés dans les classes CipherInputStream et CipherOutputStream.

La classe CipherInputStream est un FilterInputStream qui est associé à un objet de type Cipher pour permettre, selon sa configuration, de chiffrer ou déchiffrer les données lues.

Selon sa configuration choisie, les méthodes de lecture renvoient les données obtenues par la lecture du flux et traitées par l'instance de type Cipher. Si l'instance de type Cipher est configurée pour décrypter des données, les données lues du flux seront décryptées avant d'être renvoyées.

L'instance de type Cipher doit être complètement initialisée avant d'être utilisée par un CipherInputStream pour commencer la lecture des données.

Les méthodes de lecture de la classe CipherInputStream vont attendre les données retournées par l'instance de type Cipher : c'est notamment le cas si l'instance de type Cipher travaille sur des blocs, il est nécessaire d'obtenir toutes les données d'un bloc de l'InputStream.

Exemple ( code Java 1.4 ) :
package com.jmdoudoux.test.securite;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;

import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;

public class TestCipherInputStream {

  public static void main(String[] args) {

    try {
      KeyGenerator keyGen;
      keyGen = KeyGenerator.getInstance("DESede");
      keyGen.init(168);
      SecretKey cle = keyGen.generateKey();
      System.out.println("cle : " + new String(cle.getEncoded()));

      encrypterFichier(cle, "C:/java/test/donnees.txt",
          "C:/java/test/donnees_enc.txt");

      decrypterFichier(cle, "C:/java/test/donnees_enc.txt",
          "C:/java/test/donnees_dec.txt");

    } catch (Exception e) {
      e.printStackTrace();
    }
  }

  public static void encrypterFichier(SecretKey cle, String source, String cible)
      throws NoSuchAlgorithmException, NoSuchPaddingException,
      InvalidKeyException {
    encrypterDecrypterFichier(Cipher.ENCRYPT_MODE, cle, source, cible);
  }

  public static void decrypterFichier(SecretKey cle, String source, String cible)
      throws NoSuchAlgorithmException, NoSuchPaddingException,
      InvalidKeyException {
    encrypterDecrypterFichier(Cipher.DECRYPT_MODE, cle, source, cible);
  }

  public static void encrypterDecrypterFichier(int mode, SecretKey cle,
      String source, String cible) throws NoSuchAlgorithmException,
      NoSuchPaddingException, InvalidKeyException {
    Cipher cipher = Cipher.getInstance("DESede");
    cipher.init(mode, cle);

    FileInputStream fis = null;
    FileOutputStream fos = null;
    CipherInputStream cis = null;

    try {
      fis = new FileInputStream(source);
      cis = new CipherInputStream(fis, cipher);
      fos = new FileOutputStream(cible);
      byte[] b = new byte[8];
      int i = cis.read(b);
      while (i != -1) {
        fos.write(b, 0, i);
        i = cis.read(b);
      }
    } catch (IOException ioe) {
      if (fis != null) {
        try {
          fis.close();
        } catch (IOException e) {
          e.printStackTrace();
        }
      }
      if (fos != null) {
        try {
          fos.close();
        } catch (IOException e) {
          e.printStackTrace();
        }
      }
    }
  }
}

La classe CipherOutputStream est un FilterOutputStream qui peut encoder ou décoder les données qu'on lui envoie.

Elle encapsule un objet de type OutpuStream et un objet de type Cipher. Les données transmises à la méthode write() sont préalablement traitées par l'instance de type Cipher. Cet objet de type Cipher doit donc être obligatoirement configuré et initialisé avant de traiter le premier octet des données.

La classe CipherOutputStream hérite de la classe FilterOutputStream : elle permet d'encrypter ou de décrypter les données qui transitent par le flux. Elle encapsule une instance de type OutputStream ou une de ses sous-classes et une instance de type Cipher.

Il est important d'invoquer les méthodes flush() et close() de la classe CipherOutputStream pour permettre le traitement de l'intégralité des données. Si l'instance de type Cipher est configurée pour utiliser un algorithme avec bloc, il est nécessaire de lui envoyer toutes les données d'un bloc avant que celles-ci ne soient appliquées à l'OutputStream. Dans ce cas, il est important d'invoquer la méthode flush() pour s'assurer que les dernières données transmises au Cipher soient également appliquées à l'instance de type OutputStream. La méthode close() invoque la méthode doFinal() du Cipher et les méthodes flush() et close() de l'instance de type OutputStream.

 

25.5. La classe javax.crypto.SealedObject

La classe SealedObject encapsule de manière cryptée le résultat de la sérialisation d'un objet. L'objet encapsulé doit donc implémenter l'interface java.io.Serializable.

Pour créer une instance de type SealedObject, il suffit d'invoquer le constructeur de la classe SealObject en lui passant en paramètre l'objet et une instance de type Cipher correctement initialisée.

Pour décrypter l'objet encapsulé, il suffit d'invoquer la méthode getObject() en lui passant la clé.

Exemple ( code Java 1.4 ) :
package com.jmdoudoux.test.securite;

import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SealedObject;
import javax.crypto.SecretKey;

public class TestSealedObject {

  public static void main(String[] args) {

    final Personne personne = new Personne(1, "nom1", "prenom1");

    KeyGenerator keyGen;
    try {
      keyGen = KeyGenerator.getInstance("DESede");
      keyGen.init(168);
      SecretKey cle = keyGen.generateKey();
      System.out.println("cle : " + new String(cle.getEncoded()));

      SealedObject so = encrypter(personne, cle);

      Personne personneDec = decrypter(so, cle);

      System.out.println(personne.equals(personneDec));

    } catch (Exception e) {
      e.printStackTrace();
    }
  }

  public static SealedObject encrypter(final Personne personne, SecretKey cle)
      throws NoSuchAlgorithmException, NoSuchPaddingException,
      InvalidKeyException, IllegalBlockSizeException, BadPaddingException,
      IOException {
    Cipher cipher = Cipher.getInstance("DESede");
    cipher.init(Cipher.ENCRYPT_MODE, cle);

    return new SealedObject(personne, cipher);
  }

  public static Personne decrypter(SealedObject so, SecretKey cle)
      throws NoSuchAlgorithmException, NoSuchPaddingException,
      InvalidKeyException, IllegalBlockSizeException, BadPaddingException,
      ClassNotFoundException, IOException {
    Cipher cipher = Cipher.getInstance("DESede");
    cipher.init(Cipher.DECRYPT_MODE, cle);

    Personne resultat = (Personne) so.getObject(cle);
    return resultat;
  }

}

Il est aussi possible de fournir en paramètre de la méthode getObject(), une instance de type Cipher correctement initialisée.

Exemple ( code Java 1.4 ) :
// ...
  public static Personne decrypter(SealedObject so, SecretKey cle)
    throws NoSuchAlgorithmException, NoSuchPaddingException,
    InvalidKeyException, IllegalBlockSizeException, BadPaddingException,
    ClassNotFoundException, IOException {
    Cipher cipher = Cipher.getInstance("DESede");
    cipher.init(Cipher.DECRYPT_MODE, cle);

    Personne resultat = (Personne) so.getObject(cle);
    return resultat;
  }

  // ...

La classe SealedObject permet de créer une version sécurisée par cryptage d'un objet sous une forme sérialisée. L'objet doit implémenter l'interface Serializable.

 

25.6. La classe javax.crypto.Mac

Une fonction de type Message Authentication Code (MAC) est similaire à un MessageDigest : elle permet de vérifier l'intégrité de données transmises par un moyen non fiable.

Une fonction de type MAC propose un moyen de vérifier l'intégrité de données transmises en calculant une empreinte grâce à des algorithmes mathématiques et une clé secrète. Ces algorithmes reposent sur l'utilisation de fonctions de hachage nommées HMAC qui combinent une fonction de hachage et une clé secrète. Les fonctionnalités de type HMAC sont décrites dans la RFC 2014. Une fonction HMAC est utilisée en cryptographie pour calculer une valeur de hachage grâce à un algorithme de hachage (MD5, SHA-1, ...) et une clé partagée. Il est donc nécessaire de connaitre la clé pour pouvoir utiliser la fonction MAC et ainsi pouvoir vérifier l'intégrité des données reçues.

La classe Mac, ajoutée à Java 1.4, permet de mettre en oeuvre des algorithmes de type MAC. Un cas typique d'utilisation est la vérification de données transmises entre deux parties qui partagent une même clé secrète.

L'émetteur et le récepteur doivent partager la clé à utiliser lors de l'utilisation de la fonction MAC. L'émetteur calcul une valeur de hachage avec un algorithme de type MAC en utilisant la clé partagée. Le récepteur calcule lui aussi la valeur de hachage des données reçues avec le même algorithme de type MAC en utilisant lui aussi la clé partagée. Le récepteur peut alors comparer les deux valeurs de hachage pour vérifier l'intégrité des données reçues et garantir ainsi que ce sont bien celles qui ont été envoyées par l'émetteur.

La méthode static getInstance() est une fabrique qui permet de créer une instance de type Mac mettant en oeuvre l'algorithme dont le nom est fourni en paramètre. Deux autres surcharges permettent de préciser le fournisseur de l'implémentation de l'algorithme à utiliser.

Méthode

Rôle

Mac getInstance(String algorithm)

Fabrique qui renvoie une instance de type Mac pour l'algorithme précisé

Mac getInstance(String algorithm, String Provider)

Fabrique qui renvoie une instance de type Mac pour l'algorithme du provider précisé

Mac getInstance(String algorithm, Provider provider)

Fabrique qui renvoie une instance de type Mac pour l'algorithme du provider précisé


Chaque implémentation de la plate-forme Java a l'obligation de fournir un support de plusieurs algorithmes de type MAC : HmacMD5, HmacSHA1 et HmacSHA256.

D'autres algorithmes peuvent être utilisés selon les implémentations du fournisseur, par exemple :

Nom

Algorithme

Taille de l'empreinte

HmacMD5

HMAC avec la fonction de hachage MD5

128 bits

HmacSHA[1|256|384|512]

HMAC avec la fonction de hachage SHA[1|256|384|512]

160, 256, 384, 512 bits

PBEWith<mac>

HMAC initialisé avec PBE

<mac> est à remplacer par le nom de la fonction de hachage à utiliser (MD5, SHA[1|256|384|512])

 


L'instance de type Mac doit être initialisée en utilisant une des deux surcharges de la méthode init() pour fournir la clé secrète sous la forme d'une instance de type Key.

Méthode

Rôle

public void init(Key key);

Fournir la clé

public void init(Key key, AlgorithmParameterSpec params);

Fournir la clé et un objet de type AlgorithParameterSec pour transmettre d'autres paramètres à l'implémentation de l'algorithme utilisé


La clé doit implémenter l'interface SecretKey : elle peut donc être le résultat de l'invocation de la méthode KeyGenerator.generateKey() ou KeyAgreement.generateSecret().

Une valeur MAC peut être obtenue en une seule opération ou en plusieurs étapes, ce qui est pratique si on ne connait pas à l'avance la taille des données à traiter ou si la taille des données ne peut pas être stockée en une seule fois en mémoire.

Les données peuvent être fournies en un ou plusieurs blocs grâce aux méthodes suivantes :

Méthode

Rôle

byte[] doFinal(byte[] input)

Finaliser le calcul de la valeur MAC avec les données fournies en paramètre et retourner l'empreinte

void update(byte input)

void update(byte[] input)

void update(byte[] input, int inputOffset, int inputLen)

Fournir une partie des données

byte[] doFinal()

Finaliser le calcul de la valeur MAC pour les données déjà fournies et retourner l'empreinte

void doFinal(byte[] output, int outOffset)

Finaliser le calcul de la valeur MAC pour les données déjà fournies et mettre l'empreinte dans le tableau à l'offset précisé en paramètre


La méthode doFinal(byte[]) permet de calculer la valeur MAC en une seule opération.

Exemple :
package com.jmdoudoux.test.securite;

import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;

import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;

public class TestMac {

  public static void main(String[] args) {
    try {
      String resultat = calculerMAC("Mon message", "maCle", "HmacSHA256");
      System.out.println("HmacSHA256 digest : " + resultat);

      resultat = calculerMAC("Mon message", "maCle", "HmacMD5");
      System.out.println("HmacMD5 digest : " + resultat);
    } catch (NoSuchAlgorithmException e) {
      e.printStackTrace();
    } catch (UnsupportedEncodingException e) {
      e.printStackTrace();
    } catch (InvalidKeyException e) {
      e.printStackTrace();
    }
  }

  public static String calculerMAC(String message, String cle, String algorithme)
      throws UnsupportedEncodingException, NoSuchAlgorithmException,
      InvalidKeyException {
    String resultat;

    SecretKey secretKey = new SecretKeySpec(cle.getBytes("UTF-8"), algorithme);
    System.out.println("cle : " + bytesToHex(secretKey.getEncoded()));

    Mac mac = Mac.getInstance(secretKey.getAlgorithm());
    mac.init(secretKey);

    byte[] b = message.getBytes("UTF-8");
    byte[] digest = mac.doFinal(b);

    resultat = bytesToHex(digest);
    return resultat;
  }

  public static String bytesToHex(byte[] b) {
    char hexDigit[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A',
        'B', 'C', 'D', 'E', 'F' };
    StringBuffer buf = new StringBuffer();
    for (int j = 0; j < b.length; j++) {
      buf.append(hexDigit[(b[j] >> 4) & 0x0f]);
      buf.append(hexDigit[b[j] & 0x0f]);
    }
    return buf.toString();
  }

}

Résultat :
cle : 6D61436C65
HmacSHA256 digest : AF9DAA10208DDC5DCD3618C422B4E97AD23A8A3C0D648EB5648D424420FD9B02
cle : 6D61436C65
HmacMD5 digest : DDB1B8B6F8F29578E5D143A5834285DB

Plusieurs surcharges de la méthode update() permettent de calculer la valeur MAC en plusieurs invocations. Pour obtenir la valeur MAC, il faut alors invoquer une des deux surcharge de la méthode doFinal() : byte[] doFinal() ou byte[] doFinal(byte[], int).

Exemple :
package com.jmdoudoux.test.securite;

import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;

import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;

public class TestMac {

  public static void main(String[] args) {
    try {
      String resultat = calculerMAC("monfichier.txt", "maCle", "HmacSHA256");
      System.out.println("HmacSHA256 digest : " + resultat);

      resultat = calculerMAC("monfichier.txt", "maCle", "HmacMD5");
      System.out.println("HmacMD5 digest : " + resultat);
    } catch (Exception e) {
      e.printStackTrace();
    }
  }

  public static String calculerMAC(String nomFichier, String cle,
      String algorithme) throws NoSuchAlgorithmException, InvalidKeyException,
      IllegalStateException, IOException {
    String resultat;

    SecretKey secretKey = new SecretKeySpec(cle.getBytes("UTF-8"), algorithme);
    System.out.println("cle : "
        + ConvertionHelper.bytesToHex(secretKey.getEncoded()));

    Mac mac = Mac.getInstance(secretKey.getAlgorithm());
    mac.init(secretKey);

    byte[] buffer = new byte[1024];
    BufferedInputStream in = new BufferedInputStream(new FileInputStream(
        nomFichier));
    int nbLus = 0;
    try {
      while ((nbLus = in.read()) != -1) {
        mac.update(buffer, 0, nbLus);
      }
      byte[] digest = mac.doFinal();
      resultat = ConvertionHelper.bytesToHex(digest);
    } finally {
      try {
        in.close();
      } catch (IOException e) {
        e.printStackTrace();
      }
    }

    return resultat;
  }

}

Résultat :
cle : 6D61436C65
HmacSHA256 digest : 7785E591C04ADA3A1628AEA8D429B2368F1BDE3D92288D922522ECAAE62ECB21
cle : 6D61436C65
HmacMD5 digest : 13F3FEEB29B335D988757C81D89713B5

A la fin de l'invocation de la méthode doFinal(), l'instance est réinitialisée pour permettre de calculer à nouveau les empreintes de nouvelles données en utilisant l'algorithme et la clé associés.

Plusieurs méthodes permettent d'obtenir des informations :

Méthode

Rôle

String getAlgorithm()

Renvoyer le nom de l'algorithme

int getMacLength()

Retourner la taille de l'empreinte calculée avec l'algorithme

Provider getProvider()

Retourner le fournisseur de l'implémentation de l'algorithme


La méthode reset() permet de réinitialiser l'objet.

 


Développons en Java v 2.20   Copyright (C) 1999-2021 Jean-Michel DOUDOUX.   
[ Précédent ] [ Sommaire ] [ Suivant ] [ Télécharger ]      [ Accueil ] [ Commentez ]