1. 程式人生 > >【網路安全】加解密演算法最詳解

【網路安全】加解密演算法最詳解

資料簽名、加密是前後端開發經常需要使用到的技術,應用場景包括不限於使用者登入、資料交易、資訊通訊等,不同的應用場景也會需要使用到不同的簽名加密演算法,或者需要搭配不一樣的 簽名加密演算法來達到業務目標。常用的加密演算法有:

  • 對稱加密演算法;
  • 非對稱加密演算法;
  • 雜湊演算法,加鹽雜湊演算法(單向加密);
  • 數字簽名。

使用加密簽名演算法,可以達到下面的安全目標:

  • 保密性:防止使用者的資料被讀取;
  • 資料完整性:防止資料被篡改;
  • 身份驗證:確保資料發自特定的一方。

對稱加密

對稱加密演算法加密和解密時使用同一把祕鑰。操作比較簡單,加密速度快,祕鑰簡單。經常在訊息傳送方需要加密大量資料時使用。缺點是風險都在這個祕鑰上面,一旦被竊取,資訊會暴露。所以安全級別不夠高。常用對稱加密演算法有DES,3DES,AES等。在jdk中也都有封裝。

DES

DES的祕鑰為8個位元組,64個bit位。(不適應當今分散式開放網路對資料加密安全性的要求)在Java進行DES、3DES和AES三種對稱加密演算法時,常採用的是NoPadding(不填充)、Zeros填充(0填充)、PKCS5Padding填充。

一個DES的列子:


import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESKeySpec;

import java.security.Key;
import java.security.SecureRandom;
import java.util.Base64;

public class DESUtil {
    //演算法名稱
    public static final String KEY_ALGORITHM = "DES";
    //演算法名稱/加密模式/填充方式
    //DES共有四種工作模式-->> ECB:電子密碼本模式、 CBC:加密分組連結模式、CFB:加密反饋模式、OFB:輸出反饋模式
    //在Java進行DES、3DES和AES三種對稱加密演算法時,常採用的是NoPadding(不填充)、Zeros填充(0填充)、PKCS5Padding填充。
    //不同的工作模式下,初始化Cipher的程式碼不一樣
    public static final String CIPHER_ALGORITHM = "DES/ECB/NoPadding";

    private static SecretKey keyGenerator(String hexKeyStr) throws Exception {
        byte input[] = hexString2Bytes(hexKeyStr);
        DESKeySpec desKey = new DESKeySpec(input);
        SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(KEY_ALGORITHM);
        SecretKey securekey = keyFactory.generateSecret(desKey);
        return securekey;
    }

    private static int parse(char c) {
        if (c >= 'a') {
            return (c - 'a' + 10) & 0x0f;
        }
        if (c >= 'A') {
            return (c - 'A' + 10) & 0x0f;
        }
        return (c - '0') & 0x0f;
    }

    //從十六進位制字串到位元組陣列轉換
    public static byte[] hexString2Bytes(String hexstr) {
        byte[] b = new byte[hexstr.length() / 2];
        int j = 0;
        for (int i = 0; i < b.length; i++) {
            char c0 = hexstr.charAt(j++);
            char c1 = hexstr.charAt(j++);
            b[i] = (byte) ((parse(c0) << 4) | parse(c1));
        }
        return b;
    }

    /**
     * 加密資料
     * @param data 待加密資料
     * @param hexKeyStr 金鑰
     * @return 加密後的資料
     */
    public static String encrypt(String data, String hexKeyStr) throws Exception {
        Key deskey = keyGenerator(hexKeyStr);
        // 例項化Cipher物件,它用於完成實際的加密操作
        Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
        SecureRandom random = new SecureRandom();
        // 初始化Cipher物件,設定為加密模式
        cipher.init(Cipher.ENCRYPT_MODE, deskey, random);
        byte[] results = cipher.doFinal(data.getBytes());
        // 執行加密操作。加密後的結果通常都會用Base64編碼進行傳輸
        return Base64.getEncoder().encodeToString(results);
    }

