web-dev-qa-db-fra.com

Erreur de cryptage sur Android 4.2

Le code suivant fonctionne sur toutes les versions d'Android sauf la dernière version 4.2.

import Java.security.InvalidKeyException;
import Java.security.NoSuchAlgorithmException;
import Java.security.SecureRandom;

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

/**
 * Util class to perform encryption/decryption over strings. <br/>
 */
public final class UtilsEncryption
{
    /** The logging TAG */
    private static final String TAG = UtilsEncryption.class.getName();

    /** */
    private static final String KEY = "some_encryption_key";

    /**
     * Avoid instantiation. <br/>
     */
    private UtilsEncryption()
    {
    }

    /** The HEX characters */
    private final static String HEX = "0123456789ABCDEF";

    /**
     * Encrypt a given string. <br/>
     * 
     * @param the string to encrypt
     * @return the encrypted string in HEX
     */
    public static String encrypt( String cleartext )
    {
        try
        {
            byte[] result = process( Cipher.ENCRYPT_MODE, cleartext.getBytes() );
            return toHex( result );
        }
        catch ( Exception e )
        {
            System.out.println( TAG + ":encrypt:" + e.getMessage() );
        }
        return null;
    }

    /**
     * Decrypt a HEX encrypted string. <br/>
     * 
     * @param the HEX string to decrypt
     * @return the decrypted string
     */
    public static String decrypt( String encrypted )
    {
        try
        {
            byte[] enc = fromHex( encrypted );
            byte[] result = process( Cipher.DECRYPT_MODE, enc );
            return new String( result );
        }
        catch ( Exception e )
        {
            System.out.println( TAG + ":decrypt:" + e.getMessage() );
        }
        return null;
    }


    /**
     * Get the raw encryption key. <br/>
     * 
     * @param the seed key
     * @return the raw key
     * @throws NoSuchAlgorithmException
     */
    private static byte[] getRawKey()
        throws NoSuchAlgorithmException
    {
        KeyGenerator kgen = KeyGenerator.getInstance( "AES" );
        SecureRandom sr = SecureRandom.getInstance( "SHA1PRNG" );
        sr.setSeed( KEY.getBytes() );
        kgen.init( 128, sr );
        SecretKey skey = kgen.generateKey();
        return skey.getEncoded();
    }

    /**
     * Process the given input with the provided mode. <br/>
     * 
     * @param the cipher mode
     * @param the value to process
     * @return the processed value as byte[]
     * @throws InvalidKeyException
     * @throws IllegalBlockSizeException
     * @throws BadPaddingException
     * @throws NoSuchAlgorithmException
     * @throws NoSuchPaddingException
     */
    private static byte[] process( int mode, byte[] value )
        throws InvalidKeyException, IllegalBlockSizeException, BadPaddingException,     NoSuchAlgorithmException,
        NoSuchPaddingException
    {
        SecretKeySpec skeySpec = new SecretKeySpec( getRawKey(), "AES" );
        Cipher cipher = Cipher.getInstance( "AES" );
        cipher.init( mode, skeySpec );
        byte[] encrypted = cipher.doFinal( value );
        return encrypted;
    }

    /**
     * Decode an HEX encoded string into a byte[]. <br/>
     * 
     * @param the HEX string value
     * @return the decoded byte[]
     */
    protected static byte[] fromHex( String value )
    {
        int len = value.length() / 2;
        byte[] result = new byte[len];
        for ( int i = 0; i < len; i++ )
        {
            result[i] = Integer.valueOf( value.substring( 2 * i, 2 * i + 2 ), 16     ).byteValue();
        }
        return result;
    }

    /**
     * Encode a byte[] into an HEX string. <br/>
     * 
     * @param the byte[] value
     * @return the HEX encoded string
     */
    protected static String toHex( byte[] value )
    {
        if ( value == null )
        {
            return "";
        }
        StringBuffer result = new StringBuffer( 2 * value.length );
        for ( int i = 0; i < value.length; i++ )
        {
            byte b = value[i];

            result.append( HEX.charAt( ( b >> 4 ) & 0x0f ) );
            result.append( HEX.charAt( b & 0x0f ) );
        }
        return result.toString();
    }
}

Voici un petit test unitaire que j'ai créé pour reproduire l'erreur

import junit.framework.TestCase;

