程序員必須知道的加密、解密和簽名算法

  1. 對稱加密 =======

對稱加密,加密和解密使用相同的祕鑰,加密速度快、效率高。常見的有 DES(淘汰)、3DES(淘汰)、AES(用於替代 DES,是目前常用的)等。

1.1. DES(Data Encryption Standard)

DES 現在認爲是一種不安全的加密算法,已經有用窮舉法攻破 DES 密碼的報道了。3DES 是 DES 的加強版本(也被淘汰),是 DES 向 AES 過渡的加密算法。

1.2. AES(Advanced Encryption Standard)

AES 把明文按每組 16 個字節分成一組一組的、長度相等的數據,每次加密一組,直到加密完整個明文。在 AES 標準中,分組長度只能是 128 位,但是密鑰的長度可以使用 128 位、192 位或 256 位。

下面先來了解 “分組加密機制、填充模式、初始向量、加密模式” 等基本概念,最後給出 Java 代碼示例。

1.2.1. 分組密碼體制

分組密碼體制就是指將明文切成一段一段的來加密,而且每段數據的長度要求必須是 128 位 16 個字節,如果最後一段不夠 16 個字節了,就需要用 Padding 來把這段數據填滿 16 個字節,然後再把一段一段的密文拼起來形成最終密文的加密方式。

1.2.2. 填充模式 Padding

Padding 就是用來把不滿 16 個字節的分組數據填滿 16 個字節用的,它有三種模式 PKCS5、PKCS7 和 NOPADDING。

在 PKCS5 模式下,有這樣一種特殊情況,假設最後一段數據的內容剛好就是 16 個 16,這時解密端怎麼區分是填充還是數據呢?

對於這種情況,PKCS5 模式會自動幫我們在最後一段數據後再添加 16 個字節的數據,而且填充數據也是 16 個 16,這樣解密端就能知道誰是有效數據誰是填充數據了。同樣的道理,PKCS7 最後一段數據的內容是 16 個 0。

解密端需要使用和加密端同樣的 Padding 模式,才能準確的識別有效數據和填充數據。開發通常採用 PKCS7 Padding 模式。

1.2.3. 初始向量 IV

初始向量 IV 的作用是使加密更加安全可靠。使用 AES 加密時要主動提供初始向量,而且只需提供一個初始向量就夠了,後面每段數據的加密向量都是前面一段的密文。初始向量 IV 的長度規定爲 128 位 16 個字節,初始向量通常採用隨機生成。

1.2.4. 密鑰

AES 要求密鑰的長度可以是 128 位、192 位或者 256 位,位數越高,加密強度越大,但是加密的效率自然會低一些,因此要做好衡量。

1.2.5. 分組加密模式

分組加密算法只能對固定長度的分組進行加密,面對超過分組長度的明文,就需要對分組密碼算法進行迭代,以便將很長的明文全部加密。而迭代的方法就稱爲 “分組加密的模式”。分組密碼的模式有很多,常見的有:

AES 和 RSA 都屬於分組加密算法。

1.2.6. AES Java 示例

AES-128-CBC 加解密

public static String cbcEncrypt(String plain, String key, String ivSeed) {
    Assert.notNull(plain, "plain must not be null");
    Assert.notNull(key, "key must not be null");
    Assert.notNull(ivSeed, "ivSeed must not be null");
    Assert.isTrue(ivSeed.length() == 16, "ivSeed must be 16 bytes");
    String base64 = null;
        
    try {
        // 生成祕鑰
        SecretKeySpec keySpec = createKey(key);
        // 設置算法/模式/填充方式
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        // 設置偏移
        IvParameterSpec iv = new IvParameterSpec(ivSeed.getBytes(StandardCharsets.UTF_8));
        // 加密模式
        cipher.init(Cipher.ENCRYPT_MODE, keySpec, iv);
        // 加密
        byte[] encrypted = cipher.doFinal(plain.getBytes(StandardCharsets.UTF_8));
        // 轉 base64
        base64 = Base64.getEncoder().encodeToString(encrypted);
    } catch (Exception ex) {
        log.error("exception: {}", ex.getMessage());
    }

    return base64;
}

