07.音频的加密与解密案例实践深入总结

yangchong211 / 文 发表于2018-02-07 17:07 次阅读

目录介绍

  • 1.音视频为何需要加密与解密
  • 1.1 为什么要加密解密
  • 1.2 业务需求分析【先看需求】
  • 1.3 文件加解密的流程及原理【重点】
  • 2.利用java包中的加密包进行加密
  • 2.1 具体使用方法
  • 2.2 有什么优劣势
  • 3.加密音视频前N个字节
  • 3.1 如何缩短加密时间
  • 3.2 代码逻辑展示
  • 3.3 存在的问题分析
  • 4.自定义实现音视频加密解密
  • 4.1 思路其实与2目录内容相似
  • 4.2 解决那些问题
  • 4.3 如何监听加密进度,自定义监听事件
  • 5.其他问题说明
  • 5.1 版本更新情况
  • 5.2 参考链接
  • 5.2 个人博客

1.音视频为何需要加密与解密

1.1 为什么要加密解密

  • 项目开发中,之前一直是直接播放网络视频,后来要求加上视频缓存的功能,但是这些视频又都是要付费才能观看的,这就涉及到视频的版权问题。
  • 为了防止一个用户付费下载后,传播视频,就需要给视频文件加密,在播放时解密,只让视频在我的应用中播放。不过,在网上搜索了一下。找到了以下几种加密方法。

1.2 业务需求分析

  • 主要是加解密音频和视频,要求下载完后,对于有权限的视频只能在本APP中播放,而不能在其他APP播放
  • 要求加解密时间要短,一般可以保持在30到60秒之间就可以
  • 因为有的音视频权限开放,有的需要权限,这就带来了新问题。如何判断该视频是否已经加密了呢?

1.3 文件加解密的流程及原理【重点】

  • 1.3.1 加密方法
  • 存储文件时,从输入流中截取文件的字节数组,对字节数组进行加密,至于加密的方式和算法就可以视需求而定了,然后把加密后的字节数组写入到文件中,最后生成加密后的文件;
  • 1.3.2 解密方法
  • 同加密方法一样,只不过是对字节数据进行解密,最后生成明文文件; 加密算法:Android系统本身引入了javax包的Cipher类,这个类里提供了各种各样的通用的加密方式,如AES对称加密等;该程序中有个CipherUtil工具类,里面有一些简单的使用Cipher进行AES加解密的方法;当然最好还是好好学习一下Cipher类的使用;
  • 1.3.3 注意事项:
  • 之前,我看到有些APP将下载完的文件,直接更改为没有后缀名。那么也叫做加密了,因为别人不知道是什么文件,其实还是可以找到同类软件打开的。这种做法是不好的……
  • 如何判断一个文件是加密后的文件,最简单的方法就是对加密后的文件统一增加一个后缀名,然后在解密之后将这个后缀名去除,还原回原有文件格式;如:密文文件的统一后缀名为“.cipher”,明文文件名为"测试.txt",加密后的密文文件应该为“测试.txt.cipher”;
  • 加密文件时还有一个重要的注意事项,就是加密后的密文和明文的长度是否相同,如果文件时一次读取出所有字节数组进行加密的话不用担心这个问题,但是当对文件分次读取加密或分段加密的话,就不得不考虑这个问题了,最方便的方法就是保证明文和加密后的密文长度相同;如果长度不同,由于是分段加密的,密文是由一段一段子密文拼接成的,解密时会找不到每段子密文,因为不知道每段子密文的长度是多少;

2.利用java包中的加密包进行加密**

2.1 具体使用方法

  • 2.1.1 主要的加密与解密思路是这样的。
  • 首先第一步是创建一个加解密的key;然后在初始化加载密码;然后再加密某个文件(实际上就已io流输入输出的过程);解密过程也是一样的。
  • 2.1.2 具体的代码是这样的
public class FileDESUtils {

