Android外部文件加解密及应用实践

有这样的应用场景,当我们把一些重要文件放到asset文件夹中时,把.apk解压是可以直接拿到这个文件的,一些涉及到重要信息的文件我们并不想被反编译拿去,这个时候需要先对文件进行加密,然后放到Android中的资源目录下,用的时候再解密出来。
现代密码学中,加密系统的安全性是基于密钥的,而不是基于算法,现在介绍一整套加解密及应用流程,这套加密流程从实用性和安全性上来讲,我觉得还是很靠谱的,也是市面上比较常用的做法,核心逻辑其实比较简单,毕竟最难的加解密算法实现部分是现成的了,我司部分也用了这套流程,当然会比我讲的这个要复杂一些。

1、简介

主要涉及到一下几个算法的应用,RSA、AES,以及Base64编码,基本思想是用[AES算法+AES密钥]来加密文件,为了保证密钥的安全性,会通过[RSA算法+RSA私钥]对AES密钥进行加密。
对这几种算法不熟悉可以看看我司大佬的‘常用的加密方式和应用场景’这篇文章,知道大概的原理和使用方法就行,因为算法在java中都是现成的,直接拿来用就是了。

Android加解密流程图.png

把流程整理了一下,就是以上的流程图,分成三块:

  • 第1块是把加密过程给封装成一个小工具,用加密工具来对文件进行加密;
  • 第2块是把解密过程封装成解密的小工具,用解密工具来解密我们的文件好进行相关修改;
  • 第3块使我们的目的,就是把加密文件和加解密的AES算法密钥放到Android资源文件中进行具体的使用。

有一点需要补充的,就是RSA算法的公私钥,从第3块中可以发现,并没有把RSA的公钥和私钥放到资源文件中,其实大家想想就知道了,如果被加密文件、加解密的AES密钥、用于对AES密钥进行加密的RSA密钥三者都放入文件夹中,那就没有啥安全性可言了(注:加解密的算法可以改造成自己公司独有的,我司就是这么做的),所以为了保证安全性,我们的RSA公私钥是通过应用的签名(.keystore签名文件)中代码动态获取。感兴趣的可以看这篇文章:[从Java Keystore文件中提取私钥、证书]

2、第1块:加密工具进行加密

工具的java界面开发是通过java的swing包来实现的,对swing感兴趣的可以参考这篇Java Swing 图形界面开发简介,讲得非常详细。
一开始的时候是没有AES秘钥的,需要我们生成一个安全的秘钥,所以生成一个随机AES秘钥,然后保存,加密工具的操作页界面:

加密.png

2.1、生成随机秘钥

生成随机秘钥主要分为几步:

  • 通过UUID.randomUUID()生成随机数作为seed种子;
  • seed种子提供给KeyGenerator生成AES秘钥,只要seed种子生成的AES秘钥就是一致的;
  • 通过应用签名获取RSA算法需要的公钥私钥;
  • RSA通过私钥来加密AES秘钥;
  • 因为生成的秘钥是byte[],所以通过Base64编码展示出来给到界面上。
