徹底告別加解密模塊代碼拷貝-JCE核心Cpiher詳解


前提

javax.crypto.Cipher,翻譯為密碼,其實叫做密碼器更加合適。Cipher是JCA(Java Cryptographic Extension,Java加密擴展)的核心,提供基於多種加解密算法的加解密功能。在不了解Cipher之前,我們在完成一些需要加解密的模塊的時候總是需要到處拷貝代碼,甚至有些錯誤的用法也被無數次拷貝,踩坑之后又要拷貝補坑的代碼。為什么不嘗試理解Cipher然后合理地使用呢?

Cipher初始化transformation(轉換模式)的一些知識補充

轉換模式transformation一般由三個部分組成,格式是:算法/工作模式/填充模式(algorithm/mode/padding)。例如:DES/CBC/PKCS5Padding。

算法

算法就是指具體加解密算法的名稱英文字符串,例如"SHA-256"、"RSA"等,這里不對具體算法的實現原理做具體展開。

工作模式

工作模式其實主要是針對分組密碼。分組密碼是將明文消息編碼表示后的數字(簡稱明文數字)序列,划分成長度為n的組(可看成長度為n的矢量),每組分別在密鑰的控制下變換成等長的輸出數字(簡稱密文數字)序列。工作模式的出現主要基於下面原因:

  • 當需要加密的明文長度十分大(例如文件內容),由於硬件或者性能原因需要分組加密。
  • 多次使用相同的密鑰對多個分組加密,會引發許多安全問題。

從本質上講,工作模式是一項增強密碼算法或者使算法適應具體應用的技術,例如將分組密碼應用於數據塊組成的序列或者數據流。目前主要包括下面五種由NIST定義的工作模式:

模式 名稱 描述 典型應用
電子密碼本(ECB) Electronic CodeBook 用相同的密鑰分別對明文分組獨立加密 單個數據的安全傳輸(例如一個加密密鑰)
密碼分組鏈接(CBC) Cipher Block Chaining 加密算法的輸入是上一個密文組合下一個明文組的異或 面向分組的通用傳輸或者認證
密文反饋(CFB) Cipher FeedBack 一次處理s位,上一塊密文作為加密算法的輸入,產生的偽隨機數輸出與明文異或作為下一單元的密文 面向分組的通用傳輸或者認證
輸出反饋(OFB) Output FeedBack 與CFB類似,只是加密算法的輸入是上一次加密的輸出,並且使用整個分組 噪聲信道上的數據流的傳輸(如衛星通信)
計數器(CTR) Counter 每個明文分組都與一個經過加密的計數器相異或。對每個后續分組計數器遞增 面向分組的通用傳輸或者用於高速需求

上面五種工作模式可以用於3DES和AES在內的任何分組密碼,至於選擇哪一種工作模式需要結合實際情況分析。

填充模式

Padding指的是:塊加密算法要求原文數據長度為固定塊大小的整數倍,如果原文數據長度大於固定塊大小,則需要在固定塊填充數據直到整個塊的數據是完整的。例如我們約定塊的長度為128,但是需要加密的原文長度為129,那么需要分成兩個加密塊,第二個加密塊需要填充127長度的數據,填充模式決定怎么填充數據。

對數據在加密時進行填充、解密時去除填充則是通信雙方需要重點考慮的因素。對原文進行填充,主要基於以下原因:

  • 首先,考慮安全性。由於對原始數據進行了填充,使原文能夠“偽裝”在填充后的數據中,使得攻擊者很難找到真正的原文位置。
  • 其次,由於塊加密算法要求原文數據長度為固定塊大小的整數倍,如果加密原文不滿足這個條件,則需要在加密前填充原文數據至固定塊大小的整數倍。
  • 另外,填充也為發送方與接收方提供了一種標准的形式以約束加密原文的大小。只有加解密雙方知道填充方式,才可知道如何准確移去填充的數據並進行解密。

常用的填充方式至少有5種,不同編程語言實現加解密時用到的填充多數來自於這些方式或它們的變種方式。以下五種填充模式摘抄自參考資料的論文:

1.填充數據為填充字節序列的長度

這種填充方式中,填充字符串由一個字節序列組成,每個字節填充該字節序列的長度。假定塊長度為8,原文數據長度為9,則填充字節數 等於0x07;如果明文數據長度為8的整數倍,則填充字節數為0x08。填充字符串如下:

  • 原文數據1: FF FF FF FF FF FF FF FF FF
  • 填充后數據1:FF FF FF FF FF FF FF FF FF 07 07 07 07 07 07 07
  • ==========================================================
  • 原文數據2:FF FF FF FF FF FF FF FF
  • 填充后數據2:FF FF FF FF FF FF FF FF 08 08 08 08 08 08 08 08

2.填充數據為0x80后加0x00

