Chiffrement interopérable en Java, PL/SQL, Javascript, et avec des progiciels

Besoin : être capable de chiffrer des données sensibles d’une entreprise, pour pouvoir les faire transiter entre plusieurs applications.

Périmètre : chiffrement d’une chaine de caractères, pas d’un fichier ou d’un filesystem.

Enjeu : être capable de raisonnablement sécuriser le contenu, tout en étant interopérable entre différentes technologies.

Schema Chiffrement

Sommaire

Choix de l’algorithme

Peu d’hésitation, ce sera AES. Il n’a pas de faiblesse importante connue, c’est le plus utilisé et recommandé actuellement. Et il est implémenté dans plein de langages : https://en.wikipedia.org/wiki/AES_implementations.

Mais premier constat : sur les sites web qui permettent de faire en ligne du chiffrement/déchiffrement AES, je n’en ai pas trouvé deux qui soient interopérables!

La raison est simple : ces sites n’utilisent pas tous les mêmes options d’AES et/ou la même manière de générer une clé de chiffrement à partir d’un mot de passe. D’où l’importance de bien choisir les options d’AES pour qu’elles soient utilisables par tous les applicatifs :

Options AES

Longueur de la clé de chiffrement : 128, 192 ou 256 bits?

Utiliser une clé de 256 bits apporterait plus de sécurité, mais amène des problèmes d’interopérabilité (ex : le chiffrement inclus dans la JVM d’Oracle ne supporte par défaut que du 128 bits : voir plus bas). Voici mon avis :

  • Si la donnée n’est pas « secret défense » (c’est-à-dire qu’elle n’intéresserait pas une agence gouvernementale, ou un concurrent, ou un pirate fortuné), une clé de 128 bits est à utiliser, pour favoriser l’interopérabilité et (dans une moindre mesure) les performances
  • Si la donnée est vraiment sensible, une clé de 256 bits est à utiliser, pour favoriser la sécurité

Tests de compatibilité en Java, PL/SQL et Javascript

En résumé : l’interopérabilité fonctionne bien (testé en AES 128 bits). Ce n’est pas très compliqué, mais il ne faut pas se tromper sur la manière d’encoder/décoder chacune des chaines.

Cela fonctionnerait certainement tout aussi bien en 256 bits, mais je n’ai pas testé.

Disclaimer : les extraits de code ci-dessous n’ont pas été optimisés en termes de performance et gestion des erreurs.

PL/SQL Oracle

Il est nécessaire que le package DBMS_CRYPTO soit accessible par le user Oracle. Il faut pour cela exécuter la commande suivante en tant que SYS :

grant execute on sys.dbms_crypto to nom_user_oracle;

Ensuite il est possible de chiffrer/déchiffrer facilement :

CREATE OR REPLACE FUNCTION CHIFFRER(CHAINE_A_CHIFFRER IN VARCHAR2)
RETURN VARCHAR2
AS
    encryption_type    PLS_INTEGER :=
                            DBMS_CRYPTO.ENCRYPT_AES128
                          + DBMS_CRYPTO.CHAIN_CBC
                          + DBMS_CRYPTO.PAD_PKCS5;
BEGIN
    return utl_raw.cast_to_varchar2(utl_encode.base64_encode(DBMS_CRYPTO.ENCRYPT(
         src => utl_raw.cast_to_raw(CHAINE_A_CHIFFRER),
         typ => encryption_type,
         key => utl_encode.base64_decode(utl_raw.cast_to_raw('dbrCUoc4z9EFJTLBSsZtQw==')),
         iv  => utl_raw.cast_to_raw('/NYW0VJsb+oIvXyo')
    )));
END;

CREATE OR REPLACE FUNCTION DECHIFFRER(CHAINE_A_DECHIFFRER IN VARCHAR2)
RETURN VARCHAR2
AS   
    encryption_type    PLS_INTEGER :=
                            DBMS_CRYPTO.ENCRYPT_AES128
                          + DBMS_CRYPTO.CHAIN_CBC
                          + DBMS_CRYPTO.PAD_PKCS5;
BEGIN
       
                
    return utl_raw.cast_to_varchar2(DBMS_CRYPTO.DECRYPT(
        src => utl_encode.base64_decode(utl_raw.cast_to_raw(CHAINE_A_DECHIFFRER)),
        typ => encryption_type,
        key => utl_encode.base64_decode(utl_raw.cast_to_raw('dbrCUoc4z9EFJTLBSsZtQw==')),
        iv  => utl_raw.cast_to_raw('/NYW0VJsb+oIvXyo')
    ));
END;

select DECHIFFRER(CHIFFRER('https://blog.mossroy.fr')) from dual;
// Devrait afficher 'https://blog.mossroy.fr'

NB : Sur Oracle 11g, nous avons eu des erreurs sur ce package après avoir passé le patch P19 :