    /**
     * 解密資料
     * @param data 待解密資料
     * @param hexKeyStr 金鑰
     * @return 解密後的資料
     */
    public static String decrypt(String data, String hexKeyStr) throws Exception {
        Key deskey = keyGenerator(hexKeyStr);
        Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
        //初始化Cipher物件,設定為解密模式
        cipher.init(Cipher.DECRYPT_MODE, deskey);
        // 執行解密操作
        return new String(cipher.doFinal(Base64.getDecoder().decode(data)));
    }

    //Des祕鑰採用64bit為,8個位元組,如果使用字串則太短,一般使用16進位制的字串(長度16位,大於16為會擷取前16位)
    public static void main(String[] args) throws Exception {
        //模擬3DES的過程
        String source = "amigoxie";
        System.out.println("原文: " + source);
        String key = "A1B2C3D4E5F60708";
        String ke2 = "A1B2C3D4E5F60709";
        String ke3 = "A1B2C3D4E5F6070A";
        String encryptData1 = encrypt(source, key);
        String encryptData2 = decrypt(encryptData1,ke2);
        String encryptData3 = encrypt(encryptData2,ke3);
        System.out.println("加密後: " + encryptData3);
        String dencryptData1 = decrypt(encryptData3,ke3);
        String dencryptData2 = encrypt(dencryptData1,ke2);
        String dencryptData3 = decrypt(dencryptData2,key);
        System.out.println("解密後: " + dencryptData3);
        //String decryptData = decrypt(encryptData, key);
        //System.out.println("解密後: " + decryptData);
    }
}

3DES

3DES(或稱為Triple DES)是三重資料加密演算法(TDEA,Triple Data Encryption Algorithm)塊密碼的通稱。它相當於是對每個資料塊應用三次DES加密演算法。由於計算機運算能力的增強,原版DES密碼的金鑰長度變得容易被暴力破解;3DES即是設計用來提供一種相對簡單的方法,即通過增加DES的金鑰長度來避免類似的攻擊,而不是設計一種全新的塊密碼演算法。

其具體實現如下:設Ek()和Dk()代表DES演算法的加密和解密過程,K代表DES演算法使用的金鑰,P代表明文,C代表密文,這樣:

3DES加密過程為:C=Ek3(Dk2(Ek1(P)))

3DES解密過程為:P=Dk1(EK2(Dk3(C)))

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESedeKeySpec;
import java.security.Key;
import java.util.Base64;

public class ThreeDESUtil {

    // 演算法名稱
    public static final String KEY_ALGORITHM = "DESEDE";
    // 演算法名稱/加密模式/填充方式
    public static final String CIPHER_ALGORITHM = "DESEDE/ECB/NoPadding";


    private static SecretKey keyGenerator(String hexKeyStr) throws Exception {
        byte input[] = HexString2Bytes(hexKeyStr);
        DESedeKeySpec keySpec = new DESedeKeySpec(input);
        SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(KEY_ALGORITHM);
        SecretKey secretKey = keyFactory.generateSecret(keySpec);
        return secretKey;
    }

    private static int parse(char c) {
        if (c >= 'a') {
            return (c - 'a' + 10) & 0x0f;
        }
        if (c >= 'A') {
            return (c - 'A' + 10) & 0x0f;
        }
        return (c - '0') & 0x0f;
    }

    // 從十六進位制字串到位元組陣列轉換
    public static byte[] HexString2Bytes(String hexstr) {
        byte[] b = new byte[hexstr.length() / 2];
        int j = 0;
        for (int i = 0; i < b.length; i++) {
            char c0 = hexstr.charAt(j++);
            char c1 = hexstr.charAt(j++);
            b[i] = (byte) ((parse(c0) << 4) | parse(c1));
        }
        return b;
    }

    public static String encrypt(String data, String hexKeyStr) throws Exception {
        Key deskey = keyGenerator(hexKeyStr);
        Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
        cipher.init(Cipher.ENCRYPT_MODE, deskey);
        byte[] results = cipher.doFinal(data.getBytes());
        return Base64.getEncoder().encodeToString(results);
    }

    public static String decrypt(String data, String hexKeyStr) throws Exception {
        Key deskey = keyGenerator(hexKeyStr);
        Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
        cipher.init(Cipher.DECRYPT_MODE, deskey);
        return new String(cipher.doFinal(Base64.getDecoder().decode(data)));
    }