public class UtilsEncryptionTest
    extends TestCase
{
    /** A random string */
    private static String ORIGINAL = "some string to test";

    /**
     * The HEX value corresponds to ORIGINAL. <br/>
     * If you change ORIGINAL, calculate the new value on one of this sites:
     * <ul>
     * <li>http://www.string-functions.com/string-hex.aspx</li>
     * <li>http://www.yellowpipe.com/yis/tools/encrypter/index.php</li>
     * <li>http://www.convertstring.com/EncodeDecode/HexEncode</li>
     * </ul>
     */
    private static String HEX = "736F6D6520737472696E6720746F2074657374";

    public void testToHex()
    {
         String hexString = UtilsEncryption.toHex( ORIGINAL.getBytes() );

         assertNotNull( "The HEX string should not be null", hexString );
         assertTrue( "The HEX string should not be empty", hexString.length() > 0 );
         assertEquals( "The HEX string was not encoded correctly", HEX, hexString );
    }

    public void testFromHex()
    {
         byte[] stringBytes = UtilsEncryption.fromHex( HEX );

         assertNotNull( "The HEX string should not be null", stringBytes );
        assertTrue( "The HEX string should not be empty", stringBytes.length > 0 );
        assertEquals( "The HEX string was not encoded correctly", ORIGINAL, new String( stringBytes ) );
    }

    public void testWholeProcess()
    {
         String encrypted = UtilsEncryption.encrypt( ORIGINAL );
         assertNotNull( "The encrypted result should not be null", encrypted );
         assertTrue( "The encrypted result should not be empty", encrypted.length() > 0 );

         String decrypted = UtilsEncryption.decrypt( encrypted );
         assertNotNull( "The decrypted result should not be null", decrypted );
         assertTrue( "The decrypted result should not be empty", decrypted.length() > 0 );

         assertEquals( "Something went wrong", ORIGINAL, decrypted );
}

}

La ligne qui lance l'exception est la suivante:

byte[] encrypted = cipher.doFinal( value );

La trace de pile complète est:

    W/<package>.UtilsEncryption:decrypt(16414): pad block corrupted
    W/System.err(16414): javax.crypto.BadPaddingException: pad block corrupted
    W/System.err(16414):    at com.Android.org.bouncycastle.jcajce.provider.symmetric.util.BaseBlockCipher.engineDoFinal(BaseBlockCipher.Java:709)
    W/System.err(16414):    at javax.crypto.Cipher.doFinal(Cipher.Java:1111)
    W/System.err(16414):    at <package>.UtilsEncryption.process(UtilsEncryption.Java:117)
    W/System.err(16414):    at <package>.UtilsEncryption.decrypt(UtilsEncryption.Java:69)
    W/System.err(16414):    at <package>.UtilsEncryptionTest.testWholeProcess(UtilsEncryptionTest.Java:74)
    W/System.err(16414):    at Java.lang.reflect.Method.invokeNative(Native Method)
    W/System.err(16414):    at Java.lang.reflect.Method.invoke(Method.Java:511)
    W/System.err(16414):    at junit.framework.TestCase.runTest(TestCase.Java:168)
    W/System.err(16414):    at junit.framework.TestCase.runBare(TestCase.Java:134)
    W/System.err(16414):    at junit.framework.TestResult$1.protect(TestResult.Java:115)
    W/System.err(16414):    at junit.framework.TestResult.runProtected(TestResult.Java:133)
D/elapsed (  588): 14808
    W/System.err(16414):    at junit.framework.TestResult.run(TestResult.Java:118)
    W/System.err(16414):    at junit.framework.TestCase.run(TestCase.Java:124)
    W/System.err(16414):    at Android.test.AndroidTestRunner.runTest(AndroidTestRunner.Java:190)
    W/System.err(16414):    at Android.test.AndroidTestRunner.runTest(AndroidTestRunner.Java:175)
    W/System.err(16414):    at Android.test.InstrumentationTestRunner.onStart(InstrumentationTestRunner.Java:555)
    W/System.err(16414):    at Android.app.Instrumentation$InstrumentationThread.run(Instrumentation.Java:1661)

Quelqu'un a-t-il une idée de ce qui pourrait se produire? 

Merci beaucoup

29
Robert Estivill

À partir de la page Android Jellybean :

Modification des implémentations par défaut de SecureRandom et Cipher.RSA pour utiliser OpenSSL

Ils ont changé le fournisseur par défaut pour SecureRandom afin d’utiliser OpenSSL au lieu du fournisseur Crypto précédent.

Le code suivant générera deux sorties différentes sur les versions antérieures à Android 4.2 et Android 4.2:

SecureRandom Rand = SecureRandom.getInstance("SHA1PRNG");
Log.i(TAG, "Rand.getProvider(): " + Rand.getProvider().getName());

Sur les appareils antérieurs à la version 4.2:

Rand.getProvider: Crypto

Sur les appareils 4.2:

Rand.getProvider: AndroidOpenSSL

Heureusement, il est facile de revenir à l'ancien comportement:

