編碼、摘要和加密(三)——資料加密

flueky發表於2019-05-07

0. 前言

關於加密,此處沒有更加通俗易懂的解釋。與同是對於位元組型別資料處理的編碼和摘要對比:

  1. 編碼是可逆的,任何人只要知道編碼規則,就能夠進行解碼。
  2. 摘要是不可逆的,即使知道只要演算法的實現原理,也很難還原出原資料。

加密是可逆的,但只知道加密演算法並不能解密,還需要知道加密金鑰。

接下來,將針對幾個常見的加密演算法:DES3DESAESRSA 的 Java 實現及其相關進行介紹,由於之前實現過 DESAES 演算法,因此具體演算法說明,後續有空會寫到。

1. 相關概念

1.1 金鑰

在程式碼實現的過程中,金鑰即是 key 。使用對稱加密演算法時,加密和解密是同一個金鑰。使用非對稱加密演算法時,加密和解密金鑰不相同,區分為公鑰(public key)和私鑰(private key)。

見過把 base64 當做對稱加密, md5 當做非對稱加密,因此下面劃重點:

  1. 判別加密演算法最直接的方式,是否需要金鑰。
  2. 對稱加密和非對稱加密區別在於,加解密是否是同一個key。
  3. 對稱加密演算法效率優於非對稱加密演算法,建議用對稱加密演算法加密長資料,非對稱加密演算法加密端資料。

1.2 加密模式

加密模式主要體現在對稱加密演算法中。之前提到過,對稱加密演算法效率優,適合加密長資料。實際加密過程中,是將長資料劃分成固定長度的若干塊短資料進行加密操作。為防止暴力破解得出明文,因此衍生了四種加密模式。

1.2.1 電子密碼本模式

英譯 Electronic Code Book ,簡稱 ECB 模式,最簡單的加密模式。

  1. 將長資料分割成固定長度的若干塊。
  2. 分別對每塊資料用同一個金鑰進行加密。
  3. 將每塊加密出來的密文合併拼接成最終的完整密文。

上述步驟存在一個嚴重的問題,如果有重複的明文塊,那麼加密出來的密文也重複。

1.2.2 加密塊鏈模式

英譯 Cipher Block Chaining ,簡稱 CBC,基於 ECB 模式的改進版。 此處引入一個概念:初始化向量 Initialization Vector 簡稱 IV

  1. 將長資料分割成固定長度的若干塊。
  2. 將前一塊的密文與後一塊明文進行異或,再用金鑰進行加密。
  3. 將每塊加密出來的密文合併拼接成最終的完整密文。
  4. 第一塊明文沒有密文與其異或,因此需要 IV 對其異或再用金鑰加密。

1.2.3 加密反饋模式

英譯 Cipher Feedback Mode ,簡稱 CFB

  1. 將長資料分割成固定長度的若干塊。
  2. 將前一塊密文使用金鑰進行加密,再與後一塊明文進行異或。
  3. 將每塊異或後的密文合併拼接成最終的完整密文。
  4. 第一塊明文需要與用金鑰加密後的 IV 進行異或。

1.2.4 輸出反饋模式

英譯 Output Feedback Mode , 簡稱 OFB ,與 CFB 模式有些細小的區別。

  1. 將長資料分割成固定長度的若干塊。
  2. 將前一塊中間密文使用金鑰進行加密得中間密文。中間密文與明文進行異或得密文。
  3. 將每個中間密文與明文塊異或後的密文進行合併拼接成最終的完整密文。
  4. 第一塊明文需要的中間密文是用金鑰加密後的 IV 。

CFBOFB 的區別在於中間密文和密文塊的用法 。

  1. CFB 使用前一塊的密文進行加密。
  2. OFB 使用前一塊的中間密文進行加密。

1.3 填充模式

分塊加密的過程中,遇到不夠整分的塊。如,將 16 位元組作為一個明文塊。當加密 17 位元組時,不夠分成兩塊。此時需要對第二塊明文進行填充。填充後的兩個明文塊各 16 位元組共 32 位元組後再進行加密操作。