    public static void main(String[] args) throws Exception {
        String source = "amigoxie";
        System.out.println("原文: " + source);
        String key = "AAAAC3D4E5F60708A1B2C3D4E5F60709A1B2C3D4E5F6070A";
        String encryptData1 = encrypt(source, key);
        System.out.println("加密後: " + encryptData1);
        String dencryptData1 = decrypt(encryptData1,key);
        System.out.println("解密後: " + dencryptData1);
    }

}

AES

高階加密標準(英語:Advanced Encryption Standard,縮寫:AES),是一種區塊加密標準。這個標準用來替代原先的DES,已經被多方分析且廣為全世界所使用。

那麼為什麼原來的DES會被取代呢?原因就在於其使用56位金鑰,比較容易被破解。而AES可以使用128bit位、192、和256位金鑰,並且用128位分組加密和解密資料,相對來說安全很多。完善的加密演算法在理論上是無法破解的,除非使用窮盡法。

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.util.Base64;

public class AESUtil {

    private static final String UTF8 = "UTF-8";
    private static final String AES = "AES";
    private static final String AES_CBC_PKCS5_PADDING = "AES/CBC/PKCS5Padding";
    private static final String AES_CBC_NO_PADDING = "AES/CBC/NoPadding";

    /**
     * JDK只支援AES-128加密,也就是金鑰長度必須是128bit;
     * 引數為金鑰key,key的長度小於16字元時用"0"補充,
     * key長度大於16字元時擷取前16位。
     * <p>
     * 要實現256的需要依賴其他Jar包
     **/
    private static SecretKeySpec create128BitsKey(String key) {
        if (key == null) {
            key = "";
        }
        byte[] data = null;
        StringBuffer buffer = new StringBuffer(16);
        buffer.append(key);
        //小於16後面補0
        while (buffer.length() < 16) {
            buffer.append("0");
        }
        //大於16,擷取前16個字元
        if (buffer.length() > 16) {
            buffer.setLength(16);
        }
        try {
            data = buffer.toString().getBytes(UTF8);
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        return new SecretKeySpec(data, AES);
    }

    /**
     * 建立128位的偏移量,iv的長度小於16時後面補0,
     * 大於16,擷取前16個字元;
     *
     * @param iv
     * @return
     */
    private static IvParameterSpec create128BitsIV(String iv) {
        if (iv == null) {
            iv = "";
        }
        byte[] data = null;
        StringBuffer buffer = new StringBuffer(16);
        buffer.append(iv);
        while (buffer.length() < 16) {
            buffer.append("0");
        }
        if (buffer.length() > 16) {
            buffer.setLength(16);
        }
        try {
            data = buffer.toString().getBytes(UTF8);
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        return new IvParameterSpec(data);
    }

    /**
     * 填充方式為Pkcs5Padding時,最後一個塊需要填充χ個位元組,填充的值就是χ,也就是填充內容由JDK確定
     *
     * @param srcContent
     * @param password
     * @param iv
     * @return
     */
    public static String aesCbcPkcs5PaddingEncrypt(String srcContent, String password, String iv) throws Exception {
        SecretKeySpec key = create128BitsKey(password);
        IvParameterSpec ivParameterSpec = create128BitsIV(iv);
        Cipher cipher = Cipher.getInstance(AES_CBC_PKCS5_PADDING);
        cipher.init(Cipher.ENCRYPT_MODE, key, ivParameterSpec);
        byte[] data = srcContent.getBytes(Charset.forName("UTF8"));
        byte[] encryptedContent = cipher.doFinal(data);
        //為了打印出來的字串沒有亂碼,進行base64編碼
        return Base64.getEncoder().encodeToString(encryptedContent);

    }


    public static String aesCbcPkcs5PaddingDecrypt(String encryptedContent, String password, String iv) throws Exception {
        SecretKeySpec key = create128BitsKey(password);
        IvParameterSpec ivParameterSpec = create128BitsIV(iv);
        Cipher cipher = Cipher.getInstance(AES_CBC_PKCS5_PADDING);
        cipher.init(Cipher.DECRYPT_MODE, key, ivParameterSpec);
        byte[] data = Base64.getDecoder().decode(encryptedContent);
        byte[] decryptedContent = cipher.doFinal(data);
        return new String(decryptedContent,"UTF8");

    }

    /**
     * 填充方式為NoPadding時,最後一個塊的填充內容由程式設計師確定,通常為0.
     * AES/CBC/NoPadding加密的明文長度必須是16的整數倍,明文長度不滿足16時,程式設計師要擴充到16的整數倍
     *
     * @param sSrc
     * @param aesKey
     * @param aesIV
     * @return
     */
    public static byte[] aesCbcNoPaddingEncrypt(byte[] sSrc, String aesKey, String aesIV) throws Exception {
        //加密的資料長度不是16的整數倍時,原始資料後面補0,直到長度滿足16的整數倍
        int len = sSrc.length;
        //計算補0後的長度
        while (len % 16 != 0) {
            len++;
        }
        byte[] result = new byte[len];
        //在最後補0
        for (int i = 0; i < len; ++i) {
            if (i < sSrc.length) {
                result[i] = sSrc[i];
            } else {
                //填充字元'a'
                //result[i] = 'a';
                result[i] = 0;
            }
        }
        SecretKeySpec skeySpec = create128BitsKey(aesKey);
        //使用CBC模式,需要一個初始向量iv,可增加加密演算法的強度
        IvParameterSpec iv = create128BitsIV(aesIV);
        Cipher cipher = Cipher.getInstance(AES_CBC_NO_PADDING);
        cipher.init(Cipher.ENCRYPT_MODE, skeySpec, iv);
        byte[] encrypted = null;
        encrypted = cipher.doFinal(result);
        return encrypted;
    }

    public static byte[] aesCbcNoPaddingDecrypt(byte[] sSrc, String aesKey, String aesIV) throws Exception {
        SecretKeySpec skeySpec = create128BitsKey(aesKey);
        IvParameterSpec iv = create128BitsIV(aesIV);
        Cipher cipher = Cipher.getInstance(AES_CBC_NO_PADDING);
        cipher.init(Cipher.DECRYPT_MODE, skeySpec, iv);
        byte[] decryptContent = cipher.doFinal(sSrc);
        return decryptContent;
    }

    public static void main(String[] args) throws Exception {
        String source = "amigoxie";
        System.out.println("原文: " + source);
        String aeskey = "1234567890987654";
        String aesIV = "1234567890987654";
        String encryptData1 = aesCbcPkcs5PaddingEncrypt(source, aeskey, aesIV);
        System.out.println("加密後:" + encryptData1);
        String dencryptData1 = aesCbcPkcs5PaddingDecrypt(encryptData1, aeskey, aesIV);
        System.out.println("解密後:" + new String(dencryptData1));
    }
}

非對稱加密

非對稱加密,顧名思義就是加密與解密的過程不是對稱的,不是用的同一個祕鑰。非對稱加密有個公私鑰對的概念,也就是有兩把祕鑰,一把是公鑰,一把是私鑰,一對公私鑰有固定的生成方法,在加密的時候,用公鑰去加密,接收方再用對應的私鑰去解密。使用時可以由接收方生成公私鑰對,然後將公鑰傳給加密方,這樣私鑰不會在網路中傳輸,沒有被竊取的風險。比如github底層的ssh協議就是公私鑰非對稱加密。並且公鑰是可以由私鑰推匯出來的,反過來卻不行,由通過公鑰無法推匯出私鑰。常用演算法有RSA,DSA,ECC等。ECC也是比特幣底層用的比較多的演算法。通過和對稱加密的對比,可以看到,非對稱加密解決了祕鑰傳輸中的安全問題。

RSA加密演算法

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

相同長度的祕鑰,RSA和DSA的安全性差不多。一般情況下DSA多用於數字簽名,簽名的效率比RSA更高。RSA支援加密和加簽操作。所以當我們需要同時進行加密和加簽操作的時候一般選擇RSA演算法。

這邊提供一個線上生成RSA公私鑰對的網站,可以選擇生成512,1024,2048或者是4096位的祕鑰。使用起來比較方便。

下面給出一個RSA演算法的列子:


import javax.crypto.Cipher;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

public class RSAUtil {


    // 數字簽名,金鑰演算法
    private static final String RSA_KEY_ALGORITHM = "RSA";

    // 數字簽名的 簽名/驗證演算法
    private static final String SIGNATURE_ALGORITHM = "MD5withRSA";

    /**
     * 金鑰長度,DH演算法的預設金鑰長度是1024
     * 金鑰長度必須是64的倍數,在512到65536位之間
     */
    private static final int KEY_SIZE = 1024;

    private static final String PUBLIC_KEY = "publicKey";
    private static final String PRIVATE_KEY = "privateKey";

    private static final String publicKey ="MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCnS5ms1FnL7lrEOmo2zctcmZz4YF690gTs2CTY9yKWfMIoXEuf2i3SZ9wKMMsmLB+aQJhp3IvOU6SEEvkLkzFQMsioBCo7emD2Jgh44zDKCTM5lGV5qN3SmhS1pta4cByXquBUKi51SQIhj01H25rh0p/MKeCLRtAEBIi0tVxQjwIDAQAB";
    private static final String privateKey = "MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAKdLmazUWcvuWsQ6ajbNy1yZnPhgXr3SBOzYJNj3IpZ8wihcS5/aLdJn3AowyyYsH5pAmGnci85TpIQS+QuTMVAyyKgEKjt6YPYmCHjjMMoJMzmUZXmo3dKaFLWm1rhwHJeq4FQqLnVJAiGPTUfbmuHSn8wp4ItG0AQEiLS1XFCPAgMBAAECgYAjxqALfLHjUYqpkhBqveGyYMtXtkwsbcBN4f8nQtprsixXMz2c5qyL5VgB+eNHu4Ham9u8L8TaD7sL337Qwd52fVsdrDhpn5mdCWXrDdkABYbctM60YWUO83xCX3FztXszDTMESBw7h6BvuKqsQyh/vrTPtYGl75FQMBO1VqIX8QJBAPQ3+QukfAm4Eo9KFLHX/PQ4A2SYpDA0vwe01hrnOXbZExRcSIqOB0Ph7BpDA0LWgxd3Tvfm6ALOD31nuVHKgNkCQQCvXaW406yNergGHPvE/bp+4/kOVDzDue4GYQIhPwYk9UGLPo1dmntIqAeNb7zYid2H6VXcxnwGqlZDSk+I6nunAkEAlF4Y8EjKjoEEzYadEfp9E8Wf0hKl1R+GWuEzHf8RuzFf1QPHkl187nGnpcDGj2mRFMWH9TWUCHg1kuNcA+O56QJAGJ7MMnu9YQuWpH0TN9/re/8jq0hWU6BZ85lRBDvl7/Bi6Fq63CZhIp08mjXSzI/mgztWK63OId1uSPo42l8ZRwJBAMrABYiebvl9PTpxM2nuMLNUKwpdwjBXGGMXmKwftXPI6RFkEOKhqOvOiDBALcu0ZwtLsJsPXmJROXSQf9cMdRk=";


    /**
     * 初始化RSA金鑰對
     * @return RSA金鑰對
     * @throws Exception 丟擲異常
     */
    private static Map<String, String> initKey() throws Exception {
        KeyPairGenerator keygen = KeyPairGenerator.getInstance(RSA_KEY_ALGORITHM);
        //初始化金鑰生成器
        keygen.initialize(KEY_SIZE);
        KeyPair keys = keygen.genKeyPair();
        String pub_key = Base64.getEncoder().encodeToString(keys.getPublic().getEncoded());
        String pri_key = Base64.getEncoder().encodeToString(keys.getPrivate().getEncoded());
        Map<String, String> keyMap = new HashMap<String, String>();
        keyMap.put(PUBLIC_KEY, pub_key);
        keyMap.put(PRIVATE_KEY, pri_key);
        System.out.println("公鑰:" + pub_key);
        System.out.println("私鑰:" + pri_key);
        return keyMap;
    }

    public static String sign(String data, String privateKey) throws Exception {
        //自己的私鑰加簽
        byte[] byteData = data.getBytes("UTF8");
        return sign(byteData,privateKey);
    }

    public static String sign(byte[] data, String pri_key) throws Exception {
        //自己的私鑰加簽
        byte[] pri_key_bytes = Base64.getDecoder().decode(pri_key);
        PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(pri_key_bytes);
        KeyFactory keyFactory = KeyFactory.getInstance(RSA_KEY_ALGORITHM);
        // 生成私鑰
        PrivateKey priKey = keyFactory.generatePrivate(pkcs8KeySpec);
        // 例項化Signature
        Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM);
        // 初始化Signature
        signature.initSign(priKey);
        // 更新
        signature.update(data);
        return Base64.getEncoder().encodeToString(signature.sign());
    }

    public static boolean  verify(String data, String sign, String pub_key) throws Exception {
        return verify(data.getBytes("UTF8"),sign.getBytes("UTF8"),pub_key);
    }

    public static boolean verify(byte[] data, byte[] sign, String pub_key) throws Exception {
        //公鑰驗籤
        byte[] pub_key_bytes = Base64.getDecoder().decode(pub_key);
        KeyFactory keyFactory = KeyFactory.getInstance(RSA_KEY_ALGORITHM);
        // 初始化公鑰
        // 金鑰材料轉換
        X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(pub_key_bytes);
        // 產生公鑰
        PublicKey pubKey = keyFactory.generatePublic(x509KeySpec);
        // 例項化Signature
        Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM);
        // 初始化Signature
        signature.initVerify(pubKey);
        // 更新
        signature.update(data);
        // 驗證
        return signature.verify(sign);
    }