public static String cbcDecrypt(String base64, String key, String ivSeed) {
    Assert.notNull(base64, "base64 must not be null");
    Assert.notNull(key, "key must not be null");
    Assert.notNull(ivSeed, "ivSeed must not be null");
    Assert.isTrue(ivSeed.length() == 16, "ivSeed must be 16 bytes");

    String plain = null;
    try {
        // base64 解碼
        byte[] decodedBase64 = Base64.getDecoder().decode(base64);
        // 生成祕鑰
        SecretKeySpec keySpec = createKey(key, isBase64Key);
        if(null != keySpec) {
            // 設置偏移
            IvParameterSpec iv = new IvParameterSpec(ivSeed.getBytes(StandardCharsets.UTF_8));
            // 設置算法/模式/填充方式
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
            // 解密模式
            cipher.init(Cipher.DECRYPT_MODE, keySpec, iv);
            // 解密
            byte[] decrypted = cipher.doFinal(data);
            // 轉 base64
            plain = new String(decrypted, StandardCharsets.UTF_8);
        }
    } catch (Exception ex) {
        log.error("exception: {}", ex.getMessage());
    }

    return plain;
}

private static SecretKeySpec createKey(String key) {
    Assert.notNull(key, "key must not be null");

    byte[] bytesKey = key.getBytes(StandardCharsets.UTF_8);
    Assert.isTrue(bytesKey.length == 16, "key must be 16 bytes");

    // 生成祕鑰
    return new SecretKeySpec(bytesKey, "AES");
}
  1. 非對稱加密 ========

非對稱加密算法,需要兩個密鑰, 一個是公鑰 (public key),公開,任何人都可以獲取;另一個是 私鑰 (private key),不公開,由個人保存在安全的地方。公鑰用於加密,私鑰用於解密。

RSA (三位數學家名字的縮寫)算法是第一個能同時用於 加密 和 數字簽名 的非對稱加密算法,它能夠 抵抗 到目前爲止已知的 所有密碼攻擊,已被 ISO 推薦爲公鑰數據加密標準。

2.1. 使用場景

假設 A 和 B 之間要進行加密通信,那麼:

(1)B 向 A 發送加密數據

在這個過程中,只有 2 次傳遞過程,第一次是 A 傳遞公鑰給 B,第二次是 B 傳遞加密消息給 A,即使都被截獲,也沒有危險性,因爲只有 A 的私鑰才能對消息進行解密,防止了消息內容的泄露。

(2)A 向 B 發送 “已收到” 回覆

在這個過程中,算上前面的傳遞公鑰,也只有 2 次傳遞過程,一次是傳遞公鑰,第二次就是 A 傳遞加簽的消息和消息本身給 B,即使都被敵方截獲,也同樣沒有危險性,因爲只有 A 的私鑰才能對消息進行簽名,即使知道了消息內容,也無法僞造帶簽名的回覆給 B,防止了消息內容的篡改。

但是,綜合上面兩個場景會發現:

所以在實際應用中,要根據情況使用,可以雙方同時使用加密和簽名,比如 A 和 B 都有一套自己的公鑰和私鑰,當 A 要給 B 發送消息時,先用 B 的公鑰對消息加密,再對加密的消息使用 A 的私鑰加簽名,達到既不泄露也不被篡改,更能保證消息的安全性。

2.2. RAS 加密算法

2.2.1. 填充模式 Padding

Padding 常見模式如下表:

Padding 模式

RSA 常用的加密填充模式

2.2.2. RSA Java 示例

/**
 * 公鑰加密
 */
