以下代码适用于除最新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上有重大变化吗?
非常感谢
修改了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:加密
在4.2设备上:
rand.getProvider:AndroidOpenSSL
幸运的是,很容易恢复旧的行为:
SecureRandom sr = SecureRandom.getInstance( "SHA1PRNG", "Crypto" );
可以肯定的是,根据SecureRandom.setSeed
的说法完全打电话给Javadocs很危险:
播种SecureRandom可能不安全
种子是用于引导随机数生成的字节数组。为了产生加密安全的随机数,种子和算法都必须是安全的。
默认情况下,此类的实例将使用内部熵源生成初始种子,例如/ dev / urandom。这种种子是不可预测的,适合安全使用。
您也可以使用种子构造函数明确指定初始种子,或者在生成任何随机数之前调用setSeed(byte [])。指定固定种子将导致实例返回可预测的数字序列。这可能对测试有用,但不适合安全使用。
但是,对于编写单元测试,正如您所做的那样,使用setSeed
可能没问题。
正如Brigham指出的那样,在Android 4.2中,有一个security enhancement,它将SecureRandom
的默认实现从Crypto更新为OpenSSL
加密 - 修改SecureRandom和Cipher.RSA的默认实现以使用OpenSSL。使用OpenSSL 1.0.1为TLSv1.1和TLSv1.2添加了SSL套接字支持
布里格姆的答案是一个临时的解决方案,不推荐,因为虽然它解决了问题,但它仍然采取错误的方式。
推荐的方法(检查Nelenkov’s tutorial)是使用正确的密钥派生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();
所以你要尝试的是使用pseudo random generator作为key derivation function。这很糟糕,原因如下:
更准确地说是Google deprecated the use of the Crypto
provider in Android N(SDK 24)
这是一些更好的方法:
使用这个library:
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
有key stretching,这使得暴力关键的成本更高。用于弱键输入(如用户密码):
SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
KeySpec keySpec = new PBEKeySpec(passphraseOrPin, salt, iterations, outputKeyLength);
SecretKey secretKey = secretKeyFactory.generateSecret(keySpec);
return secretKey;