/**
     * 生成随机密钥
     */
    private void randomKey() {
        try {
            //生成随机数作为seed种子
            String uuid = UUID.randomUUID().toString();
            byte[] seed = uuid.getBytes("UTF-8");
            //生成AES秘钥
            byte[] rawkey = AES.getRawKey(seed);
            //获取应用签名的密钥对
            KeyPair pair = SignKey.getSignKeyPair();
            //通过RSA私钥来加密AES秘钥
            byte[] key = RSA.encrypt(rawkey, pair.getPrivate());
            //Base64编码成字符串展示
            String base64Key = Base64.encode(key);
            mKeyText.setText(base64Key);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

其中AES.getRawKey(seed)中主要是通过AES密钥生成器来生成128位的密钥,具体实现:

/**
     * 生成用AES算法来加密的密钥流,这个密钥会被应用签名{@link SignKey}的密钥进行二次加密
     */
    public static byte[] getRawKey(byte[] seed) throws Exception {
        KeyGenerator kgen = KeyGenerator.getInstance("AES");
        SecureRandom sr = SecureRandom.getInstance("SHA1PRNG");
        sr.setSeed(seed);
        //192 and 256 bits may not be available
        kgen.init(128, sr);
        SecretKey skey = kgen.generateKey();
        return skey.getEncoded();
    }

SignKey.getSignKeyPair()是获得RSA算法所需的公私钥,是从我们的应用签名来的,大家应该都很熟悉了,应用打包上传是需要签名打包的。

keystore.png

java提供了api获取testkey.keystore文件(自己用studio生成一个)的私钥和证书,把testkey.keystore文件放到目录中:

/**
 * Author:xishuang
 * Date:2018.05.06
 * Des:根据导入的应用签名,读取其中的密钥对和证书
 */
public class SignKey {
    //应用签名
    private static final String keystoreName = "testkey.keystore";
    private static final String keystorePassword = "123456";
    //应用签名的别名
    private static final String alias = "key0";
    private static final String aliasPassword = "123456";

    /**
     * 获取签名的密钥对,用来给密钥加密
     */
    public static KeyPair getSignKeyPair() {
        try {
            File storeFile = new File(keystoreName);
            if (!storeFile.exists()) {
                throw new IllegalArgumentException("还没设置签名文件!");
            }
            String keyStoreType = "JKS";
            char[] keystorepasswd = keystorePassword.toCharArray();
            char[] keyaliaspasswd = aliasPassword.toCharArray();
            KeyStore keystore = KeyStore.getInstance(keyStoreType);
            keystore.load(new FileInputStream(storeFile), keystorepasswd);

            //拿私钥
            Key key = keystore.getKey(alias, keyaliaspasswd);
            if (key instanceof PrivateKey) {
                //拿公钥
                Certificate cert = keystore.getCertificate(alias);
                PublicKey publicKey = cert.getPublicKey();
                ///公私钥存到KeyPair
                return new KeyPair(publicKey, (PrivateKey) key);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

testkey.keystore所需的参数都在跟我们打包应用签名所需一样,通过java提供的keystore类获取。然后就是用刚拿到的testkey.keystore私钥来加密AES密钥,再通过Base64转换一下编码成字符串展示出来,只是为了把密钥展示出来才转换编码的。

密钥Base64编码.png

2.2、导出密钥

把密钥导出成文件,下次直接导入密钥用来解密文件,导出密钥需要先用Base64把文本框里的Base64密钥字符串转换为Byte[]再存。

            byte[] key = Base64.decode(base64Key);
            //将raw key输出
            File keyFile = new File(dir, "testkey.dat");
            FileOutputStream fos = new FileOutputStream(keyFile);
2.3、加密文件

密钥已有,AES算法又是现成的,直接调用api加密就行了:

private static final String AES = "AES";

    /**
     * AES算法加密文件
     *
     * @param rawKey   AES密钥
     * @param fromFile 要加密的文件
     * @param toFile   加密后文件
     */
    public static void encryptFile(byte[] rawKey, File fromFile, File toFile) throws Exception {
        if (!fromFile.exists()) {
            throw new NullPointerException("文件不存在");
        }
        if (toFile.exists()) {
            toFile.delete();
        }
        SecretKeySpec skeySpec = new SecretKeySpec(rawKey, AES);
        Cipher cipher = Cipher.getInstance(AES);
        //加密模式
        cipher.init(Cipher.ENCRYPT_MODE, skeySpec);

        FileInputStream fis = new FileInputStream(fromFile);
        FileOutputStream fos = new FileOutputStream(toFile, true);
        byte[] buffer = new byte[512 * 1024 - 16];
        int offset;
        //使用加密流来加密
        CipherInputStream bis = new CipherInputStream(fis, cipher);
        while ((offset = bis.read(buffer)) != -1) {
            fos.write(buffer, 0, offset);
            fos.flush();
        }
        fos.close();
        fis.close();
    } 

选择文件,通过AES算法和AES密钥加密,最后效果如下,没有密钥能解密出来算我输。

加密前后.png

3、第2块:解密工具进行解密

解密过程其实没啥必要讲了,因为解密过程是加密过程的逆过程。
这个解密不是在应用中用的,是为了便于我们更新加密文件,修改文件之前必须要先把文件先解密。


解密.png
3.1、导入AES密钥

这个密钥就是我们前面生成的密钥,导进来后用应用签名的RSA公钥解密AES密钥即可:

            //获取被加密的密钥raw key
            String keyStr = mKeyText.getText();
            byte[] key = Base64.decode(keyStr);
            //获取应用签名密钥对,公钥解密raw key
            KeyPair keypair = SignKey.getSignKeyPair();
            byte[] rawkey = RSA.decrypt(key, keypair.getPublic());
            //用raw key去解密文件
            AES.decryptFile(rawkey, fromFile, toFile);
3.2、解密文件

拿到纯洁版AES密钥之后就可以直接调用AES算法解密文件了:

/**
     * AES算法解密文件
     *
     * @param rawKey   AES密钥
     * @param fromFile 被加密的文件
     * @param toFile   解密后文件
     */
    public static void decryptFile(byte[] rawKey, File fromFile, File toFile) throws Exception {
        if (!fromFile.exists()) {
            throw new NullPointerException("文件不存在");
        }
        if (toFile.exists()) {
            toFile.delete();
        }
        SecretKeySpec skeySpec = new SecretKeySpec(rawKey, AES);
        Cipher cipher = Cipher.getInstance(AES);
        //解密模式
        cipher.init(Cipher.DECRYPT_MODE, skeySpec);

        FileInputStream fis = new FileInputStream(fromFile);
        FileOutputStream fos = new FileOutputStream(toFile, true);
        byte[] buffer = new byte[512 * 1024 + 16];
        int offset;
        //使用解密流来解密
        CipherInputStream cipherInputStream = new CipherInputStream(fis, cipher);
        while ((offset = cipherInputStream.read(buffer)) != -1) {
            fos.write(buffer, 0, offset);
            fos.flush();
        }
        fos.close();
        fis.close();
    }

和AES加密过程一对比,会发现只是切换一下AES算法模式。

3、第3块:Android应用中解密文件

要解密文件,需要在资源文件夹中加入被加密的AES密钥,这个密钥就是上面导出来的,还有就是被加密后的文件。能正确解密的前提是你应用签名和用来给文件加密过程中用到的签名是同一个。

资源文件.png
3.1、解密AES密钥

在Android应用中解密文件与在java工具中解密文件,区别主要在于RSA密钥的获取,在java工具中应用签名testkey.keystore是开发者拥有的,可以拿到其中的全部信息,而在Android中应用是要发布到应用市场的,任何人都可以下载我们的包,应用签名只能通过Android提供的api拿到其公钥。

/**
 * Author:xishuang
 * Date:2018.05.06
 * Des:应用签名读取工具类
 */
public class SignKey {

    /**
     * 获取当前应用的签名
     *
     * @param context 上下文
     */
    public static byte[] getSign(Context context) {
        PackageManager pm = context.getPackageManager();
        try {
            PackageInfo info = pm.getPackageInfo(context.getPackageName(), PackageManager.GET_SIGNATURES);
            Signature[] signatures = info.signatures;
            if (signatures != null) {
                return signatures[0].toByteArray();
            }
        } catch (NameNotFoundException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 根据签名去获取公钥
     */
    public static PublicKey getPublicKey(byte[] signature) {
        try {
            CertificateFactory certFactory = CertificateFactory
                    .getInstance("X.509");
            X509Certificate cert = (X509Certificate) certFactory
                    .generateCertificate(new ByteArrayInputStream(signature));
            return cert.getPublicKey();
        } catch (CertificateException e) {
            e.printStackTrace();
        }
        return null;
    }

}

拿到应用签名testkey.keystore的公钥之后的流程就和在java工具中的操作基本一致了,用RSA公钥来解密AES密钥。

private static final String SIMPLE_KEY_DATA = "testkey.dat";
/**
     * 获取解密之后的文件加密密钥
     */
    private static byte[] getRawKey(Context context) throws Exception {
        //获取应用的签名密钥
        byte[] sign = SignKey.getSign(context);
        PublicKey pubKey = SignKey.getPublicKey(sign);
        //获取加密文件的密钥
        InputStream keyis = context.getAssets().open(SIMPLE_KEY_DATA);
        byte[] key = getData(keyis);
        //解密密钥
        return RSA.decrypt(key, pubKey);
    }

最后再用解密之后的AES密钥来解密文件。

3.2、AES密钥解密文件

通过资源管理器拿到加密文件的文件流,通过AES密钥来用AES算法来解密文件流。

/**
     * 获取解密之后的文件流
     */
    public static InputStream onObtainInputStream(Context context) {
        try {
            AssetManager assetmanager = context.getAssets();
            InputStream is = assetmanager.open("encrypt_测试.txt");
            byte[] rawkey = getRawKey(context);

            //使用解密流,数据写出到基础OutputStream之前先对该会先对数据进行解密
            SecretKeySpec skeySpec = new SecretKeySpec(rawkey, "AES");
            Cipher cipher = Cipher.getInstance("AES");
            cipher.init(Cipher.DECRYPT_MODE, skeySpec);
            return new CipherInputStream(is, cipher);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

拿到加密后文件流之后就达成目的了,可以解析成字符串展示出来:

private void inputData() {
        InputStream in = DecryptUtil.onObtainInputStream(this);

        try {
            BufferedReader reader = new BufferedReader(new InputStreamReader(in, "GBK"));
            StringBuilder sb = new StringBuilder();
            String line;
            while ((line = reader.readLine()) != null) {
                sb.append(line + "\n");
            }
            contentTv.setText(sb.toString());
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                in.close();
            } catch (IOException e) {
                e.printStackTrace();

            }
        }
    }

实例效果图如下,请关注红框里面内容,因为懒得新建项目,用原有项目测试了一下:

效果

目前工具使用的是市面上比较常见的加解密算法,可以换一下算法,比如DES或者其它的对称和非对称算法,甚至是自己改动的算法,想运行示例演示的话:


QQ截图20180508234849.png

就是运行一下java文件,就可以打开加解密小工具了,加解密工具界面是仿我司工具包中抽出来的小部分的,毕竟写界面好烦,感谢我司大神多年前就写出了如此工具。

请翻到最前面再看下流程图,大概会清晰很多
DEMO地址:https://github.com/JieYuShi/ImageViewProcess
搞定,收工。

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

推荐阅读更多精彩内容

  • 本文主要介绍移动端的加解密算法的分类、其优缺点特性及应用,帮助读者由浅入深地了解和选择加解密算法。文中会包含算法的...
    苹果粉阅读 11,306评论 5 29
  • 随着对于安全度的不断要求,对于数据加解密与破解之间的斗争,加解密的方式也在不断发生着变化,来看看现在流行的一些加解...
    zhouhao_180阅读 1,998评论 1 12
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,106评论 18 139
  • MP4格式是现在播放器通用的视频格式之一,这种格式的视频兼容特别好,几乎所有的播放器都支持MP4格式的视频,而用迅...
    视频转换阅读 915评论 0 0
  • 放纵欲念的毛病还可以医治,而事理上顽固不化却难以纠正;一般事物的障碍还能够排除,但是义理方面的障碍却难以化解。 世...
    上德若谷阅读 195评论 0 0