AndroidはSocketを使用して大きなファイルを暗号化して転送します

10175 ワード

前言
データ暗号化は、暗号化アルゴリズムと暗号化鍵によって明文を暗号化に変換し、復号化は復号化アルゴリズムと復号化鍵によって暗号文を明文に復元する歴史の長い技術である.その核心は暗号学である.データ暗号化は現在もコンピュータシステムが情報を保護する最も信頼できる方法である.暗号技術を利用して情報を暗号化し、情報隠蔽を実現し、情報の安全を保護する役割を果たす.
プロジェクトで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();


データ暗号化伝送
次に、伝送データの暗号解読を行う
  • スキーム1: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などのファイルでは、解析パケットに異常が発生するなどのエラーメッセージが表示されます.
  • シナリオ2:文字ストリームの使用
  • Base 64を使用してバイト配列を符号化すると、どのバイトでも対応するBase 64文字にマッピングされ、その後バイト配列に復元され、暗号化後のデータの転送に保存されるので、変換は安全です.同様に、バイト配列を16進文字列に変換するのも安全です.
    クライアントは入力ファイルからバイトストリームを読み出すため、バイトストリームを文字ストリームに変換する必要があり、サービス側は文字ストリームを受信した後、バイトストリームに変換してファイルに書き込む必要があります.テストでは、文字ストリームの復号に成功することがわかりましたが、ファイルを文字ストリームに変換して転送するのは連続的なプロセスであり、ファイルの書き出しと書き込みは煩雑で、操作中に多くの問題が発生します.
  • シナリオ3:CipherInputStream、CipherOutputStreamを使用
  • 使用中、CipherOutputStreamストリームcloseの場合にのみ、CipherInputStreamがデータを受信することが分かったが、このスキームもpassで削除するしかないことは明らかだ.
  • シナリオ4: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
    以上のすべての準備が完了したら、Android 6.0以上使用すると、次の異常に気づくことがあります.
    javax.net.ssl.SSLHandshakeException: Handshake failed
    

    異常原因:SSLSocket署名アルゴリズムデフォルトはDSA、Android 6.0(API 23)以降、KeyStoreは変更され、DSAはサポートされなくなったが、ECDSAはサポートされている.
    だからAndroid 6で0以上SSLSocketを使用する場合、DSAをECDSAに変更する必要があります...org感じ坑越入越深看底不见...そこで考えを変えてsocket暗号化という問題を解決することにした.ファイルを転送しながら暗号化して復号するのはよくない以上、クライアントがファイルを転送する前にファイルを暗号化してから転送し、サービス側がファイルを正常に受信してから、ファイルを復号することができますか.そこで次のような案がありました.
  • スキーム5:ファイルを暗号化してから転送し、サービス側がファイルを正常に受信した後、ファイルを復号化
  • ファイルを暗号化して復号するコードは次のとおりです.
    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」の2冊の本を見て、暗号学とSocketの下層原理の理解を深めるつもりです.