Android 使用 Socket 对大文件进行加密传输

niokyt 4年前
   <h2>前言</h2>    <p>数据加密,是一门历史悠久的技术,指通过加密算法和加密密钥将明文转变为密文,而解密则是通过解密算法和解密密钥将密文恢复为明文。它的核心是密码学。</p>    <p>数据加密目前仍是计算机系统对信息进行保护的一种最可靠的办法。它利用密码技术对信息进行加密,实现信息隐蔽从而起到保护信息的安全的作用。</p>    <p>项目中使用Socket进行文件传输过程时,需要先进行加密。实现的过程中踏了一些坑,下面对实现过程进行一下总结。</p>    <h2>DES加密</h2>    <p>由于加密过程中使用的是DES加密算法,下面贴一下DES加密代码:</p>    <pre>  <code class="language-java">//秘钥算法      private static final String KEY_ALGORITHM = "DES";      //加密算法:algorithm/mode/padding 算法/工作模式/填充模式      private static final String CIPHER_ALGORITHM = "DES/ECB/PKCS5Padding";      //秘钥      private static final String KEY = "12345678";//DES秘钥长度必须是8位        public static void main(String args[]) {          String data = "加密解密";          KLog.d("加密数据:" + data);          byte[] encryptData = encrypt(data.getBytes());          KLog.d("加密后的数据:" + new String(encryptData));          byte[] decryptData = decrypt(encryptData);          KLog.d("解密后的数据:" + new String(decryptData));      }        public static byte[] encrypt(byte[] data) {          //初始化秘钥          SecretKey secretKey = new SecretKeySpec(KEY.getBytes(), KEY_ALGORITHM);            try {              Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);              cipher.init(Cipher.ENCRYPT_MODE, secretKey);              byte[] result = cipher.doFinal(data);              return Base64.getEncoder().encode(result);          } catch (Exception e) {              e.printStackTrace();          }          return null;      }        public static byte[] decrypt(byte[] data) {          byte[] resultBase64 = Base64.getDecoder().decode(data);          SecretKey secretKey = new SecretKeySpec(KEY.getBytes(), KEY_ALGORITHM);            try {              Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);              cipher.init(Cipher.DECRYPT_MODE, secretKey);              byte[] result = cipher.doFinal(resultBase64);              return result;          } catch (Exception e) {              e.printStackTrace();          }          return null;      }</code></pre>    <p>输出:</p>    <pre>  <code class="language-java">加密数据:加密解密  加密后的数据:rt6XE06pElmLZMaVxrbfCQ==  解密后的数据:加密解密</code></pre>    <h2>Socket客户端部分代码:</h2>    <pre>  <code class="language-java">Socket socket = new Socket(ApiConstants.HOST, ApiConstants.PORT);      OutputStream outStream = socket.getOutputStream();        InputStream inStream = socket.getInputStream();      RandomAccessFile fileOutStream = new RandomAccessFile(file, "r");      fileOutStream.seek(0);      byte[] buffer = new byte[1024];      int len = -1;      while (((len = fileOutStream.read(buffer)) != -1)) {          outStream.write(buffer, 0, len);   // 这里进行字节流的传输      }        fileOutStream.close();      outStream.close();      inStream.close();      socket.close();</code></pre>    <h2>Socket服务端部分代码:</h2>    <pre>  <code class="language-java">Socket socket = server.accept();      InputStream inStream = socket.getInputStream();      OutputStream outStream = socket.getOutputStream();      outStream.write(response.getBytes("UTF-8"));      RandomAccessFile fileOutStream = new RandomAccessFile(file, "rwd");      fileOutStream.seek(0);      byte[] buffer = new byte[1024];      int len;      while ((len = inStream.read(buffer)) != -1) { // 从字节输入流中读取数据写入到文件中          fileOutStream.write(buffer, 0, len);      }        fileOutStream.close();      inStream.close();      outStream.close();      socket.close();</code></pre>    <h2>数据加密传输</h2>    <p>下面对传输数据进行加密解密</p>    <ul>     <li><strong>方案一:直接对io流进行加密解密</strong></li>    </ul>    <p>客户端变更如下:</p>    <pre>  <code class="language-java">while (((len = fileOutStream.read(buffer)) != -1)) {          outStream.write(DesUtil.encrypt(buffer) ,0, len); // 对字节数组进行加密      }</code></pre>    <p>服务端变更代码:</p>    <pre>  <code class="language-java">while ((len = inStream.read(buffer)) != -1) {          fileOutStream.write(DesUtil.decrypt(buffer), 0, len); // 对字节数组进行解密      }</code></pre>    <p>执行代码后,服务端解密时会报如下异常:</p>    <pre>  <code class="language-java">javax.crypto.BadPaddingException: pad block corrupted</code></pre>    <p>猜测错误原因是加密过程会对数据进行填充处理,然后在io流传输的过程中,数据有丢包现象发生,所以解密会报异常。</p>    <p>加密后的结果是字节数组,这些被加密后的字节在码表(例如UTF-8 码表)上找不到对应字符,会出现乱码,当乱码字符串再次转换为字节数组时,长度会变化,导致解密失败,所以转换后的数据是不安全的。</p>    <p>于是尝试了使用NOPadding填充模式,这样虽然可以成功解密,测试中发现对于一般文件,如.txt文件可以正常显示内容,但是.apk等文件则会有解析包出现异常等错误提示。</p>    <ul>     <li><strong>方案二:使用字符流</strong></li>    </ul>    <p>使用Base64 对字节数组进行编码,任何字节都能映射成对应的Base64 字符,之后能恢复到字节数组,利于加密后数据的保存于传输,所以转换是安全的。同样,字节数组转换成16 进制字符串也是安全的。</p>    <p>由于客户端从输入文件中读取的是字节流,需要先将字节流转换成字符流,而服务端接收到字符流后需要先转换成字节流,再将其写入到文件。测试中发现可以对字符流成功解密,但是将文件转化成字符流进行传输是个连续的过程,而文件的写出和写入又比较繁琐,操作过程中会出现很多问题。</p>    <ul>     <li><strong>方案三:使用CipherInputStream、CipherOutputStream</strong></li>    </ul>    <p>使用过程中发现只有当CipherOutputStream流close时,CipherInputStream才会接收到数据,显然这个方案也只好pass掉。</p>    <ul>     <li><strong>方案四:使用SSLSocket</strong></li>    </ul>    <p>在Android上使用SSLSocket的会稍显复杂,首先客户端和服务端需要生成秘钥和证书。生成方法可以参考 这篇 。Android证书的格式还必须是bks格式(Java使用jks格式)。一般来说,我们使用jdk的keytool只能生成jks的证书库,如果生成bks的则需要下载BouncyCastle库。</p>    <p>当以上所有的一切都准备完毕后,如果在Android6.0以上使用你会悲催的发现下面这个异常:</p>    <pre>  <code class="language-java">javax.net.ssl.SSLHandshakeException: Handshake failed</code></pre>    <p>异常原因:SSLSocket签名 算法 默认为DSA,Android6.0(API 23)以后KeyStore发生更改,不再支持DSA,但仍支持ECDSA。</p>    <p>所以如果想在Android6.0以上使用SSLSocket,需要将DSA改成ECDSA...org感觉坑越入越深看不到底呀...于是决定换个思路来解决socket加密这个问题。既然对文件边传边加密解密不好使,那能不能客户端传输文件前先对文件进行加密,然后进行传输,服务端成功接收文件后,再对文件进行解密呢。于是就有了下面这个方案。</p>    <ul>     <li>方案五:先对文件进行加密,然后传输,服务端成功接收文件后再对文件进行解密</li>    </ul>    <p>对文件进行加密解密代码如下:</p>    <pre>  <code class="language-java">public class FileDesUtil {      //秘钥算法      private static final String KEY_ALGORITHM = "DES";      //加密算法:algorithm/mode/padding 算法/工作模式/填充模式      private static final String CIPHER_ALGORITHM = "DES/ECB/PKCS5Padding";      private static final byte[] KEY = {56, 57, 58, 59, 60, 61, 62, 63};//DES 秘钥长度必须是8 位或以上        /**       * 文件进行加密并保存加密后的文件到指定目录       *       * @param fromFile 要加密的文件 如c:/test/待加密文件.txt       * @param toFile   加密后存放的文件 如c:/加密后文件.txt       */      public static void encrypt(String fromFilePath, String toFilePath) {          KLog.i("encrypting...");            File fromFile = new File(fromFilePath);          if (!fromFile.exists()) {              KLog.e("to be encrypt file no exist!");              return;          }          File toFile = getFile(toFilePath);            SecretKey secretKey = new SecretKeySpec(KEY, KEY_ALGORITHM);          InputStream is = null;          OutputStream out = null;          CipherInputStream cis = null;          try {              Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);              cipher.init(Cipher.ENCRYPT_MODE, secretKey);              is = new FileInputStream(fromFile);              out = new FileOutputStream(toFile);              cis = new CipherInputStream(is, cipher);              byte[] buffer = new byte[1024];              int r;              while ((r = cis.read(buffer)) > 0) {                  out.write(buffer, 0, r);              }          } catch (Exception e) {              KLog.e(e.toString());          } finally {              try {                  if (cis != null) {                      cis.close();                  }              } catch (IOException e) {                  e.printStackTrace();              }              try {                  if (is != null) {                      is.close();                  }              } catch (IOException e) {                  e.printStackTrace();              }              try {                  if (out != null) {                      out.close();                  }              } catch (IOException e) {                  e.printStackTrace();              }          }            KLog.i("encrypt completed");      }        @NonNull      private static File getFile(String filePath) {          File fromFile = new File(filePath);          if (!fromFile.getParentFile().exists()) {              fromFile.getParentFile().mkdirs();          }          return fromFile;      }        /**       * 文件进行解密并保存解密后的文件到指定目录       *       * @param fromFilePath 已加密的文件 如c:/加密后文件.txt       * @param toFilePath   解密后存放的文件 如c:/ test/解密后文件.txt       */      public static void decrypt(String fromFilePath, String toFilePath) {          KLog.i("decrypting...");            File fromFile = new File(fromFilePath);          if (!fromFile.exists()) {              KLog.e("to be decrypt file no exist!");              return;          }          File toFile = getFile(toFilePath);            SecretKey secretKey = new SecretKeySpec(KEY, KEY_ALGORITHM);            InputStream is = null;          OutputStream out = null;          CipherOutputStream cos = null;          try {              Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);              cipher.init(Cipher.DECRYPT_MODE, secretKey);              is = new FileInputStream(fromFile);              out = new FileOutputStream(toFile);              cos = new CipherOutputStream(out, cipher);              byte[] buffer = new byte[1024];              int r;              while ((r = is.read(buffer)) >= 0) {                  cos.write(buffer, 0, r);              }          } catch (Exception e) {              KLog.e(e.toString());          } finally {              try {                  if (cos != null) {                      cos.close();                  }              } catch (IOException e) {                  e.printStackTrace();              }              try {                  if (out != null) {                      out.close();                  }              } catch (IOException e) {                  e.printStackTrace();              }              try {                  if (is != null) {                      is.close();                  }              } catch (IOException e) {                  e.printStackTrace();              }          }            KLog.i("decrypt completed");      }  }</code></pre>    <p>使用如上这个方案就完美的绕开了上面提到的一些问题,成功的实现了使用Socket对文件进行加密传输。</p>    <h2>总结</h2>    <p>对于任何技术的使用,底层原理的理解还是很有必要的。不然遇到问题很容易就是一头雾水不知道Why!接下来准备看一下《图解加密技术》和《图解TCP/IP》这两本书,以便加深对密码学和Socket底层原理的理解。</p>    <p> </p>    <p>来自:http://www.jianshu.com/p/b2d9f84054d4</p>    <p> </p>