public static String ecbEncrypt(String data, String publicKeyBase64) throws NoSuchPaddingException, NoSuchAlgorithmException,
InvalidKeySpecException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
    Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
    
    cipher.init(Cipher.ENCRYPT_MODE, toPublicKey(publicKeyBase64));
    byte[] bytes cipher.doFinal(data.getBytes());
    
    return Base64.getEncoder().encodeToString(bytes);
}

/**
 * 私鑰解密
 */
public static String ecbDecrypt(String base64, String privateKeyBase64) throws IllegalBlockSizeException, InvalidKeyException,
InvalidKeySpecException, BadPaddingException, NoSuchAlgorithmException, NoSuchPaddingException {
    byte[] bytes = Base64.getDecoder().decode(base64.getBytes());

    Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
    cipher.init(Cipher.DECRYPT_MODE, toPrivateKey(privateKeyBase64));

    return new String(cipher.doFinal(data));
}

/**
 * 生成隨機密鑰對
 */
public static HashMap<String, String> randomKeyPair() throws NoSuchAlgorithmException {
    KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
    generator.initialize(2048);
    KeyPair pair = generator.generateKeyPair();
    if (null == pair) {
        return null;
    }

    PrivateKey pvt = pair.getPrivate();
    PublicKey pub = pair.getPublic();

    Base64.Encoder encoder = Base64.getEncoder();
    String pvtVal = encoder.encodeToString(pvt.getEncoded());
    String pubVal = encoder.encodeToString(pub.getEncoded());

    HashMap<String, String> rsaKeyMap = new HashMap<>(2);
    rsaKeyMap.put("privateKeyBase64", pvtVal);
    rsaKeyMap.put("publicKeyBase64", pubVal);

    return rsaKeyMap;
}

/**
 * privateKeyBase64 私鑰轉爲 PrivateKey 對象
 */
private static PrivateKey toPrivateKey(String privateKeyBase64) throws NoSuchAlgorithmException, InvalidKeySpecException {
    byte[] bytes = Base64.getDecoder().decode(privateKeyBase64.getBytes());

    PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(bytes);
    KeyFactory kf = KeyFactory.getInstance("RSA");

    return kf.generatePrivate(keySpec);
}

/**
 * publicKeyBase64 公鑰轉爲 PublicKey 對象
 */
private static PublicKey toPublicKey(String publicKeyBase64) throws NoSuchAlgorithmException, InvalidKeySpecException {
    byte[] bytes = Base64.getDecoder().decode(publicKeyBase64.getBytes());

    X509EncodedKeySpec ks = new X509EncodedKeySpec(bytes);
    KeyFactory kf = KeyFactory.getInstance("RSA");

    return kf.generatePublic(ks);
}
  1. 簽名 =====

加密是爲了防止信息被泄露,而簽名是爲了防止信息被篡改和僞造。

簽名 & 驗籤

3.1. 摘要算法

哈希函數(Hash function),又稱散列函數、散列算法,也叫摘要算法,它是一種不可逆的信息摘要算法。

好的散列算法具備如下特性:

常見的用途:

常見的散列算法有” 報文摘要算法 MD“、” 安全散列算法 SHA“,以及” 消息認證碼算法 MAC“。

摘要算法

3.1.1. 報文摘要算法(MD 系列)

信息摘要算法(Message-Digest Algorithm)。最常用的是 MD5 (Message-Digest Algorithm 5),是一種被廣泛使用的密碼散列函數,可以產生出一個 128 位(16 字節)的散列值(hash value),常用於確保信息傳輸完整一致。

3.1.2. 安全散列算法(SHA 系列)

安全散列算法(Secure Hash Algorithm)是一種不可逆的信息安全算法,經過量化運算和轉換,可以把任意長度的數據生成不可逆的、固定長度的字符串,這個固定長度的字符串就是對相應的原始輸入字符串的散列(也稱爲摘要),可以作爲信息的指紋。

