淺談常見的七種加密演算法及實現

零壹技術棧發表於2018-07-13

前言

數字簽名資訊加密 是前後端開發都經常需要使用到的技術,應用場景包括了使用者登入、交易、資訊通訊、oauth 等等,不同的應用場景也會需要使用到不同的簽名加密演算法,或者需要搭配不一樣的 簽名加密演算法 來達到業務目標。這裡簡單的給大家介紹幾種常見的簽名加密演算法和一些典型場景下的應用。

正文

1. 數字簽名

數字簽名,簡單來說就是通過提供 可鑑別數字資訊 驗證 自身身份 的一種方式。一套 數字簽名 通常定義兩種 互補 的運算,一個用於 簽名,另一個用於 驗證。分別由 傳送者 持有能夠 代表自己身份私鑰 (私鑰不可洩露),由 接受者 持有與私鑰對應的 公鑰 ,能夠在 接受 到來自傳送者資訊時用於 驗證 其身份。

淺談常見的七種加密演算法及實現

注意:圖中 加密過程 有別於 公鑰加密,更多 介紹戳這裡簽名 最根本的用途是要能夠唯一 證明傳送方的身份,防止 中間人攻擊CSRF 跨域身份偽造。基於這一點在諸如 裝置認證使用者認證第三方認證 等認證體系中都會使用到 簽名演算法 (彼此的實現方式可能會有差異)。

2. 加密和解密

2.1. 加密

資料加密 的基本過程,就是對原來為 明文 的檔案或資料按 某種演算法 進行處理,使其成為 不可讀 的一段程式碼,通常稱為 “密文”。通過這樣的途徑,來達到 保護資料 不被 非法人竊取、閱讀的目的。

2.2. 解密

加密逆過程解密,即將該 編碼資訊 轉化為其 原來資料 的過程。

3. 對稱加密和非對稱加密

加密演算法分 對稱加密非對稱加密,其中對稱加密演算法的加密與解密 金鑰相同,非對稱加密演算法的加密金鑰與解密 金鑰不同,此外,還有一類 不需要金鑰雜湊演算法

常見的 對稱加密 演算法主要有 DES3DESAES 等,常見的 非對稱演算法 主要有 RSADSA 等,雜湊演算法 主要有 SHA-1MD5 等。

3.1. 對稱加密

對稱加密演算法 是應用較早的加密演算法,又稱為 共享金鑰加密演算法。在 對稱加密演算法 中,使用的金鑰只有一個,傳送接收 雙方都使用這個金鑰對資料進行 加密解密。這就要求加密和解密方事先都必須知道加密的金鑰。

淺談常見的七種加密演算法及實現

  1. 資料加密過程:在對稱加密演算法中,資料傳送方明文 (原始資料) 和 加密金鑰 一起經過特殊 加密處理,生成複雜的 加密密文 進行傳送。

  2. 資料解密過程:資料接收方 收到密文後,若想讀取原資料,則需要使用 加密使用的金鑰 及相同演算法的 逆演算法 對加密的密文進行解密,才能使其恢復成 可讀明文

3.2. 非對稱加密

非對稱加密演算法,又稱為 公開金鑰加密演算法。它需要兩個金鑰,一個稱為 公開金鑰 (public key),即 公鑰,另一個稱為 私有金鑰 (private key),即 私鑰

因為 加密解密 使用的是兩個不同的金鑰,所以這種演算法稱為 非對稱加密演算法

淺談常見的七種加密演算法及實現

  1. 如果使用 公鑰 對資料 進行加密,只有用對應的 私鑰 才能 進行解密

  2. 如果使用 私鑰 對資料 進行加密,只有用對應的 公鑰 才能 進行解密

例子:甲方生成 一對金鑰 並將其中的一把作為 公鑰 向其它人公開,得到該公鑰的 乙方 使用該金鑰對機密資訊 進行加密 後再傳送給甲方,甲方再使用自己儲存的另一把 專用金鑰 (私鑰),對 加密 後的資訊 進行解密

4. 常見的簽名加密演算法

4.1. MD5演算法

MD5 用的是 雜湊函式,它的典型應用是對一段資訊產生 資訊摘要,以 防止被篡改。嚴格來說,MD5 不是一種 加密演算法 而是 摘要演算法。無論是多長的輸入,MD5 都會輸出長度為 128bits 的一個串 (通常用 16 進位制 表示為 32 個字元)。