這種填充方式中,填充字符串的第一個字節數是0x80,后面的每個字節是0x00。假定塊長度為8,原文數據長度為9或者為8的整數倍,則 填充字符串如下:

  • 原文數據1: FF FF FF FF FF FF FF FF FF
  • 填充后數據1:FF FF FF FF FF FF FF FF FF 80 00 00 00 00 00 00
  • ==========================================================
  • 原文數據2:FF FF FF FF FF FF FF FF
  • 填充后數據2:FF FF FF FF FF FF FF FF 80 00 00 00 00 00 00 00

3.填充數據的最后一個字節為填充字節序列的長度

這種填充方式中,填充字符串的最后一個字節為該序列的長度,而前面的字節可以是0x00,也可以是隨機的字節序列。假定塊長度為8,原文數據長度為9或者為8的整數倍,則填充字符串如下:

  • 原文數據1:FF FF FF FF FF FF FF FF FF
  • 填充后數據1:FF FF FF FF FF FF FF FF FF 00 00 00 00 00 00 07或FF FF FF FF FF FF FF FF FF 0A B0 0C 08 05 09 07
  • ===============================================================================
  • 原文數據2:FF FF FF FF FF FF FF FF
  • 填充后數據2:FF FF FF FF FF FF FF FF 00 00 00 00 00 00 00 08或FF FF FF FF FF FF FF FF 80 06 AB EA 03 02 01 08

4.填充數據為空格

這種填充方式中,填充字符串的每個字節為空格對應的字節數0x20。假定塊長度為8,原文數據長度為9或者為8的整數倍,則填充字符串如下:

  • 原文數據1: FF FF FF FF FF FF FF FF FF
  • 填充后數據1:FF FF FF FF FF FF FF FF FF 20 20 20 20 20 20 20
  • ===============================================================================
  • 原文數據2:FF FF FF FF FF FF FF FF
  • 填充后數據2:FF FF FF FF FF FF FF FF 20 20 20 20 20 20 20 20

5.填充數據為0x00

這種填充方式中,填充字符串的每個字節為0x00。假定塊長度為8,原文數據長度為9或者8的整數倍,則填充字符串如下:

  • 原文數據1: FF FF FF FF FF FF FF FF FF
  • 填充后數據1:FF FF FF FF FF FF FF FF FF 00 00 00 00 00 00 00
  • ===============================================================================
  • 原文數據2:FF FF FF FF FF FF FF FF
  • 填充后數據2:FF FF FF FF FF FF FF FF 00 00 00 00 00 00 00 00

transformation小結

SunJCE Provider支持的Cipher的部分詳細信息如下:

algorithm(算法) mode(工作模式) padding(填充模式)
AES EBC、CBC、PCBC、CTR、CTS、CFB、CFB8-CFB128等 NoPadding、ISO10126Padding、PKCS5Padding
AESWrap EBC NoPadding
ARCFOUR EBC NoPadding
Blowfish、DES、DESede、RC2 EBC、CBC、PCBC、CTR、CTS、CFB、CFB8-CFB128等 NoPadding、ISO10126Padding、PKCS5Padding
DESedeWrap CBC NoPadding
PBEWithMD5AndDES、PBEWithMD5AndTripleDES、PBEWithSHA1AndDESede、PBEWithSHA1AndRC2_40 CBC PKCS5Padding
RSA ECB、NONE NoPadding、PKCS1Padding等

Java原生支持的Padding(Cipher)匯總如下:

填充模式 描述
NoPadding 不采用填充模式
ISO10126Padding XML加密語法和處理文檔中有詳細描述
OAEPPadding, OAEPWith<digest>And<mgf>Padding PKCS1中定義的最優非對稱加密填充方案,digest代表消息摘要類型,mgf代表掩碼生成函數,例如:OAEPWithMD5AndMGF1Padding或者OAEPWithSHA-512AndMGF1Padding
PKCS1Padding PKCS1,RSA算法使用
PKCS5Padding PKCS5,RSA算法使用
SSL3Padding 見SSL Protocol Version 3.0的定義

其他Padding需要第三方Provider提供。

Cipher的所有公有方法

Cipher的七個主要公有屬性

  • 1、ENCRYPT_MODE,整型值1,加密模式,用於Cipher的初始化。
  • 2、DECRYPT_MODE,整型值2,解密模式,用於Cipher的初始化。
  • 3、WRAP_MODE,整型值3,包裝密鑰模式,用於Cipher的初始化。
  • 4、UNWRAP_MODE,整型值4,解包裝密鑰模式,用於Cipher的初始化。
  • 5、PUBLIC_KEY,整型值1,解包裝密鑰模式下指定密鑰類型為公鑰。
  • 6、PRIVATE_KEY,整型值2,解包裝密鑰模式下指定密鑰類型為私鑰。
  • 7、SECRET_KEY,整型值3,解包裝密鑰模式下指定密鑰類型為密鑰,主要用於不是非對稱加密的密鑰(只有一個密鑰,不包含私鑰和公鑰)。

