也就是秘密密钥加密。对称加密和解密使用同一个密钥。
加密时,我们用这个密钥初始化密码算法,密码算法对经过它的数据进行加密。
解密过程与加密类似,密码算法用同一个密钥初始化,密码算法对经过它的数据进行解密。
加密的强度依赖于密钥的长度。一般的对称密钥长度在40-128位之间,有些算法可能更长。
建议使用128位以上的对称加密算法,如:DESede(TripleDES)、Blowfish、AES
对称加密比不对称加密(如公钥加密)要快的多,比较适合于大量数据加/解密的情况下(如:文件加密)。
javax.crypto.Cipher
例:Cipher cip = Cipher.getInstance("DESede/CBC/PKCS5Padding");
参数是算法,共分为三段('/'分隔)
第一段是算法名字
模式定了Cipher如何应用加密算法。
改变模式可以允许一个块加密程序变为流加密程充。
分组加密每次加密一个数据组,这个组的位数可以是随意的,一般是64或128位。
流加密每次可以加密或解密一个字节的数据,使其更适合网络通信等。
常用的模式:
ECB(电码本):最简单的模式。同样的明文总是加密成相同的密文分组。
CBC(密码分组链接):同样的明文分组不一定加密成同样的密文块。
CBC使用前一个分组的信息加密当前的分组,这个方法存在的问题是相同的信息仍旧加密成相同的密文,因为所有的分组是同时变成密文分组。
为了解决这个问题需要一个IV(初始化向量),IV仅仅是一个初始化加密程序的随机数,它无需秘密保存,但对每一个信息来说它都是不同的。
通过这种方式,既使有两条相同的信息,只要它们的IV不同,那么它们加密后的密文也是不同的。
所以一个初始化向量就象是口令加密中的盐。
CFB(密码反馈):工作方式与CBC类似,但它可执行更小的数据块,典型的有8位。也需要一个IV。
OFB(输出反馈):在传输中能给数据提供更好的保护,防止数据丢失,其他与CFB类似。
密文中一位出错,也只造成明文中的一位出错。其他方式会造成整个块丢失。也需要一个IV。
第三段是填充符
大多数算法支持两种填充符:
1)No padding 既没有填充,这就要求加密以一个完整的块结束,而没有多余的数据。
2)RKCS#5 padding 工作方式为:一个未被填满的字节被赋予一个指定的数据,这个数值的大小就是块未被写满的字节数。
例如,一个8字节的块,其中仅有3个字节写满了数据,那么就需要对5个节字填充,而这5个字节都被赋予数值'5',这是因为有5个字节需要填充。
如果数据长度正好为8字节的倍数,那么就需要在数据结尾处加一个完整的填序块(既8个为'8'的字节)。解密后,这个填充块必须被移除。
密钥打包和解包
一种是用JCE提供的打包和解包功能,另一种是拿到Key.getEncoded()做处理。
例:
package test1;
import java.security.Key;
import java.security.SecureRandom;
import java.util.Random;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.PBEParameterSpec;
/**
*
* @author shanl
*
*/
public class Test10 {
public static void main(String[] args){
t2();
// t1();
}
/**
* 密钥使用Key.getEncoded()
*/
static void t3(){
try {
KeyGenerator keyGener = KeyGenerator.getInstance("DESede", "SunJCE");
Key key = keyGener.generateKey();
byte[] keyBys = key.getEncoded();
//用PBE对keyBys既可
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 密钥使用JCE打包/解包
*/
static void t2(){
try {
char[] passwd = "123456@".toCharArray();
PBEKeySpec keySpec = new PBEKeySpec(passwd);
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBEWithSHA1AndDESede","SunJCE");
Key passwdKey = keyFactory.generateSecret(keySpec);
int iterations = 100;
byte[] salt = new byte[8];
Random rand = new Random();
rand.nextBytes(salt);
PBEParameterSpec paramSpec = new PBEParameterSpec(salt, iterations);
KeyGenerator keyGener = KeyGenerator.getInstance("DESede", "SunJCE");
Key key = keyGener.generateKey();
byte[] bys = key.getEncoded();
System.out.println("密钥:");
showBys(bys);
Cipher cip = Cipher.getInstance("PBEWithSHA1AndDESede","SunJCE");
cip.init(Cipher.WRAP_MODE, passwdKey, paramSpec);
byte[] encryptKeyBys = cip.wrap(key);
System.out.println("加密后:");
showBys(encryptKeyBys);
cip.init(Cipher.UNWRAP_MODE, passwdKey, paramSpec);
key = cip.unwrap(encryptKeyBys, "DESede", Cipher.SECRET_KEY);
bys = key.getEncoded();
System.out.println("解密后:");
showBys(bys);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 对称密钥加/解密测试
*/
static void t1(){
try {
//SecureRandom初始化很慢,尽量使它只初始化一次
SecureRandom sr = new SecureRandom();
KeyGenerator keyGener = KeyGenerator.getInstance("DESede", "SunJCE");
keyGener.init(168);
Key key = keyGener.generateKey();
Cipher cip = Cipher.getInstance("DESede", "SunJCE");
cip.init(Cipher.ENCRYPT_MODE, key, sr);
byte[] srcBys = "TripleDES (分组密码)有时也被称为DESede,密钥长度168位(112位有效)".getBytes("UTF-8");
System.out.println("明文:");
showBys(srcBys);
//可以多次update()
// cip.update(srcBys);
//如果只有一个byte组,可以直接doFinal(srcBys);
byte[] cipBys = cip.doFinal(srcBys);
System.out.println("加密后:");
showBys(cipBys);
cip.init(Cipher.DECRYPT_MODE, key, sr);
// cip.update(cipBys);
byte[] bys = cip.doFinal(cipBys);
System.out.println("解密后:");
showBys(bys);
} catch (Exception e) {
e.printStackTrace();
}
}
static void showBys(byte[] bys){
for(byte b:bys){
System.out.print(b+" ");
}
System.out.println("\n");
}
}
基于口令的加密(PBE)
PBE使用一个口令作为密钥,这样是非常便利的,但口令加密一般不如TripleDES及Blowfish等二进制密钥安全。
口令加密使用的是杂凑加密和普通对称加密的组合。口令由消息摘要算法(如MD5和SHA)进行杂凑,杂凑的结果为对称算法构造一个数组密钥。
口令加密存的隐患之一是可能要创建一个预编译的口令列表,杂凑列表中的口令,产生一套密钥用于加密数据。
攻击者可以使用所有可能的密钥进行查找,并很快就能查到哪一个密钥是所用的加密密钥。
1.1 盐
在对数据杂凑前添加一些随机数,可以增加创建口令时可用的密钥数。
盐存在于加密的数据中,当一块新的数据被加密后,就会产生个新的盐。这意味着同样的短语加密后都会产生不同的值,甚至在口令相同的情况下也是如此。
解密时盐必顺从加密的数据中提取,并与口令组合产生解密密钥。
1.2 重复计算
重复计算是为了增加攻击者测试口令时间。
如果重复计算1000次,就需要杂凑口令字1000字,做1000次比计算一次更为复杂,攻击者要想攻击口令加密不得不花费1000倍或者更多的计算机资源。
1.3 常见的PBE算法
算法/密钥长度/默认密钥长度:
PBEWithMD5AndDES/56/56 (由于DES密钥太短,尽量不要用它)
PBEWithMD5AndTripleDES/112,168/168 (SunJCE有问题,需要下载新包)
PBEWithSHA1AndDESede/112,168/168
PBEWithSHA1AndRC2_40/40 to 1024/128
工作模式:CBC
填充方式:PKCS5Padding
PBE例:
package test1;
import java.security.Key;
import java.security.SecureRandom;
import java.util.Random;
import javax.crypto.Cipher;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.PBEParameterSpec;
/**
* PBE工具
* @author shanl
*
*/
public class PBETool {
/**加密*/
public static final int ENCRYPT_MODE = Cipher.ENCRYPT_MODE;
/**解密*/
public static final int DECRYPT_MODE = Cipher.DECRYPT_MODE;
static{
//SecureRandom初始化比较慢,所以最好不要多次初始化
sr = new SecureRandom();
}
protected static final SecureRandom sr;
protected String algorithm = "PBEWithMD5AndDES";
protected String mode = null;
protected String padding = null;
protected String provider = "SunJCE";
protected int iterations = 1000;
protected Key key = null;
protected byte[] salt = null;
private int processMode = -1;
/**
* 创建一个盐
*/
protected void initSalt(){
this.salt = new byte[8];
Random rand = new Random();
rand.nextBytes(this.salt);
}
/**
* 用密码创建Key
* @param passwd
* @throws Exception
*/
protected void initKey(char[] passwd)throws Exception{
PBEKeySpec keySpec = new PBEKeySpec(passwd);
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(algorithm,provider);
this.key = keyFactory.generateSecret(keySpec);
}
/**
* 创建新的盐完成初始化
* @param processMode 处理模式
* @param passwd 密码
*/
public void init(int processMode,char[] passwd){
try{
this.processMode = processMode;
initSalt();
initKey(passwd);
}catch(Exception ex){
throw new RuntimeException(ex);
}
}
/**
* 使用一个已存在的盐完成初始化
* @param processMode 处理模式
* @param passwd 密码
* @param salt 盐
*/
public void init(int processMode,char[] passwd, byte[] salt){
try{
try{
this.processMode = processMode;
setSalt(salt);
initKey(passwd);
}catch(Exception ex){
throw new RuntimeException(ex);
}
}catch(Exception ex){
throw new RuntimeException(ex);
}
}
/**
* 处理
* @param data
* @return
*/
public byte[] doProcess(byte[] data){
try{
String _algorithm = algorithm;
_algorithm = (null!=mode && null!=padding)? _algorithm+"/"+mode+"/"+padding: _algorithm;
PBEParameterSpec paramSpec = new PBEParameterSpec(salt, iterations);
Cipher cipher = Cipher.getInstance(_algorithm);
cipher.init(processMode, this.key, paramSpec, sr);
return cipher.doFinal(data);
}catch(Exception ex){
throw new RuntimeException(ex);
}
}
protected void setSalt(byte[] salt){
this.salt = salt;
}
public byte[] getSalt(){
return this.salt;
}
public void setAlgorithm(String algorithm) {
this.algorithm = algorithm;
}
public void setMode(String mode) {
this.mode = mode;
}
public void setPadding(String padding) {
this.padding = padding;
}
public void setProvider(String provider) {
this.provider = provider;
}
public void setIterations(int iterations) {
this.iterations = iterations;
}
}
测试类:
package test1;
public class PBEToolTest1 {
public static void main(String[] args){
// t3();
t2();
// t1();
}
/**
* 使用其他算
*/
static void t3(){
PBETool pbe = new PBETool();
char[] passwd = "123456@".toCharArray();
byte[] src = "abc123@#%".getBytes();
// pbe.setAlgorithm("PBEWithMD5AndDES");
// pbe.setAlgorithm("PBEWithMD5AndTripleDES"); //SunJCE的PBEWithMD5AndTripleDES有问题,需要下新包
pbe.setAlgorithm("PBEWithSHA1AndDESede"); //这个比PBEWithMD5AndDES更安全
// pbe.setAlgorithm("PBEWithSHA1AndRC2_40");
pbe.setProvider("SunJCE");
pbe.setMode("CBC");
// pbe.setPadding("PKCS5Padding");
showBys(src);
pbe.init(PBETool.ENCRYPT_MODE, passwd);
byte[] salt = pbe.getSalt();
byte[] bys = pbe.doProcess(src);
showBys(bys);
pbe.init(PBETool.DECRYPT_MODE, passwd, salt);
bys = pbe.doProcess(bys);
showBys(bys);
}
/**
* 多次加/解密测试
*/
static void t2(){
long l = System.currentTimeMillis();
//第一次执行 比较慢,后面很快
for(int i=0;i<10;i++){
t1();
}
System.out.println("耗时(毫秒):"+(System.currentTimeMillis()-l));
}
/**
* 加/解密测试
*/
static void t1(){
PBETool pbe = new PBETool();
char[] passwd = "123456@".toCharArray();
byte[] src = "abc123@#%".getBytes();
pbe.setMode("CBC");
// pbe.setPadding("PKCS5Padding");
showBys(src);
pbe.init(PBETool.ENCRYPT_MODE, passwd);
byte[] salt = pbe.getSalt();
byte[] bys = pbe.doProcess(src);
showBys(bys);
pbe.init(PBETool.DECRYPT_MODE, passwd, salt);
bys = pbe.doProcess(bys);
showBys(bys);
}
static void showBys(byte[] bys){
for(byte b:bys){
System.out.print(b+" ");
}
System.out.println("\n");
}
}
/**
* 加/解密用的口令
*/
static final char[] secretPasswd = ".Hj0*6%1$k".toCharArray();
static final byte[] secretSalt = {53,16,-2,92,-31,106,13,40};
static final int secretIterations = 13;
/**
* base64解密<br/>
* 需要jdk的rt.jar支持
* @param data
* @return
*/
public static
byte[] base64Decode(String data){
try {
BASE64Decoder b64Decoder = new BASE64Decoder();
return b64Decoder.decodeBuffer(data);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* base64方式加密<br/>
* 需要jdk的rt.jar支持
* @param data
* @return
*/
public static
String base64Encode(byte[] data){
try {
BASE64Encoder b64Encoder = new BASE64Encoder();
return b64Encoder.encode(data);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 对base64处理过的数据解密为原文
* @param data
* @return
*/
public static
String decrypt(String data){
String res = null;
try {
byte[] tmp = base64Decode(data);
byte[] dd = decrypt(tmp);
res = new String(dd, "UTF-8");
} catch (Exception e) {
e.printStackTrace();
}
return res;
}
/**
* 基于口令的解密方法
* @param data
* @return
*/
public static
byte[] decrypt(byte[] data){
byte[] res = null;
try{
PBEKeySpec keySpec = new PBEKeySpec(secretPasswd);
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBEWithSHA1AndDESede","SunJCE");
Key passwdKey = keyFactory.generateSecret(keySpec);
PBEParameterSpec paramSpec = new PBEParameterSpec(secretSalt, secretIterations);
Cipher cip = Cipher.getInstance("PBEWithSHA1AndDESede", "SunJCE");
cip.init(Cipher.DECRYPT_MODE, passwdKey, paramSpec);
//数据加密
res = cip.doFinal(data);
}catch(Exception ex){
ex.printStackTrace();
}
return res;
}
/**
* 对原文进行加密,加密的数据为base64格式.
* @param src
* @return
*/
public static
String encrypt(String src){
String res = null;
try {
byte[] ed = encrypt(src.getBytes("UTF-8"));
res = base64Encode(ed);
} catch (Exception e) {
e.printStackTrace();
}
return res;
}
/**
* 基于口令的加密方法
* @param src
* @return
*/
public static
byte[] encrypt(byte[] src){
byte[] res = null;
try{
PBEKeySpec keySpec = new PBEKeySpec(secretPasswd);
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBEWithSHA1AndDESede","SunJCE");
Key passwdKey = keyFactory.generateSecret(keySpec);
PBEParameterSpec paramSpec = new PBEParameterSpec(secretSalt, secretIterations);
Cipher cip = Cipher.getInstance("PBEWithSHA1AndDESede", "SunJCE");
cip.init(Cipher.ENCRYPT_MODE, passwdKey, paramSpec);
//数据加密
res = cip.doFinal(src);
}catch(Exception ex){
ex.printStackTrace();
}
return res;
}