    private static byte[] encryptByPubKey(byte[] data, byte[] pub_key) throws Exception {
        X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(pub_key);
        KeyFactory keyFactory = KeyFactory.getInstance(RSA_KEY_ALGORITHM);
        PublicKey publicKey = keyFactory.generatePublic(x509KeySpec);
        Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
        cipher.init(Cipher.ENCRYPT_MODE, publicKey);
        return cipher.doFinal(data);
    }

    public static String encryptByPubKey(String data, String pub_key) throws Exception {
        // 一般用公匙進行加密
        // RSA的公私鑰一般都會以base64編碼給出,所以先base64解碼
        byte[] pub_key_bytes = Base64.getDecoder().decode(pub_key);
        byte[] encryptedData = encryptByPubKey(data.getBytes("UTF8"), pub_key_bytes);
        return Base64.getEncoder().encodeToString(encryptedData);
    }


    private static byte[] decryptByPriKey(byte[] data, byte[] pri_key) throws Exception {
        PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(pri_key);
        KeyFactory keyFactory = KeyFactory.getInstance(RSA_KEY_ALGORITHM);
        PrivateKey privateKey = keyFactory.generatePrivate(pkcs8KeySpec);
        Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
        cipher.init(Cipher.DECRYPT_MODE, privateKey);
        return cipher.doFinal(data);
    }

