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.
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
- Mode : le mode CBC est apparemment plus sécurisé que les autres modes. Cf https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Common_modes
- Padding : utiliser un padding est plus sécurisé. Cf https://en.wikipedia.org/wiki/Padding_%28cryptography%29. Le PKCS5/PKCS7 est le plus courant, et le seul obligatoirement implémenté dans une JVM. Cf https://docs.oracle.com/javase/7/docs/api/javax/crypto/Cipher.html . Dans notre contexte, PKCS5 et PKCS7 donnent le même résultat, cf https://en.wikipedia.org/wiki/Padding_%28cryptography%29#PKCS7
- Dans notre cas, puisque la communication se fait entre des machines, on utilisera directement une clé de chiffrement (encodée en Base64), plutôt que de la générer depuis un mot de passe. Il faut également se mettre d’accord sur un « initVector » de 16 octets (ici, on a utilisé une chaine de caractères non encodée)
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 :
- Utiliser la librairie BouncyCastle https://www.bouncycastle.org/fr/ au lieu des classes du JRE, mais en utilisant l’API « light-weight », pas en tant que provider JCE : https://bouncycastle.org/specifications.html
- Ou remplacer les « security policy files » fournis par défaut dans le JRE par ceux-ci : http://www.oracle.com/technetwork/java/javase/downloads/jce-6-download-429243.html
- Il y a aussi d’autres solutions en bidouillant les options du JRE par introspection, mais ce n’est probablement pas officiellement supporté
D’autre part, l’encodage/décodage en Base64 peut se faire de plusieurs manières :
- Utiliser la librairie Commons Codec https://commons.apache.org/proper/commons-codec/ , qui fonctionne avec quasiment n’importe quelle version de Java
- A partir de la version 1.6 de Java, il y a une classe de JAXB qui semble fonctionner : https://docs.oracle.com/javase/7/docs/api/javax/xml/bind/DatatypeConverter.html. Cela dit, elle ne semble pas prévue pour un usage en-dehors de JAXB, ce qui pourrait poser des problèmes de performance, notamment. Cf https://mail-archives.apache.org/mod_mbox/tomcat-dev/201303.mbox/%3C6B65C059-2889-4A9B-9689-8B0A77EFF81F@nicholaswilliams.net%3E (et des comportements non prévus dans certains cas limites : voir les messages suivants du thread)
- A partir de la version 1.8 de Java, c’est (enfin) disponible directement dans le JRE : https://docs.oracle.com/javase/8/docs/api/java/util/Base64.Decoder.html
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 :
- En .NET : https://steelmon.wordpress.com/2013/07/01/simple-interoperable-encryption-in-java-and-net/
- En Objective-C : http://watchitlater.com/blog/2010/02/java-and-iphone-aes-interoperability/
Performances
Je n’ai pas pris le temps de faire des mesures de performance précises, mais d’autres l’ont fait pour moi :
- Le chiffrement en 128 bits est a priori plus rapide qu’en 256 bits, de l’ordre de 20% environ. Cf http://www.cryptopp.com/benchmarks.html (chiffrement logiciel. Apparemment la différence s’estompe quand le chiffrement est fait en matériel)
- Les processeurs récents implémentent l’AES en hardware (technologie AES-NI : https://en.wikipedia.org/wiki/AES_instruction_set), et les bibliothèques récentes savent l’utiliser.
- L’implémentation AES de Java supporte l’AES-NI, à partir de la version 1.7.0_40 (cf https://stackoverflow.com/questions/23058309/aes-ni-intrinsics-enabled-by-default). Cela apporterait un gain de l’ordre de 40% (cf https://software.intel.com/en-us/articles/intel-aes-ni-performance-testing-on-linuxjava-stack). C’est activé par défaut (en mode server) si le processeur le supporte (on peut tester la ligne de commande « java -XX:+PrintFlagsFinal -version » en filtrant sur la chaine « AES »)
- La bibliothèque Java BouncyCastle ne le supporte a priori pas nativement (à vérifier)
- Le TDE des bases Oracle supporte l’AES-NI, à partir de Oracle 11.2.0.2, mais uniquement si on chiffre un tablespace complet. Cf http://www.oracle.com/technetwork/database/security/tde-faq-093689.html#A12013
- Si l’implémentation est logicielle (pas d’utilisation de l’AES-NI), il ne semble pas y avoir de différence significative de performance entre les différentes implémentations Java : http://www.acad.ro/sectii2002/proceedings/doc2013-2/13-Damjanovic.pdf
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 :
- déchiffrer la donnée avant l’import dans le progiciel (en sécurisant autant que possible le dernier maillon du flux)
- et (si nécessaire) utiliser du chiffrement transparent de la base de données (ex : Oracle Transparent Database Encryption http://www.oracle.com/technetwork/database/security/twp-transparent-data-encryption-bes-130696.pdf mais attention cette technologie n’est disponible que dans certaines versions d’Oracle, qui coûtent plutôt cher)
- et (si vraiment nécessaire) chiffrer les échanges avec la base de données (exemple pour Oracle : https://docs.oracle.com/cd/B28359_01/java.111/b31224/clntsec.htm#CIHBIEHA)
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).