public static final byte[] computeMD5(byte[] content) {
    try {
        MessageDigest md5 = MessageDigest.getInstance("MD5");
        return md5.digest(content);
    } catch (NoSuchAlgorithmException e) {
        throw new RuntimeException(e);
    }
}
複製程式碼

4.2. SHA1演算法

SHA1 是和 MD5 一樣流行的 訊息摘要演算法,然而 SHA1MD5安全性更強。對於長度小於 2 ^ 64 位的訊息,SHA1 會產生一個 160 位的 訊息摘要。基於 MD5SHA1 的資訊摘要特性以及 不可逆 (一般而言),可以被應用在檢查 檔案完整性 以及 數字簽名 等場景。

public static byte[] computeSHA1(byte[] content) {
    try {
        MessageDigest sha1 = MessageDigest.getInstance("SHA1");
        return sha1.digest(content);
    } catch (NoSuchAlgorithmException e) {
        throw new RuntimeException(e);
    }
}
複製程式碼

4.3. HMAC演算法

HMAC 是金鑰相關的 雜湊運算訊息認證碼(Hash-based Message Authentication Code),HMAC 運算利用 雜湊演算法 (MD5SHA1 等),以 一個金鑰一個訊息 為輸入,生成一個 訊息摘要 作為 輸出

HMAC 傳送方接收方 都有的 key 進行計算,而沒有這把 key 的第三方,則是 無法計算 出正確的 雜湊值的,這樣就可以 防止資料被篡改

package net.pocrd.util;
import net.pocrd.annotation.NotThreadSafe;
import net.pocrd.define.ConstField;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Arrays;


@NotThreadSafe
public class HMacHelper {
    private static final Logger logger = LoggerFactory.getLogger(HMacHelper.class);
    private Mac mac;

    /**
     * MAC演算法可選以下多種演算法
     * HmacMD5/HmacSHA1/HmacSHA256/HmacSHA384/HmacSHA512
     */
    private static final String KEY_MAC = "HmacMD5";
    public HMacHelper(String key) {
        try {
            SecretKey secretKey = new SecretKeySpec(key.getBytes(ConstField.UTF8), KEY_MAC);
            mac = Mac.getInstance(secretKey.getAlgorithm());
            mac.init(secretKey);
        } catch (Exception e) {
            logger.error("create hmac helper failed.", e);
        }
    }
    public byte[] sign(byte[] content) {
        return mac.doFinal(content);
    }
    
    public boolean verify(byte[] signature, byte[] content) {
        try {
            byte[] result = mac.doFinal(content);
            return Arrays.equals(signature, result);
        } catch (Exception e) {
            logger.error("verify sig failed.", e);
        }
        return false;
    }
}
複製程式碼

測試結論HMAC 演算法例項在 多執行緒環境 下是 不安全的。但是需要在 多執行緒訪問 時,進行同步的輔助類,使用 ThreadLocal每個執行緒快取 一個例項可以避免進行鎖操作。

4.4. AES/DES/3DES演算法

AESDES3DES 都是 對稱塊加密演算法加解密 的過程是 可逆的。常用的有 AES128AES192AES256 (預設安裝的 JDK 尚不支援 AES256,需要安裝對應的 jce 補丁進行升級 jce1.7jce1.8)。

4.4.1. DES演算法

DES 加密演算法是一種 分組密碼,以 64 位為 分組對資料 加密,它的 金鑰長度56 位,加密解密同一演算法

DES 加密演算法是對 金鑰 進行保密,而 公開演算法,包括加密和解密演算法。這樣,只有掌握了和傳送方 相同金鑰 的人才能解讀由 DES加密演算法加密的密文資料。因此,破譯 DES 加密演算法實際上就是 搜尋金鑰的編碼。對於 56 位長度的 金鑰 來說,如果用 窮舉法 來進行搜尋的話,其運算次數為 2 ^ 56 次。

4.4.2. 3DES演算法

是基於 DES對稱演算法,對 一塊資料三個不同的金鑰 進行 三次加密強度更高

4.4.3. AES演算法

