詳解Android使用Socket對(duì)大文件進(jìn)行加密傳輸
前言
數(shù)據(jù)加密,是一門歷史悠久的技術(shù),指通過(guò)加密算法和加密密鑰將明文轉(zhuǎn)變?yōu)槊芪模饷軇t是通過(guò)解密算法和解密密鑰將密文恢復(fù)為明文。它的核心是密碼學(xué)。
數(shù)據(jù)加密目前仍是計(jì)算機(jī)系統(tǒng)對(duì)信息進(jìn)行保護(hù)的一種最可靠的辦法。它利用密碼技術(shù)對(duì)信息進(jìn)行加密,實(shí)現(xiàn)信息隱蔽從而起到保護(hù)信息的安全的作用。
項(xiàng)目中使用Socket進(jìn)行文件傳輸過(guò)程時(shí),需要先進(jìn)行加密。實(shí)現(xiàn)的過(guò)程中踏了一些坑,下面對(duì)實(shí)現(xiàn)過(guò)程進(jìn)行一下總結(jié)。
DES加密
由于加密過(guò)程中使用的是DES加密算法,下面貼一下DES加密代碼:
//秘鑰算法 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秘鑰長(zhǎng)度必須是8位 public static void main(String args[]) { String data = "加密解密"; KLog.d("加密數(shù)據(jù):" + data); byte[] encryptData = encrypt(data.getBytes()); KLog.d("加密后的數(shù)據(jù):" + new String(encryptData)); byte[] decryptData = decrypt(encryptData); KLog.d("解密后的數(shù)據(jù):" + 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; }
輸出:
加密數(shù)據(jù):加密解密
加密后的數(shù)據(jù):rt6XE06pElmLZMaVxrbfCQ==
解密后的數(shù)據(jù):加密解密
Socket客戶端部分代碼:
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); // 這里進(jìn)行字節(jié)流的傳輸 } fileOutStream.close(); outStream.close(); inStream.close(); socket.close();
Socket服務(wù)端部分代碼:
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) { // 從字節(jié)輸入流中讀取數(shù)據(jù)寫入到文件中 fileOutStream.write(buffer, 0, len); } fileOutStream.close(); inStream.close(); outStream.close(); socket.close();
數(shù)據(jù)加密傳輸
下面對(duì)傳輸數(shù)據(jù)進(jìn)行加密解密
方案一:直接對(duì)io流進(jìn)行加密解密
客戶端變更如下:
while (((len = fileOutStream.read(buffer)) != -1)) { outStream.write(DesUtil.encrypt(buffer) ,0, len); // 對(duì)字節(jié)數(shù)組進(jìn)行加密 }
服務(wù)端變更代碼:
while ((len = inStream.read(buffer)) != -1) { fileOutStream.write(DesUtil.decrypt(buffer), 0, len); // 對(duì)字節(jié)數(shù)組進(jìn)行解密 }
執(zhí)行代碼后,服務(wù)端解密時(shí)會(huì)報(bào)如下異常:
javax.crypto.BadPaddingException: pad block corrupted
猜測(cè)錯(cuò)誤原因是加密過(guò)程會(huì)對(duì)數(shù)據(jù)進(jìn)行填充處理,然后在io流傳輸?shù)倪^(guò)程中,數(shù)據(jù)有丟包現(xiàn)象發(fā)生,所以解密會(huì)報(bào)異常。
加密后的結(jié)果是字節(jié)數(shù)組,這些被加密后的字節(jié)在碼表(例如UTF-8 碼表)上找不到對(duì)應(yīng)字符,會(huì)出現(xiàn)亂碼,當(dāng)亂碼字符串再次轉(zhuǎn)換為字節(jié)數(shù)組時(shí),長(zhǎng)度會(huì)變化,導(dǎo)致解密失敗,所以轉(zhuǎn)換后的數(shù)據(jù)是不安全的。
于是嘗試了使用NOPadding填充模式,這樣雖然可以成功解密,測(cè)試中發(fā)現(xiàn)對(duì)于一般文件,如.txt文件可以正常顯示內(nèi)容,但是.apk等文件則會(huì)有解析包出現(xiàn)異常等錯(cuò)誤提示。
方案二:使用字符流
使用Base64 對(duì)字節(jié)數(shù)組進(jìn)行編碼,任何字節(jié)都能映射成對(duì)應(yīng)的Base64 字符,之后能恢復(fù)到字節(jié)數(shù)組,利于加密后數(shù)據(jù)的保存于傳輸,所以轉(zhuǎn)換是安全的。同樣,字節(jié)數(shù)組轉(zhuǎn)換成16 進(jìn)制字符串也是安全的。
由于客戶端從輸入文件中讀取的是字節(jié)流,需要先將字節(jié)流轉(zhuǎn)換成字符流,而服務(wù)端接收到字符流后需要先轉(zhuǎn)換成字節(jié)流,再將其寫入到文件。測(cè)試中發(fā)現(xiàn)可以對(duì)字符流成功解密,但是將文件轉(zhuǎn)化成字符流進(jìn)行傳輸是個(gè)連續(xù)的過(guò)程,而文件的寫出和寫入又比較繁瑣,操作過(guò)程中會(huì)出現(xiàn)很多問(wèn)題。
方案三:使用CipherInputStream、CipherOutputStream
使用過(guò)程中發(fā)現(xiàn)只有當(dāng)CipherOutputStream流close時(shí),CipherInputStream才會(huì)接收到數(shù)據(jù),顯然這個(gè)方案也只好pass掉。
方案四:使用SSLSocket
在Android上使用SSLSocket的會(huì)稍顯復(fù)雜,首先客戶端和服務(wù)端需要生成秘鑰和證書(shū)。Android證書(shū)的格式還必須是bks格式(Java使用jks格式)。一般來(lái)說(shuō),我們使用jdk的keytool只能生成jks的證書(shū)庫(kù),如果生成bks的則需要下載BouncyCastle庫(kù)。
當(dāng)以上所有的一切都準(zhǔn)備完畢后,如果在Android6.0以上使用你會(huì)悲催的發(fā)現(xiàn)下面這個(gè)異常:
javax.net.ssl.SSLHandshakeException: Handshake failed
異常原因:SSLSocket簽名算法默認(rèn)為DSA,Android6.0(API 23)以后KeyStore發(fā)生更改,不再支持DSA,但仍支持ECDSA。
所以如果想在Android6.0以上使用SSLSocket,需要將DSA改成ECDSA...org感覺(jué)坑越入越深看不到底呀...于是決定換個(gè)思路來(lái)解決socket加密這個(gè)問(wèn)題。既然對(duì)文件邊傳邊加密解密不好使,那能不能客戶端傳輸文件前先對(duì)文件進(jìn)行加密,然后進(jìn)行傳輸,服務(wù)端成功接收文件后,再對(duì)文件進(jìn)行解密呢。于是就有了下面這個(gè)方案。
方案五:先對(duì)文件進(jìn)行加密,然后傳輸,服務(wù)端成功接收文件后再對(duì)文件進(jìn)行解密
對(duì)文件進(jìn)行加密解密代碼如下:
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 秘鑰長(zhǎng)度必須是8 位或以上 /** * 文件進(jìn)行加密并保存加密后的文件到指定目錄 * * @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; } /** * 文件進(jìn)行解密并保存解密后的文件到指定目錄 * * @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"); } }
使用如上這個(gè)方案就完美的繞開(kāi)了上面提到的一些問(wèn)題,成功的實(shí)現(xiàn)了使用Socket對(duì)文件進(jìn)行加密傳輸。
總結(jié)
對(duì)于任何技術(shù)的使用,底層原理的理解還是很有必要的。不然遇到問(wèn)題很容易就是一頭霧水不知道Why!
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- 詳解Android 基于TCP和UDP協(xié)議的Socket通信
- Android使用WebSocket實(shí)現(xiàn)多人游戲
- 詳解OkSocket與Android的簡(jiǎn)單使用
- Android開(kāi)發(fā)之Socket通信傳輸簡(jiǎn)單示例
- android基于socket的局域網(wǎng)內(nèi)服務(wù)器與客戶端加密通信
- android socket聊天室功能實(shí)現(xiàn)
- SpringBoot webSocket實(shí)現(xiàn)發(fā)送廣播、點(diǎn)對(duì)點(diǎn)消息和Android接收
- Android中Socket大文件斷點(diǎn)上傳示例
- android Socket實(shí)現(xiàn)簡(jiǎn)單聊天功能以及文件傳輸
- 詳解Android 通過(guò)Socket 和服務(wù)器通訊(附demo)
- Android Socket接口實(shí)現(xiàn)即時(shí)通訊實(shí)例代碼
- Android完整Socket解決方案
相關(guān)文章
Android開(kāi)發(fā)實(shí)現(xiàn)圖片的上傳下載
這篇文章主要為大家詳細(xì)介紹了Android開(kāi)發(fā)實(shí)現(xiàn)圖片的上傳下載,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-09-09Android控件CardView實(shí)現(xiàn)卡片布局
這篇文章主要為大家詳細(xì)介紹了Android控件CardView實(shí)現(xiàn)卡片布局,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-10-10Android的HTTP擴(kuò)展包OkHttp中的緩存功能使用方法解析
OkHttp(GitHub主頁(yè)https://github.com/square/okhttp)是一款高人氣的第三方Android網(wǎng)絡(luò)編程包,這里我們來(lái)看一下Android的HTTP擴(kuò)展包OkHttp中的緩存功能使用方法解析:2016-07-07Android實(shí)現(xiàn)一個(gè)絲滑的自動(dòng)輪播控件實(shí)例代碼
輪播圖對(duì)大家來(lái)說(shuō)應(yīng)該再熟悉不過(guò)了,下面這篇文章主要給大家介紹了關(guān)于Android實(shí)現(xiàn)一個(gè)絲滑的自動(dòng)輪播控件的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),需要的朋友可以參考借鑒,下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2018-08-08Android系統(tǒng)開(kāi)發(fā)中l(wèi)og的使用方法及簡(jiǎn)單的原理
LOG是廣泛使用的用來(lái)記錄程序執(zhí)行過(guò)程的機(jī)制,它既可以用于程序調(diào)試,也可以用于產(chǎn)品運(yùn)營(yíng)中的事件記錄;在平時(shí)開(kāi)發(fā)過(guò)程中經(jīng)常需要與log打交道,所以很有必要了解log的使用方法及簡(jiǎn)單的原理,感興趣的朋友可以了解下啊2013-01-01Android onMeasure與onDraw及自定義屬性使用示例
這篇文章主要介紹了Android onMeasure與onDraw及自定義屬性使用示例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)吧2023-02-02