Cipher的主要方法

getInstance方法

Cipher提供三個靜態工廠方法getInstance用於構建其實例,三個方法如下:

public static final Cipher getInstance(String transformation)
                                throws NoSuchAlgorithmException,
                                       NoSuchPaddingException

public static final Cipher getInstance(String transformation,
                                       String provider)
                                throws NoSuchAlgorithmException,
                                       NoSuchProviderException,
                                       NoSuchPaddingException

public static final Cipher getInstance(String transformation,
                                       Provider provider)
                                throws NoSuchAlgorithmException,
                                       NoSuchPaddingException

其中transformation,這里稱為轉換(模式),是核心參數,見前面一個小節的解析。另外,有兩個工廠方法要求必須傳入java.security.Provider的全類名或者實例,因為Cipher要從對應的提供商中獲取指定轉換模式的實現,第一個工廠方法只有單參數transformation,它會從現成所有的java.security.Provider中匹配取出第一個滿足transformation的服務,從中實例化CipherSpi(要理解Cipher委托到內部持有的CipherSpi實例完成具體的加解密功能)。實際上Cipher實例的初始化必須依賴於轉換模式和提供商。

init方法

init方法一共有八個變體方法,此方法主要用於初始化Cipher。

//額外參數是Key(密鑰)
public final void init(int opmode,
                       Key key)
                throws InvalidKeyException

//額外參數是Key(密鑰)和SecureRandom(隨機源)
public final void init(int opmode,
                       Key key,
                       SecureRandom random)
                throws InvalidKeyException

//額外參數是Key(密鑰)和AlgorithmParameterSpec(算法參數透明定義)
public final void init(int opmode,
                       Key key,
                       AlgorithmParameterSpec params)
                throws InvalidKeyException,
                       InvalidAlgorithmParameterException 

//額外參數是Key(密鑰)、AlgorithmParameterSpec(算法參數透明定義)和SecureRandom(隨機源)
public final void init(int opmode,
                       Key key,
                       AlgorithmParameterSpec params,
                       SecureRandom random)
                throws InvalidKeyException,
                       InvalidAlgorithmParameterException

//額外參數是Key(密鑰)、AlgorithmParameters(算法參數)
public final void init(int opmode,
                       Key key,
                       AlgorithmParameters params)
                throws InvalidKeyException,
                       InvalidAlgorithmParameterException

//額外參數是Key(密鑰)、AlgorithmParameters(算法參數)、SecureRandom(隨機源)
public final void init(int opmode,
                       Key key,
                       AlgorithmParameters params,
                       SecureRandom random)
                    throws InvalidKeyException,
                       InvalidAlgorithmParameterException

//額外參數是Certificate(證書)
public final void init(int opmode,
                       Certificate certificate)
                throws InvalidKeyException

//額外參數是Certificate(證書)、SecureRandom(隨機源)
public final void init(int opmode,
                       Certificate certificate,
                       SecureRandom random)
                throws InvalidKeyException

opmode(操作模式)是必須參數,可選值是ENCRYPT_MODE、DECRYPT_MODE、WRAP_MODE和UNWRAP_MODE。Key類型參數如果不是非對稱加密,對應的類型是SecretKey,如果是非對稱加密,可以是PublicKey或者PrivateKey。SecureRandom是隨機源,因為有些算法需要每次加密結果都不相同,這個時候需要依賴系統或者傳入的隨機源,一些要求每次加解密結果相同的算法如AES不能使用此參數(或者必須指定固定的隨機源種子)。Certificate是帶有密鑰的證書實現。算法參數主要包括IV(initialization vector,初始化向量)等等。

wrap方法和unwrap方法

wrap方法用於包裝一個密鑰。

public final byte[] wrap(Key key)
                  throws IllegalBlockSizeException,
                         InvalidKeyException

wrap方法使用的時候需要注意Cipher的opmode要初始化為WRAP_MODE。

unwrap方法用於解包裝一個密鑰。

public final Key unwrap(byte[] wrappedKey,
                        String wrappedKeyAlgorithm,
                        int wrappedKeyType)
                 throws InvalidKeyException,
                        NoSuchAlgorithmException

unwrap方法使用的時候需要注意Cipher的opmode要初始化為UNWRAP_MODE,在調用unwrap方法時候,需要指定之前包裝密鑰的算法和Key的類型。

其實wrap和unwrap是一個互逆的操作:

  • wrap方法的作用是把原始的密鑰通過某種加密算法包裝為加密后的密鑰,這樣就可以避免在傳遞密鑰的時候泄漏了密鑰的明文。
  • unwrap方法的作用是把包裝(加密)后的密鑰解包裝為原始的密鑰,得到密鑰的明文。
public enum EncryptUtils {

    /**
     * 單例
     */
    SINGLETON;

    private static final String SECRECT = "passwrod";