    /**
     * 私鑰解密
     * @param data 待解密資料
     * @param pri_key 私鑰
     * @return 明文
     * @throws Exception 丟擲異常
     */
    public static String decryptByPriKey(String data, String pri_key) throws Exception {
        // 一般用私匙進行解密
        // RSA的公私鑰一般都會以base64編碼給出,所以先base64解碼
        byte[] pri_key_bytes = Base64.getDecoder().decode(pri_key);
        byte[] design = decryptByPriKey(Base64.getDecoder().decode(data), pri_key_bytes);
        return new String(design,"UTF8");
    }


    public static void main(String[] args) throws Exception {

        String datastr = "天街小雨潤如酥,草色遙看近卻無。最是一年春好處,絕勝煙柳滿皇都。";
        System.out.println("待加密資料:\n" + datastr);
        //獲取金鑰對
        String pub_key = publicKey;
        String pri_key = privateKey;

        String pubKeyStr = encryptByPubKey(datastr, pub_key);
        System.out.println("公匙加密結果:\n" + pubKeyStr);
        // 私匙解密
        String priKeyStr = decryptByPriKey(pubKeyStr, pri_key);
        System.out.println("私匙解密結果:\n" + priKeyStr);

        //換行
        System.out.println("-------------驗證簽名-------------------");

        // 數字簽名
        String str1 = "漢兵已略地";
        String str2 = "四面楚歌聲";
        System.out.println("正確的簽名:" + str1 + "\n錯誤的簽名:" + str2);
        String sign = sign(str1.getBytes(), pri_key);
        System.out.println("數字簽名:\n" + sign);
        boolean vflag1 = verify(str1.getBytes(), Base64.getDecoder().decode(sign), pub_key);
        System.out.println("數字簽名驗證結果1:\n" + vflag1);
        boolean vflag2 = verify(str2.getBytes(), Base64.getDecoder().decode(sign), pub_key);
        System.out.println("數字簽名驗證結果2:\n" + vflag2);

    }

}

