Ошибка шифрования на 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;