Android中的AES加密--上

前言

最近需要一个加密一下用户信息,想到用到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”安全供应商的相关方法

Android 9.0 加密适配

Java实现AES加密

Android KeyStore密钥存储

Android:7.0 后加密库 Crypto 被废弃后的爬坑指南

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,373评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,732评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,163评论 0 238
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,700评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,036评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,425评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,737评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,421评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,141评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,398评论 2 243
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,908评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,276评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,907评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,018评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,772评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,448评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,325评论 2 261