DSA

一般用於數字簽名場合。

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

雜湊演算法(單向加密)

單向加密演算法只能用於對資料的加密,無法被解密,其特點為定長輸出、雪崩效應。單向加密演算法用於不需要對資訊進行解密或讀取的場合,比如用來比較兩個資訊值是否一樣而不需要知道資訊具體內容,在實際中的一個典型應用就是對資料庫中的使用者資訊進行加密,比如當建立一個新使用者及密碼時,將這些資訊經過單向加密後再儲存到資料庫中。

常見的演算法包括

  • MD5;
  • SHA等

MD5

MD5即Message-Digest Algorithm 5(資訊-摘要演算法5),用於確保資訊傳輸完整一致。常用於資料庫密碼儲存。MD5值是128bit位的資料,一般情況下使用一個長度是32的十六進位制字串來顯示。 具體特點如下:

  • 壓縮性:任意長度的資料,算出的MD5值長度都是固定的。

  • 容易計算:從原資料計算出MD5值很容易。

  • 抗修改性:對原資料進行任何改動,哪怕只修改1個位元組,所得到的MD5值都有很大區別。

  • 強抗碰撞:已知原資料和其MD5值,想找到一個具有相同MD5值的資料(即偽造資料)是非常困難的。

MD5加鹽

我們知道,如果直接對密碼進行雜湊,那麼黑客可以對通過獲得這個密碼雜湊值,然後通過查雜湊值字典(例如MD5密碼破解網站),得到某使用者的密碼。加Salt可以一定程度上解決這一問題。所謂加Salt方法,就是加點“佐料”。其基本想法是這樣的:當用戶首次提供密碼時(通常是註冊時), 由系統自動往這個密碼裡撒一些“佐料”,然後再雜湊。而當用戶登入時,系統為使用者提供的程式碼撒上同樣的“佐料”,然後雜湊,再比較雜湊值,已確定密碼是否 正確。

