與server通訊要求用AES加密,但Android加密後,送到server卻報錯。
先提供加密的實作方式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| public class AESUtil { private static final String KEY_ALGORITHM = "AES"; private static final String DEFAULT_CIPHER_ALGORITHM = "AES/ECB/PKCS5Padding";
public static String encrypt(String data, String key) { try { Cipher cipher = Cipher.getInstance(DEFAULT_CIPHER_ALGORITHM); byte[] byteContent = data.getBytes(StandardCharsets.UTF_8); cipher.init(Cipher.ENCRYPT_MODE, getSecretKey(key)); byte[] result = cipher.doFinal(byteContent); return new String(Base64.encodeBase64(result), StandardCharsets.UTF_8); } catch (Exception e) { e.printStackTrace(); return null; } }
private static SecretKeySpec getSecretKey(String key) { try { KeyGenerator kg = KeyGenerator.getInstance(KEY_ALGORITHM); SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG"); secureRandom.setSeed(key.getBytes(StandardCharsets.UTF_8)); System.out.println("secureRandom.getProvider()=" + secureRandom.getProvider()); kg.init(128, secureRandom); SecretKey secretKey = kg.generateKey(); return new SecretKeySpec(secretKey.getEncoded(), KEY_ALGORITHM); } catch (Exception e) { e.printStackTrace(); return null; } }
|
同樣是對Hello world字串作加密,每次執行,在Java會得到同樣的結果StB5b2z9qPuulJ7rt2V1hQ==,但在Android跑結果都不同。
1
| AESUtil.encrypt("Hello world", "C6DFE59F58353AEF73549370CD98F0A9");
|
原因是SecureRandom不一致,Java的Provider是使用SUN version 17,Android則是AndroidOpenSSL version 1.0。
彙整資訊為下表:
|
Java |
Android |
AES加密結果 |
固定 |
每次跑都不同 |
Provider |
SUN version 17 |
AndroidOpenSSL version 1.0 |
解法是將Java實作的SecureRandom搬進Android,首先找到source code
複製SecureRandom進專案後,重新命名為SunSecureRandom,將原先彈性取用SHA演算法的寫法改為自行實作的SHA。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| private void init(byte[] seed) { digest = new SHA();
if (seed != null) { engineSetSeed(seed); } }
|
SHA新增3個method,讓SunSecureRandom可以使用。
1 2 3 4 5 6 7 8 9 10 11 12
| void update(byte[] input) { engineUpdate(input, 0, input.length); }
byte[] digest() { return engineDigest(); }
byte[] digest(byte[] input) { update(input); return digest(); }
|
原本Unsafe都是呼叫底層native,改成自行實作的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| public final class Unsafe {
private Unsafe() {}
private static final Unsafe theUnsafe = new Unsafe();
public static Unsafe getUnsafe() { return theUnsafe; }
public int getInt(byte[] o, long offset) { ByteBuffer bf = ByteBuffer.wrap(o, (int) offset, 4); return bf.getInt(); }
public void putInt(byte[] o, long offset, int x) { ByteBuffer bf = ByteBuffer.wrap(o); bf.putInt((int) offset, x); } }
|
改寫取得SecretKey的方式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| private static SecretKeySpec getSecretKey(String key) { try { KeyGenerator kg = KeyGenerator.getInstance(KEY_ALGORITHM); kg.init(128); SecretKey secretKey = kg.generateKey(); byte[] keyBytes = secretKey.getEncoded(); SunSecureRandom sunRandom = new SunSecureRandom(); sunRandom.engineSetSeed(key.getBytes(StandardCharsets.UTF_8)); sunRandom.engineNextBytes(keyBytes); return new SecretKeySpec(keyBytes, KEY_ALGORITHM); } catch (Exception e) { e.printStackTrace(); return null; } }
|
最終不管是在Java或Android上跑都可以得到同樣的加密結果,完整實作範例於此 https://github.com/usermark/AesSample