UDN-企业互联网技术人气社区

板块导航

浏览  : 1254
回复  : 0

[干货] Android使用Socket对大文件进行加密传输

[复制链接]
开花包的头像 楼主
  前言

  数据加密,是一门历史悠久的技术,指通过加密算法和加密密钥将明文转变为密文,而解密则是通过解密算法和解密密钥将密文恢复为明文。它的核心是密码学。

  数据加密目前仍是计算机系统对信息进行保护的一种最可靠的办法。它利用密码技术对信息进行加密,实现信息隐蔽从而起到保护信息的安全的作用。

  项目中使用Socket进行文件传输过程时,需要先进行加密。实现的过程中踏了一些坑,下面对实现过程进行一下总结。

  DES加密

  由于加密过程中使用的是DES加密算法,下面贴一下DES加密代码:

  1.   //秘钥算法

  2.   private static final String KEY_ALGORITHM = "DES";

  3.   //加密算法:algorithm/mode/padding 算法/工作模式/填充模式

  4.   private static final String CIPHER_ALGORITHM = "DES/ECB/PKCS5Padding";

  5.   //秘钥

  6.   private static final String KEY = "12345678";//DES秘钥长度必须是8位

  7.   public static void main(String args[]) {

  8.   String data = "加密解密";

  9.   KLog.d("加密数据:" + data);

  10.   byte[] encryptData = encrypt(data.getBytes());

  11.   KLog.d("加密后的数据:" + new String(encryptData));

  12.   byte[] decryptData = decrypt(encryptData);

  13.   KLog.d("解密后的数据:" + new String(decryptData));

  14.   }

  15.   public static byte[] encrypt(byte[] data) {

  16.   //初始化秘钥

  17.   SecretKey secretKey = new SecretKeySpec(KEY.getBytes(), KEY_ALGORITHM);

  18.   try {

  19.   Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);

  20.   cipher.init(Cipher.ENCRYPT_MODE, secretKey);

  21.   byte[] result = cipher.doFinal(data);

  22.   return Base64.getEncoder().encode(result);

  23.   } catch (Exception e) {

  24.   e.printStackTrace();

  25.   }

  26.   return null;

  27.   }

  28.   public static byte[] decrypt(byte[] data) {

  29.   byte[] resultBase64 = Base64.getDecoder().decode(data);

  30.   SecretKey secretKey = new SecretKeySpec(KEY.getBytes(), KEY_ALGORITHM);

  31.   try {

  32.   Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);

  33.   cipher.init(Cipher.DECRYPT_MODE, secretKey);

  34.   byte[] result = cipher.doFinal(resultBase64);

  35.   return result;

  36.   } catch (Exception e) {

  37.   e.printStackTrace();

  38.   }

  39.   return null;

  40.   }
复制代码


  输出:

  1.   加密数据:加密解密

  2.   加密后的数据:rt6XE06pElmLZMaVxrbfCQ==

  3.   解密后的数据:加密解密
复制代码


  Socket客户端部分代码:

  1.   Socket socket = new Socket(APIConstants.HOST, ApiConstants.PORT);

  2.   OutputStream outStream = socket.getOutputStream();

  3.   InputStream inStream = socket.getInputStream();

  4.   RandomAccessFile fileOutStream = new RandomAccessFile(file, "r");

  5.   fileOutStream.seek(0);

  6.   byte[] buffer = new byte[1024];

  7.   int len = -1;

  8.   while (((len = fileOutStream.read(buffer)) != -1)) {

  9.   outStream.write(buffer, 0, len); // 这里进行字节流的传输

  10.   }

  11.   fileOutStream.close();

  12.   outStream.close();

  13.   inStream.close();

  14.   socket.close();
复制代码


  Socket服务端部分代码:

  1.   Socket socket = server.accept();

  2.   InputStream inStream = socket.getInputStream();

  3.   OutputStream outStream = socket.getOutputStream();

  4.   outStream.write(response.getBytes("UTF-8"));

  5.   RandomAccessFile fileOutStream = new RandomAccessFile(file, "rwd");

  6.   fileOutStream.seek(0);

  7.   byte[] buffer = new byte[1024];

  8.   int len;

  9.   while ((len = inStream.read(buffer)) != -1) { // 从字节输入流中读取数据写入到文件中

  10.   fileOutStream.write(buffer, 0, len);

  11.   }

  12.   fileOutStream.close();

  13.   inStream.close();

  14.   outStream.close();

  15.   socket.close();
