参考:大型分布式网站架构设计与实践
一、数字摘要
数字摘要也称为消息摘要,它是一个唯一对应一个消息或文本的固定长度的值,它由一个单向Hash函数对消息进行计算而产生。如果消息在传递的途中改变了,接收者通过对收到的消息采用相同的Hash重新计算,新产生的摘要与原摘要进行比较,就可知道消息是否被篡改了,因此消息摘要能要验证消息的完整性。
消息摘要采用单向Hash函数,将需要计算的内容“摘要”成固定长度的串,这个串也称为数字指纹。这个串有固定的长度,且不同的明文摘要成密文,结果总是不同(相对),同样的明文摘要必定一致。
摘要生成过程如下图所示:
Hash碰撞:如果2个不同的内容但摘要缺一样,这种现象称为Hash碰撞。一个Hash函数的好坏是由发生碰撞的概率决定的,如果攻击者能够轻易地构造出两个具有相同Hash值的消息,那么这样Hash函数就比较危险。
消息摘要特点:
- 无论消息多长,计算出来的消息摘要长度是固定的。例如MD5算法计算出的摘要是128位,SHA-1算法计算出来的是160位
- 一般只要输入的消息的不同,对其进行摘要以后产生的摘要信息也不相同,但相同的输入必定产生相同的输出。(一个好消息摘要算法具备的性质:输入改变,输出也改变,两条相似的消息的摘要差别明显)
- 消息摘要并不包含原文的完整信息,因此只能进行正向的信息摘要,而无法从摘要中恢复出原来的消息,甚至根本就找不到与原信息相关的信息。(虽然可以通过穷举方式计算每一个可能消息的摘要,然后与已有摘要对比,从而恢复出摘要内容,但是会消耗巨大时间,几乎难以实现。ps太过简单的内容还是比较被试出来的,例如123456之类的)
1.1 MD5算法
MD5
(Message Digest Algorithm 5 - 信息摘要算法5)是数字摘要算法的一种实现,用来确保信息传输完整性和一致性,摘要长度为128位。
MD5由MD4、MD3、MD2改进而来,主要增强了算法复杂度和不可逆性。该算法因其普遍、稳定、快速的特点,广泛使用。
示例:java
public class Md5Util {
private static final String MD5 = "MD5";
/**
* MD5
*
* @param source
* @return
*/
public static byte[] md5(String source) {
try {
MessageDigest md = MessageDigest.getInstance(MD5);
return md.digest(getUTF8Bytes(source));
} catch (Exception e) {
// log.error("md5 error, source={}", source, e);
}
return null;
}
/**
* MD5字符串(十六进制)
*
* @return
*/
public static String md5Hex(String source) {
byte[] dataBytes = md5(source);
if (dataBytes == null || dataBytes.length == 0) {
return "";
}
return hex(dataBytes);
}
private static String hex(byte[] dataBytes) {
StringBuilder hexString = new StringBuilder();
for (byte dataByte : dataBytes) {
String hex = Integer.toHexString(0xff & dataByte);
if (hex.length() == 1) {
hexString.append('0');
}
hexString.append(hex);
}
return hexString.toString();
}
private static byte[] getUTF8Bytes(String input) {
return input.getBytes(StandardCharsets.UTF_8);
}
}
1.2 SHA
SHA
(Secure Hash Algorithm - 安全散列算法)由美国国家标准和技术协会(NIST)在1993年提出,并作为联邦信息处理标准(FIPS PUB 180)公布。
1995年又发布了一个修订版FIPS PUB 180-1,称为SHA-1
。SHA-1是基于MD4算法的,现被公认为最安全的安全散列算法之一。
示例:java
和MD5类似,只是获取了SHA-1算法的实例
public class SHAUtil {
private static final String SHA1 = "SHA-1";
/**
* SHA1
*
* @param source
* @return
*/
public static byte[] sha1(String source) {
try {
MessageDigest md = MessageDigest.getInstance(SHA1);
return md.digest(getUTF8Bytes(source));
} catch (Exception e) {
// log.error("SHA-1 error, source={}", source, e);
}
return null;
}
/**
* SHA1字符串(十六进制)
*
* @return
*/
public static String sha1Hex(String source) {
byte[] dataBytes = sha1(source);
if (dataBytes == null || dataBytes.length == 0) {
return "";
}
return hex(dataBytes);
}
private static String hex(byte[] dataBytes) {
StringBuilder hexString = new StringBuilder();
for (byte dataByte : dataBytes) {
String hex = Integer.toHexString(0xff & dataByte);
if (hex.length() == 1) {
hexString.append('0');
}
hexString.append(hex);
}
return hexString.toString();
}
private static byte[] getUTF8Bytes(String input) {
return input.getBytes(StandardCharsets.UTF_8);
}
}
二、编码
2.1 十六进制编码
计算机的计算采用的是二进制的数据表示方法,十六进制也是数据的一种表示方法,并且可以和二进制进行相互转化,每4位二进制数据位对应一位十六进制数据。
示例:java
/**
* byte数组->十六进制
*
* @param dataBytes
* @return
*/
public static String hex(byte[] dataBytes) {
StringBuilder hexString = new StringBuilder();
for (byte dataByte : dataBytes) {
String hex = Integer.toHexString(0xff & dataByte);
if (hex.length() == 1) {
hexString.append('0');
}
hexString.append(hex);
}
return hexString.toString();
}
2.2 Base64编码
Base64是一种基于64个可打印字符来表示二进制数据的方法,由于2^6=64,所以每6个为一个单元,对应某个可打印字符,三个字节有24位,对应4个Base64单元,即3个字节需要用4个可打印字符来表示。
在Base64中的可打印字符包括A-Z、a-z、0-9共62个字符,另外2个可打印符号在不同系统而不同。
很多人认为Base64是一种加密算法,用来当做加密算法来使用,实际上它只是一种编码算法。
任何人得到Base64编码内容,就可以通过固定的方法逆向得到之前的信息。
Base64编码可以将一组二进制信息编码成可打印字符,在网络上传输与展示。
示例:java
// JDK1.8起自带的工具类
// Base64编码
String java.uitl.Base64.getEncoder().encodeToString(String data);
// Base64解码
byte[] java.uitl.Base64.getDecoder().decode(String base64Str);
三、对称加密算法
对称加密算法,数据发送方将明文(原始数据)和加密密钥一起经过特殊加密算法处理后,生成复杂的加密密文进行发送,数据接收方收到密文后,若想读取原文,则需要使用加密使用的密钥及相同的算法的逆算法对加密的密文进行解密,才能使其恢复明文。
在对称加密算法中,使用的密钥只有一个,发送和接收方都必须使用这个密钥对数据进行加密和解密。
对称加密过程如下图所示:
特点:
- 优点:算法公开、计算量小、加密速度快、加密效率高,使用长密钥时难以破解
- 缺点:对称加密算法安全性依赖于密钥,泄露密钥意味着任何人都可以解密密文,所以对于密钥的保护尤其重要
常用的对称加密算法包括DES算法
、3DES算法
、AES算法
等。
3.1 DES算法
DES(Data Encryptin Standard - 数据加密标准),属于对称加密算法,明文按64位进行分组,密钥64位,实际上只有56位参与DES运算(第8 16 24 32 40 48 56 64位是校验位,使得每个密钥都有奇数个1),分组后的明文和56位的密钥按位替换或交换形成密文。
由于计算机运算能力的增强,原版DES密码的密钥长度变得容易被暴力破解,因此演变出了3DES算法。3DES是DES向AES过渡的加密算法,它使用3条56位的密钥对数据进行3次加密,是DES的一个更安全的变形。
示例:java
public class DesUtil {
private static final String DES = "DES";
public static void main(String[] args) {
// 生成56位des密钥
String key = genKey();
byte[] keyBytes = Base64.getDecoder().decode(key);
String data = "哈哈哈";
String encrytData = encrypt(data, keyBytes);
String decryptData = decrypt(encrytData, keyBytes);
// data=哈哈哈, key=euyzNwcV2tk=, encrytData=ZJ+uMoI4/ZlNy9Ev13TWxw==, decryptData=哈哈哈
System.out.println(String.format("data=%s, key=%s, encrytData=%s, decryptData=%s", data, key, encrytData, decryptData));
}
/**
* 生成DES密钥
*
* @return 64位编码密钥
*/
public static String genKey() {
try {
KeyGenerator keyGen = KeyGenerator.getInstance(DES);
keyGen.init(56);
SecretKey key = keyGen.generateKey();
return Base64.getEncoder().encodeToString(key.getEncoded());
} catch (Exception e) {
// log.error("DES generate kry error", e);
}
return "";
}
/**
* 加密
*
* @param data
* @return
*/
public static String encrypt(String data, byte[] keyBytes) {
try {
// DES算法要求有一个可信任的随机数源
SecureRandom random = new SecureRandom();
DESKeySpec desKey = new DESKeySpec(keyBytes);
// 创建一个密匙工厂,然后用它把DESKeySpec转换成
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(DES);
SecretKey key = keyFactory.generateSecret(desKey);
//Cipher对象实际完成加密操作
Cipher cipher = Cipher.getInstance(DES);
// 用密匙初始化Cipher对象
cipher.init(Cipher.ENCRYPT_MODE, key, random);
// 执行加密操作
byte[] encryptBytes = cipher.doFinal(getUTF8Bytes(data));
return Base64.getEncoder().encodeToString(encryptBytes);
} catch (Exception e) {
// log.error("DES encrypt error, data={}", data, e);
}
return "";
}
/**
* 解密
*
* @param data base64编码后的密文
* @return
*/
public static String decrypt(String data, byte[] keyBytes) {
try {
// DES算法要求有一个可信任的随机数源
SecureRandom random = new SecureRandom();
// 创建一个DESKeySpec对象
DESKeySpec desKey = new DESKeySpec(keyBytes);
// 创建一个密匙工厂
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(DES);
// 将DESKeySpec对象转换成SecretKey对象
SecretKey securekey = keyFactory.generateSecret(desKey);
// Cipher对象实际完成解密操作
Cipher cipher = Cipher.getInstance(DES);
// 用密匙初始化Cipher对象
cipher.init(Cipher.DECRYPT_MODE, securekey, random);
// 执行解密操作
byte[] decodeBytes = Base64.getDecoder().decode(data);
byte[] decryptBytes = cipher.doFinal(decodeBytes);
return bytes2String(decryptBytes);
} catch (Exception e) {
// log.error("DES decrypt error, data={}", data, e);
}
return "";
}
private static byte[] getUTF8Bytes(String input) {
return input.getBytes(StandardCharsets.UTF_8);
}
private static String bytes2String(byte[] bytes) {
return new String(bytes, StandardCharsets.UTF_8);
}
}
3.2 AES算法
AES(Advanced Encryption Standard - 高级加密标准),对称加密标准,用来替代原先的DES算法
。
AES算法作为新一代的数据加密标准,汇聚了强安全性、高性能、高效率、易用和灵活等优点,设计有三个密钥长度(128、192、256位),比DES算法的加密强度更高,更安全。
示例:(基于java)
- 当密钥长度是192、256位时,在服务器上运行会提示
java.security.InvalidKeyException:illegal Key Size
,以下几种解决方案- 下载JCE无限制权限策略文件
- 使用
commons-crypto
包下相关工具类 - 自定义classloader加载放开限制的jar或者通过反射移除限制(没试过)
方式一:下载JCE无限制权限策略文件
- JDK8的下载地址:
http://download.oracle.com/otn-pub/java/jce/8/jce_policy-8.zip
- 下载后解压,可以看到local_policy.jar、US_export_policy.jar和READEME.txt
- 如果安装了JRE,将两个jar文件放到%JRE_HOME%\lib\security目录下覆盖原来的文件。
- 如果安装了JDK,还要将两个jar文件也放到%JDK_HOME%\jre\lib\security目录下覆盖原来文件
代码如下:
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Base64;
/**
* AES工具类
*
* @author xxx
* @date 2020/10/29
*/
public class AesUtil {
public static void main(String[] args) {
// 生成256位AES密钥(AES算法支持 128/192/256 位三种密钥长度)
String key = genKey();
byte[] keyBytes = Base64.getDecoder().decode(key);
String data = "哈哈哈";
String encrytData = encrypt(data, keyBytes);
String decryptData = decrypt(encrytData, keyBytes);
// data=哈哈哈, key=eLosu+LlMVeWqWQN5+R9DqC5N+71wsw1j7+aRntlyvU=, encrytData=C0RU/D4WujkRrYCjS+skKg==, decryptData=哈哈哈
System.out.println(String.format("data=%s, key=%s, encrytData=%s, decryptData=%s", data, key, encrytData, decryptData));
}
/**
* 生成AES密钥
*
* @return 256位编码密钥
*/
public static String genKey() {
try {
KeyGenerator keyGen = KeyGenerator.getInstance(AES);
keyGen.init(256);
SecretKey key = keyGen.generateKey();
return Base64.getEncoder().encodeToString(key.getEncoded());
} catch (Exception e) {
log.error("AES generate kry error", e);
}
return "";
}
/**
* 加密
*
* @param data
* @return
*/
public static String encrypt(String data, byte[] keyBytes) {
try {
// 实例化加密类,参数为加密方式
Cipher cipher = Cipher.getInstance(TRANSFORM);
// 初始化,此方法可以采用三种方式,按加密算法要求来添加。
// (1)无第三个参数
// (2)第三个参数为SecureRandom random = new SecureRandom();中random对象,随机数。(AES不可采用这种方法)
// (3)采用此代码中的IVParameterSpec
// 加密时使用:ENCRYPT_MODE 解密时使用:DECRYPT_MODE
// CBC类型的可以在第三个参数传递偏移量iv,ECB没有偏移量
SecretKeySpec key = new SecretKeySpec(keyBytes, AES);
IvParameterSpec iv = new IvParameterSpec(getIvBytes(keyBytes));
cipher.init(Cipher.ENCRYPT_MODE, key, iv);
// 加密操作,返回加密后的字节数组,然后需要编码
// 主要编解码方式有Base64, HEX, UUE,7bit等
byte[] encryptedData = cipher.doFinal(getUTF8Bytes(data));
return Base64.getEncoder().encodeToString(encryptedData);
} catch (Exception e) {
log.error("AES encrypt error, data={}", data, e);
}
return "";
}
/**
* 解密
*
* @param data
* @return
*/
public static String decrypt(String data, byte[] keyBytes) {
try {
Cipher cipher = Cipher.getInstance(TRANSFORM);
// 与加密时不同MODE:Cipher.DECRYPT_MODE
// CBC类型的可以在第三个参数传递偏移量iv,ECB没有偏移量
SecretKeySpec key = new SecretKeySpec(keyBytes, AES);
IvParameterSpec iv = new IvParameterSpec(getIvBytes(keyBytes));
cipher.init(Cipher.DECRYPT_MODE, key, iv);
byte[] dataBytes = Base64.getDecoder().decode(data);
byte[] decryptedData = cipher.doFinal(dataBytes);
return bytes2String(decryptedData);
} catch (Exception e) {
log.error("AES decrypt error, data={}", data, e);
}
return "";
}
/**
* 获取偏移量 AES:16位 DES:8位
*
* @return 16位偏移量
*/
private static byte[] getIvBytes(byte[] keyBytes) {
return Arrays.copyOf(keyBytes, 16);
}
private static byte[] getUTF8Bytes(String input) {
return input.getBytes(StandardCharsets.UTF_8);
}
private static String bytes2String(byte[] bytes) {
return new String(bytes, StandardCharsets.UTF_8);
}
}
方式二:使用commons-crypto包方式
pom.xml引入commons-crypto
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-crypto</artifactId>
<version>1.1.0</version>
</dependency>
示例:
import org.apache.commons.crypto.cipher.CryptoCipher;
import org.apache.commons.crypto.utils.Utils;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Base64;
import java.util.Properties;
public class AesUtil {
/**
* 加密
* <p>密钥超过128位,也可正常使用
*
* @param data
* @return
*/
public static String encrypt(String data, byte[] keyBytes) {
try {
// Initializes the cipher with ENCRYPT_MODE,key and iv.
// 初始化,此方法可以采用三种方式,按加密算法要求来添加。
// (1)无第三个参数
// (2)第三个参数为SecureRandom random = new SecureRandom();中random对象,随机数。(AES不可采用这种方法)
// (3)采用此代码中的IVParameterSpec
// 加密时使用:ENCRYPT_MODE 解密时使用:DECRYPT_MODE
// CBC类型的可以在第三个参数传递偏移量iv,ECB没有偏移量
SecretKeySpec key = new SecretKeySpec(keyBytes, AES);
IvParameterSpec iv = new IvParameterSpec(getIvBytes(keyBytes));
// Creates a CryptoCipher instance with the transformation and properties.
CryptoCipher encipher = Utils.getCipherInstance(TRANSFORM, new Properties());
encipher.init(Cipher.ENCRYPT_MODE, key, iv);
byte[] dataBytes = getUTF8Bytes(data);
int bufferSize = dataBytes.length;
ByteBuffer inBuffer = ByteBuffer.allocateDirect(bufferSize);
ByteBuffer outBuffer = ByteBuffer.allocateDirect(bufferSize + 16);
inBuffer.put(dataBytes);
// ready for the cipher to read it
inBuffer.flip();
// Continues a multiple-part encryption/decryption operation for byte buffer.
int updateBytes = encipher.update(inBuffer, outBuffer);
// We should call do final at the end of encryption/decryption.
int finalBytes = encipher.doFinal(inBuffer, outBuffer);
// ready for use as decrypt
outBuffer.flip();
byte[] encoded = new byte[updateBytes + finalBytes];
outBuffer.duplicate().get(encoded);
return Base64.getEncoder().encodeToString(encoded);
} catch (Exception e) {
log.error("AES encrypt error, data={}", data, e);
}
return "";
}
/**
* 解密
* <p>密钥超过128位,也可正常使用
*
* @param data
* @return
*/
public static String decrypt(String data, byte[] keyBytes) {
try {
// Creates a CryptoCipher instance with the transformation and properties.
CryptoCipher decipher = Utils.getCipherInstance(TRANSFORM, new Properties());
SecretKeySpec key = new SecretKeySpec(keyBytes, AES);
IvParameterSpec iv = new IvParameterSpec(getIvBytes(keyBytes));
decipher.init(Cipher.DECRYPT_MODE, key, iv);
// 使用BASE64对密文进行解码
byte[] dataBytes = Base64.getDecoder().decode(data);
// 预留点byte
final int bufferSize = dataBytes.length;
ByteBuffer inBuffer = ByteBuffer.allocateDirect(bufferSize);
ByteBuffer outBuffer = ByteBuffer.allocateDirect(bufferSize + 16);
inBuffer.put(dataBytes);
inBuffer.flip();
decipher.update(inBuffer, outBuffer);
decipher.doFinal(inBuffer, outBuffer);
// ready for use
outBuffer.flip();
final ByteBuffer copy = outBuffer.duplicate();
final byte[] original = new byte[copy.remaining()];
copy.get(original);
return bytes2String(original);
} catch (Exception e) {
log.error("AES decrypt error, data={}", data, e);
}
return "";
}
/**
* 获取偏移量 AES:16位 DES:8位
*
* @return 16位偏移量
*/
private static byte[] getIvBytes(byte[] keyBytes) {
return Arrays.copyOf(keyBytes, 16);
}
private static byte[] getUTF8Bytes(String input) {
return input.getBytes(StandardCharsets.UTF_8);
}
private static String bytes2String(byte[] bytes) {
return new String(bytes, StandardCharsets.UTF_8);
}
}
四、非对称加密算法
非对称加密算法又称为公开密钥加密算法,它需要2个密钥,一个为公开密钥(public key)即公钥;另一个称为私有密钥(private key)即私钥。
公钥与私钥需要配对使用,如果用公钥对数据进行加密,只有用对应的私钥才能解密,而如果使用私钥对数据进行加密,那么只有用对应的公钥才能解密。因为加密和解密使用的是2个不同的密钥,所以这种算法称为非对称加密算法。
非对称加密算法实现信息交换的基本过程是:
- 甲方生成一对密钥并将其中的一把作为公钥向其他人公开
- 得到该公钥的乙方使用该密钥对信息进行加密后再发送给甲方
- 甲方使用自己保存的私钥对加密信息进行解密
非对称加密特点:
- 优点:
- 非对称加密算法包含2中密钥,其中一个公开,这样就不需要像对称加密那样需要传输密钥给对方进行数据加密,大大提高了加密算法的安全性
- 非对称加密算法能够保证,即使在获知公钥、加密算法和加密算法源代码的情况下,也无法获得公钥对应的私钥,因此也无法对公钥加密的密文进行解密
- 缺点:
- 由于非对称机密算法的复杂性,使得其加密解密速度远没有对称加密解密的速度快
为了解决加密解密速度问题,广泛使用对称与非对称加密算法结合使用的办法,优缺点互补,达到时间与安全的平衡:
- 对称加密算法加密速度快,用来加密较长的文件
- 非对称加密算法用来给文件密钥加密,解决了对称加密算法的密钥分发问题
当前使用最广泛的非对称加密算法RSA。
4.1 RSA算法
RSA非对称加密算法是目前最有影响力的非对称加密算法,它能够抵抗到目前为止已知的所有密码攻击,已被ISO推荐为公钥数据加密标准。
RSA算法基于一个十分简单的数论事实:将两个大素数相乘十分容易,但反过来想要对其乘积进行因式分解极其困难,因此可以将乘积公开作为加密密钥。
示例:基于java
(1)生成公钥、私钥对
- keySize最小值512,否则报错:
java.security.InvalidParameterException: RSA keys must be at least 512 bits long
- 假设使用512初始化KeyPairGenerator,RSA加密后的密文长度为512位(即64字节),此时明文最大长度不能超过53字节,超过53字节需要使用更高位的keySize,否则报错:
javax.crypto.IllegalBlockSizeException: Data must not be longer than 53 bytes
。超过53字节则需要使用更高位的keysize,例如1024,此时明文长度不能超过117字节;所以明文越长,需要使用的keysize越大 - RSA的keySize位数越高,其产生密钥对及加密、解密的速度越慢,这是基于大素数非对称加密算法的缺陷
javax.crypto.IllegalBlockSizeException: Data must not be longer than 53 bytes
报错的解决方法:
- 方式一:由于明文长度不确定,所以设置固定的keysize还是可能会报错,可以参考
calKeySize()
方法动态计算(这种方式只是在本地跑了下数据,没有在生产环境验证过),我的想法就是keysize越大,那么能加密的明文长度就越长… - 方式二:搜了下往上的博客,大多是分组加密,也可以参考下
private static final String RSA = "RSA";
/**
* 生成公钥、私钥密钥对
*
* @param keySize
* @return 密钥对
*/
public static Map<String, String> genKeyPair(int keySize) {
Map<String, String> keyMap = new HashMap<>();
try {
// KeyPairGenerator类用于生成公钥和私钥对,基于RSA算法生成对象
KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance(RSA);
// 初始化密钥对生成器,keySize最小值512
keyPairGen.initialize(keySize, new SecureRandom());
// 生成一个密钥对,分别对公钥、私钥进行Base64编码
KeyPair keyPair = keyPairGen.generateKeyPair();
PublicKey publicKey = keyPair.getPublic();
PrivateKey privateKey = keyPair.getPrivate();
String publicKeyString = Base64.getEncoder().encodeToString(publicKey.getEncoded());
String privateKeyString = Base64.getEncoder().encodeToString((privateKey.getEncoded()));
keyMap.put("publicKey", publicKeyString);
keyMap.put("privateKey", privateKeyString);
} catch (Exception e) {
// log.error("RSA generate keyPair error, keySize={}", keySize, e);
}
return keyMap;
}
/**
* 计算keysize
*
* @param keySize
* @return 密钥对
*/
private static int calKeySize(int dataLength) {
int keySize = (dataLength + 11) * 8;
return Math.max(keySize, 512);
}
(2)公钥加密
- 先对公钥Base64解码
- 公钥要先转换成X509EncodedKeySpec对象,然后通过KeyFactory生成PublicKey对象
/**
* 公钥加密
*
* @param data 加密字符串
* @param publicKey 公钥
* @return 密文
* @throws Exception 加密过程中的异常信息
*/
public static String encrypt(String data, String publicKey) {
try {
// base64编码的公钥
byte[] decoded = Base64.getDecoder().decode(publicKey);
KeyFactory keyFactory = KeyFactory.getInstance(RSA);
PublicKey pubKey = keyFactory.generatePublic(new X509EncodedKeySpec(decoded));
// RSA加密
Cipher cipher = Cipher.getInstance(RSA);
cipher.init(Cipher.ENCRYPT_MODE, pubKey);
byte[] encryptBytes = cipher.doFinal(getUTF8Bytes(data));
return Base64.getEncoder().encodeToString(encryptBytes);
} catch (Exception e) {
// log.error("RSA encrypt error, data={}, publicKey={}", data, publicKey, e);
}
return "";
}
private static byte[] getUTF8Bytes(String input) {
return input.getBytes(StandardCharsets.UTF_8);
}
(3)私钥解密
- 先对私钥Base64解码
- 私钥要先转换成PKCS8EncodedKeySpec对象,然后通过KeyFactory生成PrivateKey对象
/**
* RSA私钥解密
*
* @param data 加密字符串
* @param privateKey 私钥
* @return 明文
*/
public static String decrypt(String data, String privateKey) {
try {
// 64位解码加密后的字符串
byte[] inputByte = Base64.getDecoder().decode(getUTF8Bytes(data));
// base64编码的私钥
byte[] decoded = Base64.getDecoder().decode(privateKey);
KeyFactory keyFactory = KeyFactory.getInstance(RSA);
PrivateKey priKey = keyFactory.generatePrivate(new PKCS8EncodedKeySpec(decoded));
// RSA解密
Cipher cipher = Cipher.getInstance(RSA);
cipher.init(Cipher.DECRYPT_MODE, priKey);
byte[] decryptBytes = cipher.doFinal(inputByte);
return new String(decryptBytes, StandardCharsets.UTF_8);
} catch (Exception e) {
// log.error("RSA decrypt error, data={}, privateKey={}", data, privateKey, e);
}
return "";
}
五、数字签名
签名认证是对非对称加密技术与数字摘要技术的综合运用,指的是
- 将通信内容的摘要信息使用发送者的私钥进行加密,然后将密文与原文一起传输给信息的接收者
- 接收者通过发送者的公钥解密被加密的摘要信息,然后使用与发送者相同的摘要算法,对接收到的内容采用相同的方式产生摘要串,与解密的摘要串进行对比
- 如果相同,则说明接收到的内容是完整的,在传输过程中没有受到第三方篡改
- 否则说明通信内容已被第三方修改
数字签名的生成:
数字签名的校验:
区别于不同的摘要算法,不同的非对称加密方式,数字签名算法也不同。常见的数字签名算法包括MD5withRSA
、SHA1withRSA
等
5.1 MD5withRSA
MD5withRSA算法表示采用MD5算法生成需要发送正文的数字摘要,并使用RSA算法来对正文进行加密和解密。
示例:
import javax.crypto.Cipher;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.MessageDigest;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.Map;
public class SignUtil {
private static final String MD5 = "MD5";
private static final String RSA = "RSA";
public static void main(String[] args) {
String content = "哈哈哈";
Map<String, String> keyMap = RsaUtil.genKeyPair(512);
String publicKey = keyMap.get("publicKey");
String privateKey = keyMap.get("privateKey");
String signByMD5 = signByMD5WithRSA(content, privateKey);
System.out.println("verify md5 with rsa, result=" + verifyByMD5WithRSA(content, signByMD5, publicKey));
}
/**
* 生成签名(MD5+RSA)
*
* @param content 内容
* @param privateKey BASE64编码的私钥
* @return BASE64编码后的签名
*/
public static String signByMD5WithRSA(String content, String privateKey) {
try {
MessageDigest md = MessageDigest.getInstance(MD5);
md.reset();
byte[] md5Bytes = md.digest(getUTF8Bytes(content));
// base64编码的私钥
byte[] decoded = Base64.getDecoder().decode(privateKey);
KeyFactory keyFactory = KeyFactory.getInstance(RSA);
PrivateKey priKey = keyFactory.generatePrivate(new PKCS8EncodedKeySpec(decoded));
Cipher cipher = Cipher.getInstance(RSA);
cipher.init(Cipher.ENCRYPT_MODE, priKey);
return Base64.getEncoder().encodeToString(cipher.doFinal(md5Bytes));
} catch (Exception e) {
log.error("sign by MD5 with RSA error, content={}", content, e);
}
return null;
}
/**
* 签名校验(MD5+RSA)
*
* @param content 内容
* @param sign BASE64编码后的签名
* @param publicKey BASE64编码的公钥
* @return 校验结果
*/
public static boolean verifyByMD5WithRSA(String content, String sign, String publicKey) {
try {
MessageDigest md = MessageDigest.getInstance(MD5);
md.reset();
byte[] md5Bytes = md.digest(getUTF8Bytes(content));
// base64编码的公钥
byte[] decoded = Base64.getDecoder().decode(publicKey);
KeyFactory keyFactory = KeyFactory.getInstance(RSA);
PublicKey pubKey = keyFactory.generatePublic(new X509EncodedKeySpec(decoded));
Cipher cipher = Cipher.getInstance(RSA);
cipher.init(Cipher.DECRYPT_MODE, pubKey);
byte[] signBytes = Base64.getDecoder().decode(sign);
byte[] decryptBytes = cipher.doFinal(signBytes);
return Base64.getEncoder().encodeToString(md5Bytes).equals(Base64.getEncoder().encodeToString(decryptBytes));
} catch (Exception e) {
log.error("sign by MD5 with RSA verify error, content={}, sign={}", content, sign, e);
}
return false;
}
private static byte[] getUTF8Bytes(String input) {
return input.getBytes(StandardCharsets.UTF_8);
}
}
5.2 SHA1withRSA
与MD5withRSA类似,SHA1withRSA表示采用SHA-1算法生成正文的数字摘要,并且使用RSA算法来对摘要进行加密和解密。
import javax.crypto.Cipher;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.MessageDigest;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.Map;
public class SignUtil {
private static final String SHA1 = "SHA1";
private static final String RSA = "RSA";
public static void main(String[] args) {
String content = "哈哈哈";
Map<String, String> keyMap = RsaUtil.genKeyPair(512);
String publicKey = keyMap.get("publicKey");
String privateKey = keyMap.get("privateKey");
String signBySHA1 = signBySHA1WithRSA(content, privateKey);
System.out.println("verify md5 with rsa, result=" + verifyBySHA1WithRSA(content, signBySHA1, publicKey));
}
/**
* 生成签名(SHA1+RSA)
*
* @param content 内容
* @param privateKey BASE64编码的私钥
* @return BASE64编码后的签名
*/
public static String signBySHA1WithRSA(String content, String privateKey) {
try {
MessageDigest md = MessageDigest.getInstance(SHA1);
md.reset();
byte[] sha1Bytes = md.digest(getUTF8Bytes(content));
// base64编码的私钥
byte[] decoded = Base64.getDecoder().decode(privateKey);
KeyFactory keyFactory = KeyFactory.getInstance(RSA);
PrivateKey priKey = keyFactory.generatePrivate(new PKCS8EncodedKeySpec(decoded));
Cipher cipher = Cipher.getInstance(RSA);
cipher.init(Cipher.ENCRYPT_MODE, priKey);
return Base64.getEncoder().encodeToString(cipher.doFinal(sha1Bytes));
} catch (Exception e) {
log.error("sign by SHA1 with RSA error, content={}", content, e);
}
return null;
}
/**
* 签名校验(SHA1+RSA)
*
* @param content 内容
* @param sign BASE64编码后的签名
* @param publicKey BASE64编码的公钥
* @return 校验结果
*/
public static boolean verifyBySHA1WithRSA(String content, String sign, String publicKey) {
try {
MessageDigest md = MessageDigest.getInstance(SHA1);
md.reset();
byte[] sha1Bytes = md.digest(getUTF8Bytes(content));
// base64编码的公钥
byte[] decoded = Base64.getDecoder().decode(publicKey);
KeyFactory keyFactory = KeyFactory.getInstance(RSA);
PublicKey pubKey = keyFactory.generatePublic(new X509EncodedKeySpec(decoded));
Cipher cipher = Cipher.getInstance(RSA);
cipher.init(Cipher.DECRYPT_MODE, pubKey);
byte[] signBytes = Base64.getDecoder().decode(sign);
byte[] decryptBytes = cipher.doFinal(signBytes);
return Base64.getEncoder().encodeToString(sha1Bytes).equals(Base64.getEncoder().encodeToString(decryptBytes));
} catch (Exception e) {
log.error("sign by SHA1 with RSA verify error, content={}, sign={}", content, sign, e);
}
return false;
}
private static byte[] getUTF8Bytes(String input) {
return input.getBytes(StandardCharsets.UTF_8);
}
}
六、数字证书
数字证书(Digital Certificate)也称为电子证书,类似于日常生活中的身份证,也是另一种形式的身份认证,用于标识网络中的用户身份。数字证书集合多种密码学的加密算法,证书自带有公钥信息,可以完成相应的加密、解密操作,同时,还拥有自身信息的数字签名,可以鉴别证书的颁发机构,以及证书内容的完整性。由于证书本身含有用户的认证信息,因此可以作为用户身份识别的依据。
通常数字证书会包含以下内容:
- 对象的名称(人、服务器、组织):证书所代表的用户,可以是人、服务器、组织等
- 证书的过期时间:用来确定证书是否仍然有效
- 证书的颁发机构(谁为证书担保):为证书的真实性与有效性做担保,确保证书所携带的信息是经过校验的
- 证书颁发机构对证书信息的数字签名:数字签名用于鉴别证书的颁发机构,以及证书内容是否完整,颁发机构用私钥对证书进行签名,而校验方使用颁发机构的公钥来解密签名,与其用摘要算法生成的摘要进行比较,便可以验证证书是否由该机构所颁发,信息是否完整
- 签名算法
- 对象的公钥:对信息进行加密,信息传输到接收方,接收方将使用公钥对应的私钥进行解密
6.1
(1)X.509
不同的数字证书所包含的内容信息和格式可能不相同,因此,需要一种格式标准来规范数字证书的存储和校验。大多数的数字证书都以一种标准的格式(即X.509)来存储它们的信息。
X.509提供了一种标准的方式,将证书信息规范地存储到一系列可解析的字段当中,X.509 V3是X.509标准目前使用最为广泛的版本。
(2)证书签发
网络中的数字证书需要通过数字证书认证机构(Certificate Authority - CA)来进行颁发,只有经过CA颁发的数字证书在网络中才具备可认证性。
数字证书的签发过程实际上就是对数字证书的内容,包括证书所代表对象的公钥进行数字签名,而验证证书的过程,实际上就是校验证书的数字签名,包含了对证书有效期的验证。证书签名生成如下图所示:
VeriSign、GeoTrust和Thawte是公认的国际权威数字证书认证机构,使用最广泛的是VeriSign签发的电子商务用数字证书。这些机构都会收取昂贵的费用,例如Carcert就是一个免费的数字证书颁发国际组织。
(3)证书校验
客户端接收到数字证书时,首先会检查证书的认证机构,如果该认证机构是权威的证书认证机构,则通过该机构的根证书获得证书颁发者的公钥,通过该公钥,对证书的数字签名进行校验,并验证证书的有效期是否过期。
根证书是证书认证机构给自己颁发的数字证书,是证书信任链的起点,安装根证书则意味着对这个证书认证机构的信任。用户在使用数字证书之前必须安装颁发该证书的根证书。
大多数操作系统都会预装一些权威的证书认证机构的根证书,如VeriSign、GeoTrust等。如果证书是非权威认证机构的,用户需要自行下载安装该证书认证机构的根证书,并且保证该根证书的合法性。因为一旦安装该机构的证书,表示信任该根证书所颁发的所有证书。(可以去自己电脑里看下受信任根证书,Win、Mac等)
证书校验过程如下图所示:
(4)证书管理
任何机构或个人都可以申请数字证书,并使用数字证书对网络通信进行保护。要获得数字证书,首先需要使用数字证书管理工具,如keytool
、OpenSSL
等,然后构建CSR(Certificate Signing Request - 数字证书签发申请),提交给数字证书认证机构进行签名,最终形成数字证书。
keytool是Java的数字证书管理工具,用于数字证书的生成、导入、导出与撤销等操作。它与本地密钥库关联,并可以对本地密钥库进行管理,可以将私钥存放于密钥库中,而公钥则使用数字证书进行输出。(相关命令百度一下吧)
OpenSSL包含一个开源SSL的实现,其实现的功能远远超出了SSL协议本身。主要提供了密码算法、常用的密钥和证书封装管理功能及SSL协议,并提供了丰富的应用程序供测试或其他使用目的。这里可以用来进行密钥和证书的管理。(安装及使用百度一下吧)