天天看點

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

作者:水滴的程式員夢

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 是指分組資料缺少幾個位元組,就在資料的末尾填充幾個位元組的幾,比如缺少5個位元組,就在末尾填充5個位元組的5。
  • PKCS7 是指分組資料缺少幾個位元組,就在資料的末尾填充幾個位元組的0,比如缺少7個位元組,就在末尾填充7個位元組的0。
  • NoPadding 是指不需要填充,也就是說資料的發送方肯定會保證最後一段資料也正好是16個位元組。

在 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. 分組加密模式

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

  • ECB(電子密碼本模式 Electronic Codebook Book),相對的不安全,很少使用
  • CBC(密碼分組連結模式 Cipher Block Chaining),不支援并行計算,比 ECB 模式多了一個初始向量 IV,是這些模式中最安全的,也是最常用的模式
  • CFB(密碼回報模式 Cipher FeedBack),可被施以”重播攻擊“
  • OFB(輸出回報模式 Output FeedBack),可被主動攻擊者反轉密文而引起解密後明文中的相應比特也發生變化
  • CTR(計數器模式 Counter mode),與 OFB 一樣可被主動攻擊者反轉密文,但比 OFB多了支援并發計算的特性

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

2. 非對稱加密

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

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

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

2.1. 使用場景

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

(1)B向A發送加密資料

  • A 生成一對密鑰,私鑰由 A 自己保留不公開;而公鑰傳給 B,公開,任何人可以擷取
  • B 用該公鑰對消息進行加密,并發送給 A
  • A 接收到加密消息後,用私鑰對消息進行解密

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

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

  • A 用私鑰對消息加簽形成簽名,并将加簽的消息和消息本身一起傳遞給 B
  • B 收到消息後,用公鑰進行驗簽,如果驗簽出來的内容與消息本身一緻,證明消息是 A 回複的。

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

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

  • 第一個場景雖然被截獲的消息沒有洩露,但是可以利用截獲的公鑰,将假指令進行加密,然後傳遞給 A。
  • 第二個場景雖然截獲的消息不能被篡改,但是消息的内容可以利用公鑰驗簽來獲得,并不能防止洩露。

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

2.2. RAS 加密算法

2.2.1. 填充模式 Padding

Padding 常見模式如下表:

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

Padding 模式

RSA 常用的加密填充模式

  • RSA/None/PKCS1Padding(Java 預設的 RSA 實作)
  • RSA/ECB/PKCS1Padding

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

3. 簽名

加密是為了防止資訊被洩露,而簽名是為了防止資訊被篡改和僞造。

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

簽名 & 驗簽

3.1. 摘要算法

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

好的雜湊演算法具備如下特性:

  • 單向性(one-way)即不可逆
  • 抗沖突性(collision-resistant)即産生兩個相同散列值的機率很低(但輸入相同,則輸出的結果一定相同)
  • 雪崩效應(avalanche effect)即原始資料的微小改動,會導緻散列值的巨大差異

常見的用途:

  • 密碼保護:把使用者密碼通過散列函數加密儲存(儲存散列值),隻有使用者自己知道密碼的明文
  • 簽名 & 驗簽:比如對接口調用、對消息進行簽名,接收方進行驗簽
  • 資料完整性/一緻性校驗:比如網上提供的檔案下載下傳通常都提供散列值和算法,便于使用者校驗
  • 資料秒傳:上傳幾個G的大檔案隻用幾秒,就是通過對比檔案的散列值實作的,散列值(資訊的指紋)相同就認為是同一個檔案

常見的雜湊演算法有”封包摘要算法 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 示例代碼。希望對各位小夥伴們有幫助哦[中國贊][中國贊][中國贊]

繼續閱讀