Java密码学基础 发展历史
古典密码学 如凯撒密码、滚筒密码
近代密码学 如德国Enigma机,被图灵破解
现代密码学
编码算法
不是加密和解密,为了在网络间更方便的传输数据/本地存储字节数组而产生
Base64
Base64是网络上最常见的用于传输8Bit字节码 的编码方式之一,Base64就是一种基于64个可打印字符来表示二进制 数据的方法。可查看RFC2045~RFC2049,上面有MIME的详细规范。
Base64编码是从二进制到字符的过程,可用于在HTTP 环境下传递较长的标识信息。采用Base64编码具有不可读性,需要解码后才能阅读。
Base64由于以上优点被广泛应用于计算机的各个领域,然而由于输出内容中包括两个以上“符号类”字符(+, /, =),不同的应用场景又分别研制了Base64的各种“变种”。为统一和规范化Base64的输出,Base62x被视为无符号化的改进版本。
写个例子实现一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 public class Base64Test { private static final String UTF8 = StandardCharsets.UTF_8.name(); public static void main (String[] args) throws UnsupportedEncodingException { String str = "I love Java!" ; String encodeStr1 = Base64.getEncoder().encodeToString(str.getBytes(StandardCharsets.UTF_8)); System.out.println("编码结果: " + encodeStr1); byte [] decodeStr1 = Base64.getDecoder().decode(encodeStr1.getBytes(StandardCharsets.UTF_8)); System.out.println("解码结果: " + new String (decodeStr1,UTF8)); String encodeStr2 = org.apache.commons.codec.binary.Base64.encodeBase64String(str.getBytes(StandardCharsets.UTF_8)); System.out.println("编码结果: " + encodeStr2); byte [] decodeStr2 = org.apache.commons.codec.binary.Base64.decodeBase64(encodeStr1.getBytes(StandardCharsets.UTF_8)); System.out.println("解码结果: " + new String (decodeStr2,UTF8)); } }
打印结果:
1 2 3 4 5 6 编码结果: SSBsb3ZlIEphdmEh 解码结果: I love Java! 编码结果: SSBsb3ZlIEphdmEh 解码结果: I love Java! 进程已结束,退出代码0
Base64编码以三个字节为一组,不足的用=填充
URL编码 我们在网上冲浪时,会有一些含有中文的URL被编码成一堆%和数字的情况,其实这就是URL编码
当前端发出get请求时,请求格式为application/x-www-form-urlencoded,其实也就是URLcode编码,后端也会相应的作出处理
写个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public class URLEncodeTest { private static final String UTF8 = StandardCharsets.UTF_8.name(); public static void main (String[] args) throws UnsupportedEncodingException { String str = "我爱写Java" ; String encodeStr = URLEncoder.encode(str,UTF8); System.out.println("编码结果: " + encodeStr); String decodeStr = URLDecoder.decode(encodeStr,UTF8); System.out.println("解码结果: " + decodeStr); } }
打印结果:
1 2 3 4 编码结果: %E6%88%91%E7%88%B1%E5%86%99Java 解码结果: 我爱写Java 进程已结束,退出代码0
摘要算法 定义
消息摘要算法的主要特征是加密 过程不需要密钥 ,并且经过加密的数据无法被解密,可以被解密逆向的只有CRC32 算法,只有输入相同的明文数据经过相同的消息摘要算法才能得到相同的密文 。
摘要算法又叫Hash算法、散列函数、数字摘要、消息摘要。它是一种单向算法,用户可以通过hash算法对目标信息生成一段特定长度的唯一hash值,但不能通过这个hash值重新获得目标信息
应用场景 密码、信息完整性校验、数字签名
常见算法
MD5(Message-Digest Algorithm) 结果占128位(16byte)
SHA(Secure Hash Algorithm) 安全散列算法
sha-256
sha-0,sha-1,sha-512
MAC(Message Authentication Code) 消息验证码,是一种带有密钥的hash函数
MD2 MD4 HAVAL
java实现 MD5:
先采用原生JDK实现一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 public class MD5Test { private static final String UTF8 = StandardCharsets.UTF_8.name(); public static void main (String[] args) throws Exception { String str = "我爱Java" ; String algorithm = "MD5" ; MessageDigest messageDigest = MessageDigest.getInstance(algorithm); byte [] originBytes = str.getBytes(UTF8); byte [] digestBytes = messageDigest.digest(originBytes); String hexStr = convertBytestoHexStr(digestBytes); System.out.println("十六进制字符串:" + hexStr); } private static String convertBytestoHexStr (byte [] digestBytes) { StringBuilder stringBuilder = new StringBuilder (); for (byte b:digestBytes) { String hex = Integer.toHexString(b&0xff ); if (hex.length() == 1 ) { hex = "0" + hex; } stringBuilder.append(hex); } return stringBuilder.toString(); } }
打印结果:
1 2 3 十六进制字符串:b2f973e181bc4dedaded7887c85a0a23 进程已结束,退出代码0
因为摘要结果是字节数组,所以无法直接打印(会出现乱码),于是我手写了一个函数将字节流转化为十六进制的字符串。
这里说明一下:首先声明了一个StringBuilder
工具用于拼接字符串。由于MD5和摘要结果是由128个字符即16个字节组成的字节流 ,因此我只要遍历每个字节,将其转化为十六进制的数字。又因为在Java虚拟机中,在编译运行时期每个字节会被提升为int类型的数据,即每一个字节数据要占4个字节 (这是Java虚拟机的设计),又因为存在有的字节表示的是负数,负数在计算机中采用补码的形式存在 ,当其被提升为int
类型数据时,高位补1。例如:-127,八个bit表示为1000
0001
,最高位是符号位。当其转化为int
类型时,高位补1,结果为:1111
1111
1111
1111
1111
1111
1000
0001
,这样就满足了保持其真值不变。而在这里,我们只需要将最后一个字节转化为对应的十六进制数,所以与0xFF(1111 1111
),保留最后八个bit二进制数。比如我们的-127,1111
1111
1111
1111
1111
1111
1000
0001
&0000
0000
0000
0000
0000
0000
1111
1111
得到结果0000
0000
0000
0000
0000
0000
1000
0001
,即十六进制数81。也就是说,我们将其转为十六进制字符串时,不考虑吧其数值的正确性,只保证它在计算机中二进制流保存形式的正确性,于是只截取后八位进行转化。
当然,我们也可以偷懒,不自己手写转换方法,因为有封装好的工具类:
1 String hexStr = DigestUtils.md5Hex(digestBytes);
依然可以得到同样的结果:
1 2 3 十六进制字符串:b2f973e181bc4dedaded7887c85a0a23 进程已结束,退出代码0
而SHA-256,SHA-512等算法同MD5,只是增多了加密的字节,实现细节是一样的。感兴趣的可以自行将algorithm
改为相应的算法,即可。
MAC:
mac则是在MD5,SHA-256等算法的基础上加了一个密钥值,即我们常说的**”加盐”**,使得更加安全(通过加盐,多了一层保障)
大大降低了黑客通过枚举 或彩虹表 等手段攻破!
写个例子看看:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class MacTest { public static void main (String[] args) { String str = "我爱Java" ; String key = "666" ; String hmacMD5HexStr = new HmacUtils (HmacAlgorithms.HMAC_MD5,key.getBytes(StandardCharsets.UTF_8)).hmacHex(str.getBytes(StandardCharsets.UTF_8)); String hmacSHA256HexStr = new HmacUtils (HmacAlgorithms.HMAC_SHA_256,key.getBytes(StandardCharsets.UTF_8)).hmacHex(str.getBytes(StandardCharsets.UTF_8)); String hmacSHA512HexStr = new HmacUtils (HmacAlgorithms.HMAC_SHA_512,key.getBytes(StandardCharsets.UTF_8)).hmacHex(str.getBytes(StandardCharsets.UTF_8)); System.out.println("hmacMD5HexStr: " + hmacMD5HexStr); System.out.println("hmacSHA256HexStr: " + hmacSHA256HexStr); System.out.println("hmacSHA512HexStr: " + hmacSHA512HexStr); } }
打印结果:
1 2 3 4 5 hmacMD5HexStr: 9baa3a835a69cb9382f9374625de1876 hmacSHA256HexStr: d3c89c5945411a54a4fe96deaf0e0c2c22f61b3d43c98daae3b8b2f911d15003 hmacSHA512HexStr: ce37dd5da4c6ab796a6e2485cb35b2d3824cfaf0c99f72421aa5580caa96d4e9e5b91c46881eeaddb0f4ea65012dab753edab13e75be2190ea97d7f5eeb67c96 进程已结束,退出代码0
对称加密 百度百科定义
需要对加密和解密使用相同密钥 的加密算法 。由于其速度快,对称性加密通常在消息发送方需要加密大量数据时使用。对称性加密也称为密钥加密 。
所谓对称,就是采用这种加密方法的双方使用方式用同样的密钥 进行加密和解密。密钥 是控制加密及解密过程的指令。算法是一组规则,规定如何进行加密和解密。
因此加密的安全性不仅取决于加密算法本身,密钥管理的安全性更是重要。因为加密和解密都使用同一个密钥,如何把密钥安全地传递到解密者手上就成了必须要解决的问题
常见算法
DES(Data Encryption Standard) 已过时
AES(Advanced Encryption Standard) 代替了DES
3DES Blowfish IDEA RC4 RC5 RC6……
分类
Java程序实现AES:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 public class AESTest { public static void main (String[] args) throws Exception{ String message = "我爱Java" ; System.out.println("原文: " + message); byte [] key = "0123456789abcdef" .getBytes(StandardCharsets.UTF_8); byte [] data = message.getBytes(StandardCharsets.UTF_8); byte [] encrypted = encrypt(key,data); System.out.println("加密结果: " + Base64.getEncoder().encodeToString(encrypted)); byte [] decrypted = decrypt(key, encrypted); System.out.println("解密结果: " + new String (decrypted, StandardCharsets.UTF_8)); } public static byte [] encrypt(byte [] key, byte [] input) throws GeneralSecurityException { Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding" ); SecretKey keySpec = new SecretKeySpec (key, "AES" ); cipher.init(Cipher.ENCRYPT_MODE, keySpec); return cipher.doFinal(input); } public static byte [] decrypt(byte [] key, byte [] input) throws GeneralSecurityException { Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding" ); SecretKey keySpec = new SecretKeySpec (key, "AES" ); cipher.init(Cipher.DECRYPT_MODE, keySpec); return cipher.doFinal(input); } }
打印结果:
1 2 3 4 5 原文: 我爱Java 加密结果: 6OhWO+kamL4FXbVZdjuTpQ== 解密结果: 我爱Java 进程已结束,退出代码0
非对称加密 百度百科定义
对称加密算法 在加密 和解密时使用的是同一个秘钥 ;而非对称加密算法需要两个密钥 来进行加密和解密,这两个密钥是公开密钥 (public key,简称公钥)和私有密钥(private key,简称私钥)。
加密和解密使用的是两个不同的密钥,公钥可以给任何人,私钥总是自己保留
出现原因 对称加密使用相同的秘钥,但对不同的原始内容会采用不同的秘钥,导致秘钥数量巨大,难以维护
比如说,使用对称加密,则需要生产N*(N-1)/2个秘钥,此时每个人就需要管理N-1个秘钥,秘钥管理难度大。而采用非对称加密,在N个人之间通信的时候,只需要生产N个密钥对,每个人仅需管理好自己的密钥对!
RSA算法 非对称加密中,最典型的就是RSA算法了。RSA由由Ron Rivest,Adi Shamir,Leonard Adleman三兄弟发明,故取名RSA算法。通过RSA算法,会产生一对密钥对:公钥和私钥。通过公钥加密的内容,只有私钥可以解开,至于为什么,设计到数论密码学相关的数学知识,暂不做深究。所以,在双方通信时,一方应先向另一方索取公钥,利用对方的公钥加密自己要发送给对方的敏感内容,对方在接收到之后再用自己的私钥进行解密,只要私钥不被盗取,第三方就无法破解敏感内容!
利用Java标准库模拟一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 public class RSATest { public static void main (String[] args) throws Exception { byte [] context = "I lOVE YOU!" .getBytes(StandardCharsets.UTF_8); Person tony = new Person ("Tony" ); byte [] pk = tony.getPublicKey(); System.out.println(String.format("公钥: %x" , new BigInteger (1 , pk))); byte [] encrypted = tony.encrypt(context); System.out.println(String.format("公钥加密结果: %x" , new BigInteger (1 , encrypted))); byte [] sk = tony.getPrivateKey(); System.out.println(String.format("私钥: %x" , new BigInteger (1 , sk))); byte [] decrypted = tony.decrypt(encrypted); System.out.println(new String (decrypted, StandardCharsets.UTF_8)); } } class Person { String name; PrivateKey sk; PublicKey pk; public Person (String name) throws NoSuchAlgorithmException { 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); } }
打印结果:
1 2 3 4 5 6 公钥: 30819f300d06092a864886f70d010101050003818d0030818902818100ca2c25ebe9d687856dde5e82e10f5e7242f878f4fb18996736743b9772047a431cb0e2ec07a2cca7f94a4e9a38d375d95499326334a2239810ad6989c75596823cf15cf03dd14eafe6dec987f422729f866af13e0df4334e4eacf315a8549cf12b3a3515971ce6999d32b4f7953534b8538091b91adcc420dc30e450156b7c650203010001 公钥加密结果: 9572d508d34bcabac85df021f263be67a9a3140079222e377986761542b95d5a05da179bbe8e3324f3916c52e63435366931f12d3ce085ae7acf591dbb20253b94a5c671c32234570a3e8d7f0332b81e66c80d69dd4e1260e1362e3d377505a95d751b8716f85a7b3b01f3819742141c1ac93e65d085a1ec6792dad062b174cb 私钥: 30820275020100300d06092a864886f70d01010105000482025f3082025b02010002818100ca2c25ebe9d687856dde5e82e10f5e7242f878f4fb18996736743b9772047a431cb0e2ec07a2cca7f94a4e9a38d375d95499326334a2239810ad6989c75596823cf15cf03dd14eafe6dec987f422729f866af13e0df4334e4eacf315a8549cf12b3a3515971ce6999d32b4f7953534b8538091b91adcc420dc30e450156b7c6502030100010281805fd0d4981e57021b869aa1083e49de6520c049f3311dd3764b2483299f6be7d5eebf168cee8185a5064ce53bca3acddb967094a4d7c9103d7d89f23ece2e0e0a0b53a3bbb71919a1fcb0fa1098f7366f17746e2b63c35fad1437479472c0ae1df435afe7b9df238563dabd512351af0d448d498205dedac5d1cfb7a139cf8bf1024100f40c77f7d6108f2dec4266509590c9c172beabfe118541f1ced82fc9c00877a6afff9f9d338246fdecff85488808e9731a3cb9df6acaad06395158d5226f85cf024100d412b201a565aee62f127da4dd99151debd1699d99bc6c5ce556874361e0332621864fbaf87fb5b1521b5f9684874ce4fdf778a4a58be742e2c94fee756f1b8b02407c8c18758cf3aa7e7f426bc0d873a9e365d1d528b67c51693c6cac06c4500df02d85c14992cdfbb8ff487016d205ea4de9a7f01c0afe204b3ad93f0296ae5f9502400eb8617cb5c352198e28e569bd2bf40848a71782a5fa2b37637fd711b9487ba468ed4eb976a83eaf5938a730e67011c94f4b8f27368a7879ef0df42b64215b3302406bfbedbea8e7b86c36c42c3455a1cab7ffbcd90fef3ed2de6f5257d073eed8d4d6faedd88806b4434160b0f635c0ab9e0c5c3d5693fc616fc4943feec10507bd I lOVE YOU! 进程已结束,退出代码0
可以看到,私钥比公钥长很多!以RSA算法为例,它的密钥有256/512/1024/2048/4096等不同的长度。长度越长,密码强度越大,当然计算速度也越慢。
因此,在实际应用中,非对称加密与对称加密是一起使用的,有效解决非对称加密速度慢的问题。例如廖雪峰老师举的例子:
小明需要给小红需要传输加密文件,他俩首先交换了各自的公钥,然后:
小明生成一个随机的AES口令,然后用小红的公钥通过RSA加密这个口令,并发给小红;
小红用自己的RSA私钥解密得到AES口令;
双方使用这个共享的AES口令用AES加密通信。
可见非对称加密实际上应用在第一步 ,即加密“AES口令” 。这也是我们在浏览器中常用的HTTPS协议的做法,即浏览器和服务器先通过RSA交换AES口令,接下来双方通信实际上采用的是速度较快的AES对称加密,而不是缓慢的RSA非对称加密。
签名算法 从上面我们可以得出,当一方给对方发送消息时,选择用对方的公钥进行加密,对方在收到消息时,采用私钥进行解密。那么使用私钥加密的内容,所有拥有的公钥的人都可以轻易获取,这样有什么意义呢?
意义就在于签名啦!什么是签名? 我来举个例子:古代皇帝在颁布诏书的时候,都会盖上国玺印章,或者在给别人写信的时候,也会署名。其实这些就是签名啦!由此可见,签名的意义在于表明身份,这诏书是皇帝写的,所有都不得违背!而在签名算法中,还有一个很重要的作用,就是校验数据的完整,以防诏书被第三者截获,篡改内容!
因此,私钥加密得到的密文实际上就是数字签名,要验证这个签名是否正确,只能用私钥持有者的公钥进行解密验证。使用数字签名的目的是为了确认某个信息确实是由某个发送方发送的,任何人都不可能伪造消息,并且,发送方也不能抵赖。
在实际应用的时候,签名实际上并不是针对原始消息,而是针对原始消息的哈希进行签名。对签名进行验证实际上就是用公钥解密。
然后把解密后的哈希与原始消息的哈希进行对比。
因为用户总是使用自己的私钥进行签名,所以,私钥就相当于用户身份。而公钥用来给外部验证用户身份。
常用数字签名算法有:
MD5withRSA
SHA1withRSA
SHA256withRSA
写个Java程序模拟一下签名算法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public class SHA1withRSA { public static void main (String[] args) throws Exception { KeyPairGenerator kpGen = KeyPairGenerator.getInstance("RSA" ); kpGen.initialize(1024 ); KeyPair kp = kpGen.generateKeyPair(); PrivateKey sk = kp.getPrivate(); PublicKey pk = kp.getPublic(); byte [] message = "I LOVE YOU!" .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); } }
打印结果:
1 2 3 4 signature: bf68e25ab72a2fd7971065c460d6b6d131533c299e527111d88f7cf1a35968f109cc863b5a23c2e8f50ed6f80bbbe95a42eec14ddd7770e3ace6989230cf5d2bd3a03736fcd0f5cfd6a4a5988fe02b48c3306165f887803fe0845a964fce3210d04ef77afb42b5a47756731b1bbff5264ec4226ef8fa0db316ad74a37e7a94c6 valid? true 进程已结束,退出代码0
我画一个图梳理一下:
Tony在发送消息的同时会采用AES对内容加密,再采用RSA对AES口令加密,同时使用私钥对原文内容通过摘要算法生成的摘要进行加密,生成数字签名。Sunsan在接收时,首先用私钥解密获得AES口令,再用AES获得原文内容是”I LOVE YOU!”,感动不已,同时,用公钥解密数字签名,拿到摘要内容。Sunsan将获取的摘要和对原文生成的摘要进行比对,若一致,则表明这句表白的确来自于Tony,且内容真实,未被修改!
非对称加密和对称加密和签名算法的合作配合,保证了消息内容的“私密性”,同时也确认了发送者的”身份“和内容的“完整性”
数字证书
数字证书是集合了多种密码学算法,用于实现数据加解密、身份认证、签名等多种功能的一种安全标准。
数字证书可以防止中间人攻击,因为它采用链式签名认证,即通过根证书(Root CA)去签名下一级证书,这样层层签名,直到最终的用户证书。而Root CA证书内置于操作系统中,所以,任何经过CA认证的数字证书都可以对其本身进行校验,确保证书本身不是伪造的。
我们在上网时常用的HTTPS协议就是数字证书的应用。浏览器会自动验证证书的有效性:
数字证书储存的是公钥,其对应的私钥需要严格保存好,一旦泄露,将带来灾难性的安全事故!