Android 4.2上的加密错误

31

以下代码在除最新版本4.2之外的所有Android版本上都可运行

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是否存在重大变化?

非常感谢。


使用 PRNG 作为密钥派生函数非常糟糕。PRNG 的整个概念是它应该是不确定性的。至少只需使用 sha256,或者更好地研究一下 HKDF。 - Patrick
3个回答

62

根据Android Jellybean页面

修改了SecureRandom和Cipher.RSA的默认实现,以使用OpenSSL。

他们将SecureRandom的默认提供者更改为使用OpenSSL,而不是之前的Crypto提供者。

以下代码在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" );

毫无疑问,在Javadocs中所述的情况下,调用SecureRandom.setSeed是很危险的:

播种SecureRandom可能是不安全的

种子是用于引导随机数生成的字节数组。 为了产生密码学上安全的随机数,种子和算法都必须是安全的。

默认情况下,该类的实例将使用内部熵源(例如/dev/urandom)生成初始种子。 这个种子是不可预测的,并且适合进行安全使用。

您可以通过使用带有种子的构造函数或在生成任何随机数之前调用setSeed(byte[])来明确指定初始种子。 指定固定的种子会导致该实例返回可预测的数字序列。 这可能对于测试有用,但不适合安全使用。

然而,对于编写单元测试,就像你正在做的那样,使用setSeed可能是可以的。


非常感谢您详细的回答。我已经验证了这种行为,并且可以确认额外的“Crypto”参数确实会恢复到旧的实现方式。 - Robert Estivill
2
你尝试过在Android 4.2中解密在Android <= 4.2中加密的数据吗?我有和你一样的代码,但是我得到了“填充块损坏”异常。 - pandre
所以你的意思是,在这个修复之后,代码无法解密之前加密的内容?可能是种子问题吗? - Robert Estivill
@Robert Estivill,是的,我无法解密旧版本代码/操作系统加密的数据。种子是相同的(我正在使用secrand.setSeed(seed.getBytes());)。但是,我能够解密新版本代码/操作系统加密的数据。您是否能够解密旧版本代码/操作系统加密的数据? - pandre
你对这个有什么想法吗:https://dev59.com/4GYr5IYBdhLWcg3wvslA? - pandre

3
在Android 4.2中,正如Brigham所指出的那样,有一个安全增强,它将SecureRandom的默认实现从Crypto更新为OpenSSL。

密码学 - 修改了SecureRandom和Cipher.RSA的默认实现,使用OpenSSL。添加了对TLSv1.1和TLSv1.2的SSL套接字支持,使用OpenSSL 1.0.1

但Brigham的答案只是一个临时解决方案,不建议使用,因为虽然它解决了问题,但仍然是错误的方法。
推荐的方法(请参考Nelenkov的教程)是使用适当的密钥派生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();

0

因此,您正在尝试将伪随机生成器用作密钥派生函数。 由于以下原因,这是不好的:

  • PRNG按设计是非确定性的,而您正在依赖它是确定性的
  • 依赖错误和已弃用的实现会在某一天破坏您的应用程序
  • PRNG未被设计为良好的KDF

更准确地说,谷歌不建议在Android N(SDK 24)中使用Crypto提供程序

以下是一些更好的方法:

基于哈希消息认证码(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;

还有更多的KDFs,例如BCryptscryptArgon2


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接