复制代码


  数据加密传输

  下面对传输数据进行加密解密

  方案一:直接对io流进行加密解密

  客户端变更如下:

  1.   while (((len = fileOutStream.read(buffer)) != -1)) {

  2.   outStream.write(DesUtil.encrypt(buffer) ,0, len); // 对字节数组进行加密

  3.   }
复制代码


  服务端变更代码:

  1.   while ((len = inStream.read(buffer)) != -1) {

  2.   fileOutStream.write(DesUtil.decrypt(buffer), 0, len); // 对字节数组进行解密

  3.   }
复制代码


  执行代码后,服务端解密时会报如下异常:

 
  1.  javax.crypto.BadPaddingException: pad block corrupted
复制代码


  猜测错误原因是加密过程会对数据进行填充处理,然后在io流传输的过程中,数据有丢包现象发生,所以解密会报异常。

  加密后的结果是字节数组,这些被加密后的字节在码表(例如UTF-8 码表)上找不到对应字符,会出现乱码,当乱码字符串再次转换为字节数组时,长度会变化,导致解密失败,所以转换后的数据是不安全的。

  于是尝试了使用NOPadding填充模式,这样虽然可以成功解密,测试中发现对于一般文件,如.txt文件可以正常显示内容,但是.apk等文件则会有解析包出现异常等错误提示。

  方案二:使用字符流

  使用Base64 对字节数组进行编码,任何字节都能映射成对应的Base64 字符,之后能恢复到字节数组,利于加密后数据的保存于传输,所以转换是安全的。同样,字节数组转换成16 进制字符串也是安全的。

  由于客户端从输入文件中读取的是字节流,需要先将字节流转换成字符流,而服务端接收到字符流后需要先转换成字节流,再将其写入到文件。测试中发现可以对字符流成功解密,但是将文件转化成字符流进行传输是个连续的过程,而文件的写出和写入又比较繁琐,操作过程中会出现很多问题。

  方案三:使用CipherInputStream、CipherOutputStream

  使用过程中发现只有当CipherOutputStream流close时,CipherInputStream才会接收到数据,显然这个方案也只好pass掉。

  方案四:使用SSLSocket

  在Android上使用SSLSocket的会稍显复杂,首先客户端和服务端需要生成秘钥和证书。生成方法可以参考这篇。Android证书的格式还必须是bks格式(Java使用jks格式)。一般来说,我们使用jdk的keytool只能生成jks的证书库,如果生成bks的则需要下载BouncyCastle库。具体方法可以参考这里

  服务端的代码参考:http://blog.sina.com.cn/s/blog_792cc4290100syyf.html

  客户端的代码参考:http://blog.sina.com.cn/s/blog_792cc4290100syyt.html

  当以上所有的一切都准备完毕后,如果在Android6.0以上使用你会悲催的发现下面这个异常:

 
  1.  javax.net.ssl.SSLHandshakeException: Handshake failed