這裡的“佐料”被稱作“Salt值”,這個值是由系統隨機生成的,並且只有系統知道。這樣,即便兩個使用者使用了同一個密碼,由於系統為它們生成 的salt值不同,他們的雜湊值也是不同的。即便黑客可以通過自己的密碼和自己生成的雜湊值來找具有特定密碼的使用者,但這個機率太小了(密碼和salt值 都得和黑客使用的一樣才行)。


import java.util.Random;
import org.apache.commons.codec.binary.Hex;
import java.security.NoSuchAlgorithmException;
import java.security.MessageDigest;

public class MD5Util {

    /**
     * 普通MD5方法 容易被破解
     */
    public static String md5(String input) {
        MessageDigest md5 = null;
        try {
            md5 = MessageDigest.getInstance("md5");
        } catch (NoSuchAlgorithmException e) {
            return "check jdk";
        } catch (Exception e) {
            e.printStackTrace();
            return "";
        }
        char[] charArray = input.toCharArray();
        byte[] byteArray = new byte[charArray.length];

        for (int i = 0; i < charArray.length; i++) {
            byteArray[i] = (byte) charArray[i];
        }
        byte[] md5Bytes = md5.digest(byteArray);
        StringBuffer hexValue = new StringBuffer();
        for (int i = 0; i < md5Bytes.length; i++) {
            int val = ((int) md5Bytes[i]) & 0xff;
            if (val < 16) {
                hexValue.append("0");
            }
            hexValue.append(Integer.toHexString(val));
        }
        return hexValue.toString();
    }