後 15 位元組的填充內容,需要取決於具體的填充模式。見後續 Java 程式碼實現中介紹。

2. 程式碼實現

Java 對加密部分做了比較完整的封裝—— Cipher 類。

以下列舉幾個主要方法:

  1. getInstance 獲取 Cipher 物件,主要接收轉換型別引數物件。轉換型別引數分為 演算法演算法/模式/填充
  2. init 初始化加密引數。包括指定加解密模式、金鑰和初始化向量-IV
  3. doFinal 結束加密和解密操作,有多個過載方法,主要接收需要加密或解密的資料。

關於填充,之前簡要介紹過。在 Java 程式碼中常見的填充模式是 PKCS5Padding 。還有一種模式 NoPadding 由於對明文長度有要求,不建議使用。其他填充模式未深入瞭解,暫不誤導。

PKCS5Padding 指,需要填充多少位元組,就填充多少個位元組的數字。如 DES 演算法要求每個明文塊 8 位元組,那麼,加密 1 位元組資料,需要填充 7 個位元組,此時填充 7 個 7 。加密 7 位元組資料,需要填充 1 個 1 。加密 8 位元組資料時,為方便校驗解密後的明文正確性,需要擴充套件成 16 位元組資料,此時第二個明文塊填充 8 個 8 。

2.1 DES

DES 全稱 Data Encryption Standard ,資料加密標準演算法。固定金鑰 8 位元組,64 位。每個明文塊長度 8 位元組。

getInstance 接收引數:DES/ECB/PKCS5Padding ,其中 ECB 表示加密模式,可以用上述的其他三個模式替換以及更多 JDK 支援的模式。PKCS5Padding 表示一種填充模式。

在使用 CBCCFBOFB 時,需要在 init 方法中指定 IV

private static final String KEY_ALGORITHM = "DES";
private static final String DEFAULT_CIPHER_ALGORITHM = "DES/ECB/PKCS5Padding";

public static final byte[] encrypt(byte[] data, byte[] key) {
    try {
        Cipher cipher = Cipher.getInstance(DEFAULT_CIPHER_ALGORITHM);
        // 加密模式
        cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, KEY_ALGORITHM));
        return cipher.doFinal(data);
    } catch (Exception e) {
        e.printStackTrace();
    }
    return null;
}

public static final byte[] decrypt(byte[] data, byte[] key) {
    try {
        Cipher cipher = Cipher.getInstance(DEFAULT_CIPHER_ALGORITHM);
        // 解密模式
        cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, KEY_ALGORITHM));
        return cipher.doFinal(data);
    } catch (Exception e) {
        e.printStackTrace();
    }
    return null;
}
複製程式碼

2.2 3DES

3DES 即使用 3 次 DES 演算法。由於每個 DES 演算法處理需要 8 位元組金鑰,因此 3DES 演算法需要 24 位元組金鑰。

需要注意,3DES 演算法名稱使用 DESedeTripleDESe 表示做 DES 加密操作,d 表示做 DES 解密操作。前者表示用 DES 連續做加密 、解密、加密操作,後者表示連續做三次加密操作。每次使用的金鑰,分別是 24 位元組金鑰中不同的三段(前、中、後各8位元組)。

若使用 DESede 演算法時 24 位元組金鑰中的前兩段一樣,該演算法等同於 DES 演算法使用第三段的 8 位元組金鑰。

填充相關同 DES 演算法一樣。

private static final String KEY_ALGORITHM = "DESede";
private static final String DEFAULT_CIPHER_ALGORITHM = "DESede/CBC/PKCS5Padding";
// 初始化向量
private static final String IV = "12345678";

public static final byte[] encrypt(byte[] data, byte[] key) {
    try {
        Cipher cipher = Cipher.getInstance(DEFAULT_CIPHER_ALGORITHM);
        cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, KEY_ALGORITHM),new IvParameterSpec(IV.getBytes()));
        return cipher.doFinal(data);
    } catch (Exception e) {
        e.printStackTrace();
    }
    return null;
}