    public String wrap(String keyString) throws Exception {
        KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
        //初始化密鑰生成器,指定密鑰長度為128,指定隨機源的種子為指定的密鑰(這里是"passward")
        keyGenerator.init(128, new SecureRandom(SECRECT.getBytes()));
        SecretKey secretKey = keyGenerator.generateKey();
        SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getEncoded(), "AES");
        Cipher cipher = Cipher.getInstance("AES");
        cipher.init(Cipher.WRAP_MODE, secretKeySpec);
        SecretKeySpec key = new SecretKeySpec(keyString.getBytes(), "AES");
        byte[] bytes = cipher.wrap(key);
        return Hex.encodeHexString(bytes);
    }

    public String unwrap(String keyString) throws Exception {
        byte[] rawKey = Hex.decodeHex(keyString);
        KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
        //初始化密鑰生成器,指定密鑰長度為128,指定隨機源的種子為指定的密鑰(這里是"passward")
        keyGenerator.init(128, new SecureRandom(SECRECT.getBytes()));
        SecretKey secretKey = keyGenerator.generateKey();
        SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getEncoded(), "AES");
        Cipher cipher = Cipher.getInstance("AES");
        cipher.init(Cipher.UNWRAP_MODE, secretKeySpec);
        SecretKey key = (SecretKey) cipher.unwrap(rawKey, "AES", Cipher.SECRET_KEY);
        return new String(key.getEncoded());
    }

    public static void main(String[] args) throws Exception {
        String wrapKey = EncryptUtils.SINGLETON.wrap("doge");
        System.out.println(wrapKey);
        System.out.println(EncryptUtils.SINGLETON.unwrap(wrapKey));
    }
}

上面的例子是通過AES對密鑰進行包裝和解包裝,調用main方法,輸出:

77050742188d4b97a1d401db902b864d
doge

update方法

update方法有多個變體,其實意義相差無幾:

public final byte[] update(byte[] input)

public final byte[] update(byte[] input,
                           int inputOffset,
                           int inputLen)

public final int update(byte[] input,
                        int inputOffset,
                        int inputLen,
                        byte[] output)
                 throws ShortBufferException    

public final int update(ByteBuffer input,
                        ByteBuffer output)
                 throws ShortBufferException

update方法主要用於部分加密或者部分解密,至於加密或是解密取決於Cipher初始化時候的opmode。即使它有多個變體,但是套路是一樣的:依賴於一個輸入的緩沖區(帶有需要被加密或者被解密的數據)、返回值或者參數是一個輸出的緩沖區,一些額外的參數可以通過偏移量和長度控制加密或者解密操作的數據段。部分加密或者解密操作完畢后,必須要調用Cipher#doFinal()方法來結束加密或者解密操作。

doFinal方法

doFinal方法也存在多個變體:

/**
 * 結束多部分加密或者解密操作。
 * 此方法需要在update調用鏈執行完畢之后調用,返回的結果是加密或者解密結果的一部分。
 * 此方法正常調用結束之后Cipher會重置為初始化狀態。
 */
public final byte[] doFinal()
                     throws IllegalBlockSizeException,
                            BadPaddingException

/**
 * 結束多部分加密或者解密操作。
 * 此方法需要在update調用鏈執行完畢之后調用,傳入的output作為緩沖區接收加密或者解密結果的一部分。
 * 此方法正常調用結束之后Cipher會重置為初始化狀態。
 */
public final int doFinal(byte[] output,
                         int outputOffset)
                  throws IllegalBlockSizeException,
                         ShortBufferException,
                         BadPaddingException                         

/**
 * 結束單部分加密或者解密操作。
 * 此方法接收需要加密或者解密的完整報文,返回處理結果
 * 此方法正常調用結束之后Cipher會重置為初始化狀態。
 */
public final byte[] doFinal(byte[] input)
                     throws IllegalBlockSizeException,
                            BadPaddingException

/**
 * 結束單部分或者多部分加密或者解密操作。
 * 參數inputOffset為需要加解密的報文byte數組的起始位置,inputLen為需要加密或者解密的字節長度
 * 此方法正常調用結束之后Cipher會重置為初始化狀態。
 */
public final byte[] doFinal(byte[] input,
                            int inputOffset,
                            int inputLen)
                     throws IllegalBlockSizeException,
                            BadPaddingException      

/**
 * 結束單部分或者多部分加密或者解密操作。
 * 參數inputOffset為需要加解密的報文byte數組的起始位置,inputLen為需要加密或者解密的字節長度,output用於接收加解密的結果
 * 此方法正常調用結束之后Cipher會重置為初始化狀態。
 */
public final int doFinal(byte[] input,
                         int inputOffset,
                         int inputLen,
                         byte[] output)
                  throws ShortBufferException,
                         IllegalBlockSizeException,
                         BadPaddingException                                                         