    /**
     * 加鹽MD5
     * @author daniel
     * @time 2016-6-11 下午8:45:04
     * @param password
     * @return
     */
    public static String md5WithSalt(String password) {
        Random r = new Random();
        StringBuilder sb = new StringBuilder(16);
        sb.append(r.nextInt(99999999)).append(r.nextInt(99999999));
        int len = sb.length();
        if (len < 16) {
            for (int i = 0; i < 16 - len; i++) {
                sb.append("0");
            }
        }
        String salt = sb.toString();
        password = md5Hex(password + salt);
        char[] cs = new char[48];
        for (int i = 0; i < 48; i += 3) {
            cs[i] = password.charAt(i / 3 * 2);
            char c = salt.charAt(i / 3);
            cs[i + 1] = c;
            cs[i + 2] = password.charAt(i / 3 * 2 + 1);
        }
        return new String(cs);
    }
    /**
     * 校驗加鹽後是否和原文一致
     * @author daniel
     * @time 2016-6-11 下午8:45:39
     * @param password
     * @param md5
     * @return
     */
    public static boolean verify(String password, String md5) {
        char[] cs1 = new char[32];
        char[] cs2 = new char[16];
        for (int i = 0; i < 48; i += 3) {
            cs1[i / 3 * 2] = md5.charAt(i);
            cs1[i / 3 * 2 + 1] = md5.charAt(i + 2);
            cs2[i / 3] = md5.charAt(i + 1);
        }
        String salt = new String(cs2);
        return md5Hex(password + salt).equals(new String(cs1));
    }
    /**
     * 獲取十六進位制字串形式的MD5摘要
     */
    private static String md5Hex(String src) {
        try {
            MessageDigest md5 = MessageDigest.getInstance("md5");
            byte[] bs = md5.digest(src.getBytes());
            return new String(new Hex().encode(bs));
        } catch (Exception e) {
            return null;
        }
    }

    public static void main(String[] args) {
        String md5 = md5("admin");
        System.out.println(md5);

        String mdsSalt = md5WithSalt("admin");
        System.out.println(mdsSalt);
        System.out.println(verify("admin",mdsSalt));
    }
    
}

SHA

SHA代表安全雜湊演算法,SHA-1和SHA-2是該演算法的兩個不同版本。它們在構造(如何從原始資料建立結果雜湊)和簽名的位長方面都不同。您應該將SHA-2視為SHA-1的繼承者,因為它是一個整體改進。

首先,人們把重點放在位元長度上作為重要的區別。SHA-1是160位雜湊。SHA-2實際上是雜湊的“家族”,有各種長度,最受歡迎的是256位。

各種各樣的SHA-2雜湊可能會引起一些混亂,因為網站和作者以不同的方式表達它們。如果你看到“SHA-2”,“SHA-256”或“SHA-256位”,那些名稱指的是同一個東西。如果您看到“SHA-224”,“SHA-384”或“SHA-512”,則它們指的是SHA-2的備用位長度。您可能還會看到一些網站更明確,並寫出演算法和位元長度,例如“SHA-2 384”。

各個加密演算法的比較

  1. 雜湊演算法的比較
名稱 安全性 速度
SHA-1
MD5
  1. 對稱加密演算法比較
名稱 金鑰名稱 執行速度 安全性 資源消耗
DES 56位 較快
3DES 112位或168位
AES 128、192、256位
  1. 非對稱演算法的比較
名稱 成熟度 安全性 運算速度 資源消耗
RSA 高
ECC

Base64(編碼方式)

我們知道在計算機中任何資料都是按ascii碼儲存的,而ascii碼的128~255之間的值是不可見字元。而在網路上交換資料時,比如說從A地傳到B地,往往要經過多個路由裝置,由於不同的裝置對字元的處理方式有一些不同,這樣那些不可見字元就有可能被處理錯誤,這是不利於傳輸的。所以就先把資料先做一個Base64編碼,統統變成可見字元,這樣出錯的可能性就大降低了。

對證書來說,特別是根證書,一般都是作Base64編碼的,因為它要在網上被許多人下載。電子郵件的附件一般也作Base64編碼的,因為一個附件資料往往是有不可見字元的。標準base64只有64個字元(英文大小寫、數字和+、/)以及用作字尾等號;

Base64有很多實現,JDK預設實現、Apache包下面的實現和Spring提供的實現等。平時我們用的時候推薦使用Apache下面的實現。