天天看點

Java加密與解密藝術

緣起

網絡中為了保障資料安全性,會對資料做很多增加安全性的操作,比如加密,簽名(摘要)等。而加密又分為對稱加密和非對稱加密,每一種加密方式又有不同加密算法來實作,常見的有比如MD5,DES,AES等,本文主要對常用的加密算法做一個入門。

加密算法分類

Java加密與解密藝術

單向雜湊演算法

單向雜湊演算法又叫簽名摘要算法或摘要算法,是從一段明文中提取一段摘要出來,明文中任何一個字元的變動都會引起摘要的變動,進而實作對原文的真實性做校驗。

比如有一個壓縮包A求出來的摘要為c0bb4f54f1d8b14caf6fe1069e5f93ad,那麼使用者在下載下傳這個壓縮包到本地的時候就可以再次求一下摘要,然後與作者提供的摘要做比對,如果不一樣,說明這個壓縮包被人修改過,可能被人放入了病毒之類。

常見的單向雜湊演算法有MD系列,Sha系列,Mac算法。

MD5:md5算法是典型的消息摘要算法,其前身有MD2、MD3和MD4算法,它由MD4、MD3和MD2算法改進而來。

SHA系列:也是由MD4演化而來,包括 SHA-1,SHA-224,SHA-256,SHA-384,和 SHA-512 等幾種算法。其中,SHA-1,SHA-224 和 SHA-256 适用于長度不超過 2^64 二進制位的消息。SHA-384 和 SHA-512 适用于長度不超過 2^128 二進制位的消息。與MD5主要不同之處在于摘要長度,SHA算法的摘要長度更長,安全性更高。

HMAC:類似MD5和SHA,也是給定一個明文,通過HMAC得到這段明文的Hash值,但是與MD和SHA不同的是,HMAC運算的時候需要一個密鑰的參與,通常稱之為“鹽”,其原理為 HMAC 程序将密鑰(鹽)與消息資料混合,使用哈希函數對混合結果進行哈希計算,将所得哈希值與該密鑰混合,然後再次應用哈希函數。 輸出的哈希值長度為 160 位。此外,HMAC還能自己生成密鑰而非人為構造密鑰。調用的時候通常是

hamc("明文","鹽")

這種方式。MD5和SHA系列在日常的使用過程中為了安全通常也會加鹽,隻是這個鹽是直接拼在明文的後面,比如:

MD5("明文"+"鹽")

.HMAC可以用來做封包簽名,也就是防篡改,發送方在發送封包的時候在封包後面拼接上通過HMAC算法計算出的Hash值,接收方接收到的時候對這個Hash值進行驗證,由于密鑰(鹽)是發送方和接收方提前約定好的,是以中間層即使擷取了你的明文,他篡改了封包之後由于沒有鹽,他就無法得到運算之後的結果。這樣接收端方可以驗證這段封包的真實性。Java實作Hamc如下

import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;

public class HMACSHA1 {

    private static final String MAC_NAME = "HmacSHA1";
    private static final String ENCODING = "UTF-8";

    /*
     * 展示了一個生成指定算法密鑰的過程 初始化HMAC密鑰
     * @return
     * @throws Exception
     *
      public static String initMacKey() throws Exception {
      //得到一個 指定算法密鑰的密鑰生成器
      KeyGenerator KeyGenerator keyGenerator =KeyGenerator.getInstance(MAC_NAME);
      //生成一個密鑰
      SecretKey secretKey =keyGenerator.generateKey();
      return null;
      }
     */

    /**
     * 使用 HMAC-SHA1 簽名方法對對encryptText進行簽名
     * @param encryptText 被簽名的字元串
     * @param encryptKey  密鑰
     * @return
     * @throws Exception
     */
    public static byte[] HmacSHA1Encrypt(String encryptText, String encryptKey) throws Exception
    {
        byte[] data=encryptKey.getBytes(ENCODING);
        //根據給定的位元組數組構造一個密鑰,第二參數指定一個密鑰算法的名稱
        SecretKey secretKey = new SecretKeySpec(data, MAC_NAME);
        //生成一個指定 Mac 算法 的 Mac 對象
        Mac mac = Mac.getInstance(MAC_NAME);
        //用給定密鑰初始化 Mac 對象
        mac.init(secretKey);

        byte[] text = encryptText.getBytes(ENCODING);
        //完成 Mac 操作
        return mac.doFinal(text);
    }
}
           