/**
 * 結束單部分或者多部分加密或者解密操作。
 * 參數inputOffset為需要加解密的報文byte數組的起始位置,inputLen為需要加密或者解密的字節長度,
 * output用於接收加解密的結果,outputOffset用於設置output的起始位置
 * 此方法正常調用結束之后Cipher會重置為初始化狀態。
 */
public final int doFinal(byte[] input,
                         int inputOffset,
                         int inputLen,
                         byte[] output,
                         int outputOffset)
                  throws ShortBufferException,
                         IllegalBlockSizeException,
                         BadPaddingException 
/**
 * 結束單部分或者多部分加密或者解密操作。
 * 參數input為輸入緩沖區,output為輸出緩沖區
 * 此方法正常調用結束之后Cipher會重置為初始化狀態。
 */
public final int doFinal(ByteBuffer input,
                         ByteBuffer output)
                  throws ShortBufferException,
                         IllegalBlockSizeException,
                         BadPaddingException                                              

doFinal主要功能是結束單部分或者多部分加密或者解密操作。單部分加密或者解密適用於需要處理的報文長度較短無需分塊的情況,這個時候直接使用byte[] doFinal(byte[] input)方法即可。多部分加密或者解密適用於需要處理的報文長度長度較大,需要進行分塊的情況,這個時候需要調用多次update方法變體進行部分塊的加解密,最后調用doFinal方法變體進行部分加解密操作的結束。舉個例子,例如處理塊的大小為8,實際需要加密的報文長度為23,那么需要分三塊進行加密,前面2塊長度為8的報文需要調用update進行部分加密,部分加密的結果可以從update的返回值獲取到,最后的7長度(其實一般會填充到長度為塊長度8)的報文則調用doFinal進行加密,結束整個部分加密的操作。另外,值得注意的是只要Cipher正常調用完任一個doFinal變體方法(過程中不拋出異常),那么Cipher會重置為初始化狀態,可以繼續使用,這個可復用的特性可以降低創建Cipher實例的性能損耗。

updateADD方法

首先ADD的意思是Additional Authentication Data(額外的身份認證數據)。updateADD也有三個方法變體:

public final void updateAAD(byte[] src)

public final void updateAAD(byte[] src,
                            int offset,
                            int len)

public final void updateAAD(ByteBuffer src)

它的方法變體都只依賴一個輸入緩沖區,帶有額外的身份認證數據,一般使用在GCM或者CCM加解密算法中。如果使用此方法,它的調用必須在Cipher的updatedoFinal變體方法之前調用,其實理解起來也很簡單,身份驗證必須在實際的加解密操作之前進行。目前,updateADD的資料比較少,筆者在生產環境找那個也尚未實踐過,所以不做展開分析。

其他方法

其他方法主要是Getter方法,用於獲取Cipher的相關信息。

  • public final Provider getProvider():獲取Cipher的提供商。
  • public final String getAlgorithm():獲取Cipher使用的算法名稱。
  • public final int getBlockSize():分組加密中,每一組都有固定的長度,也稱為塊,此方法是返回塊的大小(以字節為單位)。
  • public final int getOutputSize(int inputLen):根據給定的輸入長度inputLen(以字節為單位),返回保存下一個update或doFinal操作結果所需的輸出緩沖區長度(以字節為單位)。
  • public final byte[] getIV():返回Cipher中的初始化向量的字節數組。
  • public final AlgorithmParameters getParameters():返回Cipher使用的算法參數。
  • public final ExemptionMechanism getExemptionMechanism():返回Cipher使用的豁免(exemption)機制對象。
  • public static final int getMaxAllowedKeyLength(String transformation):根據所安裝的JCE策略文件,返回指定轉換的最大密鑰長度。
  • public static final AlgorithmParameterSpec getMaxAllowedParameterSpec(String transformation):根據JCE策略文件,返回Cipher指定transformation下最大的AlgorithmParameterSpec對象。

Cipher的工作流程

下面畫一個圖來詳細分析一下Cipher的工作流程:

cipher-1

當然上圖只分析了Cipher的使用過程,其實還有一個重要的步驟就是密鑰的處理,但是密鑰的處理和具體的算法使用是相關的,所以圖中沒有體現。再放一張官方描述Cipher加載的流程:

cipher-2

主要過程包括:

  • 1、創建Cipher實例,這個時候會從平台中所有的提供商(Provider)中根據transformation匹配第一個可以使用的CipherSpi實例,"算法/工作模式/填充模式"必須完全匹配才能選中。

在${JAVA_HONE}/jre/lib/security中的java.security文件中可以看到默認加載的提供商。如果需要添加額外或者自實現的Provider,可以通過java.security.Security的靜態方法addProvider添加。

  • 2、通過Cipher實例的init方法初始化Cipher,主要參數是opmode和密鑰。
  • 3、根據初始化的方式和是否需要分組處理,選擇合適的方法進行調用。