    /**
     * 这个方法只是解释使用方法,并无意义
     */
    private void utils(){
        FileDESUtils fileDES = null;
        try {
            fileDES = new FileDESUtils("spring.sky");
            //加密调用这个方法
            fileDES.doEncryptFile("d:/a.mp4", "d:/b");
            //解密调用这个方法
            //fileDES.doDecryptFile("d:/b");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 加密解密的key
     */
    private Key mKey;
    /**
     * 解密的密码
     */
    private Cipher mDecryptCipher;
    /**
     * 加密的密码
     */
    private Cipher mEncryptCipher;

    public FileDESUtils(String key) throws Exception {
        initKey(key);
        initCipher();
    }

    /**
     * 创建一个加密解密的key
     *
     * @param keyRule
     */
    public void initKey(String keyRule) {
        byte[] keyByte = keyRule.getBytes();
        // 创建一个空的八位数组,默认情况下为0
        byte[] byteTemp = new byte[8];
        // 将用户指定的规则转换成八位数组
        for (int i = 0; i < byteTemp.length && i < keyByte.length; i++) {
            byteTemp[i] = keyByte[i];
        }
        mKey = new SecretKeySpec(byteTemp, "DES");
    }

    /***
     * 初始化加载密码
     *
     * @throws Exception
     */
    private void initCipher() throws Exception {
        mEncryptCipher = Cipher.getInstance("DES");
        mEncryptCipher.init(Cipher.ENCRYPT_MODE, mKey);
        mDecryptCipher = Cipher.getInstance("DES");
        mDecryptCipher.init(Cipher.DECRYPT_MODE, mKey);

    }

    /**
     * 加密文件
     *
     * @param in
     * @param savePath 加密后保存的位置
     */
    public void doEncryptFile(InputStream in, String savePath) {
        if (in == null) {
            System.out.println("inputstream is null");
            return;
        }
        try {
            CipherInputStream cin = new CipherInputStream(in, mEncryptCipher);
            OutputStream os = new FileOutputStream(savePath);
            byte[] bytes = new byte[1024];
            int len = -1;
            while ((len = cin.read(bytes)) > 0) {
                os.write(bytes, 0, len);
                os.flush();
            }
            os.close();
            cin.close();
            in.close();
            System.out.println("加密成功");
        } catch (Exception e) {
            System.out.println("加密失败");
            e.printStackTrace();
        }
    }

    /**
     * 加密文件
     *
     * @param filePath 需要加密的文件路径
     * @param savePath 加密后保存的位置
     * @throws FileNotFoundException
     */
    public void doEncryptFile(String filePath, String savePath) throws FileNotFoundException {
        doEncryptFile(new FileInputStream(filePath), savePath);
    }

    /**
     * 解密文件
     *
     * @param in
     */
    public void doDecryptFile(InputStream in, String path) {
        if (in == null) {
            System.out.println("inputstream is null");
            return;
        }
        try {
            CipherInputStream cin = new CipherInputStream(in, mDecryptCipher);
            OutputStream outputStream = new FileOutputStream(path);
            byte[] bytes = new byte[1024];
            int length = -1;
            while ((length = cin.read(bytes)) > 0) {
                outputStream.write(bytes, 0, length);
                outputStream.flush();
            }
            cin.close();
            in.close();
            System.out.println("解密成功");
        } catch (Exception e) {
            System.out.println("解密失败");
            e.printStackTrace();
        }
    }

    /**
     * 解密文件
     *
     * @param filePath 文件路径
     * @throws Exception
     */
    public void doDecryptFile(String filePath, String outPath) throws Exception {
        doDecryptFile(new FileInputStream(filePath), outPath);
    }
}

2.2 有什么优劣势

  • 2.2.1 这种加密解密方法存在的问题
  • 第一,加密和解密是加解密所有的字节,对于音视频文件来说,读取所有字节会耗时长。
  • 第二,音视频文件一般都是几百兆,用此方法加密的时间就长的有点离谱。

3.加密音视频前N个字节

3.1 如何缩短加密时间

  • 将音视频文件的数据流前300个字节中的每个字节与其下标进行异或运算。解密时只需将加密过的文件再进行一次异或运算即可。

3.2 代码逻辑展示

public class FileNumberEncrypted {
    private final int REVERSE_LENGTH = 1000;
    /**
     * 加解密
     * 将视频文件的数据流前1000个字节中的每个字节与其下标进行异或运算。
     * 解密时只需将加密过的文件再进行一次异或运算即可。
     * @param strFile 源文件绝对路径
     * @return
     */
    private boolean encrypt(String strFile) {
        int len = REVERSE_LENGTH;
        try {
            File f = new File(strFile);
            RandomAccessFile raf = new RandomAccessFile(f, "rw");
            long totalLen = raf.length();

            if (totalLen < REVERSE_LENGTH){
                len = (int) totalLen;
            }
            FileChannel channel = raf.getChannel();
            MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, REVERSE_LENGTH);
            byte tmp;
            for (int i = 0; i < len; ++i) {
                byte rawByte = buffer.get(i);
                tmp = (byte) (rawByte ^ i);
                buffer.put(i, tmp);
            }
            buffer.force();
            buffer.clear();
            channel.close();
            raf.close();
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
}

3.3 存在的问题分析

  • 第一,思路很好,但是有个问题,如果判断该视频是否已经加密呢?
  • 第二,RandomAccessFile不属于InputStream和OutputStream类系的,对于读写需要权限支持
  • 第三,程序创建了一个128Mb的文件,如果一次性读到内存可能导致内存溢出,但这里访问好像只是一瞬间的事,因为真正调入内存的只是其中的一小部分,其余部分则被放在交换文件上。这样你就可以很方便地修改超大型的文件了(最大可以到2 GB)。

4.自定义实现音视频加密解密

4.1 思路其实与2目录内容相似

  • 还是Android系统本身引入了javax包的Cipher类,这个类里提供了各种各样的通用的加密方式
  • 思路也几乎类似,但是更加严谨

4.2 解决那些问题

  • 判断文件是否可以加密或者解密,就看音视频的后缀名是否是自己设置的加密文件后缀名。这个在生活中很常见,一些培训机构将付费视频后缀名自定义设置,只能用专用软件打开。
  • 添加了加密和解密进度监听,通过自定义监听接口实现……
  • 加密和解密不再是对音视频文件前N个字节加解密,而是加解密时以32K个字节为单位进行加解密计算。

4.3 如何监听加密进度,自定义监听事件

  • 4.3.1 首先定义一个接口
public interface CipherProgressListener{
    /**
     * 用于加解密进度的监听器
     * @param current           当前进度值
     * @param total             总的
     */
    void onProgress(long current, long total);
}
  • 4.3.2 在加密环节添加监听
  • 加密和解密,添加监听是一样的,没区别
//此处的解密方法很简单,只是简单的异或计算
for (int j = 0; j < CIPHER_BUFFER_LENGTH; ++j) {
    rawByte = buffer.get(j);
    tmp = (byte) (rawByte ^ j);
    buffer.put(j, tmp);
    if(null != listener){
        listener.onProgress(i * CIPHER_BUFFER_LENGTH + j, totalLength);
    }
}
  • 4.3.3 具体实现
  • 可以直接参考我的案例:https://github.com/yangchong211/YCAudioPlayer
  • GitHub地址:https://github.com/yangchong211
  • 代码是这样的
public class FileCipherUtils {
    /**
     * 加密后的文件的后缀
     */
    private static final String CIPHER_TEXT_SUFFIX = ".yc";
    /**
     * 加解密时以32K个字节为单位进行加解密计算
     */
    private static final int CIPHER_BUFFER_LENGTH = 32 * 1024;
    /**
     * 加密,这里主要是演示加密的原理,没有用什么实际的加密算法
     * @param filePath  明文文件绝对路径
     * @return          是否加密成功
     */
    public static boolean encrypt(String filePath, CipherProgressListener listener) {
        if(filePath == null){
            return false;
        }
        FileChannel channel = null;
        RandomAccessFile raf = null;
        try {
            long startTime = System.currentTimeMillis();
            File f = new File(filePath);

            raf = new RandomAccessFile(f, "rw");
            long totalLength = raf.length();
            channel = raf.getChannel();
            long multiples = totalLength / CIPHER_BUFFER_LENGTH;
            long remainder = totalLength % CIPHER_BUFFER_LENGTH;

            MappedByteBuffer buffer ;
            byte tmp;
            byte rawByte;
            //先对整除部分加密
            for(int i = 0; i < multiples; i++){
                buffer = channel.map(FileChannel.MapMode.READ_WRITE,
                        i * CIPHER_BUFFER_LENGTH, (i + 1) * CIPHER_BUFFER_LENGTH);
                //此处的加密方法很简单,只是简单的异或计算
                for (int j = 0; j < CIPHER_BUFFER_LENGTH; ++j) {
                    rawByte = buffer.get(j);
                    tmp = (byte) (rawByte ^ j);
                    buffer.put(j, tmp);
                    if(null != listener){
                        listener.onProgress(i * CIPHER_BUFFER_LENGTH + j, totalLength);
                    }
                }
                buffer.force();
                buffer.clear();
            }

            //对余数部分加密
            buffer = channel.map(FileChannel.MapMode.READ_WRITE,
                    multiples * CIPHER_BUFFER_LENGTH, multiples * CIPHER_BUFFER_LENGTH + remainder);
            for (int j = 0; j < remainder; ++j) {
                rawByte = buffer.get(j);
                tmp = (byte) (rawByte ^ j);
                buffer.put(j, tmp);
                if(null != listener){
                    listener.onProgress(multiples * CIPHER_BUFFER_LENGTH + j, totalLength);
                }
            }
            buffer.force();
            buffer.clear();
            //对加密后的文件重命名,增加.cipher后缀
            //noinspection ResultOfMethodCallIgnored
            f.renameTo(new File(f.getPath() + CIPHER_TEXT_SUFFIX));
            LogUtils.e("加密用时:"+(System.currentTimeMillis() - startTime) /1000 + "s");
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        } finally {
            try {
                if(channel!=null){
                    channel.close();
                }
                if(raf!=null){
                    raf.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 解密,这里主要是演示加密的原理,没有用什么实际的加密算法
     * @param filePath  密文文件绝对路径,文件需要以.cipher结尾才会认为其实可解密密文
     * @return          是否解密成功
     */
    public static boolean decrypt(String filePath, CipherProgressListener listener) {
        if(filePath == null){
            return false;
        }
        FileChannel channel = null;
        RandomAccessFile raf = null;
        try {
            long startTime = System.currentTimeMillis();
            File f = new File(filePath);
            if(!f.getPath().toLowerCase().endsWith(CIPHER_TEXT_SUFFIX)){
                //后缀不同,认为是不可解密的密文
                return false;
            }
            raf = new RandomAccessFile(f, "rw");
            long totalLength = raf.length();
            channel = raf.getChannel();

            long multiples = totalLength / CIPHER_BUFFER_LENGTH;
            long remainder = totalLength % CIPHER_BUFFER_LENGTH;

            MappedByteBuffer buffer ;
            byte tmp;
            byte rawByte;

            //先对整除部分解密
            for(int i = 0; i < multiples; i++){
                buffer = channel.map(FileChannel.MapMode.READ_WRITE,
                        i * CIPHER_BUFFER_LENGTH, (i + 1) * CIPHER_BUFFER_LENGTH);
                //此处的解密方法很简单,只是简单的异或计算
                for (int j = 0; j < CIPHER_BUFFER_LENGTH; ++j) {
                    rawByte = buffer.get(j);
                    tmp = (byte) (rawByte ^ j);
                    buffer.put(j, tmp);
                    if(null != listener){
                        listener.onProgress(i * CIPHER_BUFFER_LENGTH + j, totalLength);
                    }
                }
                buffer.force();
                buffer.clear();
            }

            //对余数部分解密
            buffer = channel.map(FileChannel.MapMode.READ_WRITE,
                    multiples * CIPHER_BUFFER_LENGTH, multiples * CIPHER_BUFFER_LENGTH + remainder);
            for (int j = 0; j < remainder; ++j) {
                rawByte = buffer.get(j);
                tmp = (byte) (rawByte ^ j);
                buffer.put(j, tmp);
                if(null != listener){
                    listener.onProgress(multiples * CIPHER_BUFFER_LENGTH + j, totalLength);
                }
            }
            buffer.force();
            buffer.clear();
           //对加密后的文件重命名,增加.cipher后缀
            //noinspection ResultOfMethodCallIgnored
            f.renameTo(new File(f.getPath().substring(f.getPath().toLowerCase().indexOf(CIPHER_TEXT_SUFFIX))));
            LogUtils.e("解密用时:"+(System.currentTimeMillis() - startTime) / 1000 + "s");
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }finally {
            try {
                if(channel!=null){
                    channel.close();
                }
                if(raf!=null){
                    raf.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public interface CipherProgressListener{
        /**
         * 用于加解密进度的监听器
         * @param current           当前进度值
         * @param total             总的
         */
        void onProgress(long current, long total);
    }
}

5.其他问题说明

5.1 版本更新情况

  • v1.0.0 2017年11月19日
  • v1.0.1 2018年1月17日
  • v1.0.2 2018年2月7日

5.2 参考链接

  • Android 视频文件加密:http://blog.csdn.net/qq_24636637/article/details/50524243
  • Android安全开发之浅谈加密算法的坑:https://zhuanlan.zhihu.com/p/24255780?refer=alijaq
  • android中AES加解密的使用方法:http://www.jb51.net/article/96349.htm
  • android中对文件加密解密的实现:http://www.jb51.net/article/95383.htm

5.2 个人博客

收藏 赞 (0) 踩 (0)