public static final byte[] decrypt(byte[] data, byte[] key) {
    try {
        Cipher cipher = Cipher.getInstance(DEFAULT_CIPHER_ALGORITHM);
        cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, KEY_ALGORITHM));
        return cipher.doFinal(data);
    } catch (Exception e) {
        e.printStackTrace();
    }
    return null;
}
複製程式碼

2.3 AES

AES 全稱 Advanced Encryption Standard ,高階加密標準演算法,用於替代 DES

DES 只支援 8 位元組金鑰,AES 可以支援 16 位元組、24 位元組和 32 位元組金鑰。明文塊長度也可以劃分成 16 位元組 、24 位元組和 32 位元組進行填充。

    private static final String KEY_ALGORITHM = "AES";
    private static final String DEFAULT_CIPHER_ALGORITHM = "AES/ECB/PKCS5Padding";

    public static final byte[] encrypt(byte[] data, byte[] key) {
        try {
            Cipher cipher = Cipher.getInstance(DEFAULT_CIPHER_ALGORITHM);
            cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, KEY_ALGORITHM));
            return cipher.doFinal(data);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    public static final byte[] decrypt(byte[] data, byte[] key) {
        try {
            Cipher cipher = Cipher.getInstance(DEFAULT_CIPHER_ALGORITHM);
            cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, KEY_ALGORITHM));
            return cipher.doFinal(data);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
複製程式碼

以上三種對稱加密演算法的 Java 程式碼實現基本一致。

2.4 RSA

RSA是1977年由羅納德·李維斯特(Ron Rivest)、阿迪·薩莫爾(Adi Shamir)和倫納德·阿德曼(Leonard Adleman)一起提出的非對稱加密演算法,因此使用他們 3 人姓氏首字母命名。

示例程式碼中的公鑰和私鑰是隨機生成的金鑰對,金鑰對模長建議是 1024 或 2048,對應密文長度是 128 位元組和 256 位元組。模長可以大於 2048 , 越長越難破解,但是效率越低。 實際應用中,建議將金鑰對模長設定為 2048 並以檔案的形式儲存在終端。

private static PrivateKey privateKey;
private static PublicKey publicKey;
private static final String DEFAULT_CIPHER_ALGORITHM = "RSA";

static {

    KeyPairGenerator keyGener = null;
    try {
        keyGener = KeyPairGenerator.getInstance(DEFAULT_CIPHER_ALGORITHM);
        keyGener.initialize(1024);
        KeyPair keyPair = keyGener.generateKeyPair();
        privateKey = keyPair.getPrivate();
        publicKey = keyPair.getPublic();
    } catch (NoSuchAlgorithmException e) {
        e.printStackTrace();
    }
}

public static final byte[] encrypt(byte[] data) {
    try {
        Cipher cipher = Cipher.getInstance(DEFAULT_CIPHER_ALGORITHM);
        cipher.init(Cipher.ENCRYPT_MODE, publicKey);
        return cipher.doFinal(data);

    } catch (NoSuchAlgorithmException e) {
        e.printStackTrace();
    } catch (NoSuchPaddingException e) {
        e.printStackTrace();
    } catch (InvalidKeyException e) {
        e.printStackTrace();
    } catch (BadPaddingException e) {
        e.printStackTrace();
    } catch (IllegalBlockSizeException e) {
        e.printStackTrace();
    }

    return null;
}

public static final byte[] decrypt(byte[] data) {
    try {
        Cipher cipher = Cipher.getInstance(DEFAULT_CIPHER_ALGORITHM);
        cipher.init(Cipher.DECRYPT_MODE, privateKey);
        return cipher.doFinal(data);

    } catch (NoSuchAlgorithmException e) {
        e.printStackTrace();
    } catch (NoSuchPaddingException e) {
        e.printStackTrace();
    } catch (InvalidKeyException e) {
        e.printStackTrace();
    } catch (BadPaddingException e) {
        e.printStackTrace();
    } catch (IllegalBlockSizeException e) {
        e.printStackTrace();
    }

    return null;
}
複製程式碼

覺得有用?那打賞一個唄。我要打賞

此處是廣告Flueky的技術小站

相關文章