ORA-28817: La fonction PL/SQL a renvoyé une erreur.
ORA-06512: à "SYS.DBMS_CRYPTO_FFI", ligne 3
ORA-06512: à "SYS.DBMS_CRYPTO", ligne 13

Cela a été résolu en passant la commande suivante (merci au consultant qui a trouvé ça) :

alter system set "_use_fips_mode"=false;

Cf https://docs.oracle.com/cd/E50790_01/doc/doc.121/e51953/app_whatsnew.htm#DBMSO22130

Java

Le Java Runtime Environment (JRE) de Oracle fournit les classes nécessaires (depuis la version 1.4.2), mais limite (par défaut) la taille de la clé AES à 128 bits. Il y a néanmoins des solutions pour avoir des clés plus longues :

D’autre part, l’encodage/décodage en Base64 peut se faire de plusieurs manières :

Code inspiré de https://github.com/mpetersen/aes-example (en utilisant Commons Codec) :

public class AESExample {
    private static final String CIPHER = "AES/CBC/PKCS5Padding";
    private static final String BASE64_KEY = "dbrCUoc4z9EFJTLBSsZtQw==";
    private static final String INIT_VECTOR = "/NYW0VJsb+oIvXyo";
    private static SecretKeySpec secretKeySpec;
    private static IvParameterSpec ivParameterSpec;
    private static Cipher encryptCipher;
    private static Cipher decryptCipher;

    public static void init() throws Exception {
        byte[] key = Base64.decodeBase64(BASE64_KEY);
        secretKeySpec = new SecretKeySpec(key, "AES");
        ivParameterSpec = new IvParameterSpec(INIT_VECTOR.getBytes("UTF-8"));

        encryptCipher = Cipher.getInstance(CIPHER);
        encryptCipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec);
        decryptCipher = Cipher.getInstance(CIPHER);
        decryptCipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec);
    }

    public static String encrypt(String strToEncrypt) throws Exception {
        return Base64.encodeBase64String(encryptCipher.doFinal(strToEncrypt.getBytes("UTF-8")));
    }

    public static String decrypt(String strToDecrypt) throws Exception {
        return new String(decryptCipher.doFinal(Base64.decodeBase64(strToDecrypt)),Charset.forName("UTF-8"));
    }

    public static void main(String args[]) throws Exception {
        final String strToEncrypt = "ma donnée super confidentielle";
        AESExample.init();
        System.out.println("String to Encrypt: " + strToEncrypt + " - length=" + strToEncrypt.length());

        final String encodedStringBase64 = AESExample.encrypt(strToEncrypt.trim());
        System.out.println("Encrypted (base64): " + encodedStringBase64 + " - length=" + encodedStringBase64.length());

        final String decodedString = AESExample.decrypt(encodedStringBase64.trim());
        System.out.println("Decrypted : " + decodedString + " - length=" + decodedString.length());
    }
}

Javascript

Il existe plusieurs librairies pour cela, notamment https://code.google.com/p/crypto-js/

Code également inspiré de https://github.com/mpetersen/aes-example :

var AesUtil = function() {};

AesUtil.prototype.encrypt = function(key, iv, plainText) {

  var encrypted = CryptoJS.AES.encrypt(
      plainText,
      CryptoJS.enc.Base64.parse(key),
      { iv: CryptoJS.enc.Utf8.parse(iv), mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 });
  return encrypted.ciphertext.toString(CryptoJS.enc.Base64);
}

AesUtil.prototype.decrypt = function(key, iv, cipherText) {
  var cipherParams = CryptoJS.lib.CipherParams.create({
    ciphertext: CryptoJS.enc.Base64.parse(cipherText)
  });
  var decrypted = CryptoJS.AES.decrypt(
      cipherParams,
      CryptoJS.enc.Base64.parse(key),
      { iv: CryptoJS.enc.Utf8.parse(iv), mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 });
  return decrypted.toString(CryptoJS.enc.Utf8);
}

Autres langages

Je n’ai pas vérifié, mais c’est a priori interopérable avec d’autres langages :

Performances

Je n’ai pas pris le temps de faire des mesures de performance précises, mais d’autres l’ont fait pour moi :

Longueur du champ chiffré

La longueur du champ chiffré est indépendante de la longueur de la clé de chiffrement (128 à 256 bits).

Cf http://www.obviex.com/articles/CiphertextSize.pdf

La règle de calcul est la suivante :

CipherText = PlainText + Block - (PlainText MOD Block)

Mais si on stocke la donnée chiffrée en Base64, cela prend de la place supplémentaire, calculée ainsi :

Base64 = (Bytes + 2 - ((Bytes + 2) MOD 3)) / 3 * 4

La règle à appliquer dans notre cas est donc :

Taille texte chiffré = (taille + 18 – (taille mod 16) – ((taille + 18 – (taille mod 16)) mod 3)) / 3 * 4

