javax.crypto.Cipher,翻譯爲密碼,其實叫作密碼器更加合適。Cipher是JCA(Java Cryptographic Extension,Java加密擴展)的核心,提供基於多種加解密算法的加解密功能。在不瞭解Cipher以前,咱們在完成一些須要加解密的模塊的時候老是須要處處拷貝代碼,甚至有些錯誤的用法也被無數次拷貝,踩坑以後又要拷貝補坑的代碼。爲何不嘗試理解Cipher而後合理地使用呢?java
轉換模式transformation通常由三個部分組成,格式是:算法/工做模式/填充模式(algorithm/mode/padding)。例如:DES/CBC/PKCS5Padding。算法
算法就是指具體加解密算法的名稱英文字符串,例如"SHA-256"、"RSA"等,這裏不對具體算法的實現原理作具體展開。apache
工做模式其實主要是針對分組密碼。分組密碼是將明文消息編碼表示後的數字(簡稱明文數字)序列,劃分紅長度爲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長度的數據,填充模式決定怎麼填充數據。網絡
對數據在加密時進行填充、解密時去除填充則是通訊雙方須要重點考慮的因素。對原文進行填充,主要基於如下緣由:app
經常使用的填充方式至少有5種,不一樣編程語言實現加解密時用到的填充多數來自於這些方式或它們的變種方式。如下五種填充模式摘抄自參考資料的論文:dom
1.填充數據爲填充字節序列的長度編程語言
這種填充方式中,填充字符串由一個字節序列組成,每一個字節填充該字節序列的長度。假定塊長度爲8,原文數據長度爲9,則填充字節數 等於0x07;若是明文數據長度爲8的整數倍,則填充字節數爲0x08。填充字符串以下:
2.填充數據爲0x80後加0x00
這種填充方式中,填充字符串的第一個字節數是0x80,後面的每一個字節是0x00。假定塊長度爲8,原文數據長度爲9或者爲8的整數倍,則 填充字符串以下:
3.填充數據的最後一個字節爲填充字節序列的長度
這種填充方式中,填充字符串的最後一個字節爲該序列的長度,而前面的字節能夠是0x00,也能夠是隨機的字節序列。假定塊長度爲8,原文數據長度爲9或者爲8的整數倍,則填充字符串以下:
4.填充數據爲空格
這種填充方式中,填充字符串的每一個字節爲空格對應的字節數0x20。假定塊長度爲8,原文數據長度爲9或者爲8的整數倍,則填充字符串以下:
5.填充數據爲0x00
這種填充方式中,填充字符串的每一個字節爲0x00。假定塊長度爲8,原文數據長度爲9或者8的整數倍,則填充字符串以下:
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提供三個靜態工廠方法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方法一共有八個變體方法,此方法主要用於初始化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方法用於包裝一個密鑰。
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是一個互逆的操做:
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方法有多個變體,其實意義相差無幾:
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方法也存在多個變體:
/** * 結束多部分加密或者解密操做。 * 此方法須要在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實例的性能損耗。
首先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的update
和doFinal
變體方法以前調用,其實理解起來也很簡單,身份驗證必須在實際的加解密操做以前進行。目前,updateADD
的資料比較少,筆者在生產環境找那個也還沒有實踐過,因此不作展開分析。
其餘方法主要是Getter方法,用於獲取Cipher的相關信息。
下面畫一個圖來詳細分析一下Cipher的工做流程:
固然上圖只分析了Cipher的使用過程,其實還有一個重要的步驟就是密鑰的處理,可是密鑰的處理和具體的算法使用是相關的,因此圖中沒有體現。再放一張官方描述Cipher加載的流程:
主要過程包括:
一、建立Cipher實例,這個時候會從平臺中全部的提供商(Provider)中根據transformation匹配第一個可使用的CipherSpi實例,"算法/工做模式/填充模式"必須徹底匹配才能選中。
在${JAVA_HONE}/jre/lib/security中的java.security文件中能夠看到默認加載的提供商。若是須要添加額外或者自實現的Provider,能夠經過java.security.Security的靜態方法addProvider添加。
三、根據初始化的方式和是否須要分組處理,選擇合適的方法進行調用。
爲了方便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的全部提供商和支持的加解密服務,簡單寫個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這惟一的因素)去判斷和尋找解決方案便可。關於加解密算法原理、工做模式等相關知識能夠參考下面的資料。
參考資料:
另外,一些特殊的方法例如Ciper#updateADD
暫時沒遇到使用場景,這裏就不寫沒實踐過的Demo。下一篇文章將會介紹一些主流的加解密算法的基本原理和經過Cipher對這些算法進行加解密應用。
(本文完 c-7-d)