数据签名、加密是前后端开发经常需要使用到的技术,应用场景包括不限于用户登入、数据交易、信息通讯等,不同的应用场景也会需要使用到不同的签名加密算法,或者需要搭配不一样的 签名加密算法来达到业务目标。常用的加密算法有:
- 对称加密算法;
- 非对称加密算法;
- 哈希算法,加盐哈希算法(单向加密);
- 数字签名。
使用加密签名算法,可以达到下面的安全目标:
- 保密性:防止用户的数据被读取;
- 数据完整性:防止数据被篡改;
- 身份验证:确保数据发自特定的一方。
对称加密#
对称加密算法加密和解密时使用同一把秘钥。操作比较简单,加密速度快,秘钥简单。经常在消息发送方需要加密大量数据时使用。缺点是风险都在这个秘钥上面,一旦被窃取,信息会暴露。所以安全级别不够高。常用对称加密算法有DES,3DES,AES等。在jdk中也都有封装。
DES#
DES的秘钥为8个字节,64个bit位。(不适应当今分布式开放网络对数据加密安全性的要求)在Java进行DES、3DES和AES三种对称加密算法时,常采用的是NoPadding(不填充)、Zeros填充(0填充)、PKCS5Padding填充。
一个DES的列子:
import javax.crypto.Cipher; import javax.crypto.SecretKey; import javax.crypto.SecretKeyFactory; import javax.crypto.spec.DESKeySpec; import java.security.Key; import java.security.SecureRandom; import java.util.Base64; public class DESUtil { //算法名称 public static final String KEY_ALGORITHM = "DES"; //算法名称/加密模式/填充方式 //DES共有四种工作模式-->> ECB:电子密码本模式、 CBC:加密分组链接模式、CFB:加密反馈模式、OFB:输出反馈模式 //在Java进行DES、3DES和AES三种对称加密算法时,常采用的是NoPadding(不填充)、Zeros填充(0填充)、PKCS5Padding填充。 //不同的工作模式下,初始化Cipher的代码不一样 public static final String CIPHER_ALGORITHM = "DES/ECB/NoPadding"; private static SecretKey keyGenerator(String hexKeyStr) throws Exception { byte input[] = hexString2Bytes(hexKeyStr); DESKeySpec desKey = new DESKeySpec(input); SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(KEY_ALGORITHM); SecretKey securekey = keyFactory.generateSecret(desKey); return securekey; } private static int parse(char c) { if (c >= 'a') { return (c - 'a' + 10) & 0x0f; } if (c >= 'A') { return (c - 'A' + 10) & 0x0f; } return (c - '0') & 0x0f; } //从十六进制字符串到字节数组转换 public static byte[] hexString2Bytes(String hexstr) { byte[] b = new byte[hexstr.length() / 2]; int j = 0; for (int i = 0; i < b.length; i++) { char c0 = hexstr.charAt(j++); char c1 = hexstr.charAt(j++); b[i] = (byte) ((parse(c0) << 4) | parse(c1)); } return b; } /** * 加密数据 * @param data 待加密数据 * @param hexKeyStr 密钥 * @return 加密后的数据 */ public static String encrypt(String data, String hexKeyStr) throws Exception { Key deskey = keyGenerator(hexKeyStr); // 实例化Cipher对象,它用于完成实际的加密操作 Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM); SecureRandom random = new SecureRandom(); // 初始化Cipher对象,设置为加密模式 cipher.init(Cipher.ENCRYPT_MODE, deskey, random); byte[] results = cipher.doFinal(data.getBytes()); // 执行加密操作。加密后的结果通常都会用Base64编码进行传输 return Base64.getEncoder().encodeToString(results); } /** * 解密数据 * @param data 待解密数据 * @param hexKeyStr 密钥 * @return 解密后的数据 */ public static String decrypt(String data, String hexKeyStr) throws Exception { Key deskey = keyGenerator(hexKeyStr); Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM); //初始化Cipher对象,设置为解密模式 cipher.init(Cipher.DECRYPT_MODE, deskey); // 执行解密操作 return new String(cipher.doFinal(Base64.getDecoder().decode(data))); } //Des秘钥采用64bit为,8个字节,如果使用字符串则太短,一般使用16进制的字符串(长度16位,大于16为会截取前16位) public static void main(String[] args) throws Exception { //模拟3DES的过程 String source = "amigoxie"; System.out.println("原文: " + source); String key = "A1B2C3D4E5F60708"; String ke2 = "A1B2C3D4E5F60709"; String ke3 = "A1B2C3D4E5F6070A"; String encryptData1 = encrypt(source, key); String encryptData2 = decrypt(encryptData1,ke2); String encryptData3 = encrypt(encryptData2,ke3); System.out.println("加密后: " + encryptData3); String dencryptData1 = decrypt(encryptData3,ke3); String dencryptData2 = encrypt(dencryptData1,ke2); String dencryptData3 = decrypt(dencryptData2,key); System.out.println("解密后: " + dencryptData3); //String decryptData = decrypt(encryptData, key); //System.out.println("解密后: " + decryptData); } }
3DES#
3DES(或称为Triple DES)是三重数据加密算法(TDEA,Triple Data Encryption Algorithm)块密码的通称。它相当于是对每个数据块应用三次DES加密算法。由于计算机运算能力的增强,原版DES密码的密钥长度变得容易被暴力破解;3DES即是设计用来提供一种相对简单的方法,即通过增加DES的密钥长度来避免类似的攻击,而不是设计一种全新的块密码算法。
其具体实现如下:设Ek()和Dk()代表DES算法的加密和解密过程,K代表DES算法使用的密钥,P代表明文,C代表密文,这样:
3DES加密过程为:C=Ek3(Dk2(Ek1(P)))
3DES解密过程为:P=Dk1(EK2(Dk3(C)))
import javax.crypto.Cipher; import javax.crypto.SecretKey; import javax.crypto.SecretKeyFactory; import javax.crypto.spec.DESedeKeySpec; import java.security.Key; import java.util.Base64; public class ThreeDESUtil { // 算法名称 public static final String KEY_ALGORITHM = "DESEDE"; // 算法名称/加密模式/填充方式 public static final String CIPHER_ALGORITHM = "DESEDE/ECB/NoPadding"; private static SecretKey keyGenerator(String hexKeyStr) throws Exception { byte input[] = HexString2Bytes(hexKeyStr); DESedeKeySpec keySpec = new DESedeKeySpec(input); SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(KEY_ALGORITHM); SecretKey secretKey = keyFactory.generateSecret(keySpec); return secretKey; } private static int parse(char c) { if (c >= 'a') { return (c - 'a' + 10) & 0x0f; } if (c >= 'A') { return (c - 'A' + 10) & 0x0f; } return (c - '0') & 0x0f; } // 从十六进制字符串到字节数组转换 public static byte[] HexString2Bytes(String hexstr) { byte[] b = new byte[hexstr.length() / 2]; int j = 0; for (int i = 0; i < b.length; i++) { char c0 = hexstr.charAt(j++); char c1 = hexstr.charAt(j++); b[i] = (byte) ((parse(c0) << 4) | parse(c1)); } return b; } public static String encrypt(String data, String hexKeyStr) throws Exception { Key deskey = keyGenerator(hexKeyStr); Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM); cipher.init(Cipher.ENCRYPT_MODE, deskey); byte[] results = cipher.doFinal(data.getBytes()); return Base64.getEncoder().encodeToString(results); } public static String decrypt(String data, String hexKeyStr) throws Exception { Key deskey = keyGenerator(hexKeyStr); Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM); cipher.init(Cipher.DECRYPT_MODE, deskey); return new String(cipher.doFinal(Base64.getDecoder().decode(data))); } public static void main(String[] args) throws Exception { String source = "amigoxie"; System.out.println("原文: " + source); String key = "AAAAC3D4E5F60708A1B2C3D4E5F60709A1B2C3D4E5F6070A"; String encryptData1 = encrypt(source, key); System.out.println("加密后: " + encryptData1); String dencryptData1 = decrypt(encryptData1,key); System.out.println("解密后: " + dencryptData1); } }
AES#
高级加密标准(英语:Advanced Encryption Standard,缩写:AES),是一种区块加密标准。这个标准用来替代原先的DES,已经被多方分析且广为全世界所使用。
那么为什么原来的DES会被取代呢?原因就在于其使用56位密钥,比较容易被破解。而AES可以使用128bit位、192、和256位密钥,并且用128位分组加密和解密数据,相对来说安全很多。完善的加密算法在理论上是无法破解的,除非使用穷尽法。
import javax.crypto.Cipher; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import java.io.UnsupportedEncodingException; import java.nio.charset.Charset; import java.util.Base64; public class AESUtil { private static final String UTF8 = "UTF-8"; private static final String AES = "AES"; private static final String AES_CBC_PKCS5_PADDING = "AES/CBC/PKCS5Padding"; private static final String AES_CBC_NO_PADDING = "AES/CBC/NoPadding"; /** * JDK只支持AES-128加密,也就是密钥长度必须是128bit; * 参数为密钥key,key的长度小于16字符时用"0"补充, * key长度大于16字符时截取前16位。 * <p> * 要实现256的需要依赖其他Jar包 **/ private static SecretKeySpec create128BitsKey(String key) { if (key == null) { key = ""; } byte[] data = null; StringBuffer buffer = new StringBuffer(16); buffer.append(key); //小于16后面补0 while (buffer.length() < 16) { buffer.append("0"); } //大于16,截取前16个字符 if (buffer.length() > 16) { buffer.setLength(16); } try { data = buffer.toString().getBytes(UTF8); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } return new SecretKeySpec(data, AES); } /** * 创建128位的偏移量,iv的长度小于16时后面补0, * 大于16,截取前16个字符; * * @param iv * @return */ private static IvParameterSpec create128BitsIV(String iv) { if (iv == null) { iv = ""; } byte[] data = null; StringBuffer buffer = new StringBuffer(16); buffer.append(iv); while (buffer.length() < 16) { buffer.append("0"); } if (buffer.length() > 16) { buffer.setLength(16); } try { data = buffer.toString().getBytes(UTF8); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } return new IvParameterSpec(data); } /** * 填充方式为Pkcs5Padding时,最后一个块需要填充χ个字节,填充的值就是χ,也就是填充内容由JDK确定 * * @param srcContent * @param password * @param iv * @return */ public static String aesCbcPkcs5PaddingEncrypt(String srcContent, String password, String iv) throws Exception { SecretKeySpec key = create128BitsKey(password); IvParameterSpec ivParameterSpec = create128BitsIV(iv); Cipher cipher = Cipher.getInstance(AES_CBC_PKCS5_PADDING); cipher.init(Cipher.ENCRYPT_MODE, key, ivParameterSpec); byte[] data = srcContent.getBytes(Charset.forName("UTF8")); byte[] encryptedContent = cipher.doFinal(data); //为了打印出来的字符串没有乱码,进行base64编码 return Base64.getEncoder().encodeToString(encryptedContent); } public static String aesCbcPkcs5PaddingDecrypt(String encryptedContent, String password, String iv) throws Exception { SecretKeySpec key = create128BitsKey(password); IvParameterSpec ivParameterSpec = create128BitsIV(iv); Cipher cipher = Cipher.getInstance(AES_CBC_PKCS5_PADDING); cipher.init(Cipher.DECRYPT_MODE, key, ivParameterSpec); byte[] data = Base64.getDecoder().decode(encryptedContent); byte[] decryptedContent = cipher.doFinal(data); return new String(decryptedContent,"UTF8"); } /** * 填充方式为NoPadding时,最后一个块的填充内容由程序员确定,通常为0. * AES/CBC/NoPadding加密的明文长度必须是16的整数倍,明文长度不满足16时,程序员要扩充到16的整数倍 * * @param sSrc * @param aesKey * @param aesIV * @return */ public static byte[] aesCbcNoPaddingEncrypt(byte[] sSrc, String aesKey, String aesIV) throws Exception { //加密的数据长度不是16的整数倍时,原始数据后面补0,直到长度满足16的整数倍 int len = sSrc.length; //计算补0后的长度 while (len % 16 != 0) { len++; } byte[] result = new byte[len]; //在最后补0 for (int i = 0; i < len; ++i) { if (i < sSrc.length) { result[i] = sSrc[i]; } else { //填充字符'a' //result[i] = 'a'; result[i] = 0; } } SecretKeySpec skeySpec = create128BitsKey(aesKey); //使用CBC模式,需要一个初始向量iv,可增加加密算法的强度 IvParameterSpec iv = create128BitsIV(aesIV); Cipher cipher = Cipher.getInstance(AES_CBC_NO_PADDING); cipher.init(Cipher.ENCRYPT_MODE, skeySpec, iv); byte[] encrypted = null; encrypted = cipher.doFinal(result); return encrypted; } public static byte[] aesCbcNoPaddingDecrypt(byte[] sSrc, String aesKey, String aesIV) throws Exception { SecretKeySpec skeySpec = create128BitsKey(aesKey); IvParameterSpec iv = create128BitsIV(aesIV); Cipher cipher = Cipher.getInstance(AES_CBC_NO_PADDING); cipher.init(Cipher.DECRYPT_MODE, skeySpec, iv); byte[] decryptContent = cipher.doFinal(sSrc); return decryptContent; } public static void main(String[] args) throws Exception { String source = "amigoxie"; System.out.println("原文: " + source); String aeskey = "1234567890987654"; String aesIV = "1234567890987654"; String encryptData1 = aesCbcPkcs5PaddingEncrypt(source, aeskey, aesIV); System.out.println("加密后:" + encryptData1); String dencryptData1 = aesCbcPkcs5PaddingDecrypt(encryptData1, aeskey, aesIV); System.out.println("解密后:" + new String(dencryptData1)); } }
非对称加密#
非对称加密,顾名思义就是加密与解密的过程不是对称的,不是用的同一个秘钥。非对称加密有个公私钥对的概念,也就是有两把秘钥,一把是公钥,一把是私钥,一对公私钥有固定的生成方法,在加密的时候,用公钥去加密,接收方再用对应的私钥去解密。使用时可以由接收方生成公私钥对,然后将公钥传给加密方,这样私钥不会在网络中传输,没有被窃取的风险。比如github底层的ssh协议就是公私钥非对称加密。并且公钥是可以由私钥推导出来的,反过来却不行,由通过公钥无法推导出私钥。常用算法有RSA,DSA,ECC等。ECC也是比特币底层用的比较多的算法。通过和对称加密的对比,可以看到,非对称加密解决了秘钥传输中的安全问题。
RSA加密算法#
RSA
加密算法是目前最有影响力的公钥加密算法,并且被普遍认为是目前最优秀的公钥方案 之一。RSA 是第一个能同时用于加密和数字签名的算法,它能够抵抗到目前为止已知的所有密码攻击,已被ISO推荐为公钥数据加密标准。
相同长度的秘钥,RSA和DSA的安全性差不多。一般情况下DSA多用于数字签名,签名的效率比RSA更高。RSA支持加密和加签操作。所以当我们需要同时进行加密和加签操作的时候一般选择RSA算法。
这边提供一个在线生成RSA公私钥对的网站,可以选择生成512,1024,2048或者是4096位的秘钥。使用起来比较方便。
下面给出一个RSA算法的列子:
import javax.crypto.Cipher; import java.security.*; import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.X509EncodedKeySpec; import java.util.Base64; import java.util.HashMap; import java.util.Map; public class RSAUtil { // 数字签名,密钥算法 private static final String RSA_KEY_ALGORITHM = "RSA"; // 数字签名的 签名/验证算法 private static final String SIGNATURE_ALGORITHM = "MD5withRSA"; /** * 密钥长度,DH算法的默认密钥长度是1024 * 密钥长度必须是64的倍数,在512到65536位之间 */ private static final int KEY_SIZE = 1024; private static final String PUBLIC_KEY = "publicKey"; private static final String PRIVATE_KEY = "privateKey"; private static final String publicKey ="MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCnS5ms1FnL7lrEOmo2zctcmZz4YF690gTs2CTY9yKWfMIoXEuf2i3SZ9wKMMsmLB+aQJhp3IvOU6SEEvkLkzFQMsioBCo7emD2Jgh44zDKCTM5lGV5qN3SmhS1pta4cByXquBUKi51SQIhj01H25rh0p/MKeCLRtAEBIi0tVxQjwIDAQAB"; private static final String privateKey = "MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAKdLmazUWcvuWsQ6ajbNy1yZnPhgXr3SBOzYJNj3IpZ8wihcS5/aLdJn3AowyyYsH5pAmGnci85TpIQS+QuTMVAyyKgEKjt6YPYmCHjjMMoJMzmUZXmo3dKaFLWm1rhwHJeq4FQqLnVJAiGPTUfbmuHSn8wp4ItG0AQEiLS1XFCPAgMBAAECgYAjxqALfLHjUYqpkhBqveGyYMtXtkwsbcBN4f8nQtprsixXMz2c5qyL5VgB+eNHu4Ham9u8L8TaD7sL337Qwd52fVsdrDhpn5mdCWXrDdkABYbctM60YWUO83xCX3FztXszDTMESBw7h6BvuKqsQyh/vrTPtYGl75FQMBO1VqIX8QJBAPQ3+QukfAm4Eo9KFLHX/PQ4A2SYpDA0vwe01hrnOXbZExRcSIqOB0Ph7BpDA0LWgxd3Tvfm6ALOD31nuVHKgNkCQQCvXaW406yNergGHPvE/bp+4/kOVDzDue4GYQIhPwYk9UGLPo1dmntIqAeNb7zYid2H6VXcxnwGqlZDSk+I6nunAkEAlF4Y8EjKjoEEzYadEfp9E8Wf0hKl1R+GWuEzHf8RuzFf1QPHkl187nGnpcDGj2mRFMWH9TWUCHg1kuNcA+O56QJAGJ7MMnu9YQuWpH0TN9/re/8jq0hWU6BZ85lRBDvl7/Bi6Fq63CZhIp08mjXSzI/mgztWK63OId1uSPo42l8ZRwJBAMrABYiebvl9PTpxM2nuMLNUKwpdwjBXGGMXmKwftXPI6RFkEOKhqOvOiDBALcu0ZwtLsJsPXmJROXSQf9cMdRk="; /** * 初始化RSA密钥对 * @return RSA密钥对 * @throws Exception 抛出异常 */ private static Map<String, String> initKey() throws Exception { KeyPairGenerator keygen = KeyPairGenerator.getInstance(RSA_KEY_ALGORITHM); //初始化密钥生成器 keygen.initialize(KEY_SIZE); KeyPair keys = keygen.genKeyPair(); String pub_key = Base64.getEncoder().encodeToString(keys.getPublic().getEncoded()); String pri_key = Base64.getEncoder().encodeToString(keys.getPrivate().getEncoded()); Map<String, String> keyMap = new HashMap<String, String>(); keyMap.put(PUBLIC_KEY, pub_key); keyMap.put(PRIVATE_KEY, pri_key); System.out.println("公钥:" + pub_key); System.out.println("私钥:" + pri_key); return keyMap; } public static String sign(String data, String privateKey) throws Exception { //自己的私钥加签 byte[] byteData = data.getBytes("UTF8"); return sign(byteData,privateKey); } public static String sign(byte[] data, String pri_key) throws Exception { //自己的私钥加签 byte[] pri_key_bytes = Base64.getDecoder().decode(pri_key); PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(pri_key_bytes); KeyFactory keyFactory = KeyFactory.getInstance(RSA_KEY_ALGORITHM); // 生成私钥 PrivateKey priKey = keyFactory.generatePrivate(pkcs8KeySpec); // 实例化Signature Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM); // 初始化Signature signature.initSign(priKey); // 更新 signature.update(data); return Base64.getEncoder().encodeToString(signature.sign()); } public static boolean verify(String data, String sign, String pub_key) throws Exception { return verify(data.getBytes("UTF8"),sign.getBytes("UTF8"),pub_key); } public static boolean verify(byte[] data, byte[] sign, String pub_key) throws Exception { //公钥验签 byte[] pub_key_bytes = Base64.getDecoder().decode(pub_key); KeyFactory keyFactory = KeyFactory.getInstance(RSA_KEY_ALGORITHM); // 初始化公钥 // 密钥材料转换 X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(pub_key_bytes); // 产生公钥 PublicKey pubKey = keyFactory.generatePublic(x509KeySpec); // 实例化Signature Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM); // 初始化Signature signature.initVerify(pubKey); // 更新 signature.update(data); // 验证 return signature.verify(sign); } private static byte[] encryptByPubKey(byte[] data, byte[] pub_key) throws Exception { X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(pub_key); KeyFactory keyFactory = KeyFactory.getInstance(RSA_KEY_ALGORITHM); PublicKey publicKey = keyFactory.generatePublic(x509KeySpec); Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm()); cipher.init(Cipher.ENCRYPT_MODE, publicKey); return cipher.doFinal(data); } public static String encryptByPubKey(String data, String pub_key) throws Exception { // 一般用公匙进行加密 // RSA的公私钥一般都会以base64编码给出,所以先base64解码 byte[] pub_key_bytes = Base64.getDecoder().decode(pub_key); byte[] encryptedData = encryptByPubKey(data.getBytes("UTF8"), pub_key_bytes); return Base64.getEncoder().encodeToString(encryptedData); } private static byte[] decryptByPriKey(byte[] data, byte[] pri_key) throws Exception { PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(pri_key); KeyFactory keyFactory = KeyFactory.getInstance(RSA_KEY_ALGORITHM); PrivateKey privateKey = keyFactory.generatePrivate(pkcs8KeySpec); Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm()); cipher.init(Cipher.DECRYPT_MODE, privateKey); return cipher.doFinal(data); } /** * 私钥解密 * @param data 待解密数据 * @param pri_key 私钥 * @return 明文 * @throws Exception 抛出异常 */ public static String decryptByPriKey(String data, String pri_key) throws Exception { // 一般用私匙进行解密 // RSA的公私钥一般都会以base64编码给出,所以先base64解码 byte[] pri_key_bytes = Base64.getDecoder().decode(pri_key); byte[] design = decryptByPriKey(Base64.getDecoder().decode(data), pri_key_bytes); return new String(design,"UTF8"); } public static void main(String[] args) throws Exception { String datastr = "天街小雨润如酥,草色遥看近却无。最是一年春好处,绝胜烟柳满皇都。"; System.out.println("待加密数据:\n" + datastr); //获取密钥对 String pub_key = publicKey; String pri_key = privateKey; String pubKeyStr = encryptByPubKey(datastr, pub_key); System.out.println("公匙加密结果:\n" + pubKeyStr); // 私匙解密 String priKeyStr = decryptByPriKey(pubKeyStr, pri_key); System.out.println("私匙解密结果:\n" + priKeyStr); //换行 System.out.println("-------------验证签名-------------------"); // 数字签名 String str1 = "汉兵已略地"; String str2 = "四面楚歌声"; System.out.println("正确的签名:" + str1 + "\n错误的签名:" + str2); String sign = sign(str1.getBytes(), pri_key); System.out.println("数字签名:\n" + sign); boolean vflag1 = verify(str1.getBytes(), Base64.getDecoder().decode(sign), pub_key); System.out.println("数字签名验证结果1:\n" + vflag1); boolean vflag2 = verify(str2.getBytes(), Base64.getDecoder().decode(sign), pub_key); System.out.println("数字签名验证结果2:\n" + vflag2); } }
DSA#
一般用于数字签名场合。
ECC#
ECC 也是一种 非对称加密算法,主要优势是在某些情况下,它比其他的方法使用 更小的密钥,比如 RSA 加密算法,提供 相当的或更高等级 的安全级别。不过一个缺点是 加密和解密操作 的实现比其他机制 时间长 (相比 RSA 算法,该算法对 CPU 消耗严重)。
import net.pocrd.annotation.NotThreadSafe; import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey; import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.crypto.Cipher; import java.io.ByteArrayOutputStream; import java.security.KeyFactory; import java.security.Security; import java.security.Signature; import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.X509EncodedKeySpec; @NotThreadSafe public class EccHelper { private static final Logger logger = LoggerFactory.getLogger(EccHelper.class); private static final int SIZE = 4096; private BCECPublicKey publicKey; private BCECPrivateKey privateKey; static { Security.addProvider(new BouncyCastleProvider()); } public EccHelper(String publicKey, String privateKey) { this(Base64Util.decode(publicKey), Base64Util.decode(privateKey)); } public EccHelper(byte[] publicKey, byte[] privateKey) { try { KeyFactory keyFactory = KeyFactory.getInstance("EC", "BC"); if (publicKey != null && publicKey.length > 0) { this.publicKey = (BCECPublicKey)keyFactory.generatePublic(new X509EncodedKeySpec(publicKey)); } if (privateKey != null && privateKey.length > 0) { this.privateKey = (BCECPrivateKey)keyFactory.generatePrivate(new PKCS8EncodedKeySpec(privateKey)); } } catch (ClassCastException e) { throw new RuntimeException("", e); } catch (Exception e) { throw new RuntimeException(e); } } public EccHelper(String publicKey) { this(Base64Util.decode(publicKey)); } public EccHelper(byte[] publicKey) { try { KeyFactory keyFactory = KeyFactory.getInstance("EC", "BC"); if (publicKey != null && publicKey.length > 0) { this.publicKey = (BCECPublicKey)keyFactory.generatePublic(new X509EncodedKeySpec(publicKey)); } } catch (Exception e) { throw new RuntimeException(e); } } public byte[] encrypt(byte[] content) { if (publicKey == null) { throw new RuntimeException("public key is null."); } try { Cipher cipher = Cipher.getInstance("ECIES", "BC"); cipher.init(Cipher.ENCRYPT_MODE, publicKey); int size = SIZE; ByteArrayOutputStream baos = new ByteArrayOutputStream((content.length + size - 1) / size * (size + 45)); int left = 0; for (int i = 0; i < content.length; ) { left = content.length - i; if (left > size) { cipher.update(content, i, size); i += size; } else { cipher.update(content, i, left); i += left; } baos.write(cipher.doFinal()); } return baos.toByteArray(); } catch (Exception e) { throw new RuntimeException(e); } } public byte[] decrypt(byte[] secret) { if (privateKey == null) { throw new RuntimeException("private key is null."); } try { Cipher cipher = Cipher.getInstance("ECIES", "BC"); cipher.init(Cipher.DECRYPT_MODE, privateKey); int size = SIZE + 45; ByteArrayOutputStream baos = new ByteArrayOutputStream((secret.length + size + 44) / (size + 45) * size); int left = 0; for (int i = 0; i < secret.length; ) { left = secret.length - i; if (left > size) { cipher.update(secret, i, size); i += size; } else { cipher.update(secret, i, left); i += left; } baos.write(cipher.doFinal()); } return baos.toByteArray(); } catch (Exception e) { logger.error("ecc decrypt failed.", e); } return null; } public byte[] sign(byte[] content) { if (privateKey == null) { throw new RuntimeException("private key is null."); } try { Signature signature = Signature.getInstance("SHA1withECDSA", "BC"); signature.initSign(privateKey); signature.update(content); return signature.sign(); } catch (Exception e) { throw new RuntimeException(e); } } public boolean verify(byte[] sign, byte[] content) { if (publicKey == null) { throw new RuntimeException("public key is null."); } try { Signature signature = Signature.getInstance("SHA1withECDSA", "BC"); signature.initVerify(publicKey); signature.update(content); return signature.verify(sign); } catch (Exception e) { logger.error("ecc verify failed.", e); } return false; } }
哈希算法(单向加密)#
单向加密算法只能用于对数据的加密,无法被解密,其特点为定长输出、雪崩效应。单向加密算法用于不需要对信息进行解密或读取的场合,比如用来比较两个信息值是否一样而不需要知道信息具体内容,在实际中的一个典型应用就是对数据库中的用户信息进行加密,比如当创建一个新用户及密码时,将这些信息经过单向加密后再保存到数据库中。
常见的算法包括
- MD5;
- SHA等
MD5#
MD5即Message-Digest Algorithm 5(信息-摘要算法5),用于确保信息传输完整一致。常用于数据库密码存储。MD5值是128bit位的数据,一般情况下使用一个长度是32的十六进制字符串来显示。 具体特点如下:
-
压缩性:任意长度的数据,算出的MD5值长度都是固定的。
-
容易计算:从原数据计算出MD5值很容易。
-
抗修改性:对原数据进行任何改动,哪怕只修改1个字节,所得到的MD5值都有很大区别。
-
强抗碰撞:已知原数据和其MD5值,想找到一个具有相同MD5值的数据(即伪造数据)是非常困难的。
MD5加盐
我们知道,如果直接对密码进行散列,那么黑客可以对通过获得这个密码散列值,然后通过查散列值字典(例如MD5密码破解网站),得到某用户的密码。加Salt可以一定程度上解决这一问题。所谓加Salt方法,就是加点“佐料”。其基本想法是这样的:当用户首次提供密码时(通常是注册时), 由系统自动往这个密码里撒一些“佐料”,然后再散列。而当用户登录时,系统为用户提供的代码撒上同样的“佐料”,然后散列,再比较散列值,已确定密码是否 正确。
这里的“佐料”被称作“Salt值”,这个值是由系统随机生成的,并且只有系统知道。这样,即便两个用户使用了同一个密码,由于系统为它们生成 的salt值不同,他们的散列值也是不同的。即便黑客可以通过自己的密码和自己生成的散列值来找具有特定密码的用户,但这个几率太小了(密码和salt值 都得和黑客使用的一样才行)。
import java.util.Random; import org.apache.commons.codec.binary.Hex; import java.security.NoSuchAlgorithmException; import java.security.MessageDigest; public class MD5Util { /** * 普通MD5方法 容易被破解 */ public static String md5(String input) { MessageDigest md5 = null; try { md5 = MessageDigest.getInstance("md5"); } catch (NoSuchAlgorithmException e) { return "check jdk"; } catch (Exception e) { e.printStackTrace(); return ""; } char[] charArray = input.toCharArray(); byte[] byteArray = new byte[charArray.length]; for (int i = 0; i < charArray.length; i++) { byteArray[i] = (byte) charArray[i]; } byte[] md5Bytes = md5.digest(byteArray); StringBuffer hexValue = new StringBuffer(); for (int i = 0; i < md5Bytes.length; i++) { int val = ((int) md5Bytes[i]) & 0xff; if (val < 16) { hexValue.append("0"); } hexValue.append(Integer.toHexString(val)); } return hexValue.toString(); } /** * 加盐MD5 * @author daniel * @time 2016-6-11 下午8:45:04 * @param password * @return */ public static String md5WithSalt(String password) { Random r = new Random(); StringBuilder sb = new StringBuilder(16); sb.append(r.nextInt(99999999)).append(r.nextInt(99999999)); int len = sb.length(); if (len < 16) { for (int i = 0; i < 16 - len; i++) { sb.append("0"); } } String salt = sb.toString(); password = md5Hex(password + salt); char[] cs = new char[48]; for (int i = 0; i < 48; i += 3) { cs[i] = password.charAt(i / 3 * 2); char c = salt.charAt(i / 3); cs[i + 1] = c; cs[i + 2] = password.charAt(i / 3 * 2 + 1); } return new String(cs); } /** * 校验加盐后是否和原文一致 * @author daniel * @time 2016-6-11 下午8:45:39 * @param password * @param md5 * @return */ public static boolean verify(String password, String md5) { char[] cs1 = new char[32]; char[] cs2 = new char[16]; for (int i = 0; i < 48; i += 3) { cs1[i / 3 * 2] = md5.charAt(i); cs1[i / 3 * 2 + 1] = md5.charAt(i + 2); cs2[i / 3] = md5.charAt(i + 1); } String salt = new String(cs2); return md5Hex(password + salt).equals(new String(cs1)); } /** * 获取十六进制字符串形式的MD5摘要 */ private static String md5Hex(String src) { try { MessageDigest md5 = MessageDigest.getInstance("md5"); byte[] bs = md5.digest(src.getBytes()); return new String(new Hex().encode(bs)); } catch (Exception e) { return null; } } public static void main(String[] args) { String md5 = md5("admin"); System.out.println(md5); String mdsSalt = md5WithSalt("admin"); System.out.println(mdsSalt); System.out.println(verify("admin",mdsSalt)); } }
SHA#
SHA代表安全散列算法,SHA-1和SHA-2是该算法的两个不同版本。它们在构造(如何从原始数据创建结果散列)和签名的位长方面都不同。您应该将SHA-2视为SHA-1的继承者,因为它是一个整体改进。
首先,人们把重点放在比特长度上作为重要的区别。SHA-1是160位散列。SHA-2实际上是哈希的“家族”,有各种长度,最受欢迎的是256位。
各种各样的SHA-2哈希可能会引起一些混乱,因为网站和作者以不同的方式表达它们。如果你看到“SHA-2”,“SHA-256”或“SHA-256位”,那些名称指的是同一个东西。如果您看到“SHA-224”,“SHA-384”或“SHA-512”,则它们指的是SHA-2的备用位长度。您可能还会看到一些网站更明确,并写出算法和比特长度,例如“SHA-2 384”。
各个加密算法的比较#
- 散列算法的比较
名称 | 安全性 | 速度 |
---|---|---|
SHA-1 | 高 | 慢 |
MD5 | 中 | 快 |
- 对称加密算法比较
名称 | 密钥名称 | 运行速度 | 安全性 | 资源消耗 |
---|---|---|---|---|
DES | 56位 | 较快 | 低 | 中 |
3DES | 112位或168位 | 慢 | 中 | 高 |
AES | 128、192、256位 | 快 | 高 | 低 |
- 非对称算法的比较
名称 | 成熟度 | 安全性 | 运算速度 | 资源消耗 |
---|---|---|---|---|
RSA | 高 | 高 | 中 | 中 |
ECC | 高 | 高 | 慢 | 高 |
Base64(编码方式)#
我们知道在计算机中任何数据都是按ascii码存储的,而ascii码的128~255之间的值是不可见字符。而在网络上交换数据时,比如说从A地传到B地,往往要经过多个路由设备,由于不同的设备对字符的处理方式有一些不同,这样那些不可见字符就有可能被处理错误,这是不利于传输的。所以就先把数据先做一个Base64编码,统统变成可见字符,这样出错的可能性就大降低了。
对证书来说,特别是根证书,一般都是作Base64编码的,因为它要在网上被许多人下载。电子邮件的附件一般也作Base64编码的,因为一个附件数据往往是有不可见字符的。标准base64只有64个字符(英文大小写、数字和+、/)以及用作后缀等号;
Base64有很多实现,JDK默认实现、Apache包下面的实现和Spring提供的实现等。平时我们用的时候推荐使用Apache下面的实现。