加密与安全
编码算法
-
URL编码:
-
URL编码总是大写
-
将URL中的数据进行编码
-
URL编码规则
如果字符是A~Z,a~z,0~9以及-、_、.、*,则保持不变; 如果是其他字符,先转换为UTF-8编码,然后对每个字节以%XX表示。
-
-
Base64:
-
原理是把3字节的二进制数据按6bit一组,用4个int整数表示,然后查表,把int整数用索引对应到字符,得到编码后的字符串。
-
Base64编码可以把任意长度的二进制数据变为纯文本,且只包含
A
`Z`、`a`z
、0
~9
、+
、/
、=
这些字符 -
因为是6bit一组,则一组最大数就是63
6位整数的范围总是0~63,所以,能用64个字符表示:字符A~Z对应索引0~25,字符a~z对应索引26~51,字符0~9对应索引52~61,最后两个索引62、63分别用字符+和/表示 这样就可以把任意二进制的数据编码成纯文本的形式
-
哈希算法
常用的哈希算法有:
算法 | 输出长度(位) | 输出长度(字节) |
---|---|---|
MD5 | 128 bits | 16 bytes |
SHA-1 | 160 bits | 20 bytes |
RipeMD-160 | 160 bits | 20 bytes |
SHA-256 | 256 bits | 32 bytes |
SHA-512 | 512 bits | 64 bytes |
根据碰撞概率,哈希算法的输出长度越长,就越难产生碰撞,也就越安全
哈希碰撞是指,两个不同的输入得到了相同的输出
public class Main {
public static void main(String[] args) throws Exception {
// 创建一个MessageDigest实例:
MessageDigest md = MessageDigest.getInstance("MD5");
// 反复调用update输入数据:
md.update("Hello".getBytes("UTF-8"));
md.update("World".getBytes("UTF-8"));
byte[] result = md.digest(); // 16 bytes: 68e109f0f40ca72a15e05cc22786f8e6
System.out.println(new BigInteger(1, result).toString(16));
}
}
//使用MessageDigest时,我们首先根据哈希算法获取一个MessageDigest实例,然后,反复调用update(byte[])输入数据。当输入结束后,调用digest()方法获得byte[]数组表示的摘要,最后,把它转换为十六进制的字符串
-
用途:
- 验证下载的文件是否被篡改,下载前后的hash值相同,说明没有被篡改
- 存储用户的口令(密码)
-
如果有个彩虹表,即口令和MD5对照表,有可能对简单的密码进行暴力破解
-
防止彩虹表攻击的方法是增加salt,然后再进行MD5加密
使用第三方库的Hash加密
-
需要用BouncyCastle进行注册
public class Main { public static void main(String[] args) throws Exception { // 注册BouncyCastle: Security.addProvider(new BouncyCastleProvider()); // 按名称正常调用: MessageDigest md = MessageDigest.getInstance("RipeMD160"); md.update("HelloWorld".getBytes("UTF-8")); byte[] result = md.digest(); System.out.println(new BigInteger(1, result).toString(16)); } }
-
BouncyCastle就是一个提供了很多哈希算法和加密算法的第三方库。它提供了Java标准库没有的一些算法,例如,RipeMD160哈希算法
Hmac算法
-
通过加salt,使得hash算法能够抵御彩虹表攻击,但是一般情况下用户输入的口令(salt)也是不安全的,因此通过Java标准库的KeyGenerator生成一个安全的随机的key,而不是自己输入口令,这就出现了Hmac算法
HmacMD5 ≈ md5(secure_random_key, input)
public class Hash { public static void main(String[] args) { testHmac("bobo"); testHmac("bobo"); testHmac("hello123"); testHmac("hello123"); } public static void testHmac(String pwd){ String input = pwd; try { String enc = Hash.hash2(input); System.out.println(enc); } catch (InvalidKeyException e) { e.printStackTrace(); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } } //缓存存储secretkey private static Map<String, byte[]> store = new HashMap<>(); //返回消息摘要 public static String hash2(String source) throws InvalidKeyException, UnsupportedEncodingException, NoSuchAlgorithmException { SecretKey key = new SecretKeySpec(getSecretyKey(source), "HmacMD5"); Mac mac = Mac.getInstance("HmacMD5"); mac.init(key); mac.update(source.getBytes("UTF-8")); byte[] result = mac.doFinal(); return (new BigInteger(1, result).toString(16)); } //获取对应secretKey,如不存在随机生成并放入缓存。 private static byte[] getSecretyKey(String id) throws NoSuchAlgorithmException { byte[] skey = store.get(id); if(skey == null){ KeyGenerator keyGen = KeyGenerator.getInstance("HmacMD5"); SecretKey key = keyGen.generateKey(); skey = key.getEncoded(); store.put(id, skey); } return skey; } }
和MD5相比,使用HmacMD5的步骤是:
- 通过名称
HmacMD5
获取KeyGenerator
实例; - 通过
KeyGenerator
创建一个SecretKey
实例; - 通过名称
HmacMD5
获取Mac
实例; - 用
SecretKey
初始化Mac
实例; - 对
Mac
实例反复调用update(byte[])
输入数据; - 调用
Mac
实例的doFinal()
获取最终的哈希值。
- 通过名称
对称加密算法AES
算法 | 密钥长度 | 工作模式 | 填充模式 |
---|---|---|---|
DES | 56/64 | ECB/CBC/PCBC/CTR/… | NoPadding/PKCS5Padding/… |
AES | 128/192/256 | ECB/CBC/PCBC/CTR/… | NoPadding/PKCS5Padding/PKCS7Padding/… |
IDEA | 128 | ECB | PKCS5Padding/PKCS7Padding/… |
-
DES算法由于密钥过短,可以在短时间内被暴力破解,所以现在已经不安全了
-
AES算法是目前应用最广泛的加密算法
-
加密解密过程,可以简单的看成如下过程
从程序的角度看,所谓加密,就是这样一个函数,它接收密码和明文,然后输出密文: secret = encrypt(key, message); 而解密则相反,它接收密码和密文,然后输出明文: plain = decrypt(key, secret);
ECB模式是最简单的AES加密模式,它只需要一个固定长度的密钥,固定的明文会生成固定的密文,这种一对一的加密方式会导致安全性降低,更好的方式是通过CBC模式,它需要一个随机数作为IV参数,这样对于同一份明文,每次生成的密文都不同:
public class Main {
public static void main(String[] args) throws Exception {
// 原文:
String message = "Hello, world!";
System.out.println("Message: " + message);
// 256位密钥 = 32 bytes Key:
byte[] key = "1234567890abcdef1234567890abcdef".getBytes("UTF-8");
// 加密:
byte[] data = message.getBytes("UTF-8");
byte[] encrypted = encrypt(key, data);
System.out.println("Encrypted: " + Base64.getEncoder().encodeToString(encrypted));
// 解密:
byte[] decrypted = decrypt(key, encrypted);
System.out.println("Decrypted: " + new String(decrypted, "UTF-8"));
}
// 加密:
public static byte[] encrypt(byte[] key, byte[] input) throws GeneralSecurityException {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
// CBC模式需要生成一个16 bytes的initialization vector:
SecureRandom sr = SecureRandom.getInstanceStrong();
byte[] iv = sr.generateSeed(16);
IvParameterSpec ivps = new IvParameterSpec(iv);
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivps);
byte[] data = cipher.doFinal(input);
// IV不需要保密,把IV和密文一起返回:
return join(iv, data);
}
// 解密:
public static byte[] decrypt(byte[] key, byte[] input) throws GeneralSecurityException {
// 把input分割成IV和密文:
byte[] iv = new byte[16];
byte[] data = new byte[input.length - 16];
System.arraycopy(input, 0, iv, 0, 16);
System.arraycopy(input, 16, data, 0, data.length);
// 解密:
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
IvParameterSpec ivps = new IvParameterSpec(iv);
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivps);
return cipher.doFinal(data);
}
public static byte[] join(byte[] bs1, byte[] bs2) {
byte[] r = new byte[bs1.length + bs2.length];
System.arraycopy(bs1, 0, r, 0, bs1.length);
System.arraycopy(bs2, 0, r, bs1.length, bs2.length);
return r;
}
}
注意:上述口令,都是固定的256位
口令加密PBE
PBE
-
实际上用户输入的口令并不能直接作为AES的密钥进行加密(除非长度恰好是128/192/256位),并且用户输入的口令一般都有规律,安全性远远不如安全随机数产生的随机口令。因此,用户输入的口令,通常还需要使用PBE算法,采用随机数杂凑计算出真正的密钥,再进行加密:
PBE就是Password Based Encryption的缩写,它的作用如下:
key = generate(userPassword, secureRandomPassword);
-
用法:
//PBE的作用就是把用户输入的口令和一个安全随机的口令采用杂凑后计算出真正的密钥。以AES密钥为例,我们让用户输入一个口令,然后生成一个随机数,通过PBE算法计算出真正的AES口令,再进行加密,代码如下 public class Main { public static void main(String[] args) throws Exception { // 把BouncyCastle作为Provider添加到java.security: Security.addProvider(new BouncyCastleProvider()); // 原文: String message = "Hello, world!"; // 加密口令:不需要特定的位数 String password = "hello12345"; // 16 bytes随机Salt: byte[] salt = SecureRandom.getInstanceStrong().generateSeed(16); System.out.printf("salt: %032x\n", new BigInteger(1, salt)); // 加密: byte[] data = message.getBytes("UTF-8"); byte[] encrypted = encrypt(password, salt, data); System.out.println("encrypted: " + Base64.getEncoder().encodeToString(encrypted)); // 解密: byte[] decrypted = decrypt(password, salt, encrypted); System.out.println("decrypted: " + new String(decrypted, "UTF-8")); } // 加密: public static byte[] encrypt(String password, byte[] salt, byte[] input) throws GeneralSecurityException { PBEKeySpec keySpec = new PBEKeySpec(password.toCharArray()); SecretKeyFactory skeyFactory = SecretKeyFactory.getInstance("PBEwithSHA1and128bitAES-CBC-BC"); SecretKey skey = skeyFactory.generateSecret(keySpec); PBEParameterSpec pbeps = new PBEParameterSpec(salt, 1000); Cipher cipher = Cipher.getInstance("PBEwithSHA1and128bitAES-CBC-BC"); cipher.init(Cipher.ENCRYPT_MODE, skey, pbeps); return cipher.doFinal(input); } // 解密: public static byte[] decrypt(String password, byte[] salt, byte[] input) throws GeneralSecurityException { PBEKeySpec keySpec = new PBEKeySpec(password.toCharArray()); SecretKeyFactory skeyFactory = SecretKeyFactory.getInstance("PBEwithSHA1and128bitAES-CBC-BC"); SecretKey skey = skeyFactory.generateSecret(keySpec); PBEParameterSpec pbeps = new PBEParameterSpec(salt, 1000); Cipher cipher = Cipher.getInstance("PBEwithSHA1and128bitAES-CBC-BC"); cipher.init(Cipher.DECRYPT_MODE, skey, pbeps); return cipher.doFinal(input); } } //使用PBE时,我们还需要引入BouncyCastle,并指定算法是PBEwithSHA1and128bitAES-CBC-BC。观察代码,实际上真正的AES密钥是调用Cipher的init()方法时同时传入SecretKey和PBEParameterSpec实现的。在创建PBEParameterSpec的时候,我们还指定了循环次数1000,循环次数越多,暴力破解需要的计算量就越大
密钥交换算法DH
**现实问题:**假设一方通过AES加密之后,将密文发送给对方时,对方需要密钥才能解密密文。现在问题来了:如何传递密钥?
要解决这个问题,密钥交换算法即DH算法:Diffie-Hellman算法应运而生。
DH算法解决了密钥在双方不直接传递密钥的情况下完成密钥交换,这个神奇的交换原理完全由数学理论支持
双方都有一个自己随机产生的私钥,同时计算得出一个公钥,而公钥可以进行随意的传播,然后对方根据另一方的公钥和自己的私钥就可以得到真正的密钥,同时这个密钥双方计算出来都是一样的,即被双方协商出来的密钥。
拓展:
甲首选选择一个素数p,例如509,底数g,任选,例如5,随机数a,例如123,然后计算A=g^a mod p,结果是215,然后,甲发送p=509,g=5,A=215给乙;
乙方收到后,也选择一个随机数b,例如,456,然后计算B=g^b mod p,结果是181,乙再同时计算s=A^b mod p,结果是121;
乙把计算的B=181发给甲,甲计算s=B^a mod p的余数,计算结果与乙算出的结果一样,都是121。
DH算法的本质就是双方各自生成自己的私钥和公钥,私钥仅对自己可见,然后交换公钥,并根据自己的私钥和对方的公钥,生成最终的密钥secretKey
,DH算法通过数学定律保证了双方各自计算出的secretKey
是相同的。
public class Main {
public static void main(String[] args) {
// Bob和Alice:
Person bob = new Person("Bob");
Person alice = new Person("Alice");
// 各自生成KeyPair:
bob.generateKeyPair();
alice.generateKeyPair();
// 双方交换各自的PublicKey:
// Bob根据Alice的PublicKey生成自己的本地密钥:
bob.generateSecretKey(alice.publicKey.getEncoded());
// Alice根据Bob的PublicKey生成自己的本地密钥:
alice.generateSecretKey(bob.publicKey.getEncoded());
// 检查双方的本地密钥是否相同:
bob.printKeys();
alice.printKeys();
// 双方的SecretKey相同,后续通信将使用SecretKey作为密钥进行AES加解密...
}
}
class Person {
public final String name;
public PublicKey publicKey;
private PrivateKey privateKey;
private byte[] secretKey;
public Person(String name) {
this.name = name;
}
// 生成本地KeyPair:
public void generateKeyPair() {
try {
KeyPairGenerator kpGen = KeyPairGenerator.getInstance("DH");
kpGen.initialize(512);
KeyPair kp = kpGen.generateKeyPair();
this.privateKey = kp.getPrivate();
this.publicKey = kp.getPublic();
} catch (GeneralSecurityException e) {
throw new RuntimeException(e);
}
}
public void generateSecretKey(byte[] receivedPubKeyBytes) {
try {
// 从byte[]恢复PublicKey:
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(receivedPubKeyBytes);
KeyFactory kf = KeyFactory.getInstance("DH");
PublicKey receivedPublicKey = kf.generatePublic(keySpec);
// 生成本地密钥:
KeyAgreement keyAgreement = KeyAgreement.getInstance("DH");
keyAgreement.init(this.privateKey); // 自己的PrivateKey
keyAgreement.doPhase(receivedPublicKey, true); // 对方的PublicKey
// 生成SecretKey密钥:
this.secretKey = keyAgreement.generateSecret();
} catch (GeneralSecurityException e) {
throw new RuntimeException(e);
}
}
public void printKeys() {
System.out.printf("Name: %s\n", this.name);
System.out.printf("Private key: %x\n", new BigInteger(1, this.privateKey.getEncoded()));
System.out.printf("Public key: %x\n", new BigInteger(1, this.publicKey.getEncoded()));
System.out.printf("Secret key: %x\n", new BigInteger(1, this.secretKey));
}
}
非对称加密 RSA
非对称加密就是加密和解密使用的不是相同的密钥:只有同一个公钥-私钥对才能正常加解密(而对称加密是加密解密必须是相同的密钥否则无法解密,从而出现了密钥交换算法)
解释:
因此,如果小明要加密一个文件发送给小红,他应该首先向小红索取她的公钥,然后,他用小红的公钥加密,把加密文件发送给小红,此文件只能由小红的私钥解开,因为小红的私钥在她自己手里,所以,除了小红,没有任何人能解开此文件
因此,加密的时候要知道对方的公钥
非对称加密相比对称加密的显著优点在于,对称加密需要协商密钥,而非对称加密可以安全地公开各自的公钥,在N个人之间通信的时候:使用非对称加密只需要N个密钥对(知道对方的公钥就能加密,而只有对方的私钥才能解密),每个人只管理自己的密钥对。而使用对称加密需要则需要N*(N-1)/2个密钥(需要双方协商密钥,因此相当于多边形的对角线加所有的边数),因此每个人需要管理N-1个密钥,密钥管理难度大,而且非常容易泄漏
-
非对称加密不需要协商密钥
-
非对称加密算法的时候,对于一个公钥-私钥对,通常是用公钥加密,私钥解密
-
既然非对称加密这么好,那我们抛弃对称加密,完全使用非对称加密行不行?也不行。因为非对称加密的缺点就是运算速度非常慢,比对称加密要慢很多。
-
通常情况对称加密和非对称加密一起使用,用非对称加密来加密对称加密使用的密钥。
所以,在实际应用的时候,非对称加密总是和对称加密一起使用。假设小明需要给小红需要传输加密文件,他俩首先交换了各自的公钥,然后: 小明生成一个随机的AES口令,然后用小红的公钥通过RSA加密这个口令,并发给小红; 小红用自己的RSA私钥解密得到AES口令; 双方使用这个共享的AES口令用AES加密通信 可见非对称加密实际上应用在第一步,即加密“AES口令”。这也是我们在浏览器中常用的HTTPS协议的做法,即浏览器和服务器先通过RSA交换AES口令,接下来双方通信实际上采用的是速度较快的AES对称加密,而不是缓慢的RSA非对称加密。
public class Main { public static void main(String[] args) throws Exception { // 明文: byte[] plain = "Hello, encrypt use RSA".getBytes("UTF-8"); // 创建公钥/私钥对: Person alice = new Person("Alice"); // 用Alice的公钥加密: byte[] pk = alice.getPublicKey(); System.out.println(String.format("public key: %x", new BigInteger(1, pk))); byte[] encrypted = alice.encrypt(plain); System.out.println(String.format("encrypted: %x", new BigInteger(1, encrypted))); // 用Alice的私钥解密: byte[] sk = alice.getPrivateKey(); System.out.println(String.format("private key: %x", new BigInteger(1, sk))); byte[] decrypted = alice.decrypt(encrypted); System.out.println(new String(decrypted, "UTF-8")); } } class Person { String name; // 私钥: PrivateKey sk; // 公钥: PublicKey pk; public Person(String name) throws GeneralSecurityException { this.name = name; // 生成公钥/私钥对: KeyPairGenerator kpGen = KeyPairGenerator.getInstance("RSA"); kpGen.initialize(1024); KeyPair kp = kpGen.generateKeyPair(); this.sk = kp.getPrivate(); this.pk = kp.getPublic(); } // 把私钥导出为字节 public byte[] getPrivateKey() { return this.sk.getEncoded(); } // 把公钥导出为字节 public byte[] getPublicKey() { return this.pk.getEncoded(); } // 用公钥加密: public byte[] encrypt(byte[] message) throws GeneralSecurityException { Cipher cipher = Cipher.getInstance("RSA"); cipher.init(Cipher.ENCRYPT_MODE, this.pk); return cipher.doFinal(message); } // 用私钥解密: public byte[] decrypt(byte[] input) throws GeneralSecurityException { Cipher cipher = Cipher.getInstance("RSA"); cipher.init(Cipher.DECRYPT_MODE, this.sk); return cipher.doFinal(input); } }
签名算法
引出签名算法:
- 我们使用非对称加密算法的时候,对于一个公钥-私钥对,通常是用公钥加密,私钥解密
- 如果使用私钥加密,公钥解密是否可行呢?实际上是完全可行的
用途:
不过我们再仔细想一想,私钥是保密的,而公钥是公开的,用私钥加密,那相当于所有人都可以用公钥解密。这个加密有什么意义?
这个加密的意义在于,如果小明用自己的私钥加密了一条消息,比如小明喜欢小红,然后他公开了加密消息,由于任何人都可以用小明的公钥解密,从而使得任何人都可以确认小明喜欢小红这条消息肯定是小明发出的,其他人不能伪造这个消息,小明也不能抵赖这条消息不是自己写的
因此,私钥加密得到的密文实际上就是数字签名,要验证这个签名是否正确,只能用私钥持有者的公钥进行解密验证。使用数字签名的目的是为了确认某个信息确实是由某个发送方发送的,任何人都不可能伪造消息,并且,发送方也不能抵赖
在实际应用的时候,签名实际上并不是针对原始消息,而是针对原始消息的哈希进行签名,即:
signature = encrypt(privateKey, sha256(message))
对签名进行验证实际上就是用公钥解密:
hash = decrypt(publicKey, signature)
常用数字签名算法有:
- MD5withRSA
- SHA1withRSA
- SHA256withRSA
public class Main {
public static void main(String[] args) throws GeneralSecurityException {
// 生成RSA公钥/私钥:
KeyPairGenerator kpGen = KeyPairGenerator.getInstance("RSA");
kpGen.initialize(1024);
KeyPair kp = kpGen.generateKeyPair();
PrivateKey sk = kp.getPrivate();
PublicKey pk = kp.getPublic();
// 待签名的消息:
byte[] message = "Hello, I am Bob!".getBytes(StandardCharsets.UTF_8);
// 用私钥签名:
Signature s = Signature.getInstance("SHA1withRSA");
s.initSign(sk);
s.update(message);
byte[] signed = s.sign();
System.out.println(String.format("signature: %x", new BigInteger(1, signed)));
// 用公钥验证:
Signature v = Signature.getInstance("SHA1withRSA");
v.initVerify(pk);
v.update(message);
boolean valid = v.verify(signed);
System.out.println("valid? " + valid);
}
}
数字证书
我们知道,摘要算法用来确保数据没有被篡改,非对称加密算法可以对数据进行加解密,签名算法可以确保数据完整性和抗否认性,把这些算法集合到一起,并搞一套完善的标准,这就是数字证书。
因此,数字证书就是集合了多种密码学算法,用于实现数据加解密、身份认证、签名等多种功能的一种安全标准。
数字证书可以防止中间人攻击,因为它采用链式签名认证,即通过根证书(Root CA)去签名下一级证书,这样层层签名,直到最终的用户证书。而Root CA证书内置于操作系统中,所以,任何经过CA认证的数字证书都可以对其本身进行校验,确保证书本身不是伪造的