Ошибка шифрования на Android 4.2

Следующий код работает на всех версиях Android, кроме последней 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();
    }
}

Вот небольшой модульный тест, который я создал, чтобы воспроизвести ошибку

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 );
}

}

Строка, выдающая исключение:

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

Полная трассировка стека:

    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)

Кто-нибудь знает, что может происходить? Кто-нибудь знает о критических изменениях на Android 4.2 в любом из упомянутых классов?

большое спасибо

3 ответа

Решение

Со страницы Android Jellybean:

Изменены стандартные реализации SecureRandom и Cipher.RSA для использования OpenSSL

Они изменили поставщика по умолчанию для SecureRandom использовать OpenSSL вместо предыдущего провайдера Crypto.

Следующий код произведет два разных вывода на pre-Android 4.2 и Android 4.2:

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

На устройствах до 4.2:

rand.getProvider: Crypto

На 4.2 устройствах:

rand.getProvider: AndroidOpenSSL

К счастью, легко вернуться к старому поведению:

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

Конечно, звонить опасно SecureRandom.setSeed вообще в свете Javadocs, которые утверждают:

Посев SecureRandom может быть небезопасным

Начальное число - это массив байтов, используемых для запуска генерации случайных чисел. Чтобы получить криптографически безопасные случайные числа, и начальное число, и алгоритм должны быть безопасными.

По умолчанию экземпляры этого класса будут генерировать начальное начальное число, используя внутренний источник энтропии, такой как /dev/urandom. Это семя непредсказуемо и подходит для безопасного использования.

В качестве альтернативы вы можете указать начальное начальное число явно с помощью засеянного конструктора или путем вызова setSeed(byte[]) до того, как будут сгенерированы случайные числа. Указание фиксированного начального числа приведет к тому, что экземпляр вернет предсказуемую последовательность чисел. Это может быть полезно для тестирования, но не подходит для безопасного использования.

Тем не менее, для написания юнит-тестов, как вы делаете, используя setSeed может быть в порядке.

Как отметил Бригам, в Android 4.2 появилось улучшение безопасности, которое обновило стандартную реализацию SecureRandom от Crypto до OpenSSL

Криптография - Изменены стандартные реализации SecureRandom и Cipher.RSA для использования OpenSSL. Добавлена ​​поддержка сокетов SSL для TLSv1.1 и TLSv1.2 с использованием OpenSSL 1.0.1

Ответ Бу Бригама является временным решением и не рекомендуется, потому что, хотя он решает проблему, он все равно поступает неправильно.

Рекомендуемый способ (см. Руководство Неленкова) состоит в том, чтобы использовать надлежащие ключи получения PKCS (Стандарт криптографии с открытым ключом), который определяет две функции получения ключей, PBKDF1 и PBKDF2, из которых PBKDF2 более рекомендуется.

Вот как ты должен получить ключ,

    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();

Итак, что вы пытаетесь использовать псевдослучайный генератор в качестве функции вывода ключа. Это плохо по следующим причинам:

  • PRNG по своей природе недетерминированы, и вы полагаетесь на то, что они детерминированы
  • Опора на ошибку и устаревшие реализации однажды сломает ваше приложение.
  • PRNG не предназначены для хороших KDF

Точнее, Google осудил использование Crypto провайдер в Android N (SDK 24)

Вот несколько лучших методов:

Функция получения ключа на основе хешированного кода аутентификации (HMAC) (HKDF)

Используя эту библиотеку:

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 (функция получения ключа на основе пароля 2)

имеет растяжение ключа, что делает его более дорогим. Используйте это для ввода слабых клавиш (например, пароля пользователя):

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

Есть еще KDF, такие как BCrypt, scrypt и Argon2

Другие вопросы по тегам