Cipher的使用

為了方便Cipher的使用,最好先引入apache-codec依賴,這樣能簡化Hex、Base64等操作。

<dependency>
    <groupId>commons-codec</groupId>
    <artifactId>commons-codec</artifactId>
    <version>1.11</version>
</dependency>

大多數情況下,加密后的byte數組的中元素取值不在Unicode碼點的范圍內,表面上看到的就是亂碼,實際上它們是有意義的,因此需要考慮把這種byte數組轉換為非亂碼的字符串以便傳輸,常見的方式有Hex(二進制轉換為十六進制)、Base64等等。下面舉例中沒有針對異常類型進行處理統一外拋,切勿模仿,還有,所有的字符串轉化為字節數組都沒有指定字符編碼,因此只能使用非中文的明文進行處理。

加密模式

加密模式下,Cipher只能用於加密,主要由init方法中的opmode決定。舉個例子:

public String encryptByAes(String content, String password) throws Exception {
	//這里指定了算法為AES_128,工作模式為EBC,填充模式為NoPadding
	Cipher cipher = Cipher.getInstance("AES_128/ECB/NoPadding");
	KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
	//因為AES要求密鑰的長度為128,我們需要固定的密碼,因此隨機源的種子需要設置為我們的密碼數組
	keyGenerator.init(128, new SecureRandom(password.getBytes()));
	SecretKey secretKey = keyGenerator.generateKey();
	SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getEncoded(), "AES");
	//基於加密模式和密鑰初始化Cipher
	cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec);
	//單部分加密結束,重置Cipher
	byte[] bytes = cipher.doFinal(content.getBytes());
	//加密后的密文由二進制序列轉化為十六進制序列,依賴apache-codec包
	return Hex.encodeHexString(bytes);
}

其實整個過程Cipher的使用都很簡單,比較復雜的反而是密鑰生成的過程。上面的例子需要注意,因為使用了填充模式為NoPadding,輸入的需要加密的報文長度必須是16(128bit)的倍數。

解密模式

解密模式的使用大致和加密模式是相同的,把處理過程逆轉過來就行:

public String decryptByAes(String content, String password) throws Exception {
	//這里要把十六進制的序列轉化回二進制的序列,依賴apache-codec包
	byte[] bytes = Hex.decodeHex(content);
	//這里指定了算法為AES_128,工作模式為EBC,填充模式為NoPadding
	Cipher cipher = Cipher.getInstance("AES_128/ECB/NoPadding");
	KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
	//因為AES要求密鑰的長度為128,我們需要固定的密碼,因此隨機源的種子需要設置為我們的密碼數組
	keyGenerator.init(128, new SecureRandom(password.getBytes()));
	SecretKey secretKey = keyGenerator.generateKey();
	SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getEncoded(), "AES");
	//基於解密模式和密鑰初始化Cipher
	cipher.init(Cipher.DECRYPT_MODE, secretKeySpec);
	//單部分加密結束,重置Cipher
	byte[] result = cipher.doFinal(bytes);
	return new String(result);
}

上面的例子需要注意,因為使用了填充模式為NoPadding,輸入的需要加密的報文長度必須是16(128bit)的倍數。

包裝密鑰模式和解包裝密鑰模式

密鑰的包裝和解包裝模式是一對互逆的操作,主要作用是通過算法對密鑰進行加解密,從而提高密鑰泄漏的難度。

public enum EncryptUtils {

    /**
     * 單例
     */
    SINGLETON;

    private static final String SECRECT = "passwrod";

    public String wrap(String keyString) throws Exception {
        KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
        //初始化密鑰生成器,指定密鑰長度為128,指定隨機源的種子為指定的密鑰(這里是"passward")
        keyGenerator.init(128, new SecureRandom(SECRECT.getBytes()));
        SecretKey secretKey = keyGenerator.generateKey();
        SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getEncoded(), "AES");
        Cipher cipher = Cipher.getInstance("AES");
        cipher.init(Cipher.WRAP_MODE, secretKeySpec);
        SecretKeySpec key = new SecretKeySpec(keyString.getBytes(), "AES");
        byte[] bytes = cipher.wrap(key);
        return Hex.encodeHexString(bytes);
    }

    public String unwrap(String keyString) throws Exception {
        byte[] rawKey = Hex.decodeHex(keyString);
        KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
        //初始化密鑰生成器,指定密鑰長度為128,指定隨機源的種子為指定的密鑰(這里是"passward")
        keyGenerator.init(128, new SecureRandom(SECRECT.getBytes()));
        SecretKey secretKey = keyGenerator.generateKey();
        SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getEncoded(), "AES");
        Cipher cipher = Cipher.getInstance("AES");
        cipher.init(Cipher.UNWRAP_MODE, secretKeySpec);
        SecretKey key = (SecretKey) cipher.unwrap(rawKey, "AES", Cipher.SECRET_KEY);
        return new String(key.getEncoded());
    }

    public static void main(String[] args) throws Exception {
        String wrapKey = EncryptUtils.SINGLETON.wrap("doge");
        System.out.println(wrapKey);
        System.out.println(EncryptUtils.SINGLETON.unwrap(wrapKey));
    }
}