SHA-224,SHA-256,SHA-384,SHA-512 統稱爲 SHA-2,而 SHA-1 算法已經不夠安全,不建議繼續使用。

3.1.3. 消息認證碼(MAC 系列)

消息認證碼算法(Message Authentication Code)是含有加密密鑰的散列算法,它在 MD 和 SHA 算法特性的基礎上加入了加密密鑰,通過特別的計算方式來構造消息認證碼 (MAC) 的方法。因 MAC 算法融合了密鑰散列函數,通常也稱爲 HMAC 算法(Hash-based Message Authentication Code,散列消息認證碼)。

常見的有:HMAC-SHA224、HMAC-SHA256、HMAC-SHA384、HMAC-SHA512

3.2. 簽名驗籤原理

3.2.1. 簽名

對需要發送的報文 originData 計算摘要(相關摘要算法有 md5、sha256 等)特徵值 signBlock。使用私鑰 privateKey 對 signBlock 加密獲得數字簽名 signatureData。將 signatureData 與 originData 打包發一起送給對方。

3.2.2. 驗籤

接收方接收到數據後,把消息拆分爲 signatureData 與 originData 。對 originData 計算特徵值 signBlock,使用的算法必須要和發送方一致。使用公鑰 publicKey 對 signatureData 解密,獲得 signBlock1。比較 signBlock 和 signBlock1,若匹配則驗證成功,報文未被篡改。

3.3. RSA 簽名示例

/**
* 用私鑰對數據進行簽名並返回簽名後的base64
* @param data 代簽名的字符串
* @param base64PrivateKey 私鑰
*/
public static String sign(String data, String base64PrivateKey) 
throws InvalidKeySpecException, InvalidKeyException, NoSuchAlgorithmException, SignatureException {
    PrivateKey key = toPrivateKey(base64PrivateKey);
    Signature signature = Signature.getInstance("SHA256withRSA");
    signature.initSign(key);
    signature.update(data.getBytes());
    return new String(Base64.getEncoder().encode(signature.sign()));
}

/**
* 驗籤
* @param data 原始數據
* @param base64PublicKey 公鑰
* @param sign 私鑰簽名後的數據
*/
public static boolean verify(String data, String base64PublicKey, String sign) 
throws InvalidKeySpecException, InvalidKeyException, NoSuchAlgorithmException, SignatureException {
    PublicKey key = toPublicKey(base64PublicKey);
    Signature signature = Signature.getInstance("SHA256withRSA");
    signature.initVerify(key);
    signature.update(data.getBytes());
    return signature.verify(Base64.getDecoder().decode(sign.getBytes()));
}

/**
* base64PrivateKey 私鑰轉爲 PrivateKey 對象
*/
private static PrivateKey toPrivateKey(String base64PrivateKey) throws NoSuchAlgorithmException, InvalidKeySpecException {
    byte[] bytes = Base64.getDecoder().decode(base64PrivateKey.getBytes());

    PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(bytes);
    KeyFactory kf = KeyFactory.getInstance("RSA");

    return kf.generatePrivate(keySpec);
}
/**
* base64PublicKey 公鑰轉爲 PublicKey 對象
*/
private static PublicKey toPublicKey(String base64PublicKey) throws NoSuchAlgorithmException, InvalidKeySpecException {
    byte[] bytes = Base64.getDecoder().decode(base64PublicKey.getBytes());
    X509EncodedKeySpec ks = new X509EncodedKeySpec(bytes);
    KeyFactory kf = KeyFactory.getInstance("RSA");
    return kf.generatePublic(ks);
}

小結

本文主要介紹了常用的對稱加密算法、非對稱加密算法;常用的摘要算法、簽名算法。以及使用算法需要了解的基本概念(比如填充模式、IV 等),算法的使用場景,並且分別給出了 Java 示例代碼。希望對各位小夥伴們有幫助哦

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/g2lCS-SdV8W3GVq349oOag