JDK安全模块JCE核心Cipher使用详解(国内首家JDK下载镜像上线了)
大家好,又見面了,我是你們的朋友風(fēng)君子。
目錄
JDK安全模塊JCE核心Cipher使用詳解
前提
Cipher初始化transformation(轉(zhuǎn)換模式)的一些知識(shí)補(bǔ)充
算法
工作模式
填充模式
transformation小結(jié)
Cipher的屬性和方法
Cipher的七個(gè)主要公有屬性
getInstance方法
init方法
wrap方法和unwrap方法
update方法
doFinal方法
updateADD方法
其他方法
Cipher的工作流程
Cipher的使用
加密模式
解密模式
包裝密鑰模式和解包裝密鑰模式
分組(部分)加密和分組解密
查看當(dāng)前JDK中Cipher的所有提供商
擴(kuò)展
小結(jié)
JDK安全模塊JCE核心Cipher使用詳解
前提
javax.crypto.Cipher,翻譯為密碼,其實(shí)叫做密碼器更加合適。Cipher是JCA(Java Cryptographic Extension,Java加密擴(kuò)展)的核心,提供基于多種加解密算法的加解密功能。在不了解Cipher之前,我們?cè)谕瓿梢恍┬枰咏饷艿哪K的時(shí)候總是需要到處拷貝代碼,甚至有些錯(cuò)誤的用法也被無數(shù)次拷貝,踩坑之后又要拷貝補(bǔ)坑的代碼。為什么不嘗試?yán)斫釩ipher然后合理地使用呢?
Cipher初始化transformation(轉(zhuǎn)換模式)的一些知識(shí)補(bǔ)充
轉(zhuǎn)換模式transformation一般由三個(gè)部分組成,格式是:算法/工作模式/填充模式(algorithm/mode/padding)。例如:DES/CBC/PKCS5Padding。
算法
算法就是指具體加解密算法的名稱英文字符串,例如”SHA-256”、”RSA”等,這里不對(duì)具體算法的實(shí)現(xiàn)原理做具體展開。
工作模式
工作模式其實(shí)主要是針對(duì)分組密碼。分組密碼是將明文消息編碼表示后的數(shù)字(簡(jiǎn)稱明文數(shù)字)序列,劃分成長(zhǎng)度為n的組(可看成長(zhǎng)度為n的矢量),每組分別在密鑰的控制下變換成等長(zhǎng)的輸出數(shù)字(簡(jiǎn)稱密文數(shù)字)序列。工作模式的出現(xiàn)主要基于下面原因:
- 當(dāng)需要加密的明文長(zhǎng)度十分大(例如文件內(nèi)容),由于硬件或者性能原因需要分組加密。
- 多次使用相同的密鑰對(duì)多個(gè)分組加密,會(huì)引發(fā)許多安全問題。
從本質(zhì)上講,工作模式是一項(xiàng)增強(qiáng)密碼算法或者使算法適應(yīng)具體應(yīng)用的技術(shù),例如將分組密碼應(yīng)用于數(shù)據(jù)塊組成的序列或者數(shù)據(jù)流。目前主要包括下面五種由NIST定義的工作模式:
| 模式 | 名稱 | 描述 | 典型應(yīng)用 |
|---|---|---|---|
| 電子密碼本(ECB) | Electronic CodeBook | 用相同的密鑰分別對(duì)明文分組獨(dú)立加密 | 單個(gè)數(shù)據(jù)的安全傳輸(例如一個(gè)加密密鑰) |
| 密碼分組鏈接(CBC) | Cipher Block Chaining | 加密算法的輸入是上一個(gè)密文組合下一個(gè)明文組的異或 | 面向分組的通用傳輸或者認(rèn)證 |
| 密文反饋(CFB) | Cipher FeedBack | 一次處理s位,上一塊密文作為加密算法的輸入,產(chǎn)生的偽隨機(jī)數(shù)輸出與明文異或作為下一單元的密文 | 面向分組的通用傳輸或者認(rèn)證 |
| 輸出反饋(OFB) | Output FeedBack | 與CFB類似,只是加密算法的輸入是上一次加密的輸出,并且使用整個(gè)分組 | 噪聲信道上的數(shù)據(jù)流的傳輸(如衛(wèi)星通信) |
| 計(jì)數(shù)器(CTR) | Counter | 每個(gè)明文分組都與一個(gè)經(jīng)過加密的計(jì)數(shù)器相異或。對(duì)每個(gè)后續(xù)分組計(jì)數(shù)器遞增 | 面向分組的通用傳輸或者用于高速需求 |
上面五種工作模式可以用于3DES和AES在內(nèi)的任何分組密碼,至于選擇哪一種工作模式需要結(jié)合實(shí)際情況分析。
填充模式
Padding指的是:塊加密算法要求原文數(shù)據(jù)長(zhǎng)度為固定塊大小的整數(shù)倍,如果原文數(shù)據(jù)長(zhǎng)度大于固定塊大小,則需要在固定塊填充數(shù)據(jù)直到整個(gè)塊的數(shù)據(jù)是完整的。例如我們約定塊的長(zhǎng)度為128,但是需要加密的原文長(zhǎng)度為129,那么需要分成兩個(gè)加密塊,第二個(gè)加密塊需要填充127長(zhǎng)度的數(shù)據(jù),填充模式?jīng)Q定怎么填充數(shù)據(jù)。
對(duì)數(shù)據(jù)在加密時(shí)進(jìn)行填充、解密時(shí)去除填充則是通信雙方需要重點(diǎn)考慮的因素。對(duì)原文進(jìn)行填充,主要基于以下原因:
- 首先,考慮安全性。由于對(duì)原始數(shù)據(jù)進(jìn)行了填充,使原文能夠“偽裝”在填充后的數(shù)據(jù)中,使得攻擊者很難找到真正的原文位置。
- 其次,由于塊加密算法要求原文數(shù)據(jù)長(zhǎng)度為固定塊大小的整數(shù)倍,如果加密原文不滿足這個(gè)條件,則需要在加密前填充原文數(shù)據(jù)至固定塊大小的整數(shù)倍。
- 另外,填充也為發(fā)送方與接收方提供了一種標(biāo)準(zhǔn)的形式以約束加密原文的大小。只有加解密雙方知道填充方式,才可知道如何準(zhǔn)確移去填充的數(shù)據(jù)并進(jìn)行解密。
常用的填充方式至少有5種,不同編程語言實(shí)現(xiàn)加解密時(shí)用到的填充多數(shù)來自于這些方式或它們的變種方式。以下五種填充模式摘抄自參考資料的論文:
1.填充數(shù)據(jù)為填充字節(jié)序列的長(zhǎng)度:
這種填充方式中,填充字符串由一個(gè)字節(jié)序列組成,每個(gè)字節(jié)填充該字節(jié)序列的長(zhǎng)度。假定塊長(zhǎng)度為8,原文數(shù)據(jù)長(zhǎng)度為9,則填充字節(jié)數(shù) 等于0x07;如果明文數(shù)據(jù)長(zhǎng)度為8的整數(shù)倍,則填充字節(jié)數(shù)為0x08。填充字符串如下:
- 原文數(shù)據(jù)1: FF FF FF FF FF FF FF FF FF
- 填充后數(shù)據(jù)1:FF FF FF FF FF FF FF FF FF 07 07 07 07 07 07 07
- ==========================================================
- 原文數(shù)據(jù)2:FF FF FF FF FF FF FF FF
- 填充后數(shù)據(jù)2:FF FF FF FF FF FF FF FF 08 08 08 08 08 08 08 08
2.填充數(shù)據(jù)為0x80后加0x00:
這種填充方式中,填充字符串的第一個(gè)字節(jié)數(shù)是0x80,后面的每個(gè)字節(jié)是0x00。假定塊長(zhǎng)度為8,原文數(shù)據(jù)長(zhǎng)度為9或者為8的整數(shù)倍,則 填充字符串如下:
- 原文數(shù)據(jù)1: FF FF FF FF FF FF FF FF FF
- 填充后數(shù)據(jù)1:FF FF FF FF FF FF FF FF FF 80 00 00 00 00 00 00
- ==========================================================
- 原文數(shù)據(jù)2:FF FF FF FF FF FF FF FF
- 填充后數(shù)據(jù)2:FF FF FF FF FF FF FF FF 80 00 00 00 00 00 00 00
3.填充數(shù)據(jù)的最后一個(gè)字節(jié)為填充字節(jié)序列的長(zhǎng)度:
這種填充方式中,填充字符串的最后一個(gè)字節(jié)為該序列的長(zhǎng)度,而前面的字節(jié)可以是0x00,也可以是隨機(jī)的字節(jié)序列。假定塊長(zhǎng)度為8,原文數(shù)據(jù)長(zhǎng)度為9或者為8的整數(shù)倍,則填充字符串如下:
- 原文數(shù)據(jù)1:FF FF FF FF FF FF FF FF FF
- 填充后數(shù)據(jù)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
- ===============================================================================
- 原文數(shù)據(jù)2:FF FF FF FF FF FF FF FF
- 填充后數(shù)據(jù)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.填充數(shù)據(jù)為空格:
這種填充方式中,填充字符串的每個(gè)字節(jié)為空格對(duì)應(yīng)的字節(jié)數(shù)0x20。假定塊長(zhǎng)度為8,原文數(shù)據(jù)長(zhǎng)度為9或者為8的整數(shù)倍,則填充字符串如下:
- 原文數(shù)據(jù)1: FF FF FF FF FF FF FF FF FF
- 填充后數(shù)據(jù)1:FF FF FF FF FF FF FF FF FF 20 20 20 20 20 20 20
- ===============================================================================
- 原文數(shù)據(jù)2:FF FF FF FF FF FF FF FF
- 填充后數(shù)據(jù)2:FF FF FF FF FF FF FF FF 20 20 20 20 20 20 20 20
5.填充數(shù)據(jù)為0x00:
這種填充方式中,填充字符串的每個(gè)字節(jié)為0x00。假定塊長(zhǎng)度為8,原文數(shù)據(jù)長(zhǎng)度為9或者8的整數(shù)倍,則填充字符串如下:
- 原文數(shù)據(jù)1: FF FF FF FF FF FF FF FF FF
- 填充后數(shù)據(jù)1:FF FF FF FF FF FF FF FF FF 00 00 00 00 00 00 00
- ===============================================================================
- 原文數(shù)據(jù)2:FF FF FF FF FF FF FF FF
- 填充后數(shù)據(jù)2:FF FF FF FF FF FF FF FF 00 00 00 00 00 00 00 00
transformation小結(jié)
SunJCE Provider支持的Cipher的部分詳細(xì)信息如下:
| 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)匯總?cè)缦拢?/p>
| 填充模式 | 描述 |
|---|---|
| NoPadding | 不采用填充模式 |
| ISO10126Padding | XML加密語法和處理文檔中有詳細(xì)描述(填充數(shù)據(jù)的最后一個(gè)字節(jié)為填充字節(jié)序列的長(zhǎng)度) |
| OAEPPadding, OAEPWith\<digest>And\<mgf>Padding | PKCS1中定義的最優(yōu)非對(duì)稱加密填充方案,digest代表消息摘要類型,mgf代表掩碼生成函數(shù),例如:OAEPWithMD5AndMGF1Padding或者OAEPWithSHA-512AndMGF1Padding |
| PKCS1Padding | PKCS1,RSA算法使用 |
| PKCS5Padding | PKCS5,RSA算法使用 |
| SSL3Padding | 見SSL Protocol Version 3.0的定義 |
其他Padding需要第三方Provider提供。
Cipher的屬性和方法
Cipher的七個(gè)主要公有屬性
- 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,解包裝密鑰模式下指定密鑰類型為密鑰,主要用于不是非對(duì)稱加密的密鑰(只有一個(gè)密鑰,不包含私鑰和公鑰)。
getInstance方法
Cipher提供三個(gè)靜態(tài)工廠方法getInstance()用于構(gòu)建其實(shí)例,三個(gè)方法如下:
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,這里稱為轉(zhuǎn)換(模式),是核心參數(shù),見前面一個(gè)小節(jié)的解析。另外,有兩個(gè)工廠方法要求必須傳入java.security.Provider的全類名或者實(shí)例,因?yàn)镃ipher要從對(duì)應(yīng)的提供商中獲取指定轉(zhuǎn)換模式的實(shí)現(xiàn),第一個(gè)工廠方法只有單參數(shù)transformation,它會(huì)從現(xiàn)成所有的java.security.Provider中匹配取出第一個(gè)滿足transformation的服務(wù),從中實(shí)例化CipherSpi(要理解Cipher委托到內(nèi)部持有的CipherSpi實(shí)例完成具體的加解密功能)。實(shí)際上Cipher實(shí)例的初始化必須依賴于轉(zhuǎn)換模式和提供商。
init方法
init()方法一共有八個(gè)變體方法,此方法主要用于初始化Cipher。
//額外參數(shù)是Key(密鑰)
public final void init(int opmode,
Key key)
throws InvalidKeyException
//額外參數(shù)是Key(密鑰)和SecureRandom(隨機(jī)源)
public final void init(int opmode,
Key key,
SecureRandom random)
throws InvalidKeyException
//額外參數(shù)是Key(密鑰)和AlgorithmParameterSpec(算法參數(shù)透明定義)
public final void init(int opmode,
Key key,
AlgorithmParameterSpec params)
throws InvalidKeyException,
InvalidAlgorithmParameterException
//額外參數(shù)是Key(密鑰)、AlgorithmParameterSpec(算法參數(shù)透明定義)和SecureRandom(隨機(jī)源)
public final void init(int opmode,
Key key,
AlgorithmParameterSpec params,
SecureRandom random)
throws InvalidKeyException,
InvalidAlgorithmParameterException
//額外參數(shù)是Key(密鑰)、AlgorithmParameters(算法參數(shù))
public final void init(int opmode,
Key key,
AlgorithmParameters params)
throws InvalidKeyException,
InvalidAlgorithmParameterException
//額外參數(shù)是Key(密鑰)、AlgorithmParameters(算法參數(shù))、SecureRandom(隨機(jī)源)
public final void init(int opmode,
Key key,
AlgorithmParameters params,
SecureRandom random)
throws InvalidKeyException,
InvalidAlgorithmParameterException
//額外參數(shù)是Certificate(證書)
public final void init(int opmode,
Certificate certificate)
throws InvalidKeyException
//額外參數(shù)是Certificate(證書)、SecureRandom(隨機(jī)源)
public final void init(int opmode,
Certificate certificate,
SecureRandom random)
throws InvalidKeyException
opmode(操作模式)是必須參數(shù),可選值是ENCRYPT_MODE、DECRYPT_MODE、WRAP_MODE和UNWRAP_MODE。Key類型參數(shù)如果不是非對(duì)稱加密,對(duì)應(yīng)的類型是SecretKey,如果是非對(duì)稱加密,可以是PublicKey或者PrivateKey。SecureRandom是隨機(jī)源,因?yàn)橛行┧惴ㄐ枰看渭用芙Y(jié)果都不相同,這個(gè)時(shí)候需要依賴系統(tǒng)或者傳入的隨機(jī)源,一些要求每次加解密結(jié)果相同的算法如AES不能使用此參數(shù)。Certificate是帶有密鑰的證書實(shí)現(xiàn)。算法參數(shù)主要包括IV(initialization vector,初始化向量)等等。
wrap方法和unwrap方法
wrap方法用于包裝一個(gè)密鑰。
public final byte[] wrap(Key key)
throws IllegalBlockSizeException,
InvalidKeyException
wrap方法使用的時(shí)候需要注意Cipher的opmode要初始化為WRAP_MODE。
unwrap方法用于解包裝一個(gè)密鑰。
public final Key unwrap(byte[] wrappedKey,
String wrappedKeyAlgorithm,
int wrappedKeyType)
throws InvalidKeyException,
NoSuchAlgorithmException
unwrap方法使用的時(shí)候需要注意Cipher的opmode要初始化為UNWRAP_MODE,在調(diào)用unwrap方法時(shí)候,需要指定之前包裝密鑰的算法和Key的類型。
其實(shí)wrap和unwrap是一個(gè)互逆的操作:
- wrap方法的作用是把原始的密鑰通過某種加密算法包裝為加密后的密鑰,這樣就可以避免在傳遞密鑰的時(shí)候泄漏了密鑰的明文。
- unwrap方法的作用是把包裝(加密)后的密鑰解包裝為原始的密鑰,得到密鑰的明文。
public enum EncryptUtils {
/**
* 單例
*/
SINGLETON;
private static final String SECRECT = "passwrod";
public String wrap(String keyString) throws Exception {
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
//初始化密鑰生成器,指定密鑰長(zhǎng)度為128,指定隨機(jī)源的種子為指定的密鑰(這里是"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");
//初始化密鑰生成器,指定密鑰長(zhǎng)度為128,指定隨機(jī)源的種子為指定的密鑰(這里是"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對(duì)密鑰進(jìn)行包裝和解包裝,調(diào)用main方法,輸出:
|
update方法
update方法有多個(gè)變體,其實(shí)意義相差無幾:
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初始化時(shí)候的opmode。即使它有多個(gè)變體,但是套路是一樣的:依賴于一個(gè)輸入的緩沖區(qū)(帶有需要被加密或者被解密的數(shù)據(jù))、返回值或者參數(shù)是一個(gè)輸出的緩沖區(qū),一些額外的參數(shù)可以通過偏移量和長(zhǎng)度控制加密或者解密操作的數(shù)據(jù)段。部分加密或者解密操作完畢后,必須要調(diào)用Cipher#doFinal()方法來結(jié)束加密或者解密操作。
doFinal方法
doFinal()方法也存在多個(gè)變體:
/**
* 結(jié)束多部分加密或者解密操作。
* 此方法需要在update調(diào)用鏈執(zhí)行完畢之后調(diào)用,返回的結(jié)果是加密或者解密結(jié)果的一部分。
* 此方法正常調(diào)用結(jié)束之后Cipher會(huì)重置為初始化狀態(tài)。
*/
public final byte[] doFinal()
throws IllegalBlockSizeException,
BadPaddingException
/**
* 結(jié)束多部分加密或者解密操作。
* 此方法需要在update調(diào)用鏈執(zhí)行完畢之后調(diào)用,傳入的output作為緩沖區(qū)接收加密或者解密結(jié)果的一部分。
* 此方法正常調(diào)用結(jié)束之后Cipher會(huì)重置為初始化狀態(tài)。
*/
public final int doFinal(byte[] output,
int outputOffset)
throws IllegalBlockSizeException,
ShortBufferException,
BadPaddingException
/**
* 結(jié)束單部分加密或者解密操作。
* 此方法接收需要加密或者解密的完整報(bào)文,返回處理結(jié)果
* 此方法正常調(diào)用結(jié)束之后Cipher會(huì)重置為初始化狀態(tài)。
*/
public final byte[] doFinal(byte[] input)
throws IllegalBlockSizeException,
BadPaddingException
/**
* 結(jié)束單部分或者多部分加密或者解密操作。
* 參數(shù)inputOffset為需要加解密的報(bào)文byte數(shù)組的起始位置,inputLen為需要加密或者解密的字節(jié)長(zhǎng)度
* 此方法正常調(diào)用結(jié)束之后Cipher會(huì)重置為初始化狀態(tài)。
*/
public final byte[] doFinal(byte[] input,
int inputOffset,
int inputLen)
throws IllegalBlockSizeException,
BadPaddingException
/**
* 結(jié)束單部分或者多部分加密或者解密操作。
* 參數(shù)inputOffset為需要加解密的報(bào)文byte數(shù)組的起始位置,inputLen為需要加密或者解密的字節(jié)長(zhǎng)度,output用于接收加解密的結(jié)果
* 此方法正常調(diào)用結(jié)束之后Cipher會(huì)重置為初始化狀態(tài)。
*/
public final int doFinal(byte[] input,
int inputOffset,
int inputLen,
byte[] output)
throws ShortBufferException,
IllegalBlockSizeException,
BadPaddingException
/**
* 結(jié)束單部分或者多部分加密或者解密操作。
* 參數(shù)inputOffset為需要加解密的報(bào)文byte數(shù)組的起始位置,inputLen為需要加密或者解密的字節(jié)長(zhǎng)度,
* output用于接收加解密的結(jié)果,outputOffset用于設(shè)置output的起始位置
* 此方法正常調(diào)用結(jié)束之后Cipher會(huì)重置為初始化狀態(tài)。
*/
public final int doFinal(byte[] input,
int inputOffset,
int inputLen,
byte[] output,
int outputOffset)
throws ShortBufferException,
IllegalBlockSizeException,
BadPaddingException
/**
* 結(jié)束單部分或者多部分加密或者解密操作。
* 參數(shù)input為輸入緩沖區(qū),output為輸出緩沖區(qū)
* 此方法正常調(diào)用結(jié)束之后Cipher會(huì)重置為初始化狀態(tài)。
*/
public final int doFinal(ByteBuffer input,
ByteBuffer output)
throws ShortBufferException,
IllegalBlockSizeException,
BadPaddingException
doFinal()主要功能是結(jié)束單部分或者多部分加密或者解密操作。單部分加密或者解密適用于需要處理的報(bào)文長(zhǎng)度較短無需分塊的情況,這個(gè)時(shí)候直接使用byte[] doFinal(byte[] input)方法即可。多部分加密或者解密適用于需要處理的報(bào)文長(zhǎng)度長(zhǎng)度較大,需要進(jìn)行分塊的情況,這個(gè)時(shí)候需要調(diào)用多次update方法變體進(jìn)行部分塊的加解密,最后調(diào)用doFinal方法變體進(jìn)行部分加解密操作的結(jié)束。舉個(gè)例子,例如處理塊的大小為8,實(shí)際需要加密的報(bào)文長(zhǎng)度為23,那么需要分三塊進(jìn)行加密,前面2塊長(zhǎng)度為8的報(bào)文需要調(diào)用update進(jìn)行部分加密,部分加密的結(jié)果可以從update的返回值獲取到,最后的7長(zhǎng)度(其實(shí)一般會(huì)填充到長(zhǎng)度為塊長(zhǎng)度8)的報(bào)文則調(diào)用doFinal進(jìn)行加密,結(jié)束整個(gè)部分加密的操作。另外,值得注意的是只要Cipher正常調(diào)用完任一個(gè)doFinal變體方法(過程中不拋出異常),那么Cipher會(huì)重置為初始化狀態(tài),可以繼續(xù)使用,這個(gè)可復(fù)用的特性可以降低創(chuàng)建Cipher實(shí)例的性能損耗。
updateADD方法
首先ADD的意思是Additional Authentication Data(額外的身份認(rèn)證數(shù)據(jù))。updateADD()也有三個(gè)方法變體:
public final void updateAAD(byte[] src)
public final void updateAAD(byte[] src,
int offset,
int len)
public final void updateAAD(ByteBuffer src)
它的方法變體都只依賴一個(gè)輸入緩沖區(qū),帶有額外的身份認(rèn)證數(shù)據(jù),一般使用在GCM或者CCM加解密算法中。如果使用此方法,它的調(diào)用必須在Cipher的update和doFinal變體方法之前調(diào)用,其實(shí)理解起來也很簡(jiǎn)單,身份驗(yàn)證必須在實(shí)際的加解密操作之前進(jìn)行。目前,updateADD的資料比較少,筆者在生產(chǎn)環(huán)境找那個(gè)也尚未實(shí)踐過,所以不做展開分析。
其他方法
其他方法主要是Getter方法,用于獲取Cipher的相關(guān)信息。
public final Provider getProvider():獲取Cipher的提供商。public final String getAlgorithm():獲取Cipher使用的算法名稱。public final int getBlockSize():分組加密中,每一組都有固定的長(zhǎng)度,也稱為塊,此方法是返回塊的大小(以字節(jié)為單位)。public final int getOutputSize(int inputLen):根據(jù)給定的輸入長(zhǎng)度inputLen(以字節(jié)為單位),返回保存下一個(gè)update或doFinal操作結(jié)果所需的輸出緩沖區(qū)長(zhǎng)度(以字節(jié)為單位)。public final byte[] getIV():返回Cipher中的初始化向量的字節(jié)數(shù)組。public final AlgorithmParameters getParameters():返回Cipher使用的算法參數(shù)。public final ExemptionMechanism getExemptionMechanism():返回Cipher使用的豁免(exemption)機(jī)制對(duì)象。public static final int getMaxAllowedKeyLength(String transformation):根據(jù)所安裝的JCE策略文件,返回指定轉(zhuǎn)換的最大密鑰長(zhǎng)度。public static final AlgorithmParameterSpec getMaxAllowedParameterSpec(String transformation):根據(jù)JCE策略文件,返回Cipher指定transformation下最大的AlgorithmParameterSpec對(duì)象。
Cipher的工作流程
下面畫一個(gè)圖來詳細(xì)分析一下Cipher的工作流程:
當(dāng)然上圖只分析了Cipher的使用過程,其實(shí)還有一個(gè)重要的步驟就是密鑰的處理,但是密鑰的處理和具體的算法使用是相關(guān)的,所以圖中沒有體現(xiàn)。再放一張官方描述Cipher加載的流程:
主要過程包括:
- 1、創(chuàng)建Cipher實(shí)例,這個(gè)時(shí)候會(huì)從平臺(tái)中所有的提供商(Provider)中根據(jù)transformation匹配第一個(gè)可以使用的CipherSpi實(shí)例,”算法/工作模式/填充模式”必須完全匹配才能選中。
在${JAVA_HONE}/jre/lib/security中的java.security文件中可以看到默認(rèn)加載的提供商。如果需要添加額外或者自實(shí)現(xiàn)的Provider,可以通過java.security.Security的靜態(tài)方法addProvider添加。
- 2、通過Cipher實(shí)例的init方法初始化Cipher,主要參數(shù)是opmode和密鑰。
- 3、根據(jù)初始化的方式和是否需要分組處理,選擇合適的方法進(jìn)行調(diào)用,一般以
doFinal()方法作結(jié)得到返回結(jié)果。
Cipher的使用
為了方便Cipher的使用,最好先引入apache-codec依賴,這樣能簡(jiǎn)化Hex、Base64等操作。
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.11</version>
</dependency>
大多數(shù)情況下,加密后的byte數(shù)組的中元素取值不在Unicode碼點(diǎn)的范圍內(nèi),表面上看到的就是亂碼,實(shí)際上它們是有意義的,因此需要考慮把這種byte數(shù)組轉(zhuǎn)換為非亂碼的字符串以便傳輸,常見的方式有Hex(二進(jìn)制轉(zhuǎn)換為十六進(jìn)制)、Base64等等。下面舉例中沒有針對(duì)異常類型進(jìn)行處理統(tǒng)一外拋,切勿模仿,還有,所有的字符串轉(zhuǎn)化為字節(jié)數(shù)組都沒有指定字符編碼,因此只能使用非中文的明文進(jìn)行處理。
加密模式
加密模式下,Cipher只能用于加密,主要由init()方法中的opmode決定。舉個(gè)例子:
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");
//因?yàn)锳ES要求密鑰的長(zhǎng)度為128,我們需要固定的密碼,因此隨機(jī)源的種子需要設(shè)置為我們的密碼數(shù)組
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);
//單部分加密結(jié)束,重置Cipher
byte[] bytes = cipher.doFinal(content.getBytes());
//加密后的密文由二進(jìn)制序列轉(zhuǎn)化為十六進(jìn)制序列,依賴apache-codec包
return Hex.encodeHexString(bytes);
}
其實(shí)整個(gè)過程Cipher的使用都很簡(jiǎn)單,比較復(fù)雜的反而是密鑰生成的過程。上面的例子需要注意,因?yàn)槭褂昧颂畛淠J綖镹oPadding,輸入的需要加密的報(bào)文長(zhǎng)度必須是16(128bit)的倍數(shù)。
解密模式
解密模式的使用大致和加密模式是相同的,把處理過程逆轉(zhuǎn)過來就行:
public String decryptByAes(String content, String password) throws Exception {
//這里要把十六進(jìn)制的序列轉(zhuǎn)化回二進(jìn)制的序列,依賴apache-codec包
byte[] bytes = Hex.decodeHex(content);
//這里指定了算法為AES_128,工作模式為EBC,填充模式為NoPadding
Cipher cipher = Cipher.getInstance("AES_128/ECB/NoPadding");
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
//因?yàn)锳ES要求密鑰的長(zhǎng)度為128,我們需要固定的密碼,因此隨機(jī)源的種子需要設(shè)置為我們的密碼數(shù)組
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);
//單部分加密結(jié)束,重置Cipher
byte[] result = cipher.doFinal(bytes);
return new String(result);
}
上面的例子需要注意,因?yàn)槭褂昧颂畛淠J綖镹oPadding,輸入的需要加密的報(bào)文長(zhǎng)度必須是16(128bit)的倍數(shù)。
包裝密鑰模式和解包裝密鑰模式
密鑰的包裝和解包裝模式是一對(duì)互逆的操作,主要作用是通過算法對(duì)密鑰進(jìn)行加解密,從而提高密鑰泄漏的難度。
public enum EncryptUtils {
/**
* 單例
*/
SINGLETON;
private static final String SECRECT = "passwrod";
public String wrap(String keyString) throws Exception {
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
//初始化密鑰生成器,指定密鑰長(zhǎng)度為128,指定隨機(jī)源的種子為指定的密鑰(這里是"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");
//初始化密鑰生成器,指定密鑰長(zhǎng)度為128,指定隨機(jī)源的種子為指定的密鑰(這里是"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));
}
}
分組(部分)加密和分組解密
當(dāng)一個(gè)需要加密的報(bào)文十分長(zhǎng)的時(shí)候,我們可以考慮把報(bào)文切割成多個(gè)小段,然后針對(duì)每個(gè)小段進(jìn)行加密,這就是分組加密。分組解密的過程類同,可以看作是分組加密的逆向過程。下面還是用AES算法為例舉個(gè)例子:
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");
//因?yàn)锳ES要求密鑰的長(zhǎng)度為128,我們需要固定的密碼,因此隨機(jī)源的種子需要設(shè)置為我們的密碼數(shù)組
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));
//多部分加密結(jié)束,得到最后一段加密的結(jié)果,重置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");
//因?yàn)锳ES要求密鑰的長(zhǎng)度為128,我們需要固定的密碼,因此隨機(jī)源的種子需要設(shè)置為我們的密碼數(shù)組
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));
//多部分解密結(jié)束,得到最后一段解密的結(jié)果,重置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));
}
}
上面的分段下標(biāo)已經(jīng)在注釋中給出,分段的規(guī)則由實(shí)際情況考慮,一般AES加解密報(bào)文不大的時(shí)候可以直接單部分加解密即可,這里僅僅是為了做展示。
查看當(dāng)前JDK中Cipher的所有提供商
我們可以直接查看當(dāng)前的使用的JDK中Cipher的所有提供商和支持的加解密服務(wù),簡(jiǎn)單寫個(gè)main函數(shù)就行:
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()));
}
}
}
}
}
}
筆者編寫這篇文章的時(shí)候使用的JDK是JDK8的最后一個(gè)更新的版本8u181(1.8.0_181),運(yùn)行main函數(shù)輸出如下:
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
.....輸出內(nèi)容太多忽略剩余部分
擴(kuò)展
因?yàn)镴ava原生支持的transformation是有限的,有些時(shí)候我們需要使用一些算法其他工作模式或者填充模式原生無法支持,這個(gè)時(shí)候我們需要引入第三方的Provider甚至自己實(shí)現(xiàn)Provider。常見的第三方Provider是bouncycastle(BC),目前BC的最新依賴為:
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>1.60</version>
</dependency>
舉個(gè)例子,Java原生是不支持AESWRAP算法的,因此可以引入BC的依賴,再使用轉(zhuǎn)換模式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);
}
}
上面的例子需要注意,因?yàn)槭褂昧薃ESWRAP算法,輸入的需要加密的報(bào)文長(zhǎng)度必須是8的倍數(shù)。
小結(jié)
熟練掌握Cipher的用法、轉(zhuǎn)換模式transformation的一些知識(shí)之后,影響我們編寫加解密模塊代碼的主要因素就是加解密算法的原理或者使用,這些需要我們?nèi)W(xué)習(xí)專門的加解密算法相關(guān)的知識(shí)。另外,有些時(shí)候我們發(fā)現(xiàn)不同平臺(tái)或者不同語言使用同一個(gè)加密算法不能相互解密加密,其實(shí)原因很簡(jiǎn)單,絕大部分原因是工作模式選取或者填充模式選取的不同導(dǎo)致的,排除掉這兩點(diǎn),剩下的可能性就是算法的實(shí)現(xiàn)不相同,依據(jù)這三點(diǎn)因素(或者說就是transformation這唯一的因素)去判斷和尋找解決方案即可。關(guān)于加解密算法原理、工作模式等相關(guān)知識(shí)可以參考下面的資料。
參考資料:
- 《密碼編碼學(xué)與網(wǎng)絡(luò)安全-原理與實(shí)踐(第六版)》
- 《信息安全原理與實(shí)踐(第2版)》
- 《關(guān)于加密數(shù)據(jù)的填充方式的研究》
- JDK8文檔
另外,一些特殊的方法例如Ciper#updateADD()暫時(shí)沒遇到使用場(chǎng)景,這里就不寫沒實(shí)踐過的Demo。
https://www.throwable.club/2019/02/16/java-security-cipher/#%E6%89%A9%E5%B1%95
PS:對(duì)稱加密的算法與密鑰長(zhǎng)度選擇
總結(jié)
以上是生活随笔為你收集整理的JDK安全模块JCE核心Cipher使用详解(国内首家JDK下载镜像上线了)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 负债十五万左右,信用卡十万,网贷四五万,
- 下一篇: 【图像超分辨】RDN