AES 加密演算法是密碼學中的 高階加密標準,該加密演算法採用 對稱分組密碼體制,金鑰長度的最少支援為 128 位、 192 位、256 位,分組長度 128 位,演算法應易於各種硬體和軟體實現。這種加密演算法是美國聯邦政府採用的 區塊加密標準

AES 本身就是為了取代 DES 的,AES 具有更好的 安全性效率靈活性

import net.pocrd.annotation.NotThreadSafe;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.SecureRandom;

@NotThreadSafe
public class AesHelper {
    private SecretKeySpec keySpec;
    private IvParameterSpec iv;

    public AesHelper(byte[] aesKey, byte[] iv) {
        if (aesKey == null || aesKey.length < 16 || (iv != null && iv.length < 16)) {
            throw new RuntimeException("錯誤的初始金鑰");
        }
        if (iv == null) {
            iv = Md5Util.compute(aesKey);
        }
        keySpec = new SecretKeySpec(aesKey, "AES");
        this.iv = new IvParameterSpec(iv);
    }

    public AesHelper(byte[] aesKey) {
        if (aesKey == null || aesKey.length < 16) {
            throw new RuntimeException("錯誤的初始金鑰");
        }
        keySpec = new SecretKeySpec(aesKey, "AES");
        this.iv = new IvParameterSpec(Md5Util.compute(aesKey));
    }