分組(部分)加密和分組解密

當一個需要加密的報文十分長的時候,我們可以考慮把報文切割成多個小段,然后針對每個小段進行加密,這就是分組加密。分組解密的過程類同,可以看作是分組加密的逆向過程。下面還是用AES算法為例舉個例子:

import org.apache.commons.codec.binary.Hex;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.security.SecureRandom;

/**
 * @author throwable
 * @version v1.0
 * @description
 * @since 2018/8/15 1:06
 */
public enum Part {

	/**
	 * SINGLETON
	 */
	SINGLETON;

	private static final String PASSWORD = "throwable";

	private Cipher createCipher() throws Exception {
		return Cipher.getInstance("AES");
	}

	public String encrypt(String content) throws Exception {
		Cipher cipher = createCipher();
		KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
		//因為AES要求密鑰的長度為128,我們需要固定的密碼,因此隨機源的種子需要設置為我們的密碼數組
		keyGenerator.init(128, new SecureRandom(PASSWORD.getBytes()));
		SecretKey secretKey = keyGenerator.generateKey();
		SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getEncoded(), "AES");
		//基於加密模式和密鑰初始化Cipher
		cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec);
		byte[] raw = content.getBytes();
		StringBuilder builder = new StringBuilder();
		//[0,9]
		byte[] first = cipher.update(raw, 0, 10);
		builder.append(Hex.encodeHexString(first));
		//[10,19]
		byte[] second = cipher.update(raw, 10, 10);
		builder.append(Hex.encodeHexString(second));
		//[20,25]
		byte[] third = cipher.update(raw, 20, 6);
		builder.append(Hex.encodeHexString(third));
		//多部分加密結束,得到最后一段加密的結果,重置Cipher
		byte[] bytes = cipher.doFinal();
		String last = Hex.encodeHexString(bytes);
		builder.append(last);
		return builder.toString();
	}

	public String decrypt(String content) throws Exception {
		byte[] raw = Hex.decodeHex(content);
		Cipher cipher = createCipher();
		KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
		//因為AES要求密鑰的長度為128,我們需要固定的密碼,因此隨機源的種子需要設置為我們的密碼數組
		keyGenerator.init(128, new SecureRandom(PASSWORD.getBytes()));
		SecretKey secretKey = keyGenerator.generateKey();
		SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getEncoded(), "AES");
		//基於解密模式和密鑰初始化Cipher
		cipher.init(Cipher.DECRYPT_MODE, secretKeySpec);
		StringBuilder builder = new StringBuilder();
		//[0,14]
		byte[] first = cipher.update(raw, 0, 15);
		builder.append(new String(first));
		//[15,29]
		byte[] second = cipher.update(raw, 15, 15);
		builder.append(new String(second));
		//[30,31]
		byte[] third = cipher.update(raw, 30, 2);
		builder.append(new String(third));
		//多部分解密結束,得到最后一段解密的結果,重置Cipher
		byte[] bytes = cipher.doFinal();
		builder.append(new String(bytes));
		return builder.toString();
	}

	public static void main(String[] args) throws Exception{
		String raw = "abcdefghijklmnopqrstyuwxyz";
		String e = Part.SINGLETON.encrypt(raw);
		System.out.println(e);
		System.out.println(Part.SINGLETON.decrypt(e));
	}
}

上面的分段下標已經在注釋中給出,分段的規則由實際情況考慮,一般AES加解密報文不大的時候可以直接單部分加解密即可,這里僅僅是為了做展示。

查看當前JDK中Cipher的所有提供商

我們可以直接查看當前的使用的JDK中Cipher的所有提供商和支持的加解密服務,簡單寫個main函數就行:

import java.security.Provider;
import java.security.Security;
import java.util.Set;

public class Main {

	public static void main(String[] args) throws Exception {
		Provider[] providers = Security.getProviders();
		if (null != providers) {
			for (Provider provider : providers) {
				Set<Provider.Service> services = provider.getServices();
				for (Provider.Service service : services) {
					if ("Cipher".equals(service.getType())) {
						System.out.println(String.format("provider:%s,type:%s,algorithm:%s", service.getProvider(), service.getType(), service.getAlgorithm()));
					}
				}
			}
		}
	}
}

筆者使用的JDK是JDK8的最后一個更新的版本8u181(1.8.0_181),運行main函數輸出如下:

provider:SunJCE version 1.8,type:Cipher,algorithm:RSA
provider:SunJCE version 1.8,type:Cipher,algorithm:DES
provider:SunJCE version 1.8,type:Cipher,algorithm:DESede
provider:SunJCE version 1.8,type:Cipher,algorithm:DESedeWrap
provider:SunJCE version 1.8,type:Cipher,algorithm:PBEWithMD5AndDES
provider:SunJCE version 1.8,type:Cipher,algorithm:PBEWithMD5AndTripleDES
provider:SunJCE version 1.8,type:Cipher,algorithm:PBEWithSHA1AndDESede
provider:SunJCE version 1.8,type:Cipher,algorithm:PBEWithSHA1AndRC2_40
provider:SunJCE version 1.8,type:Cipher,algorithm:PBEWithSHA1AndRC2_128
.....輸出內容太多忽略剩余部分

擴展

因為Java原生支持的transformation是有限的,有些時候我們需要使用一些算法其他工作模式或者填充模式原生無法支持,這個時候我們需要引入第三方的Provider甚至自己實現Provider。常見的第三方Provider是bouncycastle(BC),目前BC的最新依賴為:

<dependency>
    <groupId>org.bouncycastle</groupId>
    <artifactId>bcprov-jdk15on</artifactId>
    <version>1.60</version>
</dependency>

舉個例子,Java原生是不支持AESWRAP算法的,因此可以引入BC的依賴,再使用轉換模式AESWRAP。

import org.apache.commons.codec.binary.Hex;
import org.bouncycastle.jce.provider.BouncyCastleProvider;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.security.Security;

public enum EncryptUtils {

	/**
	 * SINGLETON
	 */
	SINGLETON;

	private static final String SECRET = "throwable";
	private static final String CHARSET = "UTF-8";

    //裝載BC提供商
	static {
		Security.addProvider(new BouncyCastleProvider());
	}


	private Cipher createAesCipher() throws Exception {
		return Cipher.getInstance("AESWRAP");
	}

	public String encryptByAes(String raw) throws Exception {
		Cipher aesCipher = createAesCipher();
		KeyGenerator keyGenerator = KeyGenerator.getInstance("AESWRAP");
		keyGenerator.init(128, new SecureRandom(SECRET.getBytes(CHARSET)));
		SecretKey secretKey = keyGenerator.generateKey();
		SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getEncoded(), "AESWRAP");
		aesCipher.init(Cipher.ENCRYPT_MODE, secretKeySpec);
		byte[] bytes = aesCipher.doFinal(raw.getBytes(CHARSET));
		return Hex.encodeHexString(bytes);
	}

	public String decryptByAes(String raw) throws Exception {
		byte[] bytes = Hex.decodeHex(raw);
		Cipher aesCipher = createAesCipher();
		KeyGenerator keyGenerator = KeyGenerator.getInstance("AESWRAP");
		keyGenerator.init(128, new SecureRandom(SECRET.getBytes(CHARSET)));
		SecretKey secretKey = keyGenerator.generateKey();
		SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getEncoded(), "AESWRAP");
		aesCipher.init(Cipher.DECRYPT_MODE, secretKeySpec);
		return new String(aesCipher.doFinal(bytes), CHARSET);
	}

	public static void main(String[] args) throws Exception {
		String raw = "throwable-a-doge";
		String en = EncryptUtils.SINGLETON.encryptByAes(raw);
		System.out.println(en);
		String de = EncryptUtils.SINGLETON.decryptByAes(en);
		System.out.println(de);
	}
}

上面的例子需要注意,因為使用了AESWRAP算法,輸入的需要加密的報文長度必須是8的倍數。

小結

熟練掌握Cipher的用法、轉換模式transformation的一些知識之后,影響我們編寫加解密模塊代碼的主要因素就是加解密算法的原理或者使用,這些需要我們去學習專門的加解密算法相關的知識。另外,有些時候我們發現不同平台或者不同語言使用同一個加密算法不能相互解密加密,其實原因很簡單,絕大部分原因是工作模式選取或者填充模式選取的不同導致的,排除掉這兩點,剩下的可能性就是算法的實現不相同,依據這三點因素(或者說就是transformation這唯一的因素)去判斷和尋找解決方案即可。關於加解密算法原理、工作模式等相關知識可以參考下面的資料。

參考資料:

  • 《密碼編碼學與網絡安全-原理與實踐(第六版)》
  • 《信息安全原理與實踐(第2版)》
  • 《關於加密數據的填充方式的研究》
  • JavaSE8 API文檔

另外,一些特殊的方法例如Ciper#updateADD暫時沒遇到使用場景,這里就不寫沒實踐過的Demo。下一篇文章將會介紹一些主流的加解密算法的基本原理和通過Cipher對這些算法進行加解密應用。

(本文完 c-7-d)

技術公眾號(《Throwable文摘》),不定期推送筆者原創技術文章(絕不抄襲或者轉載):

娛樂公眾號(《天天沙雕》),甄選奇趣沙雕圖文和視頻不定期推送,緩解生活工作壓力:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM