前言
最近需要一个加密一下用户信息,想到用到AES,加密,没想到苦难重重。
第一版
随便上晚上找了一下代码如下:
//偏移量
public static final String VIPARA = "1234567876543210"; //AES 为16bytes. DES 为8bytes
//编码方式
public static final String CODE_TYPE = "UTF-8";
//填充类型
public static final String AES_TYPE = "AES/ECB/PKCS5Padding";
//私钥
private static final String AES_KEY="1111222233334444"; //AES固定格式为128/192/256 bits.即:16/24/32bytes。DES固定格式为128bits,即8bytes。
/**
* 加密
*
* @param cleartext
* @return
*/
public static String encrypt(String cleartext) {
try {
//两个参数,第一个为私钥字节数组, 第二个为加密方式 AES或者DES
SecretKeySpec key = new SecretKeySpec(AES_KEY.getBytes(), "AES");
//实例化加密类,参数为加密方式,要写全
Cipher cipher = Cipher.getInstance(AES_TYPE); //PKCS5Padding比PKCS7Padding效率高,PKCS7Padding可支持IOS加解密
//初始化,此方法可以采用三种方式,按加密算法要求来添加。(1)无第三个参数(2)第三个参数为SecureRandom random = new SecureRandom();中random对象,随机数。(AES不可采用这种方法)(3)采用此代码中的IVParameterSpec
//加密时使用:ENCRYPT_MODE; 解密时使用:DECRYPT_MODE;
cipher.init(Cipher.ENCRYPT_MODE, key); //CBC类型的可以在第三个参数传递偏移量zeroIv,ECB没有偏移量
//加密操作,返回加密后的字节数组,然后需要编码。主要编解码方式有Base64, HEX, UUE,7bit等等。此处看服务器需要什么编码方式
byte[] encryptedData = cipher.doFinal(cleartext.getBytes(CODE_TYPE));
return Base64.encodeToString(encryptedData,Base64.DEFAULT);
} catch (Exception e) {
e.printStackTrace();
return "";
}
}
/**
* 解密
*
* @param encrypted
* @return
*/
public static String decrypt(String encrypted) {
try {
byte[] byteMi = Base64.decode(encrypted,Base64.DEFAULT);
SecretKeySpec key = new SecretKeySpec(
AES_KEY.getBytes(), "AES");
Cipher cipher = Cipher.getInstance(AES_TYPE);
//与加密时不同MODE:Cipher.DECRYPT_MODE
cipher.init(Cipher.DECRYPT_MODE, key); //CBC类型的可以在第三个参数传递偏移量zeroIv,ECB没有偏移量
byte[] decryptedData = cipher.doFinal(byteMi);
return new String(decryptedData, CODE_TYPE);
} catch (Exception e) {
e.printStackTrace();
return "";
}
}
测试一下,OK,没问题,但是觉得好像哪里不对,我本来是为了安全考虑才加密数据的,结果这样把加密的密钥写在类文件是不是不太合适? 所以,又找了一下看如何安全一点。
第二版
先只展示加密,解密道理相同,最后会附上完整代码
public static String encrypt(String key, String content) {
try {
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
@SuppressLint("DeletedProvider") SecureRandom secureRandom =SecureRandom.getInstance("SHA1PRNG","Crypto");
secureRandom.setSeed(key.getBytes());
keyGenerator.init(128, secureRandom);
SecretKey secretKey = keyGenerator.generateKey();
byte[] keyEncoded = secretKey.getEncoded();
SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes(), "AES");
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec);
byte[] doFinal = cipher.doFinal(content.getBytes(CODE_TYPE));
return Base64.encodeToString(doFinal, Base64.DEFAULT);
} catch (Exception e) {
e.printStackTrace();
}
return "error encrypt";
}
又是再网上找了一段代码,比之前多了下面这几行:
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
@SuppressLint("DeletedProvider") SecureRandom secureRandom =SecureRandom.getInstance("SHA1PRNG","Crypto");
secureRandom.setSeed(key.getBytes());
keyGenerator.init(128, secureRandom);
SecretKey secretKey = keyGenerator.generateKey();
byte[] keyEncoded = secretKey.getEncoded();
简单介绍下新增这几个类的作用:
- KeyGenerator 密钥生成器,传入AES,说明我们最后要生成的时AES的密钥
- SecureRandom 安全随机算法,他的作用时将我们的密钥经过一定的算法("SHA1PRNG"强随机算法),并通过"Crypto"安全供应商返回,其实说白了。就是给密钥利用随机算法加盐,使得密钥更安全。
- 最后返回新的密钥keyEncoded
问题也时出现再这里AndroidN(API=27),不再支持SHA1PRNG算法的实现以及Crypto这个安全供应商,原因是不安全,也不可靠参考原因
第三版
兼容版本
@SuppressLint("DeletedProvider")
public static String encrypt(String key, String content) {
try {
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
SecureRandom secureRandom =null;
// 在4.2以上版本中,SecureRandom获取方式发生了改变
int sdk_version = android.os.Build.VERSION.SDK_INT;
// Android 6.0 以上
if (sdk_version > 23) {
secureRandom = SecureRandom.getInstance("SHA1PRNG", new AesUtil.CryptoProvider());
//4.2及以上
} else if (android.os.Build.VERSION.SDK_INT >= 17) {
secureRandom = SecureRandom.getInstance("SHA1PRNG", "Crypto");
} else {
secureRandom = SecureRandom.getInstance("SHA1PRNG");
}
secureRandom.setSeed(key.getBytes());
keyGenerator.init(128, secureRandom);
SecretKey secretKey = keyGenerator.generateKey();
byte[] keyEncoded = secretKey.getEncoded();
// Log.e(TAG, "keyEncoded: " + keyEncoded.length);
SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes(), "AES");
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec);
byte[] doFinal = cipher.doFinal(content.getBytes(CODE_TYPE));
return Base64.encodeToString(doFinal, Base64.DEFAULT);
} catch (Exception e) {
e.printStackTrace();
}
return "error encrypt";
}
测试后好像更安全了,因为密钥多进行了一次加密。 现在要考虑的问题是,如何保存要是密钥字符串,本地文件好像也不安全,JNI编译后后生成so,单单加密一个用户信息,有点太重了。
那么放在哪里呢?
第四版 KeyStore
这个是Google建议使用的,翻译如下:
Android的Keystore系统可以把密钥保持在一个难以从设备中取出数据的容器中。
当密钥保存到Keystore之后,可以在不取出密钥的状态下进行私密操作。
此外,它提供了限制何时以何种方式使用密钥的方法,比如使用密钥时需要用户认证或限制密钥只能在加密模式下使用
简单来说就是,我们生成密钥,然后保存再自己手机的内部缓存目录(也就是只有应用自己可见的目录),KeyStore是个系统,一个应用程式只能编辑、保存、取出自己的密钥。这样就大大提升密钥的安全性,再加上之前的代码,问题就解决了。
具体参考这篇译文Android保存私密信息-强大的keyStore(译)
源码:
public class CryptoUtils {
private static final String TAG = "TAG";
public static final String DEFAULT_SECRETKEY_NAME = "default_secretkey_name";
public static final String STORE_FILE_NAME = "crypto";
private KeyStore keyStore;
private CryptoUtils(KeyStore keyStore) {
this.keyStore = keyStore;
}
@SuppressWarnings("ResultOfMethodCallIgnored")
public synchronized static CryptoUtils getInstance(Context context) {
KeyStore keyStore;
File file = new File(context.getFilesDir(), STORE_FILE_NAME);
Log.e(TAG, "file path: " + file.getPath());
try {
keyStore = getKeyStore(file);
initKey(keyStore, file);
CryptoUtils crypto = new CryptoUtils(keyStore);
return crypto;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
private static void initKey(KeyStore keyStore, File file) throws Exception {
if (!keyStore.containsAlias(DEFAULT_SECRETKEY_NAME)) { // 秘钥不存在,则生成秘钥
KeyGenerator keyGenerator = generateKeyGenerator();
SecretKey secretKey = keyGenerator.generateKey();
storeKey(keyStore, file, secretKey);
}
}
private static void storeKey(KeyStore keyStore, File file, SecretKey secretKey) throws Exception {
if (Build.VERSION.SDK_INT >= 23) {
keyStore.setKeyEntry(DEFAULT_SECRETKEY_NAME, secretKey, null, null);
} else {
keyStore.setKeyEntry(DEFAULT_SECRETKEY_NAME, secretKey, null, null);
FileOutputStream fos = null;
try {
fos = new FileOutputStream(file);
keyStore.store(fos, null);
fos.close();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (fos != null) {
fos.close();
}
}
}
}
private static KeyStore getKeyStore(File file) throws Exception {
if (Build.VERSION.SDK_INT >= 23) {
KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
keyStore.load(null);
return keyStore;
} else {
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
if (!file.exists()) {
boolean isSuccess = file.createNewFile();
if (!isSuccess) {
throw new SecurityException("创建内部存储文件失败");
}
keyStore.load(null, null);
} else if (file.length() <= 0) {
keyStore.load(null, null);
} else {
FileInputStream fis = null;
try {
fis = new FileInputStream(file);
keyStore.load(fis, null);
fis.close();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (fis != null) {
fis.close();
}
}
}
return keyStore;
}
}
@SuppressLint("DeletedProvider")
private static KeyGenerator generateKeyGenerator() throws Exception {
KeyGenerator keyGenerator;
if (Build.VERSION.SDK_INT >= 23) {
keyGenerator = KeyGenerator.getInstance("AES", "AndroidKeyStore");
keyGenerator.init(new KeyGenParameterSpec.Builder(DEFAULT_SECRETKEY_NAME,
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
.setUserAuthenticationRequired(false)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
.build());
} else {
keyGenerator = KeyGenerator.getInstance("AES");
SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG", "Crypto");
secureRandom.setSeed(generateSeed());
keyGenerator.init(128, secureRandom);
}
return keyGenerator;
}
private static byte[] generateSeed() {
try {
ByteArrayOutputStream seedBuffer = new ByteArrayOutputStream();
DataOutputStream seedBufferOut =
new DataOutputStream(seedBuffer);
seedBufferOut.writeLong(System.currentTimeMillis());
seedBufferOut.writeLong(System.nanoTime());
seedBufferOut.writeInt(android.os.Process.myPid());
seedBufferOut.writeInt(android.os.Process.myUid());
seedBufferOut.write(Build.BOARD.getBytes());
return seedBuffer.toByteArray();
} catch (IOException e) {
throw new SecurityException("Failed to generate seed", e);
}
}
/**
* AES加密
*
* @param content
* @return
*/
public EncryptData aesEncrypt(String alias, String content) {
try {
SecretKey secretKey = getSecretKey(keyStore);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
byte[] bytes = cipher.doFinal(StringUtils.string2Bytes(content));
byte[] iv = cipher.getIV();
String encryptString = Base64.encodeToString(bytes, Base64.NO_WRAP);
return new EncryptData(alias, encryptString, iv);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* AES解密
*
* @param encryptData
* @return
*/
public String aesDecrypt(EncryptData encryptData) {
try {
SecretKey secretKey = getSecretKey(keyStore);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");
cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(encryptData.getIv()));
byte[] bytes = cipher.doFinal(Base64.decode(encryptData.getEncryptString()
, Base64.NO_WRAP));
return StringUtils.bytes2String(bytes);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
private static SecretKey getSecretKey(KeyStore keyStore) {
try {
return (SecretKey) keyStore.getKey(DEFAULT_SECRETKEY_NAME, null);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
文章参考 :
『译文』Security "Crypto" provider deprecated in Android N - Android N中不再支持“Crypto”安全供应商的相关方法
网友评论