Android使用Socket對大文件進行加密傳輸

前言

數據加密,是一門歷史悠久的技術,指通過加密算法和加密密鑰將明文轉變為密文,而解密則是通過解密算法和解密密鑰將密文恢復為明文。它的核心是密碼學。
數據加密目前仍是計算機系統對信息進行保護的一種最可靠的辦法。它利用密碼技術對信息進行加密,實現信息隱蔽從而起到保護信息的安全的作用。

項目中使用Socket進行文件傳輸過程時,需要先進行加密。實現的過程中踏了一些坑,下面對實現過程進行一下總結。

DES加密

由于加密過程中使用的是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秘鑰長度必須是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;
    }

輸出:

加密數據:加密解密
加密后的數據:rt6XE06pElmLZMaVxrbfCQ==
解密后的數據:加密解密

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);   // 這里進行字節流的傳輸
    }

    fileOutStream.close();
    outStream.close();
    inStream.close();
    socket.close();

Socket服務端部分代碼:

    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();

數據加密傳輸

下面對傳輸數據進行加密解密

  • 方案一:直接對io流進行加密解密

客戶端變更如下:

    while (((len = fileOutStream.read(buffer)) != -1)) {
        outStream.write(DesUtil.encrypt(buffer) ,0, len); // 對字節數組進行加密
    }

服務端變更代碼:

    while ((len = inStream.read(buffer)) != -1) {
        fileOutStream.write(DesUtil.decrypt(buffer), 0, len); // 對字節數組進行解密
    }

執行代碼后,服務端解密時會報如下異常:

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以上使用你會悲催的發現下面這個異常:

javax.net.ssl.SSLHandshakeException: Handshake failed

異常原因:SSLSocket簽名算法默認為DSA,Android6.0(API 23)以后KeyStore發生更改,不再支持DSA,但仍支持ECDSA。

所以如果想在Android6.0以上使用SSLSocket,需要將DSA改成ECDSA...org感覺坑越入越深看不到底呀...于是決定換個思路來解決socket加密這個問題。既然對文件邊傳邊加密解密不好使,那能不能客戶端傳輸文件前先對文件進行加密,然后進行傳輸,服務端成功接收文件后,再對文件進行解密呢。于是就有了下面這個方案。

  • 方案五:先對文件進行加密,然后傳輸,服務端成功接收文件后再對文件進行解密

對文件進行加密解密代碼如下:

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");
    }
}

使用如上這個方案就完美的繞開了上面提到的一些問題,成功的實現了使用Socket對文件進行加密傳輸。

總結

對于任何技術的使用,底層原理的理解還是很有必要的。不然遇到問題很容易就是一頭霧水不知道Why!接下來準備看一下《圖解加密技術》和《圖解TCP/IP》這兩本書,以便加深對密碼學和Socket底層原理的理解。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,002評論 6 542
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,400評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,136評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,714評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,452評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,818評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,812評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,997評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,552評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,292評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,510評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,035評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,721評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,121評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,429評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,235評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,480評論 2 379

推薦閱讀更多精彩內容