SecureRandom sr = SecureRandom.getInstance( "SHA1PRNG", "Crypto" );

Pour être sûr, il est dangereux d’appeler SecureRandom.setSeed du tout à la lumière du Javadocs qui indique: 

Semer SecureRandom peut ne pas être sécurisé

Une graine est un tableau d'octets utilisé pour amorcer la génération de nombres aléatoires. Pour produire des nombres aléatoires sécurisés de manière cryptographique, la graine et l'algorithme doivent être sécurisés.

Par défaut, les instances de cette classe généreront une graine initiale à l'aide d'une source d'entropie interne, telle que/dev/urandom. Cette graine est imprévisible et appropriée pour une utilisation sécurisée.

Vous pouvez également spécifier explicitement le germe initial avec le constructeur ou en appelant setSeed (byte []) avant que des nombres aléatoires aient été générés. Si vous spécifiez une valeur de départ fixe, l’instance renverra une séquence prévisible de nombres. _ {Cela peut être utile pour les tests mais pas pour une utilisation sécurisée.

Cependant, pour écrire des tests unitaires, comme vous le faites, utiliser setSeed peut convenir.

61
Brigham

Comme Brigham a souligné que, dans Android 4.2, il existait une amélioration de la sécurité , qui a mis à jour l'implémentation par défaut de SecureRandom de Crypto à OpenSSL.

Cryptography - Modification des implémentations par défaut de SecureRandom et Cipher.RSA pour utiliser OpenSSL. Ajout du support SSL Socket pour TLSv1.1 et TLSv1.2 sous OpenSSL 1.0.1

la réponse de bu Brigham est une solution temporaire et non recommandée, car même si elle résout le problème, elle ne fonctionne toujours pas correctement. 

La méthode recommandée (vérifier dans le didacticiel de Nelenkov ) consiste à utiliser les dérivations de clé appropriées PKCS (norme de cryptographie à clé publique), qui définit deux fonctions de dérivation de clé, PBKDF1 et PBKDF2, parmi lesquelles PBKDF2 est plus recommandé.

Voici comment vous devriez obtenir la clé,

    int iterationCount = 1000;
    int saltLength = 8; // bytes; 64 bits
    int keyLength = 256;
    SecureRandom random = new SecureRandom();
    byte[] salt = new byte[saltLength];
    random.nextBytes(salt);
    KeySpec keySpec = new PBEKeySpec(seed.toCharArray(), salt,
            iterationCount, keyLength);
    SecretKeyFactory keyFactory = SecretKeyFactory
            .getInstance("PBKDF2WithHmacSHA1");
    byte[] raw = keyFactory.generateSecret(keySpec).getEncoded();
3
Sufiyan Ghori

Donc, ce que vous essayez d’utiliser, c’est d’utiliser le générateur pseudo-aléatoire comme fonction de dérivation de la touche . C'est mauvais pour les raisons suivantes:

  • Les PRNG sont par nature non déterministes et vous vous en remettez à déterminisme
  • S'appuyer sur un bogue et des implémentations obsolètes vont casser votre application un jour
  • Les PRNG ne sont pas conçus pour être de bons FDK

Plus précisément, Google déconseille l'utilisation du fournisseur Crypto sous Android N (SDK 24)

Voici quelques meilleures méthodes:

Fonction de dérivation de clé basée sur le code d'authentification de message haché (HMAC) (HKDF)

En utilisant cette bibliothèque :

String userInput = "this is a user input with bad entropy";

HKDF hkdf = HKDF.fromHmacSha256();

//extract the "raw" data to create output with concentrated entropy
byte[] pseudoRandomKey = hkdf.extract(staticSalt32Byte, userInput.getBytes(StandardCharsets.UTF_8));

//create expanded bytes for e.g. AES secret key and IV
byte[] expandedAesKey = hkdf.expand(pseudoRandomKey, "aes-key".getBytes(StandardCharsets.UTF_8), 16);

//Example boilerplate encrypting a simple string with created key/iv
SecretKey key = new SecretKeySpec(expandedAesKey, "AES"); //AES-128 key

PBKDF2 (fonction de dérivation de clé basée sur un mot de passe 2)

a key stretching ce qui rend plus onéreuse de forcer brutalement la clé. Utilisez ceci pour la saisie de clé faible (comme un mot de passe utilisateur):

SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
    KeySpec keySpec = new PBEKeySpec(passphraseOrPin, salt, iterations, outputKeyLength);
    SecretKey secretKey = secretKeyFactory.generateSecret(keySpec);
    return secretKey;

Il y a plus de KDF comme BCrypt , scrypt et Argon2

0
patrickf