對稱加密算法

DES:使用最廣泛的算法,DES 的密鑰的位數太短,隻有56 比特,加密強度不夠,而如果要提高加密強度(例如增加密鑰長度),則系統開銷呈指數增長。

3DES:解決DES密鑰太短的問題,密鑰長度112-168位,增加了疊代次數,速度較慢,效率不高。使用方式同DES。

AES:解決3DES加密效率低,速度慢的問題,成為DES算法的替代者,使用方式同DES。AES是下一代的加密算法标準,速度快,安全級别高。對記憶體的需求非常低。AES的密鑰長度比DES大, AES同時還有AES128,AES192,AES256這些,後面的數字分别代表密鑰的比特長度。AES所允許的密鑰長度有128(bit),192(bit),256(bit)三個長度,對應的明文就是128/8=16位元組,24位元組和32位元組。但是由于美國出口限制,導緻目前隻能用16個位元組的密鑰,用24和32位元組的密鑰會報錯

java.security.InvalidKeyException: Illegal key size or default parameters

不管是DES,3DES還是AES的又有五種加密模式:CBC、ECB、CTR、OCF、CFB,每一種加密模式其實作機理又不一樣,加密出來的密文也不盡相同。

非對稱加密算法

RSA:該算法有一個公鑰和一個私鑰。用公鑰加密隻有這個私鑰能解開,用私鑰加密也隻有公鑰能解開。在日常使用中通常私鑰是隻有服務端有,而公鑰是多個用戶端有。這樣服務端發送的封包每一個用戶端都可以解開(此處的應用場景通常用于簽名,因為私鑰隻有服務端有,是以用戶端能夠解密出來這一段密文,就說明封包是服務端發送的而不是第三方發送的)。而用戶端發送的封包也隻有服務端能夠解開,用戶端互相之間是解不開的。故日常用私鑰簽名,公鑰加密。

編碼算法

嚴格來說編碼算法并不屬于加密算法,其原理隻是對原文進行編碼使之變成了一串肉眼無法辨識的編碼,但如果通過計算機,能夠很容易的再解碼為明文,沒有任何安全性可言。

BASE64:由于某些系統中隻能使用ASCII字元。Base64就是用來将非ASCII字元的資料轉換成ASCII字元的一種方法。由于ASCII碼表是不包含中文的,是以Base64理論上應該不能作用于中文,但實際測試過程中确實是可以,這是什麼原因我也不是很清楚,猜測隻是把中文轉成了字元而已。

URIEncode:由于url中對一些字元有限制,比如“+”,“/”等,而傳統base64編碼後會出現這些字元,這些字元會影響URL的使用,是以出現了UrlBase64編碼。

JDK中對加密算法的支援

jdk中對加密算法有不錯的支援,我們來看看具體有哪些。

jdk加密相關jar包以及package:

  1. rt.jar:java.security.*包,定義了消息摘要算法的實作(不包括mac算法)以及加解密算法的使用接口
  2. jce.jar:java.crypto.*包,DES,3DES以及AES中使用較多,RSA中會用到Cipher類。其中包含了DES,3DES以及AES的實作;比較常用的類Cipher,KeyGenerator,SecretKey,Key,SecretKeyFactory
  3. rt.jar:java.security.cert.*包,提供用于解析和管理證書的類和接口,如Certificate以及CertificateFactory
  4. Bouncy Castle:加密元件,安全提供者,javajce的補充(如java6不支援MD4,SSH-224,但bc支援),以及一些工具類,如Base64編碼(UrlBase64類)以及十六進制編碼(Hex類)。

此外還有很多開源組織也提供了很多不錯的工具類,比如apache的commons-codec,apache的commons是一個工具包,而commons-codec就是這個工具包中用于加解密的。

package com.bxoon.security;

import org.junit.jupiter.api.Test;
import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESKeySpec;
import java.security.*;