复制代码


  异常原因:SSLSocket签名算法默认为DSA,Android6.0(API 23)以后KeyStore发生更改,不再支持DSA,但仍支持ECDSA。

  所以如果想在Android6.0以上使用SSLSocket,需要将DSA改成ECDSA...org感觉坑越入越深看不到底呀...于是决定换个思路来解决socket加密这个问题。既然对文件边传边加密解密不好使,那能不能客户端传输文件前先对文件进行加密,然后进行传输,服务端成功接收文件后,再对文件进行解密呢。于是就有了下面这个方案。

  方案五:先对文件进行加密,然后传输,服务端成功接收文件后再对文件进行解密

  对文件进行加密解密代码如下:

  1.   public class FileDesUtil {

  2.   //秘钥算法

  3.   private static final String KEY_ALGORITHM = "DES";

  4.   //加密算法:algorithm/mode/padding 算法/工作模式/填充模式

  5.   private static final String CIPHER_ALGORITHM = "DES/ECB/PKCS5Padding";

  6.   private static final byte[] KEY = {56, 57, 58, 59, 60, 61, 62, 63};//DES 秘钥长度必须是8 位或以上

  7.   /**

  8.   * 文件进行加密并保存加密后的文件到指定目录

  9.   *

  10.   * @param fromFile 要加密的文件 如c:/test/待加密文件.txt

  11.   * @param toFile 加密后存放的文件 如c:/加密后文件.txt

  12.   */

  13.   public static void encrypt(String fromFilePath, String toFilePath) {

  14.   KLog.i("encrypting...");

  15.   File fromFile = new File(fromFilePath);

  16.   if (!fromFile.exists()) {

  17.   KLog.e("to be encrypt file no exist!");

  18.   return;

  19.   }

  20.   File toFile = getFile(toFilePath);

  21.   SecretKey secretKey = new SecretKeySpec(KEY, KEY_ALGORITHM);

  22.   InputStream is = null;

  23.   OutputStream out = null;

  24.   CipherInputStream cis = null;

  25.   try {

  26.   Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);

  27.   cipher.init(Cipher.ENCRYPT_MODE, secretKey);

  28.   is = new FileInputStream(fromFile);

  29.   out = new FileOutputStream(toFile);

  30.   cis = new CipherInputStream(is, cipher);

  31.   byte[] buffer = new byte[1024];

  32.   int r;

  33.   while ((r = cis.read(buffer)) > 0) {

  34.   out.write(buffer, 0, r);

  35.   }

  36.   } catch (Exception e) {

  37.   KLog.e(e.toString());

  38.   } finally {

  39.   try {

  40.   if (cis != null) {

  41.   cis.close();

  42.   }

  43.   } catch (IOException e) {

  44.   e.printStackTrace();

  45.   }

  46.   try {

  47.   if (is != null) {

  48.   is.close();

  49.   }

  50.   } catch (IOException e) {

  51.   e.printStackTrace();

  52.   }

  53.   try {

  54.   if (out != null) {

  55.   out.close();

  56.   }

  57.   } catch (IOException e) {

  58.   e.printStackTrace();

  59.   }

  60.   }

  61.   KLog.i("encrypt completed");

  62.   }

  63.   @NonNull

  64.   private static File getFile(String filePath) {

  65.   File fromFile = new File(filePath);

  66.   if (!fromFile.getParentFile().exists()) {

  67.   fromFile.getParentFile().mkdirs();

  68.   }

  69.   return fromFile;

  70.   }

  71.   /**

  72.   * 文件进行解密并保存解密后的文件到指定目录

  73.   *

  74.   * @param fromFilePath 已加密的文件 如c:/加密后文件.txt

  75.   * @param toFilePath 解密后存放的文件 如c:/ test/解密后文件.txt

  76.   */

  77.   public static void decrypt(String fromFilePath, String toFilePath) {

  78.   KLog.i("decrypting...");

  79.   File fromFile = new File(fromFilePath);

  80.   if (!fromFile.exists()) {

  81.   KLog.e("to be decrypt file no exist!");

  82.   return;

  83.   }

  84.   File toFile = getFile(toFilePath);

  85.   SecretKey secretKey = new SecretKeySpec(KEY, KEY_ALGORITHM);

  86.   InputStream is = null;

  87.   OutputStream out = null;

  88.   CipherOutputStream cos = null;

  89.   try {

  90.   Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);

  91.   cipher.init(Cipher.DECRYPT_MODE, secretKey);

  92.   is = new FileInputStream(fromFile);

  93.   out = new FileOutputStream(toFile);

  94.   cos = new CipherOutputStream(out, cipher);

  95.   byte[] buffer = new byte[1024];

  96.   int r;

  97.   while ((r = is.read(buffer)) >= 0) {

  98.   cos.write(buffer, 0, r);

  99.   }

  100.   } catch (Exception e) {

  101.   KLog.e(e.toString());

  102.   } finally {

  103.   try {

  104.   if (cos != null) {

  105.   cos.close();

  106.   }

  107.   } catch (IOException e) {

  108.   e.printStackTrace();

  109.   }

  110.   try {

  111.   if (out != null) {

  112.   out.close();

  113.   }

  114.   } catch (IOException e) {

  115.   e.printStackTrace();

  116.   }

  117.   try {

  118.   if (is != null) {

  119.   is.close();

  120.   }

  121.   } catch (IOException e) {

  122.   e.printStackTrace();

  123.   }

  124.   }

  125.   KLog.i("decrypt completed");

  126.   }

  127.   }
复制代码


  使用如上这个方案就完美的绕开了上面提到的一些问题,成功的实现了使用Socket对文件进行加密传输。

  总结

  对于任何技术的使用,底层原理的理解还是很有必要的。不然遇到问题很容易就是一头雾水不知道Why!接下来准备看一下《图解加密技术》和《图解TCP/IP》这两本书,以便加深对密码学和Socket底层原理的理解。

原文作者:咖枯 来源:开发者头条

相关帖子

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

关于我们
联系我们
  • 电话:010-86393388
  • 邮件:udn@yonyou.com
  • 地址:北京市海淀区北清路68号
移动客户端下载
关注我们
  • 微信公众号:yonyouudn
  • 扫描右侧二维码关注我们
  • 专注企业互联网的技术社区
版权所有:用友网络科技股份有限公司82041 京ICP备05007539号-11 京公网网备安1101080209224 Powered by Discuz!
快速回复 返回列表 返回顶部