    public byte[] encrypt(byte[] data) {
        byte[] result = null;
        Cipher cipher = null;
        try {
            cipher = Cipher.getInstance("AES/CFB/NoPadding");
            cipher.init(Cipher.ENCRYPT_MODE, keySpec, iv);
            result = cipher.doFinal(data);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return result;
    }

    public byte[] decrypt(byte[] secret) {
        byte[] result = null;
        Cipher cipher = null;
        try {
            cipher = Cipher.getInstance("AES/CFB/NoPadding");
            cipher.init(Cipher.DECRYPT_MODE, keySpec, iv);
            result = cipher.doFinal(secret);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return result;
    }

    public static byte[] randomKey(int size) {
        byte[] result = null;
        try {
            KeyGenerator gen = KeyGenerator.getInstance("AES");
            gen.init(size, new SecureRandom());
            result = gen.generateKey().getEncoded();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return result;
    }
}
複製程式碼

4.5. RSA演算法

RSA 加密演算法是目前最有影響力的 公鑰加密演算法,並且被普遍認為是目前 最優秀的公鑰方案 之一。RSA 是第一個能同時用於 加密數字簽名 的演算法,它能夠 抵抗 到目前為止已知的 所有密碼攻擊,已被 ISO 推薦為公鑰資料加密標準。

RSA 加密演算法 基於一個十分簡單的數論事實:將兩個大 素數 相乘十分容易,但想要對其乘積進行 因式分解 卻極其困難,因此可以將 乘積 公開作為 加密金鑰

import net.pocrd.annotation.NotThreadSafe;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.crypto.Cipher;
import java.io.ByteArrayOutputStream;
import java.security.KeyFactory;
import java.security.Security;
import java.security.Signature;
import java.security.interfaces.RSAPrivateCrtKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;

@NotThreadSafe
public class RsaHelper {
    private static final Logger logger = LoggerFactory.getLogger(RsaHelper.class);
    private RSAPublicKey publicKey;
    private RSAPrivateCrtKey privateKey;

    static {
        Security.addProvider(new BouncyCastleProvider()); //使用bouncycastle作為加密演算法實現
    }

    public RsaHelper(String publicKey, String privateKey) {
        this(Base64Util.decode(publicKey), Base64Util.decode(privateKey));
    }

    public RsaHelper(byte[] publicKey, byte[] privateKey) {
        try {
            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
            if (publicKey != null && publicKey.length > 0) {
                this.publicKey = (RSAPublicKey)keyFactory.generatePublic(new X509EncodedKeySpec(publicKey));
            }
            if (privateKey != null && privateKey.length > 0) {
                this.privateKey = (RSAPrivateCrtKey)keyFactory.generatePrivate(new PKCS8EncodedKeySpec(privateKey));
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public RsaHelper(String publicKey) {
        this(Base64Util.decode(publicKey));
    }

    public RsaHelper(byte[] publicKey) {
        try {
            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
            if (publicKey != null && publicKey.length > 0) {
                this.publicKey = (RSAPublicKey)keyFactory.generatePublic(new X509EncodedKeySpec(publicKey));
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public byte[] encrypt(byte[] content) {
        if (publicKey == null) {
            throw new RuntimeException("public key is null.");
        }

        if (content == null) {
            return null;
        }

        try {
            Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
            cipher.init(Cipher.ENCRYPT_MODE, publicKey);
            int size = publicKey.getModulus().bitLength() / 8 - 11;
            ByteArrayOutputStream baos = new ByteArrayOutputStream((content.length + size - 1) / size * (size + 11));
            int left = 0;
            for (int i = 0; i < content.length; ) {
                left = content.length - i;
                if (left > size) {
                    cipher.update(content, i, size);
                    i += size;
                } else {
                    cipher.update(content, i, left);
                    i += left;
                }
                baos.write(cipher.doFinal());
            }
            return baos.toByteArray();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public byte[] decrypt(byte[] secret) {
        if (privateKey == null) {
            throw new RuntimeException("private key is null.");
        }

        if (secret == null) {
            return null;
        }

        try {
            Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
            cipher.init(Cipher.DECRYPT_MODE, privateKey);
            int size = privateKey.getModulus().bitLength() / 8;
            ByteArrayOutputStream baos = new ByteArrayOutputStream((secret.length + size - 12) / (size - 11) * size);
            int left = 0;
            for (int i = 0; i < secret.length; ) {
                left = secret.length - i;
                if (left > size) {
                    cipher.update(secret, i, size);
                    i += size;
                } else {
                    cipher.update(secret, i, left);
                    i += left;
                }
                baos.write(cipher.doFinal());
            }
            return baos.toByteArray();
        } catch (Exception e) {
            logger.error("rsa decrypt failed.", e);
        }
        return null;
    }

    public byte[] sign(byte[] content) {
        if (privateKey == null) {
            throw new RuntimeException("private key is null.");
        }
        if (content == null) {
            return null;
        }
        try {
            Signature signature = Signature.getInstance("SHA1WithRSA");
            signature.initSign(privateKey);
            signature.update(content);
            return signature.sign();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public boolean verify(byte[] sign, byte[] content) {
        if (publicKey == null) {
            throw new RuntimeException("public key is null.");
        }
        if (sign == null || content == null) {
            return false;
        }
        try {
            Signature signature = Signature.getInstance("SHA1WithRSA");
            signature.initVerify(publicKey);
            signature.update(content);
            return signature.verify(sign);
        } catch (Exception e) {
            logger.error("rsa verify failed.", e);
        }
        return false;
    }
}
複製程式碼

4.6. ECC演算法

ECC 也是一種 非對稱加密演算法,主要優勢是在某些情況下,它比其他的方法使用 更小的金鑰,比如 RSA 加密演算法,提供 相當的或更高等級 的安全級別。不過一個缺點是 加密和解密操作 的實現比其他機制 時間長 (相比 RSA 演算法,該演算法對 CPU 消耗嚴重)。

import net.pocrd.annotation.NotThreadSafe;
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey;
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.crypto.Cipher;
import java.io.ByteArrayOutputStream;
import java.security.KeyFactory;
import java.security.Security;
import java.security.Signature;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;

@NotThreadSafe
public class EccHelper {
    private static final Logger logger = LoggerFactory.getLogger(EccHelper.class);
    private static final int SIZE = 4096;
    private BCECPublicKey  publicKey;
    private BCECPrivateKey privateKey;

    static {
        Security.addProvider(new BouncyCastleProvider());
    }

    public EccHelper(String publicKey, String privateKey) {
        this(Base64Util.decode(publicKey), Base64Util.decode(privateKey));
    }

    public EccHelper(byte[] publicKey, byte[] privateKey) {
        try {
            KeyFactory keyFactory = KeyFactory.getInstance("EC", "BC");
            if (publicKey != null && publicKey.length > 0) {
                this.publicKey = (BCECPublicKey)keyFactory.generatePublic(new X509EncodedKeySpec(publicKey));
            }
            if (privateKey != null && privateKey.length > 0) {
                this.privateKey = (BCECPrivateKey)keyFactory.generatePrivate(new PKCS8EncodedKeySpec(privateKey));
            }
        } catch (ClassCastException e) {
            throw new RuntimeException("", e);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public EccHelper(String publicKey) {
        this(Base64Util.decode(publicKey));
    }

    public EccHelper(byte[] publicKey) {
        try {
            KeyFactory keyFactory = KeyFactory.getInstance("EC", "BC");
            if (publicKey != null && publicKey.length > 0) {
                this.publicKey = (BCECPublicKey)keyFactory.generatePublic(new X509EncodedKeySpec(publicKey));
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public byte[] encrypt(byte[] content) {
        if (publicKey == null) {
            throw new RuntimeException("public key is null.");
        }
        try {
            Cipher cipher = Cipher.getInstance("ECIES", "BC");
            cipher.init(Cipher.ENCRYPT_MODE, publicKey);
            int size = SIZE;
            ByteArrayOutputStream baos = new ByteArrayOutputStream((content.length + size - 1) / size * (size + 45));
            int left = 0;
            for (int i = 0; i < content.length; ) {
                left = content.length - i;
                if (left > size) {
                    cipher.update(content, i, size);
                    i += size;
                } else {
                    cipher.update(content, i, left);
                    i += left;
                }
                baos.write(cipher.doFinal());
            }
            return baos.toByteArray();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public byte[] decrypt(byte[] secret) {
        if (privateKey == null) {
            throw new RuntimeException("private key is null.");
        }
        try {
            Cipher cipher = Cipher.getInstance("ECIES", "BC");
            cipher.init(Cipher.DECRYPT_MODE, privateKey);
            int size = SIZE + 45;
            ByteArrayOutputStream baos = new ByteArrayOutputStream((secret.length + size + 44) / (size + 45) * size);
            int left = 0;
            for (int i = 0; i < secret.length; ) {
                left = secret.length - i;
                if (left > size) {
                    cipher.update(secret, i, size);
                    i += size;
                } else {
                    cipher.update(secret, i, left);
                    i += left;
                }
                baos.write(cipher.doFinal());
            }
            return baos.toByteArray();
        } catch (Exception e) {
            logger.error("ecc decrypt failed.", e);
        }
        return null;
    }

    public byte[] sign(byte[] content) {
        if (privateKey == null) {
            throw new RuntimeException("private key is null.");
        }
        try {
            Signature signature = Signature.getInstance("SHA1withECDSA", "BC");
            signature.initSign(privateKey);
            signature.update(content);
            return signature.sign();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public boolean verify(byte[] sign, byte[] content) {
        if (publicKey == null) {
            throw new RuntimeException("public key is null.");
        }
        try {
            Signature signature = Signature.getInstance("SHA1withECDSA", "BC");
            signature.initVerify(publicKey);
            signature.update(content);
            return signature.verify(sign);
        } catch (Exception e) {
            logger.error("ecc verify failed.", e);
        }
        return false;
    }
}
複製程式碼

5. 各種加密演算法對比

5.1. 雜湊演算法比較

名稱 安全性 速度
SHA-1
MD5

5.2. 對稱加密演算法比較

名稱 金鑰名稱 執行速度 安全性 資源消耗
DES 56位 較快
3DES 112位或168位
AES 128、192、256位

5.3. 非對稱加密演算法比較

名稱 成熟度 安全性 運算速度 資源消耗
RSA
ECC

5.4. 對稱演算法與非對稱加密演算法

5.4.1. 對稱演算法

  1. 金鑰管理:比較難,不適合網際網路,一般用於內部系統

  2. 安全性:中

  3. 加密速度:快好 幾個數量級 (軟體加解密速度至少快 100 倍,每秒可以加解密數 M 位元 資料),適合大資料量的加解密處理

5.4.2. 非對稱演算法

  1. 金鑰管理:金鑰容易管理

  2. 安全性:高

  3. 加密速度:比較慢,適合 小資料量 加解密或資料簽名

小結

本文介紹了 數字簽名加密和解密對稱加密和非對稱加密,然後詳細介紹了 MD5SHA-1HMACDES/AESRSAECC 這幾種加密演算法和程式碼示例。


歡迎關注技術公眾號: 零壹技術棧

零壹技術棧

本帳號將持續分享後端技術乾貨,包括虛擬機器基礎,多執行緒程式設計,高效能框架,非同步、快取和訊息中介軟體,分散式和微服務,架構學習和進階等學習資料和文章。

相關文章