/**
 * MD5加密
 */
public class MD5Util {

    /**
     * 功能簡述: 測試MD5單向加密.
     * @throws Exception
     */
    @Test
    public void test01() throws Exception {
        String plainText = "Hello , world !";
        MessageDigest md5 = MessageDigest.getInstance("md5");
        byte[] cipherData = md5.digest(plainText.getBytes());
        StringBuilder builder = new StringBuilder();
        for(byte cipher : cipherData) {
            String toHexStr = Integer.toHexString(cipher & 0xff);
            builder.append(toHexStr.length() == 1 ? "0" + toHexStr : toHexStr);
        }
        System.out.println(builder.toString());
    }

    /**
     * 功能簡述: 使用BASE64進行雙向加密/解密.
     * @throws Exception
     */
    @Test
    public void test02() throws Exception {
        BASE64Encoder encoder = new BASE64Encoder();
        BASE64Decoder decoder = new BASE64Decoder();
        String plainText = "Hello , world !";
        String cipherText = encoder.encode(plainText.getBytes());
        System.out.println("cipherText : " + cipherText);
        //cipherText : SGVsbG8gLCB3b3JsZCAh
        System.out.println("plainText : " +
                new String(decoder.decodeBuffer(cipherText)));
    }

    /**
     * 功能簡述: 使用DES對稱加密/解密.
     * @throws Exception
     */
    @Test
    public void test03() throws Exception {
        String plainText = "Hello , world !";
        String key = "12345678";    //要求key至少長度為8個字元

        SecureRandom random = new SecureRandom();
        DESKeySpec keySpec = new DESKeySpec(key.getBytes());
        SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("des");
        SecretKey secretKey = keyFactory.generateSecret(keySpec);

        Cipher cipher = Cipher.getInstance("des");
        cipher.init(Cipher.ENCRYPT_MODE, secretKey, random);
        byte[] cipherData = cipher.doFinal(plainText.getBytes());
        System.out.println("cipherText : " + new BASE64Encoder().encode(cipherData));
        //PtRYi3sp7TOR69UrKEIicA==

        cipher.init(Cipher.DECRYPT_MODE, secretKey, random);
        byte[] plainData = cipher.doFinal(cipherData);
        System.out.println("plainText : " + new String(plainData));
        //Hello , world !
    }

    /**
     * 功能簡述: 使用RSA非對稱加密/解密.
     * @throws Exception
     */
    @Test
    public void test04() throws Exception {
        String plainText = "Hello , world !";

        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("rsa");
        keyPairGenerator.initialize(1024);
        KeyPair keyPair = keyPairGenerator.generateKeyPair();

        PublicKey publicKey = keyPair.getPublic();
        PrivateKey privateKey = keyPair.getPrivate();
        SecureRandom random = new SecureRandom();

        Cipher cipher = Cipher.getInstance("rsa");
        cipher.init(Cipher.ENCRYPT_MODE, privateKey, random);
        byte[] cipherData = cipher.doFinal(plainText.getBytes());
        System.out.println("cipherText : " + new BASE64Encoder().encode(cipherData));

        cipher.init(Cipher.DECRYPT_MODE, publicKey, random);
        byte[] plainData = cipher.doFinal(cipherData);
        System.out.println("plainText : " + new String(plainData));
        //Hello , world !

        Signature signature  = Signature.getInstance("MD5withRSA");
        signature.initSign(privateKey);
        signature.update(cipherData);
        byte[] signData = signature.sign();
        System.out.println("signature : " + new BASE64Encoder().encode(signData));
        //ADfoeKQn6eEHgLF8ETMXan3TfFO03R5u+cQEWtAQ2lRblLZw1DpzTlJJt1RXjU451I84v3297LhR
        //co64p6Sq3kVt84wnRsQw5mucZnY+jRZNdXpcbwh2qsh8287NM2hxWqp4OOCf/+vKKXZ3pbJMNT/4
        ///t9ewo+KYCWKOgvu5QQ=

        signature.initVerify(publicKey);
        signature.update(cipherData);
        boolean status = signature.verify(signData);
        System.out.println("status : " + status);
        //true
    }

}