Attention à l’encoding : il peut augmenter significativement le nombre d’octets nécessaires. Ci-dessus, la taille de la chaine initiale est exprimée en octets. S’il peut y avoir des caractères non ASCII (accents etc), et avec un encodage UTF-8, il peut y avoir plusieurs octets par caractère (maximum 4).

Génération aléatoire des clés et InitVector

Source : http://blog.cloudme.org/2013/08/interoperable-aes-encryption-with-java-and-javascript/

    public static String generateRandomKey() throws NoSuchAlgorithmException {
        KeyGenerator keyGen = KeyGenerator.getInstance("AES");
        keyGen.init(128); // ajuster suivant la taille voulue
        SecretKey secretKey = keyGen.generateKey();
        return Base64.encodeBase64String(secretKey.getEncoded());
    }

Pour l’initVector, la classe SecureRandom est adaptée à cela, et génère des octets aléatoires. Dans notre cas, pour avoir une chaine de caractères, je passe par un encodage Base64.

NB : sur le principe, ce passage par du Base64 affaiblit un peu le caractère aléatoire de mon initVector. Mais cela n’a d’importance dans notre contexte (un seul initVector, voire 2). Pour faire mieux, il aurait fallu prendre un initVector sur 16 octets généré par SecureRandom, et le convertir en Base64 (24 caractères).

    public static String generateRandomInitVector() {
        byte[] salt = new byte[12]; // 12 octets donneront 16 caractères encodés en Base64
        new SecureRandom().nextBytes(salt);
        return Base64.encodeBase64String(salt).substring(0, 16);
    }

Interopérabilité avec des progiciels

Dans un monde idéal, chaque donnée sensible serait systématiquement stockée chiffrée partout, y compris en base de données. Cela protégerait de beaucoup de scenario de piratage.

Dans ce cas, il faut chiffrer/déchiffrer la donnée à la volée (lorsqu’on a besoin de l’afficher ou la modifier). C’est tout à fait possible quand on a la main sur le code des applications concernées, mais c’est beaucoup plus compliqué quand la donnée doit transiter dans des progiciels.

Après avoir sondé quelques éditeurs des progiciels utilisés, rares sont les progiciels qui permettent facilement ce genre de choses. En fait, il n’y a pas de souci pour ceux qui peuvent être adaptés avec des langages standards (Java, .NET etc). Mais ça devient compliqué pour ceux qui utilisent des langages spécifiques (comme bon nombre d’ERP), ne serait-ce que parce que leur langage propriétaire ne sait pas faire d’AES.

Dans ce cas, on est obligé de faire des compromis, comme celui-ci :

Schema Chiffrement 2

C’est moins bien à pas mal d’égards que du chiffrement de bout en bout, mais c’est toujours mieux que tout laisser en clair.

Problématique des copies entre environnements

Quand on a plusieurs environnements (dev/recette/pré-prod/prod), il arrive qu’on ait besoin de recopier les données de prod dans les autres environnements (pour tester avec des données récentes).

Que faire pour éviter de compromettre les données sensibles?

  • utiliser des clés de chiffrement différentes en production
  • et/ou avoir des procédures de suppression (ou anonymisation) des données quand elles sont copiées dans un autre environnement
  • ou s’appuyer sur le chiffrement transparent de la base de données pour que les données ne puissent pas être déchiffrées ailleurs qu’en production

Ne pas affaiblir le chiffrement avec des données prédictibles

Il faut faire attention à ne pas chiffrer des données dont la valeur est prédictible (ou alors dont le nombre de valeurs possibles est faible).

En effet, si une personne malveillante peut avoir des couples valeur chiffrée/valeur déchiffrée (même sans être certain à 100% que ce soit juste), cela affaiblit beaucoup la robustesse du chiffrement.

Exemples dans le cadre de coordonnées bancaires :

  • Le titulaire d’un compte en banque est en général le nom-prénom de la personne concernée. Si on a ce nom-prénom par ailleurs, ça donne la correspondance
  • Le code BIC d’une banque ne peut pas avoir énormément de valeurs différentes, surtout dans le périmètre d’un seul pays. Donc on peut parfois deviner la correspondance, et aussi faire des corrélations : deux personnes ayant la même valeur chiffrée du code BIC sont dans la même banque

Si le cas se produit sur une donnée qui est pourtant sensible, il y a des solutions :

  • Soit ne pas chiffrer cette donnée seule (exemple : la concaténer avec un autre champ, et chiffrer la chaine concaténée)
  • Soit utiliser un « salt » (dans notre cas, ça pourrait être l’initVector) différent pour chaque valeur, mais c’est plus compliqué

Il arrive donc qu’en chiffrant plus de données, on affaiblisse la sécurité de l’ensemble.

Il faut donc faire du cas par cas, et ne pas dissocier les aspects fonctionnels (disparité et prédictibilité des valeurs) et les aspects techniques (chiffrement).

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *