序
前边的几篇文章,已经讲了几个对称加密的算法了,今天这篇文章再介绍最后一种对称加密算法 — — PBE,这种加密算法,对我的认知来说,并没有 DES、3DES、AES 那么流行,也不尽然,其实是我之前并没有这方面的需求,当然接触他的机会也就很少了,因此,可想而知,没听过显然在正常不过了。
概念
PBE,全称为“Password Base Encryption”,中文名“基于口令加密”,是一种基于密码的加密算法,其特点是使用口令代替了密钥,而口令由用户自己掌管,采用随机数杂凑多重加密等方法保证数据的安全性。
PBE算法没有密钥的概念,把口令当做密钥了。因为密钥长短影响算法安全性,还不方便记忆,这里我们直接换成我们自己常用的口令就大大不同了,便于我们的记忆。但是单纯的口令很容易被字典法给穷举出来,所以我们这里给口令加了点“盐”,这个盐和口令组合,想破解就难了。
同时我们将盐和口令合并后用消息摘要算法进行迭代很多次来构建密钥初始化向量的基本材料,使破译更加难了。
原理
首先,是基于口令的加密原理图
然后,是基于口令的解密原理图
由于这种加密方式的口令容易记忆,不用放在物理媒体上,因此增加了口令的安全性。密钥空间较小,安全性不高,用字典法比较容易破译。攻击者可产生一套密钥列表,用所有可能的密钥进行查找。
常用算法有
- PBEWithSHAAndDES
- PBEWithSHAAndBlowfish
- PBEWithSHAAnd128BitRC4
- PBEWithSHAAndIDEA-CBC
- PBEWithSHAAnd3-KeyTripleDES-CBC
其次,是基于口令和盐的加密原理图
最后,是基于口令和盐的解密原理图
当然,这种加密方式也具有口令容易记忆的特点,不用放在物理媒体上,因此增加了口令的安全性。可防止攻击者用事先产生的密钥列表进行查找。通过增加消息摘要的计算次数增加了攻击者测试口令的时间。
常用算法有
- PBEWithSHAAndDES
- PBEWithSHAAndBlowfish
- PBEWithSHAAnd128BitRC4
- PBEWithSHAAndIDEA-CBC
- PBEWithSHAAnd3-KeyTripleDES-CBC
代码实现
- <span style="font-family:Comic Sans MS;font-size:12px;">package com.sica.pbe;
- import sun.misc.BASE64Encoder;
- import javax.crypto.*;
- import javax.crypto.spec.PBEKeySpec;
- import javax.crypto.spec.PBEParameterSpec;
- import java.io.UnsupportedEncodingException;
- import java.security.InvalidAlgorithmParameterException;
- import java.security.InvalidKeyException;
- import java.security.Key;
- import java.security.NoSuchAlgorithmException;
- import java.security.spec.InvalidKeySpecException;
- import java.util.Random;
- /**
- * Created by xiang.li on 2015/2/28.
- * PBE 加解密工具类
- */
- public class PBE {
- /**
- * 定义加密方式
- * 支持以下任意一种算法
- * <p/>
- * <pre>
- * PBEWithMD5AndDES
- * PBEWithMD5AndTripleDES
- * PBEWithSHA1AndDESede
- * PBEWithSHA1AndRC2_40
- * </pre>
- */
- private final static String KEY_PBE = "PBEWITHMD5andDES";
- private final static int SALT_COUNT = 100;
- /**
- * 初始化盐(salt)
- *
- * @return
- */
- public static byte[] init() {
- byte[] salt = new byte[8];
- Random random = new Random();
- random.nextBytes(salt);
- return salt;
- }
- /**
- * 转换密钥
- *
- * @param key 字符串
- * @return
- */
- public static Key stringToKey(String key) {
- SecretKey secretKey = null;
- try {
- PBEKeySpec keySpec = new PBEKeySpec(key.toCharArray());
- SecretKeyFactory factory = SecretKeyFactory.getInstance(KEY_PBE);
- secretKey = factory.generateSecret(keySpec);
- } catch (NoSuchAlgorithmException e) {
- e.printStackTrace();
- } catch (InvalidKeySpecException e) {
- e.printStackTrace();
- }
- return secretKey;
- }
- /**
- * PBE 加密
- *
- * @param data 需要加密的字节数组
- * @param key 密钥
- * @param salt 盐
- * @return
- */
- public static byte[] encryptPBE(byte[] data, String key, byte[] salt) {
- byte[] bytes = null;
- try {
- // 获取密钥
- Key k = stringToKey(key);
- PBEParameterSpec parameterSpec = new PBEParameterSpec(salt, SALT_COUNT);
- Cipher cipher = Cipher.getInstance(KEY_PBE);
- cipher.init(Cipher.ENCRYPT_MODE, k, parameterSpec);
- bytes = cipher.doFinal(data);
- } catch (NoSuchAlgorithmException e) {
- e.printStackTrace();
- } catch (NoSuchPaddingException e) {
- e.printStackTrace();
- } catch (InvalidAlgorithmParameterException e) {
- e.printStackTrace();
- } catch (InvalidKeyException e) {
- e.printStackTrace();
- } catch (BadPaddingException e) {
- e.printStackTrace();
- } catch (IllegalBlockSizeException e) {
- e.printStackTrace();
- }
- return bytes;
- }
- /**
- * PBE 解密
- *
- * @param data 需要解密的字节数组
- * @param key 密钥
- * @param salt 盐
- * @return
- */
- public static byte[] decryptPBE(byte[] data, String key, byte[] salt) {
- byte[] bytes = null;
- try {
- // 获取密钥
- Key k = stringToKey(key);
- PBEParameterSpec parameterSpec = new PBEParameterSpec(salt, SALT_COUNT);
- Cipher cipher = Cipher.getInstance(KEY_PBE);
- cipher.init(Cipher.DECRYPT_MODE, k, parameterSpec);
- bytes = cipher.doFinal(data);
- } catch (NoSuchAlgorithmException e) {
- e.printStackTrace();
- } catch (NoSuchPaddingException e) {
- e.printStackTrace();
- } catch (InvalidAlgorithmParameterException e) {
- e.printStackTrace();
- } catch (InvalidKeyException e) {
- e.printStackTrace();
- } catch (BadPaddingException e) {
- e.printStackTrace();
- } catch (IllegalBlockSizeException e) {
- e.printStackTrace();
- }
- return bytes;
- }
- /**
- * BASE64 加密
- *
- * @param key 需要加密的字节数组
- * @return 字符串
- * @throws Exception
- */
- public static String encryptBase64(byte[] key) throws Exception {
- return (new BASE64Encoder()).encodeBuffer(key);
- }
- /**
- * 测试方法
- *
- * @param args
- */
- public static void main(String[] args) {
- // 加密前的原文
- String str = "hello world !!!";
- // 口令
- String key = "qwert";
- // 初始化盐
- byte[] salt = init();
- // 采用PBE算法加密
- byte[] encData = encryptPBE(str.getBytes(), key, salt);
- // 采用PBE算法解密
- byte[] decData = decryptPBE(encData, key, salt);
- String encStr = null;
- String decStr = null;
- try {
- encStr = encryptBase64(encData);
- decStr = new String(decData, "UTF-8");
- } catch (UnsupportedEncodingException e) {
- e.printStackTrace();
- } catch (Exception e) {
- e.printStackTrace();
- }
- System.out.println("加密前:" + str);
- System.out.println("加密后:" + encStr);
- System.out.println("解密后:" + decStr);
- }
- }</span><span style="font-family:微软雅黑;font-size: 14px;">
- </span>
结束语
到这里,对称加密算法就算是结束了,其实回想起来,PBE 并不算是一种新算法,他只不过是把密钥概念转变成了“口令 + 盐”的方式而已,至于加解密的原理,还是用到常用的对称加密算法,比如 DES 和 AES 等,因此说,没有什么新东西。
对称加密总算是告一段落了,我打算下次讲几个非对称加密的算法,之后这个系列的文章就要画上句号了。好的,小伙伴们,下次再见喽,欢迎大家与我交流,我们共同学习、共同进步。
计算‘text’的 HMAC:
最显著和最重要的区别是 SHA-1 摘要比 MD5 摘要长 32 位。使用强行技术,产生任何一个报文使其摘要等于给定报摘要的难度对 MD5 是 2^128 数量级的操作,而对 SHA-1 则是 2^160 数量级的操作。这样,SHA-1 对强行攻击有更大的强度。
序
距离上一次写博客感觉已经很长时间了,先吐槽一下,这个月以来,公司一直在加班,又是发版、上线,又是新项目太紧,具体的就不多说了,想听我吐槽的小伙伴,可以私信给我(*^__^*) 。上一篇文章,已经把对称加密的算法讲完了。从今天开始,要说说非对称加密了。因为,非对称加密真的是太重要了,我们的日常生活中,都离不开非对称加密。
概念
在说 RSA 之前,首先聊聊什么是非对称加密。在讲对称加密的时候,就曾经说过,对称加密算法在加密和解密时使用的是同一个秘钥,加解密双方必须使用同一个密钥才能进行正常的沟通。而非对称加密则不然,非对称加密算法需要两个密钥来进行加密和解密,分别是公钥和私钥。
需要注意的一点,这个公钥和私钥必须是一对的,如果用公钥对数据进行加密,那么只有使用对应的私钥才能解密,反之亦然。由于加密和解密使用的是两个不同的密钥,因此,这种算法叫做非对称加密算法。
工作过程
如下图,甲乙之间使用非对称加密的方式传输数据。
- 乙方生成一对密钥(公钥和私钥)并将公钥向其它方公开;
- 得到该公钥的甲方使用该密钥对机密信息进行加密后再发送给乙方;
- 乙方再用自己保存的另一把专用密钥(私钥)对加密后的信息进行解密;
- 乙方只能用其专用密钥(私钥)解密由对应的公钥加密后的信息;
- 在传输过程中,即使攻击者截获了传输的密文,并得到了乙的公钥,也无法破解密文,因为只有乙的私钥才能解密密文。
在非对称加密中使用的主要算法有:RSA、Elgamal、背包算法、Rabin、D-H、ECC(椭圆曲线加密算法)等。今天主要是介绍 RSA ,至于其他的算法,后续会选择几个进行介绍。
RSA
其实,在早在 1978 年的时候,RSA就已经出现了,它是第一个既能用于数据加密也能用于数字签名的算法。它易于理解和操作,也很流行。其原理就如上面的工作过程所述。
RSA 算法基于一个十分简单的数论事实:将两个大素数相乘十分容易,但是想要对其乘积进行因式分解却极其困难,因此可以将乘积公开作为加密密钥。
代码实现
下面来看一下具体的代码实现。
- <span style="font-family:Comic Sans MS;font-size:12px;">package com.test.rsa;
- import com.google.common.collect.Maps;
- import sun.misc.BASE64Decoder;
- import sun.misc.BASE64Encoder;
- import javax.crypto.Cipher;
- import java.security.*;
- import java.security.interfaces.RSAPrivateKey;
- import java.security.interfaces.RSAPublicKey;
- import java.security.spec.PKCS8EncodedKeySpec;
- import java.security.spec.X509EncodedKeySpec;
- import java.util.Map;
- /**
- * Created by xiang.li on 2015/3/3.
- * RSA 加解密工具类
- */
- public class RSA {
- /**
- * 定义加密方式
- */
- private final static String KEY_RSA = "RSA";
- /**
- * 定义签名算法
- */
- private final static String KEY_RSA_SIGNATURE = "MD5withRSA";
- /**
- * 定义公钥算法
- */
- private final static String KEY_RSA_PUBLICKEY = "RSAPublicKey";
- /**
- * 定义私钥算法
- */
- private final static String KEY_RSA_PRIVATEKEY = "RSAPrivateKey";
- /**
- * 初始化密钥
- * @return
- */
- public static Map<String, Object> init() {
- Map<String, Object> map = null;
- try {
- KeyPairGenerator generator = KeyPairGenerator.getInstance(KEY_RSA);
- generator.initialize(1024);
- KeyPair keyPair = generator.generateKeyPair();
- // 公钥
- RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
- // 私钥
- RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
- // 将密钥封装为map
- map = Maps.newHashMap();
- map.put(KEY_RSA_PUBLICKEY, publicKey);
- map.put(KEY_RSA_PRIVATEKEY, privateKey);
- } catch (NoSuchAlgorithmException e) {
- e.printStackTrace();
- }
- return map;
- }
- /**
- * 用私钥对信息生成数字签名
- * @param data 加密数据
- * @param privateKey 私钥
- * @return
- */
- public static String sign(byte[] data, String privateKey) {
- String str = "";
- try {
- // 解密由base64编码的私钥
- byte[] bytes = decryptBase64(privateKey);
- // 构造PKCS8EncodedKeySpec对象
- PKCS8EncodedKeySpec pkcs = new PKCS8EncodedKeySpec(bytes);
- // 指定的加密算法
- KeyFactory factory = KeyFactory.getInstance(KEY_RSA);
- // 取私钥对象
- PrivateKey key = factory.generatePrivate(pkcs);
- // 用私钥对信息生成数字签名
- Signature signature = Signature.getInstance(KEY_RSA_SIGNATURE);
- signature.initSign(key);
- signature.update(data);
- str = encryptBase64(signature.sign());
- } catch (Exception e) {
- e.printStackTrace();
- }
- return str;
- }
- /**
- * 校验数字签名
- * @param data 加密数据
- * @param publicKey 公钥
- * @param sign 数字签名
- * @return 校验成功返回true,失败返回false
- */
- public static boolean verify(byte[] data, String publicKey, String sign) {
- boolean flag = false;
- try {
- // 解密由base64编码的公钥
- byte[] bytes = decryptBase64(publicKey);
- // 构造X509EncodedKeySpec对象
- X509EncodedKeySpec keySpec = new X509EncodedKeySpec(bytes);
- // 指定的加密算法
- KeyFactory factory = KeyFactory.getInstance(KEY_RSA);
- // 取公钥对象
- PublicKey key = factory.generatePublic(keySpec);
- // 用公钥验证数字签名
- Signature signature = Signature.getInstance(KEY_RSA_SIGNATURE);
- signature.initVerify(key);
- signature.update(data);
- flag = signature.verify(decryptBase64(sign));
- } catch (Exception e) {
- e.printStackTrace();
- }
- return flag;
- }
- /**
- * 私钥解密
- * @param data 加密数据
- * @param key 私钥
- * @return
- */
- public static byte[] decryptByPrivateKey(byte[] data, String key) {
- byte[] result = null;
- try {
- // 对私钥解密
- byte[] bytes = decryptBase64(key);
- // 取得私钥
- PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(bytes);
- KeyFactory factory = KeyFactory.getInstance(KEY_RSA);
- PrivateKey privateKey = factory.generatePrivate(keySpec);
- // 对数据解密
- Cipher cipher = Cipher.getInstance(factory.getAlgorithm());
- cipher.init(Cipher.DECRYPT_MODE, privateKey);
- result = cipher.doFinal(data);
- } catch (Exception e) {
- e.printStackTrace();
- }
- return result;
- }
- /**
- * 私钥解密
- * @param data 加密数据
- * @param key 公钥
- * @return
- */
- public static byte[] decryptByPublicKey(byte[] data, String key) {
- byte[] result = null;
- try {
- // 对公钥解密
- byte[] bytes = decryptBase64(key);
- // 取得公钥
- X509EncodedKeySpec keySpec = new X509EncodedKeySpec(bytes);
- KeyFactory factory = KeyFactory.getInstance(KEY_RSA);
- PublicKey publicKey = factory.generatePublic(keySpec);
- // 对数据解密
- Cipher cipher = Cipher.getInstance(factory.getAlgorithm());
- cipher.init(Cipher.DECRYPT_MODE, publicKey);
- result = cipher.doFinal(data);
- } catch (Exception e) {
- e.printStackTrace();
- }
- return result;
- }
- /**
- * 公钥加密
- * @param data 待加密数据
- * @param key 公钥
- * @return
- */
- public static byte[] encryptByPublicKey(byte[] data, String key) {
- byte[] result = null;
- try {
- byte[] bytes = decryptBase64(key);
- // 取得公钥
- X509EncodedKeySpec keySpec = new X509EncodedKeySpec(bytes);
- KeyFactory factory = KeyFactory.getInstance(KEY_RSA);
- PublicKey publicKey = factory.generatePublic(keySpec);
- // 对数据加密
- Cipher cipher = Cipher.getInstance(factory.getAlgorithm());
- cipher.init(Cipher.ENCRYPT_MODE, publicKey);
- result = cipher.doFinal(data);
- } catch (Exception e) {
- e.printStackTrace();
- }
- return result;
- }
- /**
- * 私钥加密
- * @param data 待加密数据
- * @param key 私钥
- * @return
- */
- public static byte[] encryptByPrivateKey(byte[] data, String key) {
- byte[] result = null;
- try {
- byte[] bytes = decryptBase64(key);
- // 取得私钥
- PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(bytes);
- KeyFactory factory = KeyFactory.getInstance(KEY_RSA);
- PrivateKey privateKey = factory.generatePrivate(keySpec);
- // 对数据加密
- Cipher cipher = Cipher.getInstance(factory.getAlgorithm());
- cipher.init(Cipher.ENCRYPT_MODE, privateKey);
- result = cipher.doFinal(data);
- } catch (Exception e) {
- e.printStackTrace();
- }
- return result;
- }
- /**
- * 获取公钥
- * @param map
- * @return
- */
- public static String getPublicKey(Map<String, Object> map) {
- String str = "";
- try {
- Key key = (Key) map.get(KEY_RSA_PUBLICKEY);
- str = encryptBase64(key.getEncoded());
- } catch (Exception e) {
- e.printStackTrace();
- }
- return str;
- }
- /**
- * 获取私钥
- * @param map
- * @return
- */
- public static String getPrivateKey(Map<String, Object> map) {
- String str = "";
- try {
- Key key = (Key) map.get(KEY_RSA_PRIVATEKEY);
- str = encryptBase64(key.getEncoded());
- } catch (Exception e) {
- e.printStackTrace();
- }
- return str;
- }
- /**
- * BASE64 解密
- * @param key 需要解密的字符串
- * @return 字节数组
- * @throws Exception
- */
- public static byte[] decryptBase64(String key) throws Exception {
- return (new BASE64Decoder()).decodeBuffer(key);
- }
- /**
- * BASE64 加密
- * @param key 需要加密的字节数组
- * @return 字符串
- * @throws Exception
- */
- public static String encryptBase64(byte[] key) throws Exception {
- return (new BASE64Encoder()).encodeBuffer(key);
- }
- /**
- * 测试方法
- * @param args
- */
- public static void main(String[] args) {
- String privateKey = "";
- String publicKey = "";
- // 生成公钥私钥
- Map<String, Object> map = init();
- publicKey = getPublicKey(map);
- privateKey = getPrivateKey(map);
- System.out.println("公钥: \n\r" + publicKey);
- System.out.println("私钥: \n\r" + privateKey);
- System.out.println("公钥加密--------私钥解密");
- String word = "你好,世界!";
- byte[] encWord = encryptByPublicKey(word.getBytes(), publicKey);
- String decWord = new String(decryptByPrivateKey(encWord, privateKey));
- System.out.println("加密前: " + word + "\n\r" + "解密后: " + decWord);
- System.out.println("私钥加密--------公钥解密");
- String english = "Hello, World!";
- byte[] encEnglish = encryptByPrivateKey(english.getBytes(), privateKey);
- String decEnglish = new String(decryptByPublicKey(encEnglish, publicKey));
- System.out.println("加密前: " + english + "\n\r" + "解密后: " + decEnglish);
- System.out.println("私钥签名——公钥验证签名");
- // 产生签名
- String sign = sign(encEnglish, privateKey);
- System.out.println("签名:\r" + sign);
- // 验证签名
- boolean status = verify(encEnglish, publicKey, sign);
- System.out.println("状态:\r" + status);
- }
- }</span>
加解密结果
结束语
其实,看似很复杂的过程,用一句话就可以描述:使用公钥加密、私钥解密,完成了乙方到甲方的一次数据传递,通过私钥加密、公钥解密,同时通过私钥签名、公钥验证签名,完成了一次甲方到乙方的数据传递与验证,两次数据传递完成一整套的数据交互。
非对称加密算法的出现,就是为了解决只有一把密钥的加解密,只要这一把密钥丢失或者被公开,那么加密数据就很容易被攻击。同时,也正是由于非对称加密算法的出现,才有了后面的数字签名、数字证书等等。
好了,今天就到这吧,下一篇继续非对称加密,至于哪一种,到时候就知道了,这里先保密,(*^__^*) 嘻嘻。
序
这篇文章继续介绍对称加密算法,至于今天的主角,不用说,也是个厉害的角色 — — AES。AES 的出现,就是为了来替代原先的 DES 标准。现在来说,AES 的用途还是非常广泛的。
概念
AES,全称为“Advanced Encryption Standard”,中文名“高级加密标准”,在密码学中又称 Rijndael 加密法,是美国联邦政府采用的一种区块加密标准。AES 加密算法作为新一代的数据加密标准汇聚了强安全性、高性能、高效率、易用和灵活等优点。AES 设计有三个密钥长度:128,192,256 位。相对而言,AES 的 128 密钥比 DES 的 56 密钥强了 1021 倍。
原理
AES 加密算法主要包括三个方面:轮变化、圈数和密钥扩展。AES 是分组密钥,算法输入 128 位数据,密钥长度也是 128 位。用 Nr 表示对一个数据分组加密的轮数。每一轮都需要一个与输入分组具有相同长度的扩展密钥 Expandedkey(i) 的参与。由于外部输入的加密密钥 K 长度有限,所以在算法中要用一个密钥扩展程序 (Keyexpansion) 把外部密钥 K 扩展成更长的比特串,以生成各轮的加密和解密密钥。
- 圈变化
AES 每一个圈变换由以下三个层组成:
非线性层 — — 进行Subbyte变换;
线行混合层 — — 进行 ShiftRow 和 MixColumn 运算;
密钥加层 — — 进行 AddRoundKey 运算。
- 轮变化
对不同的分组长度,其对应的轮变化次数是不同的。这里就不再多说,有需要的可以继续上网了解。
- 密钥扩展
AES 算法利用外部输入密钥 K (密钥串的字数为Nk),通过密钥的扩展程序得到共计 4(Nr+1) 字的扩展密钥。它涉及如下三个模块:
① 位置变换 (rotword) — — 把一个 4 字节的序列 [A,B,C,D] 变化成 [B,C,D,A];
② S 盒变换 (subword) — — 对一个 4 字节进行 S 盒代替;
③ 变换 Rcon[i] — — Rcon[i] 表示 32 位比特字 [xi-1,00,00,00]。
应用
CCMP 是一种基于 AES 加密算法和 CCM 认证机制,可以大大提高无线网络的安全程度。
WPA(无线局域网受保护访问协议)是一种新型的无线安全技术,它是 IEEE802.11i 的一个子集,核心是 IEEE802.1x 和 TKIP,可用以下公式表示几者之间的关系:
WPA=802.1x+EPA+TKIP+MIC
其中 MIC(信息完整性检查码)是 CCMP 加密技术中的一个重要组成部分:
CCMP 首先用 CBC-MAC 数据源认证模式对消息中的明文头、明文头长度和负载计算出一个 MIC,然后使用计数模式对负载和 MIC 进行加密,产生 128 比特的密匙,其中计数模式的核心算法就是 AES 加密算法。
代码实现
AES 的代码实现,这里就不再提供了,可以参考
DES 中的代码实现,只需要把 算法定义换成 AES 即可,而且,在 DES 的代码中也都有详细的说明,需要的可以跳过去参考一下。
结束语
说到对称加密,在开发中是经常用到的加密算法,主要是涉及到的一些敏感数据,比如,个人信息、家庭住址、手机号码、密保问题、密保答案等等。至于密码什么的,一般都不怎么用对称加密算法(当然也有用的),密码这一类的信息,可以考虑用单向加密算法,比如,MD5、或者 SHA 等,考虑到安全性,可以外加一点 salt,当然,这些在开发的时候就是需要考虑的事了,这里就不细说了。
序
上一篇文章中简单的介绍了一种非对称加密算法 — — RSA,今天这篇文章,继续介绍另一种非对称加密算法 — —
DH。当然,可能有很多人对这种加密算法并不是很熟悉,不过没关系,希望今天这篇文章能帮助你熟悉他。
概念
DH,全称为“Diffie-Hellman”,他是一种确保共享KEY安全穿越不安全网络的方法,也就是常说的密钥一致协议。由公开密钥密码体制的奠基人Diffie和Hellman所提出的一种思想。
简单的说就是允许两名用户在公开媒体上交换信息以生成“一致”的、可以共享的密钥。也就是由甲方产出一对密钥(公钥、私钥),乙方依照甲方公钥产生乙方密钥对(公钥、私钥)。
以此为基线,作为数据传输保密基础,同时双方使用同一种对称加密算法构建本地密钥(SecretKey)对数据加密。这样,在互通了本地密钥(SecretKey)算法后,甲乙双方公开自己的公钥,使用对方的公钥和刚才产生的私钥加密数据,同时可以使用对方的公钥和自己的私钥对数据解密。不单单是甲乙双方两方,可以扩展为多方共享数据通讯,这样就完成了网络交互数据的安全通讯!
原理
- 通信方A和通信方B约定一个初始数g,g是公开的,如g=5;
- A生成一个随机数a,a是保密的,如a=6;
- A计算g^a发送给B,g^a=5^6;
- B生成一个随机数b,b是保密的,如b=15;
- B计算g^b发送给A,g^b=5^15;
- A接收到g^b后,再使用保密的a,计算(g^b)^a=g^(a*b)=5^(6*15) ;
- B接收到g^a后,再使用保密的b,计算(g^a)^b=g^(a*b)=5^(6*15);
- 这样通信方A和B得到一个相同的“密钥”g^(a*b)=5^(6*15)。
整个通信过程中g、g^a、g^b是公开的,但由于g、a、b都是整数,通过g和g^a得到a还是比较容易的,b也是如此,所以最终的“密钥”g^(a*b)还是可以被计算出来的。所以实际的过程还需要在基本原理上加入新的计算——模运算。
- 通信方A和通信方B约定一个初始数g,如g=5,一个质数p,如p=23,g和p是公开的 ;
- A生成一个随机数a,a是保密的,如a=6 ;
- A计算g^a%p发送给B,g^a%p=5^6%23=8 ;
- B生成一个随机数b,b是保密的,如b=15 ;
- B计算g^b%p发送给A,g^b%p=5^15%23=19 ;
- A接收到g^b%p后,再使用保密的a,计算(g^b%p)^a%p=19^6%23=2 ;
- B接收到g^a%p后,再使用保密的b,计算(g^a%p)^b%p=8^15%23=2 ;
- 这样通信方A和B得到一个相同的密钥:2。
(g^b%p)^a%p=(g^a%p)^b%p 证明:
如果a=2:
- (g^b%p)^a%p=(g^b%p)^2%p=(g^b-n*p)^2%p=(g^(2*b)-2*g^b*n*p+(n*p)^2)%p=g^(2*b)%p ;
- 可以看出(g^b-n*p)^2展开后除g^(2*b)外,其它都是p的倍数,所以整个算式的结果是g^(2*b)%p ;
- 同理对(g^b-n*p)^a展开后除g^(a*b)外,其它都是p的倍数,所以整个算式的结果是g^(a*b)%p ;
- 同样可以得出(g^a%p)^b%p=g^(a*b)%p ;
- 所以(g^b%p)^a%p=(g^a%p)^b%p 。
整个通信过程中g、p、g^a%p、g^b%p是公开的,这时通过g、p、g^a%p得到a比较难,同样通过g、p、g^b%p得到b比较难,所以最终的密钥是比较安全的。
以g=5、p=23、g^a%p=8计算a为例,a=log(5, (8+23*n)),这个只能将n的可能值逐个带入公式试验才能得到a的值。如果a、p是比较大的数那么计算更加困难。
需要注意的是,为了防止应用优化算法计算上述问题,质数p不是随便选择的,需要符合一定的条件。随机数a、b的生成算法也必需注意,应使结果尽可能随机,不能出现可预测的规律,否则会使破解变的容易。
代码实现
- <span style="font-family:Comic Sans MS;font-size:12px;">package com.test.dh;
- import com.google.common.collect.Maps;
- import sun.misc.BASE64Decoder;
- import sun.misc.BASE64Encoder;
- import javax.crypto.*;
- import javax.crypto.interfaces.DHPrivateKey;
- import javax.crypto.interfaces.DHPublicKey;
- import javax.crypto.spec.DHParameterSpec;
- import java.security.*;
- import java.security.spec.PKCS8EncodedKeySpec;
- import java.security.spec.X509EncodedKeySpec;
- import java.util.Map;
- /**
- * Created by xiang.li on 2015/3/4.
- * DH 加解密工具类
- */
- public class DH {
- /**
- * 定义加密方式
- */
- private static final String KEY_DH = "DH";
- /**
- * 默认密钥字节数
- */
- private static final int KEY_SIZE = 1024;
- /**
- * DH加密下需要一种对称加密算法对数据加密,这里我们使用DES,也可以使用其他对称加密算法
- */
- private static final String KEY_DH_DES = "DES";
- private static final String KEY_DH_PUBLICKEY = "DHPublicKey";
- private static final String KEY_DH_PRIVATEKEY = "DHPrivateKey";
- /**
- * 初始化甲方密钥
- * @return
- */
- public static Map<String, Object> init() {
- Map<String, Object> map = null;
- try {
- KeyPairGenerator generator = KeyPairGenerator.getInstance(KEY_DH);
- generator.initialize(KEY_SIZE);
- KeyPair keyPair = generator.generateKeyPair();
- // 甲方公钥
- DHPublicKey publicKey = (DHPublicKey) keyPair.getPublic();
- // 甲方私钥
- DHPrivateKey privateKey = (DHPrivateKey) keyPair.getPrivate();
- map = Maps.newHashMap();
- map.put(KEY_DH_PUBLICKEY, publicKey);
- map.put(KEY_DH_PRIVATEKEY, privateKey);
- } catch (NoSuchAlgorithmException e) {
- e.printStackTrace();
- }
- return map;
- }
- /**
- * 初始化乙方密钥
- * @param key 甲方密钥
- * @return
- */
- public static Map<String, Object> init(String key) {
- Map<String, Object> map = null;
- try {
- // 解析甲方密钥
- byte[] bytes = decryptBase64(key);
- X509EncodedKeySpec keySpec = new X509EncodedKeySpec(bytes);
- KeyFactory factory = KeyFactory.getInstance(KEY_DH);
- PublicKey publicKey = factory.generatePublic(keySpec);
- // 由甲方公钥构建乙方密钥
- DHParameterSpec spec = ((DHPublicKey) publicKey).getParams();
- KeyPairGenerator generator = KeyPairGenerator.getInstance(KEY_DH);
- generator.initialize(spec);
- KeyPair keyPair = generator.generateKeyPair();
- // 乙方公钥
- DHPublicKey dhPublicKey = (DHPublicKey) keyPair.getPublic();
- // 乙方私钥
- DHPrivateKey dhPrivateKey = (DHPrivateKey) keyPair.getPrivate();
- map = Maps.newHashMap();
- map.put(KEY_DH_PUBLICKEY, dhPublicKey);
- map.put(KEY_DH_PRIVATEKEY, dhPrivateKey);
- } catch (Exception e) {
- e.printStackTrace();
- }
- return map;
- }
- /**
- * DH 加密
- * @param data 带加密数据
- * @param publicKey 甲方公钥
- * @param privateKey 乙方私钥
- * @return
- */
- public static byte[] encryptDH(byte[] data, String publicKey, String privateKey) {
- byte[] bytes = null;
- try {
- // 生成本地密钥
- SecretKey secretKey = getSecretKey(publicKey, privateKey);
- // 数据加密
- Cipher cipher = Cipher.getInstance(secretKey.getAlgorithm());
- cipher.init(Cipher.ENCRYPT_MODE, secretKey);
- bytes = cipher.doFinal(data);
- } catch (NoSuchAlgorithmException e) {
- e.printStackTrace();
- } catch (NoSuchPaddingException e) {
- e.printStackTrace();
- } catch (InvalidKeyException e) {
- e.printStackTrace();
- } catch (BadPaddingException e) {
- e.printStackTrace();
- } catch (IllegalBlockSizeException e) {
- e.printStackTrace();
- }
- return bytes;
- }
- /**
- * DH 解密
- * @param data 待解密数据
- * @param publicKey 乙方公钥
- * @param privateKey 甲方私钥
- * @return
- */
- public static byte[] decryptDH(byte[] data, String publicKey, String privateKey) {
- byte[] bytes = null;
- try {
- // 生成本地密钥
- SecretKey secretKey = getSecretKey(publicKey, privateKey);
- // 数据解密
- Cipher cipher = Cipher.getInstance(secretKey.getAlgorithm());
- cipher.init(Cipher.DECRYPT_MODE, secretKey);
- bytes = cipher.doFinal(data);
- } catch (NoSuchAlgorithmException e) {
- e.printStackTrace();
- } catch (NoSuchPaddingException e) {
- e.printStackTrace();
- } catch (InvalidKeyException e) {
- e.printStackTrace();
- } catch (BadPaddingException e) {
- e.printStackTrace();
- } catch (IllegalBlockSizeException e) {
- e.printStackTrace();
- }
- return bytes;
- }
- /**
- * 取得私钥
- * @param map
- * @return
- */
- public static String getPrivateKey(Map<String, Object> map) {
- String str = "";
- try {
- Key key = (Key) map.get(KEY_DH_PRIVATEKEY);
- str = encryptBase64(key.getEncoded());
- } catch (Exception e) {
- e.printStackTrace();
- }
- return str;
- }
- /**
- * 取得公钥
- * @param map
- * @return
- */
- public static String getPublicKey(Map<String, Object> map) {
- String str = "";
- try {
- Key key = (Key) map.get(KEY_DH_PUBLICKEY);
- str = encryptBase64(key.getEncoded());
- } catch (Exception e) {
- e.printStackTrace();
- }
- return str;
- }
- /**
- * 构建本地密钥
- * @param publicKey 公钥
- * @param privateKey 私钥
- * @return
- */
- private static SecretKey getSecretKey(String publicKey, String privateKey) {
- SecretKey secretKey = null;
- try {
- // 初始化公钥
- byte[] publicBytes = decryptBase64(publicKey);
- KeyFactory factory = KeyFactory.getInstance(KEY_DH);
- X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicBytes);
- PublicKey localPublicKey = factory.generatePublic(keySpec);
- // 初始化私钥
- byte[] privateBytes = decryptBase64(privateKey);
- PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(privateBytes);
- PrivateKey localPrivateKey = factory.generatePrivate(spec);
- KeyAgreement agreement = KeyAgreement.getInstance(factory.getAlgorithm());
- agreement.init(localPrivateKey);
- agreement.doPhase(localPublicKey, true);
- // 生成本地密钥
- secretKey = agreement.generateSecret(KEY_DH_DES);
- } catch (Exception e) {
- e.printStackTrace();
- }
- return secretKey;
- }
- /**
- * BASE64 解密
- * @param key 需要解密的字符串
- * @return 字节数组
- * @throws Exception
- */
- public static byte[] decryptBase64(String key) throws Exception {
- return (new BASE64Decoder()).decodeBuffer(key);
- }
- /**
- * BASE64 加密
- * @param key 需要加密的字节数组
- * @return 字符串
- * @throws Exception
- */
- public static String encryptBase64(byte[] key) throws Exception {
- return (new BASE64Encoder()).encodeBuffer(key);
- }
- /**
- * 测试方法
- * @param args
- */
- public static void main(String[] args) {
- // 生成甲方密钥对
- Map<String, Object> mapA = init();
- String publicKeyA = getPublicKey(mapA);
- String privateKeyA = getPrivateKey(mapA);
- System.out.println("甲方公钥:\n" + publicKeyA);
- System.out.println("甲方私钥:\n" + privateKeyA);
- // 由甲方公钥产生本地密钥对
- Map<String, Object> mapB = init(publicKeyA);
- String publicKeyB = getPublicKey(mapB);
- String privateKeyB = getPrivateKey(mapB);
- System.out.println("乙方公钥:\n" + publicKeyB);
- System.out.println("乙方私钥:\n" + privateKeyB);
- String word = "abc";
- System.out.println("原文: " + word);
- // 由甲方公钥,乙方私钥构建密文
- byte[] encWord = encryptDH(word.getBytes(), publicKeyA, privateKeyB);
- // 由乙方公钥,甲方私钥解密
- byte[] decWord = decryptDH(encWord, publicKeyB, privateKeyA);
- System.out.println("解密: " + new String(decWord));
- }
- }</span><span style="font-family: 微软雅黑; font-size: 14px;">
- </span>
结果
结束语
这个机制的巧妙在于需要安全通信的双方可以用这个方法确定对称密钥。然后可以用这个密钥进行加密和解密。但是注意,这个密钥交换协议/算法只能用于密钥的交换,而不能进行消息的加密和解密。双方确定要用的密钥后,要使用其他对称密钥操作加密算法实际加密和解密消息。
序
上一篇文章讲的是对称加密算法 — —
DES,这篇文章打算在 DES 的基础上,继续多讲一点,也就是 3 重 DES — — Triple DES。
背景
至于 3DES 为什么会出现呢?其实,这个不难想到。由于 DES 是一种非常简便的加密算法,但是密钥长度比较短,计算量比较小,相对来说,比较容易被破解。因此,在 DES 的基础上,使用三重数据加密算法,对数据进行加密,这样来说,破解的概率就小了很多。
概念
3DES,也就是“Triple DES”,中文名“三重数据加密算法”,它相当于是对每个数据块应用三次 DES 加密算法。由于计算机运算能力的增强,原版 DES 密码的密钥长度变得容易被暴力破解;3DES 即是设计用来提供一种相对简单的方法,即通过增加 DES 的密钥长度来避免类似的攻击,而不是设计一种全新的块密码算法。
原理
使用 3 条 56 位的密钥对数据进行三次加密。3DES(即 Triple DES)是 DES 向 AES 过渡的加密算法(1999年,NIST 将 3-DES 指定为过渡的加密标准)。
其具体实现如下:设 Ek() 和 Dk() 代表 DES 算法的加密和解密过程,K 代表 DES 算法使用的密钥,P 代表明文,C 代表密文,这样:
3DES 加密过程为:
C = Ek3 ( Dk2 ( Ek1 ( P ) ) )
3DES 解密过程为:
P = Dk1 ( EK2 ( Dk3 ( C ) ) )
代码实现
3DES 的代码实现,与 DES 很类似,其实可以参考上一篇文章 — — DES 的代码实现,把算法定义改为 DESede 即可。不过,考虑到参考的方便性,这里还是贴出 3DES 加解密的代码实现,供大家参考。
<span style="font-family:Comic Sans MS;font-size:12px;">package com.sica.des;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.io.ByteArrayOutputStream;
import java.security.Security;
/**
* Created by xiang.li on 2015/3/19.
* TripleDES(3DES) 加解密工具类
*/
public class TripleDES {
private static final String Algorithm = "DESede"; // 定义 加密算法,可用 DES,DESede,Blowfish
private static final String hexString="0123456789ABCDEF";
/**
*
* @param keybyte 加密密钥,长度为24字节
* @param src 字节数组(根据给定的字节数组构造一个密钥。 )
* @return
*/
public static byte[] encryptMode(byte[] keybyte, byte[] src) {
try {
// 根据给定的字节数组和算法构造一个密钥
SecretKey deskey = new SecretKeySpec(keybyte, Algorithm);
// 加密
Cipher c1 = Cipher.getInstance(Algorithm);
c1.init(Cipher.ENCRYPT_MODE, deskey);
return c1.doFinal(src);
} catch (java.security.NoSuchAlgorithmException e1) {
e1.printStackTrace();
} catch (javax.crypto.NoSuchPaddingException e2) {
e2.printStackTrace();
} catch (java.lang.Exception e3) {
e3.printStackTrace();
}
return null;
}
/**
*
* @param keybyte 密钥
* @param src 需要解密的数据
* @return
*/
public static byte[] decryptMode(byte[] keybyte, byte[] src) {
try {
// 生成密钥
SecretKey deskey = new SecretKeySpec(keybyte, Algorithm);
// 解密
Cipher c1 = Cipher.getInstance(Algorithm);
c1.init(Cipher.DECRYPT_MODE, deskey);
return c1.doFinal(src);
} catch (java.security.NoSuchAlgorithmException e1) {
e1.printStackTrace();
} catch (javax.crypto.NoSuchPaddingException e2) {
e2.printStackTrace();
} catch (java.lang.Exception e3) {
e3.printStackTrace();
}
return null;
}
/**
* 字符串转为16进制
* @param str
* @return
*/
public static String encode(String str)
{
//根据默认编码获取字节数组
byte[] bytes=str.getBytes();
StringBuilder sb=new StringBuilder(bytes.length*2);
//将字节数组中每个字节拆解成2位16进制整数
for(int i=0;i<bytes.length;i++)
{
sb.append(hexString.charAt((bytes[i]&0xf0)>>4));
sb.append(hexString.charAt((bytes[i]&0x0f)>>0));
}
return sb.toString();
}
/**
*
* @param bytes
* @return
* 将16进制数字解码成字符串,适用于所有字符(包括中文)
*/
public static String decode(String bytes)
{
ByteArrayOutputStream baos=new ByteArrayOutputStream(bytes.length()/2);
//将每2位16进制整数组装成一个字节
for(int i=0;i<bytes.length();i+=2)
baos.write((hexString.indexOf(bytes.charAt(i))<<4 |hexString.indexOf(bytes.charAt(i+1))));
return new String(baos.toByteArray());
}
// 转换成十六进制字符串
public static String byte2hex(byte[] b) {
String hs = "";
String stmp = "";
for (int n = 0; n < b.length; n++) {
stmp = (java.lang.Integer.toHexString(b[n] & 0XFF));
if (stmp.length() == 1)
hs = hs + "0" + stmp;
else
hs = hs + stmp;
if (n < b.length - 1)
hs = hs + "";
}
return hs.toUpperCase();
}
public static void main(String[] args) {
// 添加新安全算法,如果用JCE就要把它添加进去
//这里addProvider方法是增加一个新的加密算法提供者(个人理解没有找到好的答案,求补充)
Security.addProvider(new com.sun.crypto.provider.SunJCE());
//byte数组(用来生成密钥的)
final byte[] keyBytes = { 0x11, 0x22, 0x4F, 0x58, (byte)0x88, 0x10,
0x40, 0x38, 0x28, 0x25, 0x79, 0x51, (byte) 0xCB, (byte) 0xDD,
0x55, 0x66, 0x77, 0x29, 0x74, (byte) 0x98, 0x30, 0x40, 0x36,
(byte) 0xE2 };
String szSrc = "This is a 3DES test. 测试";
System.out.println("加密前的字符串:" + szSrc);
byte[] encoded = encryptMode(keyBytes, szSrc.getBytes());
System.out.println("加密后的字符串:" + byte2hex(encoded));
byte[] srcBytes = decryptMode(keyBytes, encoded);
System.out.println("解密后的字符串:" + new String(srcBytes));
}
}</span>
结束语
由于 3DES 是在 DES 的基础上发展来的,因此,3DES 的基本原理也与 DES 相差无二,只不过又在其基础上得到了一些改进,了解了这些,再使用 3DES 时,就真的很容易了,虽然 sun 的实现你不了解,但大概的原理清楚了,再了解一下 sun 的源码,自己实现还是很容易的。
序
前几篇文章讲的都是单向加密算法,其中涉及到了 BASE64、MD5、SHA、HMAC 等几个比较常见的加解密算法。这篇文章,以及后面几篇,打算介绍几个对称加密算法,比如:DES、3DES(TripleDES)、AES 等。那么,这篇文章主要是对 DES 大概讲一下。
背景
在讨论 DES 之前,首先了解一下什么是对称加密算法吧。对于对称加密算法,他应用的时间比较早,技术相对来说比较成熟,在对称加密算法中,数据发信方将明文(原始数据)和加密密钥一起经过特殊加密算法处理后,使其变成复杂的加密密文发送出去。收信方收到密文后,若想解读原文,则需要使用加密用过的密钥及相同算法的逆算法对密文进行解密,才能使其恢复成可读明文。
在对称加密算法中,使用的密钥只有一个,发收信双方都使用这个密钥对数据进行加密和解密,这就要求解密方事先必须知道加密密钥。对称加密算法的特点是算法公开、计算量小。不足之处是,交易双方都使用同样钥匙,安全性得不到保证。
概念
那么,什么是 DES?他是怎么来的?相信很多人都很感兴趣,因为以前在开发的时候,对进度的要求比较严,很多时候根本就没有时间来了解这些东西。因此,今天专门来研究研究这个东西。
DES,全称为“Data Encryption Standard”,中文名为“数据加密标准”,是一种使用密钥加密的块算法。DES 算法为密码体制中的对称密码体制,又被称为美国数据加密标准,是 1972 年美国 IBM 公司研制的对称密码体制加密算法。 明文按 64 位进行分组,密钥长 64 位,密钥事实上是 56 位参与 DES 运算(第8、16、24、32、40、48、56、64 位是校验位, 使得每个密钥都有奇数个 1)分组后的明文组和 56 位的密钥按位替代或交换的方法形成密文组的加密方法。
基本原理
入口参数有三个:key、data、mode。key 为加密解密使用的密钥,data 为加密
解密的数据,mode 为其工作模式。当模式为加密模式时,明文按照 64 位进行分组,形成明文组,key 用于对数据加密,当模式为解密模式时,key 用于对数据解密。实际运用中,密钥只用到了 64 位中的 56 位,这样才具有高的安全性。
主要流程
DES 算法把 64 位的明文输入块变为 64 位的密文输出块,它所使用的密钥也是 64 位,其算法主要分为两步:
- 初始置换
其功能是把输入的 64 位数据块按位重新组合,并把输出分为 L0、R0 两部分,每部分各长 32 位,其置换规则为将输入的第 58 位换到第一位,第 50 位换到第 2 位 …… 依此类推,最后一位是原来的第 7 位。L0、R0 则是换位输出后的两部分,L0 是输出的左 32 位,R0 是右 32 位,例:设置换前的输入值为 D1 D2 D3 …… D64,则经过初始置换后的结果为:L0 = D58 D50 …… D8;R0 = D57 D49 …… D7。
- 逆置换
经过 16 次迭代运算后,得到 L16、R16,将此作为输入,进行逆置换,逆置换正好是初始置换的逆运算,由此即得到密文输出。
整个算法 的主流程图如下:
分组模式
- ECB模式
ECB,中文名“电子密码本模式”,是最古老、最简单的模式,将加密的数据分成若干组,每组的大小跟加密密钥长度相同。然后每组都用相同的密钥加密,比如 DES 算法,如果最后一个分组长度不够 64 位,要补齐 64 位。如图所示:
- CBC模式
CBC,中文名“加密块链模式”,与 ECB 模式最大的不同是加入了初始向量。他的特点是,每次加密的密文长度为 64位 ( 8 个字节),当相同的明文使用相同的密钥和初始向量的时候 CBC 模式总是产生相同的密文。
- CFB模式
CFB,中文名“加密反馈模式”,加密反馈模式克服了需要等待 8 个字节才能加密的缺点,它采用了分组密码作为流密码的密钥流生成器。他的特点是,每次加密的 Pi 和 Ci 不大于 64 位;加密算法和解密算法相同,不能适用于公钥算法;使用相同的密钥和初始向量的时候,相同明文使用 CFB 模式加密输出相同的密文;可以使用不同的初始化变量使相同的明文产生不同的密文,防止字典攻击;加密强度依赖于密钥长度;加密块长度过小时,会增加循环的数量,导致开销增加;加密块长度应时 8 位的整数倍(即字节为单位);一旦某位数据出错,会影响目前和其后 8 个块的数据。
- OFB模式
OFB,中文名“输出反馈模式”,与 CFB 模式不同之处在于, 加密位移寄存器与密文无关了,仅与加密 key 和加密算法有关,做法是不再把密文输入到加密移位寄存器,而是把输出的分组密文(Oi)输入到一位寄存器。因为密文没有参与链操作,所以使得 OFB 模式更容易受到攻击;不会进行错误传播,某位密文发生错误,只会影响该位对应的明文,而不会影响别的位;不是自同步的,如果加密和解密两个操作失去同步,那么系统需要重新初始化;每次重新同步时,应使用不同的初始向量。可以避免产生相同的比特流,避免“已知明文”攻击。
- CTR模式
CTR,中文名“计数模式”,是对一系列输入数据块(称为计数)进行加密,产生一系列的输出块,输出块与明文异或得到密文。对于最后的数据块,可能是长 u 位的局部数据块,这 u 位就将用于异或操作,而剩下的 b-u 位将被丢弃(b表示块的长度)。
代码实现
- <span style="font-family:Comic Sans MS;"><span style="font-size:12px;">package com.sica.des;
- import com.google.common.base.Strings;
- import sun.misc.BASE64Decoder;
- import sun.misc.BASE64Encoder;
- import javax.crypto.Cipher;
- import javax.crypto.KeyGenerator;
- import javax.crypto.SecretKey;
- import javax.crypto.SecretKeyFactory;
- import javax.crypto.spec.DESKeySpec;
- import java.security.InvalidKeyException;
- import java.security.Key;
- import java.security.NoSuchAlgorithmException;
- import java.security.SecureRandom;
- import java.security.spec.InvalidKeySpecException;
- /**
- * Created by xiang.li on 2015/2/28.
- * DES 加解密工具类
- *
- * <pre>
- * 支持 DES、DESede(TripleDES,就是3DES)、AES、Blowfish、RC2、RC4(ARCFOUR)
- * DES key size must be equal to 56
- * DESede(TripleDES) key size must be equal to 112 or 168
- * AES key size must be equal to 128, 192 or 256,but 192 and 256 bits may not be available
- * Blowfish key size must be multiple of 8, and can only range from 32 to 448 (inclusive)
- * RC2 key size must be between 40 and 1024 bits
- * RC4(ARCFOUR) key size must be between 40 and 1024 bits
- * 具体内容 需要关注 JDK Document http://.../docs/technotes/guides/security/SunProviders.html
- * </pre>
- */
- public class DES {
- /**
- * 定义加密方式
- */
- private final static String KEY_DES = "DES";
- private final static String KEY_AES = "AES"; // 测试
- /**
- * 全局数组
- */
- private final static String[] hexDigits = { "0", "1", "2", "3", "4", "5",
- "6", "7", "8", "9", "a", "b", "c", "d", "e", "f" };
- /**
- * 初始化密钥
- * @return
- */
- public static String init() {
- return init(null);
- }
- /**
- * 初始化密钥
- * @param seed 初始化参数
- * @return
- */
- public static String init(String seed) {
- SecureRandom secure = null;
- String str = "";
- try {
- if (null != secure) {
- // 带参数的初始化
- secure = new SecureRandom(decryptBase64(seed));
- } else {
- // 不带参数的初始化
- secure = new SecureRandom();
- }
- KeyGenerator generator = KeyGenerator.getInstance(KEY_DES);
- generator.init(secure);
- SecretKey key = generator.generateKey();
- str = encryptBase64(key.getEncoded());
- } catch (Exception e) {
- e.printStackTrace();
- }
- return str;
- }
- /**
- * 转换密钥
- * @param key 密钥的字节数组
- * @return
- */
- private static Key byteToKey(byte[] key) {
- SecretKey secretKey = null;
- try {
- DESKeySpec dks = new DESKeySpec(key);
- SecretKeyFactory factory = SecretKeyFactory.getInstance(KEY_DES);
- secretKey = factory.generateSecret(dks);
- // 当使用其他对称加密算法时,如AES、Blowfish等算法时,用下述代码替换上述三行代码
- // secretKey = new SecretKeySpec(key, KEY_DES);
- } catch (InvalidKeyException e) {
- e.printStackTrace();
- } catch (NoSuchAlgorithmException e) {
- e.printStackTrace();
- } catch (InvalidKeySpecException e) {
- e.printStackTrace();
- }
- return secretKey;
- }
- /**
- * DES 解密
- * @param data 需要解密的字符串
- * @param key 密钥
- * @return
- */
- public static String decryptDES(String data, String key) {
- // 验证传入的字符串
- if (Strings.isNullOrEmpty(data)) {
- return "";
- }
- // 调用解密方法完成解密
- byte[] bytes = decryptDES(hexString2Bytes(data), key);
- // 将得到的字节数组变成字符串返回
- return new String(bytes);
- }
- /**
- * DES 解密
- * @param data 需要解密的字节数组
- * @param key 密钥
- * @return
- */
- public static byte[] decryptDES(byte[] data, String key) {
- byte[] bytes = null;
- try {
- Key k = byteToKey(decryptBase64(key));
- Cipher cipher = Cipher.getInstance(KEY_DES);
- cipher.init(Cipher.DECRYPT_MODE, k);
- bytes = cipher.doFinal(data);
- } catch (Exception e) {
- e.printStackTrace();
- }
- return bytes;
- }
- /**
- * DES 加密
- * @param data 需要加密的字符串
- * @param key 密钥
- * @return
- */
- public static String encryptDES(String data, String key) {
- // 验证传入的字符串
- if (Strings.isNullOrEmpty(data)) {
- return "";
- }
- // 调用加密方法完成加密
- byte[] bytes = encryptDES(data.getBytes(), key);
- // 将得到的字节数组变成字符串返回
- return byteArrayToHexString(bytes);
- }
- /**
- * DES 加密
- * @param data 需要加密的字节数组
- * @param key 密钥
- * @return
- */
- public static byte[] encryptDES(byte[] data, String key) {
- byte[] bytes = null;
- try {
- Key k = byteToKey(decryptBase64(key));
- Cipher cipher = Cipher.getInstance(KEY_DES);
- cipher.init(Cipher.ENCRYPT_MODE, k);
- bytes = cipher.doFinal(data);
- } catch (Exception e) {
- e.printStackTrace();
- }
- return bytes;
- }
- /**
- * BASE64 解密
- * @param key 需要解密的字符串
- * @return 字节数组
- * @throws Exception
- */
- public static byte[] decryptBase64(String key) throws Exception {
- return (new BASE64Decoder()).decodeBuffer(key);
- }
- /**
- * BASE64 加密
- * @param key 需要加密的字节数组
- * @return 字符串
- * @throws Exception
- */
- public static String encryptBase64(byte[] key) throws Exception {
- return (new BASE64Encoder()).encodeBuffer(key);
- }
- /**
- * 将一个字节转化成十六进制形式的字符串
- * @param b 字节数组
- * @return 字符串
- */
- private static String byteToHexString(byte b) {
- int ret = b;
- //System.out.println("ret = " + ret);
- if (ret < 0) {
- ret += 256;
- }
- int m = ret / 16;
- int n = ret % 16;
- return hexDigits[m] + hexDigits[n];
- }
- /**
- * 转换字节数组为十六进制字符串
- * @param bytes 字节数组
- * @return 十六进制字符串
- */
- private static String byteArrayToHexString(byte[] bytes) {
- StringBuffer sb = new StringBuffer();
- for (int i = 0; i < bytes.length; i++) {
- sb.append(byteToHexString(bytes[i]));
- }
- return sb.toString();
- }
- /**
- * 转换十六进制字符串为字节数组
- * @param hexstr 十六进制字符串
- * @return
- */
- 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 c 字符
- * @return
- */
- 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;
- }
- /**
- * 测试方法
- * @param args
- */
- public static void main(String[] args) {
- String key = DES.init();
- System.out.println("DES密钥:\n" + key);
- String word = "123";
- String encWord = encryptDES(word, key);
- System.out.println(word + "\n加密后:\n" + encWord);
- System.out.println(word + "\n解密后:\n" + decryptDES(encWord, key));
- }
- }</span><span style="font-size: 14px;">
- </span></span>
结束语
到这里,这篇文章也就差不多要结束了,希望以上的内容对各位看官有稍许的帮助,哪怕一点也好。其实,在日常的开发中,如果不是进度控制的特别严格,对于这些原理性的东西,我们还是需要知道的,对于那些细节的东西,可以不用死记硬背,有网的话,随用随查就可以了。但这个前提是,原理性的东西必须要懂,知道了原理,就会有解决思路,有了思路,解决问题是迟早的事,细节嘛,不用那么纠结,做的时候考虑到就行了,毕竟时间是有限的。
序
上一篇文章中简单的介绍了第二种单向加密算法 — —
SHA,同时也给出了 SHA-1 的 Java 代码。有这方面需求的童鞋可以去参考一下。今天这篇文章将要介绍第三种单向加密算法 — — HMAC,其实,这种加密算法并不是那么常用,最起码,在我写系列博客之前,我是没有听说过它的。当然,这并不是说 HMAC 不出名,肯定是我孤落寡闻了。
背景
之所以在单向加密算法中介绍 HMAC 这种“不常见的”算法,一是因为“没见过”,二是因为毕竟是同属于单向加密算法中的一种,而且还是基于密钥的哈希算法的认证协议。因此,还是决定简单的介绍一下。
正文
HMAC,全称为“Hash Message Authentication Code”,中文名“散列消息鉴别码”,主要是利用哈希算法,以一个密钥和一个消息为输入,生成一个消息摘要作为输出。一般的,消息鉴别码用于验证传输于两个共
同享有一个密钥的单位之间的消息。HMAC 可以与任何迭代散列函数捆绑使用。MD5 和 SHA-1 就是这种散列函数。
HMAC 还可以使用一个用于计算和确认消息鉴别值的密钥。
HMAC,散列消息鉴别码,是基于密钥的 Hash 算法的认证协议。它的实现原理是,用公开函数和密钥产生一个固定长度的值作为认证标识,用这个标识鉴别消息的完整性。使用一个密钥生成一个固定大小的小数据块,即 MAC,并将其加入到消息中,然后传输。接收方利用与发送方共享的密钥进行鉴别认证等。
这种结构的主要作用是:
- 不用修改就可以使用适合的散列函数,而且散列函数在软件方面表现的很好, 并且源码是公开和通用的。
- 可以保持散列函数原有的性能而不致使其退化。
- 可以使得基于合理的关于底层散列函数假设的消息鉴别机制的加密强度分析 便于理解。
- 当发现或需要运算速度更快或更安全的散列函数时,可以很容易的实现底层 散列函数的替换。
定义 HMAC 需要一个加密用散列函数(表示为 H)和一个密钥 K。我们假设 H 是
一个将数据块用一个基本的迭代压缩函数来加密的散列函数。我们用 B 来表示数据块
的字长。(以上提到的散列函数的分割数据块字长 B = 64),用 L 来表示散列函数的
输出数据字长(MD5中 L = 16 , SHA-1 中 L = 20)。鉴别密钥的长度可以是小于等于数
据块字长的任何正整数值。应用程序中使用的密钥长度若是比 B 大,则首先用使用散列
函数 H 作用于它,然后用 H 输出的 L 长度字符串作为在 HMAC 中实际使用的密钥。
一般情况下,推荐的最小密钥 K 长度是 L 个字长。(与 H 的输出数据长度相等)。
我们将定义两个固定且不同的字符串 ipad,opad:
(‘i’,‘o’表示内部与外部)
- ipad = the byte 0x36 repeated B times
- opad = the byte 0x5C repeated B times
- H (K XOR opad, H (K XOR ipad, text))
计算步骤
- 在密钥 K 后面添加 0 来创建一个子长为 B 的字符串。(例如,如果 K 的字长是 20 字节,B=60 字节,则 K 后会加入 44 个零字节0x00)
- 将上一步生成的 B 字长的字符串与 ipad 做异或运算
- 将数据流 text 填充至第二步的结果字符串中
- 用 H 作用于第三步生成的数据流
- 将第一步生成的 B 字长字符串与 opad 做异或运算
- 再将第四步的结果填充进第五步的结果中
- 用 H 作用于第六步生成的数据流,输出最终结果
密钥
用于 HMAC 的密钥可以是任意长度(比 B 长的密钥将首先被 H 处理)。但当密钥
长度小于 L 时,会降低函数的安全强度。长度大于
L 的密钥也是可以的,但额外的长度并不能显著的提高函数的安全强度。
密钥必须随机选取(或使用强大的基于随机种子的伪随机生成方法),并且要周期
性的更新。目前的攻击没有指出一个有效的更换密钥的频率,因为那些攻击实际上并
不可行。然而,周期性更新密钥是一个对付函数和密钥所存在的潜在缺陷的基本
的安全措施,并可以降低泄漏密钥带来的危害。
代码实现
- <span style="font-family:Comic Sans MS;font-size:12px;">package com.sica.hmac;
- import com.google.common.base.Strings;
- import sun.misc.BASE64Decoder;
- import sun.misc.BASE64Encoder;
- import javax.crypto.KeyGenerator;
- import javax.crypto.Mac;
- import javax.crypto.SecretKey;
- import javax.crypto.spec.SecretKeySpec;
- import java.security.NoSuchAlgorithmException;
- /**
- * Created by xiang.li on 2015/2/27.
- */
- public class HMAC {
- /**
- * 定义加密方式
- * MAC算法可选以下多种算法
- * <pre>
- * HmacMD5
- * HmacSHA1
- * HmacSHA256
- * HmacSHA384
- * HmacSHA512
- * </pre>
- */
- private final static String KEY_MAC = "HmacMD5";
- /**
- * 全局数组
- */
- private final static String[] hexDigits = { "0", "1", "2", "3", "4", "5",
- "6", "7", "8", "9", "a", "b", "c", "d", "e", "f" };
- /**
- * 构造函数
- */
- public HMAC() {
- }
- /**
- * BASE64 加密
- * @param key 需要加密的字节数组
- * @return 字符串
- * @throws Exception
- */
- public static String encryptBase64(byte[] key) throws Exception {
- return (new BASE64Encoder()).encodeBuffer(key);
- }
- /**
- * BASE64 解密
- * @param key 需要解密的字符串
- * @return 字节数组
- * @throws Exception
- */
- public static byte[] decryptBase64(String key) throws Exception {
- return (new BASE64Decoder()).decodeBuffer(key);
- }
- /**
- * 初始化HMAC密钥
- * @return
- */
- public static String init() {
- SecretKey key;
- String str = "";
- try {
- KeyGenerator generator = KeyGenerator.getInstance(KEY_MAC);
- key = generator.generateKey();
- str = encryptBase64(key.getEncoded());
- } catch (NoSuchAlgorithmException e) {
- e.printStackTrace();
- } catch (Exception e) {
- e.printStackTrace();
- }
- return str;
- }
- /**
- * HMAC加密
- * @param data 需要加密的字节数组
- * @param key 密钥
- * @return 字节数组
- */
- public static byte[] encryptHMAC(byte[] data, String key) {
- SecretKey secretKey;
- byte[] bytes = null;
- try {
- secretKey = new SecretKeySpec(decryptBase64(key), KEY_MAC);
- Mac mac = Mac.getInstance(secretKey.getAlgorithm());
- mac.init(secretKey);
- bytes = mac.doFinal(data);
- } catch (Exception e) {
- e.printStackTrace();
- }
- return bytes;
- }
- /**
- * HMAC加密
- * @param data 需要加密的字符串
- * @param key 密钥
- * @return 字符串
- */
- public static String encryptHMAC(String data, String key) {
- if (Strings.isNullOrEmpty(data)) {
- return null;
- }
- byte[] bytes = encryptHMAC(data.getBytes(), key);
- return byteArrayToHexString(bytes);
- }
- /**
- * 将一个字节转化成十六进制形式的字符串
- * @param b 字节数组
- * @return 字符串
- */
- private static String byteToHexString(byte b) {
- int ret = b;
- //System.out.println("ret = " + ret);
- if (ret < 0) {
- ret += 256;
- }
- int m = ret / 16;
- int n = ret % 16;
- return hexDigits[m] + hexDigits[n];
- }
- /**
- * 转换字节数组为十六进制字符串
- * @param bytes 字节数组
- * @return 十六进制字符串
- */
- private static String byteArrayToHexString(byte[] bytes) {
- StringBuffer sb = new StringBuffer();
- for (int i = 0; i < bytes.length; i++) {
- sb.append(byteToHexString(bytes[i]));
- }
- return sb.toString();
- }
- /**
- * 测试方法
- * @param args
- */
- public static void main(String[] args) throws Exception {
- String key = HMAC.init();
- System.out.println("Mac密钥:\n" + key);
- String word = "123";
- System.out.println(encryptHMAC(word, key));
- }
- }</span>
结束语
看完这篇文章之后,HMAC 你已经了解了很多了,以后再遇到这个名词,当然你也可以说出个一二三来。不过,在应用中,或许一般情况下用不到,如果考虑安全方面的因素,我想,这种不可逆的加密算法还是不错的,因为你需要额外提供一组密钥,而这组密钥对于外人来说是不知道的,因此,安全性相比较来说还是很可靠的。
序
上一篇文章中介绍了基本的单向加密算法 — —
MD5,也大致的说了说它实现的原理。这篇文章继续之前提到的单向加密,主要讲的是 SHA,同 MD5 一样,SHA 同样也是一个系列,它包括 SHA-1,SHA-224,SHA-256,SHA-384,和 SHA-512 等几种算法。其中,SHA-1,SHA-224 和 SHA-256 适用于长度不超过 2^64 二进制位的消息。SHA-384 和 SHA-512 适用于长度不超过 2^128 二进制位的消息。
背景
开始正文之前,简单的说一下背景。乍一说 SHA 你可能不知道,但说到散列和散列算法,你一定会知道,也就是平常所指的 Hash。那么,先了解一下什么是散列。散列,是信息的提炼,通常其长度要比信息小得多,且为一个固定长度。加密性强的散列一定是不可逆的,这就意味着通过散列结果,无法推出任何部分的原始信息。说的很明确,散列的结果是不可逆的,根据散列结果,无法推出原始信息。
正文
了解了背景之后,我们就开始介绍 SHA 了。
SHA,全称为“Secure Hash Algorithm”,中文名“安全哈希算法”,主要适用于数字签名标准(Digital Signature Standard DSS)里面定义的数字签名算法(Digital Signature Algorithm DSA)。对于长度小于 2^64 位的消息,SHA1 会产生一个 160 位的消息摘要。
该算法的思想是接收一段明文,然后以一种不可逆的方式将它转换成一段(通常更小)密文,也可以简单的理解为取一串输入码(称为预映射或信息),并把它们转化为长度较短、位数固定的输出序列即散列值的过程。
上边也提到了,SHA 规定了很多种算法,包括了 SHA-1,SHA-224,SHA-256,等很多种。这里我以 SHA-1 为例,讲一下 SHA-1 是如何工作的。
SHA-1 有两个特点:
- 不可以从消息摘要中复原信息
- 两个不同的消息,不会产生同样的消息摘要
SHA-1 是一种数据加密算法,主要是接收一段明文,然后以一种不可逆的方式将它转换成一段密文,也可以简单的理解为取一串输入码,并把它们转化为长度较短、位数固定的输出序列即散列值的过程。
单向散列函数的安全性在于其产生散列值的操作过程具有较强的单向性。如果在输入序列中嵌入密码,那么任何人在不知道密码的情况下都不能产生正确的散列值,从而保证了其安全性。SHA 将输入流按照每块 512 位(64 个字节)进行分块,并产生 20 个字节的被称为信息认证代码或信息摘要的输出。
该算法输入报文的长度不限,产生的输出是一个 160 位的报文摘要。输入是按 512 位的分组进行处理的。SHA-1 是不可逆的、防冲突,并具有良好的雪崩效应。
通过散列算法可实现数字签名实现,数字签名的原理是将要传送的明文通过一种函数运算(Hash)转换成报文摘要(不同的明文对应不同的报文摘要),报文摘要加密后与明文一起传送给接受方,接受方将接受的明文产生新的报文摘要与发送方的发来报文摘要解密比较,比较结果一致表示明文未被改动,如果不一致表示明文已被篡改。
SHA-1 与 MD5 的比较
因为二者均由 MD4 导出,SHA-1 和 MD5 彼此很相似。相应的,他们的强度和其他特性也是相似,但还有以下几点不同:
- 对强行攻击的安全性
- 对密码分析的安全性
由于 MD5 的设计,易受密码分析的攻击,SHA-1 显得不易受这样的攻击。
- 速度
在相同的硬件上,SHA-1 的运行速度比 MD5 慢。
代码实现
- <span style="font-family:Comic Sans MS;font-size:12px;">package com.sica.sha;
- import com.google.common.base.Strings;
- import java.security.MessageDigest;
- /**
- * Created by xiang.li on 2015/2/11.
- */
- public class SHA {
- /**
- * 定义加密方式
- */
- private final static String KEY_SHA = "SHA";
- private final static String KEY_SHA1 = "SHA-1";
- /**
- * 全局数组
- */
- private final static String[] hexDigits = { "0", "1", "2", "3", "4", "5",
- "6", "7", "8", "9", "a", "b", "c", "d", "e", "f" };
- /**
- * 构造函数
- */
- public SHA() {
- }
- /**
- * SHA 加密
- * @param data 需要加密的字节数组
- * @return 加密之后的字节数组
- * @throws Exception
- */
- public static byte[] encryptSHA(byte[] data) throws Exception {
- // 创建具有指定算法名称的信息摘要
- // MessageDigest sha = MessageDigest.getInstance(KEY_SHA);
- MessageDigest sha = MessageDigest.getInstance(KEY_SHA1);
- // 使用指定的字节数组对摘要进行最后更新
- sha.update(data);
- // 完成摘要计算并返回
- return sha.digest();
- }
- /**
- * SHA 加密
- * @param data 需要加密的字符串
- * @return 加密之后的字符串
- * @throws Exception
- */
- public static String encryptSHA(String data) throws Exception {
- // 验证传入的字符串
- if (Strings.isNullOrEmpty(data)) {
- return "";
- }
- // 创建具有指定算法名称的信息摘要
- MessageDigest sha = MessageDigest.getInstance(KEY_SHA);
- // 使用指定的字节数组对摘要进行最后更新
- sha.update(data.getBytes());
- // 完成摘要计算
- byte[] bytes = sha.digest();
- // 将得到的字节数组变成字符串返回
- return byteArrayToHexString(bytes);
- }
- /**
- * 将一个字节转化成十六进制形式的字符串
- * @param b 字节数组
- * @return 字符串
- */
- private static String byteToHexString(byte b) {
- int ret = b;
- //System.out.println("ret = " + ret);
- if (ret < 0) {
- ret += 256;
- }
- int m = ret / 16;
- int n = ret % 16;
- return hexDigits[m] + hexDigits[n];
- }
- /**
- * 转换字节数组为十六进制字符串
- * @param bytes 字节数组
- * @return 十六进制字符串
- */
- private static String byteArrayToHexString(byte[] bytes) {
- StringBuffer sb = new StringBuffer();
- for (int i = 0; i < bytes.length; i++) {
- sb.append(byteToHexString(bytes[i]));
- }
- return sb.toString();
- }
- /**
- * 测试方法
- * @param args
- */
- public static void main(String[] args) throws Exception {
- String key = "123";
- System.out.println(encryptSHA(key));
- }
- }</span>
结束语
看到这,我想 SHA-1 的简单原理你应该是了解了,而且,对于应用来说也并不难,可以参考上述的 Java 代码。回过头来在想想 MD5,通过上面的文章,你就能知道,其实,SHA-1 与 MD5 是同出一辙的,只是他们各自的实现方式不同,SHA-1 在位操作数量级上也要比 MD5 更加的复杂,因此,对于安全的考虑,SHA-1 相对来说还是很可靠的。
至于什么时候会用到它,那么就要考虑 SHA-1 的特点了。很明确的,不可逆性,以及唯一性。那么,我想,适用于 MD5 的加密的,也同样适用于 SHA-1。而且,在安全性上来说,SHA-1 更胜于 MD5,如果是对速度有严格要求的话,那么,还是优先考虑 MD5 吧。
序
上一篇文章中,介绍了最基础的编码方式 — —
BASE64,也简单的提了一下编码的原理。这篇文章继续加解密的系列,当然也是介绍比较基础的加密方式 — — MD5,MD5 属于单向加密算法,是不可逆的加密方式,也就是说,采用了 MD5 加密方式加密之后,就不能对加密的结果进行解密,得到原有的字符串,这是不可以的。
背景
相信在我们的生活中,MD5 用到的还是很广泛的。在说 MD5 之前,首先来了解一下单向加密算法都有哪些。当然,MD5 是其中之一,除此之外还有,SHA,HMAC 等这几种算法。不过,今天这篇文章,我们只介绍 MD5,至于 SHA 和 HMAC 在后续的文章中会陆续的介绍。
正文
MD5,全称为“Message Digest Algorithm 5”,中文名“消息摘要算法第五版”,它是计算机安全领域广泛使用的一种散列函数,用以提供消息的完整性保护。严格来说,它是一种摘要算法,是确保信息完整性的。不过,在某种意义上来说,也可以算作一种加密算法。
MD5 算法具有很多特点:
- 压缩性:任意长度的数据,算出的MD5值长度都是固定的。
- 容易计算:从原数据计算出MD5值很容易。
- 抗修改性:对原数据进行任何改动,哪怕只修改1个字节,所得到的MD5值都有很大区别。
- 弱抗碰撞:已知原数据和其MD5值,想找到一个具有相同MD5值的数据(即伪造数据)是非常困难的。
- 强抗碰撞:想找到两个不同的数据,使它们具有相同的MD5值,是非常困难的。
MD5 的作用是让大容量信息在用数字签名软件签署私人密钥前被"压缩"成一种保密的格式(就是把一个任意长度的字节串变换成一定长的十六进制数字串)。
MD5 其实在我们生活中是很常用的,似乎你并没有注意到,当你下载了一个镜像之后,你会发现下载页面还提供了一组 MD5 值,那么这组 MD5 值是用来做什么的呢?了解了 MD5 的作用之后,你就不难想到,MD5 是用来验证文件的一致性的,当你下载好镜像之后,你需要对该镜像做一次 MD5 的校验,得到的 MD5 值与下载页面提供的 MD5 值进行对比,以此来验证该镜像是否被篡改。
为什么 MD5 就可以进行一致性校验呢?
其实,MD5 就和人的指纹一样,每个人的指纹都是唯一的,而文件的 MD5 值也是唯一的。至于为什么会这样呢?下面我们看一下 MD5 的工作原理。
对 MD5 算法简要的叙述可以为:MD5 以 512 位分组来处理输入的信息,且每一分组又被划分为 16 个 32 位子分组,经过了一系列的处理后,算法的输出由四个 32 位分组组成,将这四个 32 位分组级联后将生成一个 128 位散列值。
总体流程如下图所示, 表示第 i 个分组,每次的运算都由前一轮的 128 位结果值和第 i 块 512 bit 值进行运算。
填充
在 MD5 算法中,首先需要对信息进行填充,使其位长对 512 求余的结果等于 448,并且填充必须进行,即使其位长对 512 求余的结果等于 448。因此,信息的位长(Bits Length)将被扩展至 N * 512 + 448,N 为一个非负整数,N 可以是零。
填充的方法如下:
1) 在信息的后面填充一个 1 和无数个 0,直到满足上面的条件时才停止用 0 对信息的填充。
2) 在这个结果后面附加一个以 64 位二进制表示的填充前信息长度(单位为Bit),如果二
进制表示的填充前信息长度超过 64 位,则取低 64 位。
经过这两步的处理,信息的位长 = N * 512 + 448 + 64 = (N + 1)* 512,即长度恰好是 512 的整数倍。这样做的原因是为满足后面处理中对信息长度的要求。
初始化变量
初始的 128 位值为初试链接变量,这些参数用于第一轮的运算,以大端字节序来表示,他们分别为: A = 0x01234567,B = 0x89ABCDEF,C = 0xFEDCBA98,D = 0x76543210。
(每一个变量给出的数值是高字节存于内存低地址,低字节存于内存高地址,即大端字节序。在程序中变量 A、B、C、D 的值分别为0x67452301,0xEFCDAB89,0x98BADCFE,0x10325476)
处理分组数据
每一分组的算法流程如下:
第一分组需要将上面四个链接变量复制到另外四个变量中:A 到 a,B 到 b,C 到 c,D 到 d。从第二分组开始的变量为上一分组的运算结果,即 A = a, B = b, C = c, D = d。
主循环有四轮,每轮循环都很相似。第一轮进行 16 次操作。每次操作对 a、b、c 和 d 中的其中三个作一次非线性函数运算,然后将所得结果加上第四个变量,文本的一个子分组和一个常数。再将所得结果向左环移一个不定的数,并加上 a、b、c 或 d 中之一。最后用该结果取代 a、b、c 或 d 中之一。
输出
最后的输出是 a、b、c 和 d 的级联。
代码
这里提供一个 Java 版本的实现,不过需要说明的一点是,这个 Java 实现对于英文的 MD5 是没有问题的,但对于中文会有点问题,因此,推荐只作为学习为目的来参考。如果是项目中的生产需要,请选择 jdk 中自带的 MD5 加密函数。
- <span style="font-family:Arial;font-size:12px;">package com.sica.md5.impl;
- /**
- * Created by xiang.li on 2015/2/26.
- */
- public class MD5 {
- /**
- * 单例
- */
- private static MD5 instance;
- /**
- * 四个链接变量
- */
- private final int A = 0x67452301;
- private final int B = 0xefcdab89;
- private final int C = 0x98badcfe;
- private final int D = 0x10325476;
- /**
- * ABCD的临时变量
- */
- private int Atemp;
- private int Btemp;
- private int Ctemp;
- private int Dtemp;
- /**
- * 常量ti
- * 公式:floor(abs(sin(i+1))×(2pow32)
- */
- private final int[] K = {
- 0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee,
- 0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501, 0x698098d8,
- 0x8b44f7af, 0xffff5bb1, 0x895cd7be, 0x6b901122, 0xfd987193,
- 0xa679438e, 0x49b40821, 0xf61e2562, 0xc040b340, 0x265e5a51,
- 0xe9b6c7aa, 0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8,
- 0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed, 0xa9e3e905,
- 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a, 0xfffa3942, 0x8771f681,
- 0x6d9d6122, 0xfde5380c, 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60,
- 0xbebfbc70, 0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x04881d05,
- 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665, 0xf4292244,
- 0x432aff97, 0xab9423a7, 0xfc93a039, 0x655b59c3, 0x8f0ccc92,
- 0xffeff47d, 0x85845dd1, 0x6fa87e4f, 0xfe2ce6e0, 0xa3014314,
- 0x4e0811a1, 0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391
- };
- /**
- * 向左位移数,计算方法未知
- */
- private final int[] s = {
- 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7,
- 12, 17, 22, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20,
- 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 6, 10,
- 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21
- };
- /**
- * 私有构造函数
- */
- private MD5() {
- }
- /**
- * 单例模式
- * @return
- */
- public static MD5 getInstance() {
- if (instance == null) {
- instance = new MD5();
- }
- return instance;
- }
- /**
- * 初始化函数
- */
- private void init() {
- Atemp = A;
- Btemp = B;
- Ctemp = C;
- Dtemp = D;
- }
- /**
- * 移动一定位数
- * @param a
- * @param s
- * @return
- */
- private int shift(int a, int s) {
- return (a << s) | (a >>> (32 - s)); // 右移的时候,高位一定要补零,而不是补充符号位
- }
- /**
- * 主循环
- * @param M
- */
- private void mainLoop(int[] M) {
- int F;
- int g;
- int a = Atemp;
- int b = Btemp;
- int c = Ctemp;
- int d = Dtemp;
- for (int i = 0; i < 64; i++) {
- if (i < 16) {
- F = (b & c) | ((~b) & d);
- g = i;
- }else if (i < 32) {
- F = (d & b) | ((~d) & c);
- g = (5 * i + 1) % 16;
- }else if (i < 48) {
- F = b ^ c ^ d;
- g = (3 * i + 5) % 16;
- } else {
- F = c ^ (b | (~d));
- g = (7 * i) % 16;
- }
- int tmp = d;
- d = c;
- c = b;
- b = b + shift(a + F + K[i] + M[g], s[i]);
- a = tmp;
- }
- Atemp += a;
- Btemp += b;
- Ctemp += c;
- Dtemp += d;
- }
- /**
- * 填充函数
- * 处理后应满足bits≡448(mod512),字节就是bytes≡56(mode64)
- * 填充方式为先加一个0,其它位补零
- * 最后加上64位的原来长度
- * @param str
- * @return
- */
- private int[] add(String str) {
- int num = ((str.length() + 8) / 64) + 1; // 以512位,64个字节为一组
- int[] strByte = new int[num * 16]; // 64/4=16,所以有16个整数
- for (int i = 0; i < num * 16; i++) {
- // 全部初始化为0
- strByte[i] = 0;
- }
- int j;
- for (j = 0; j < str.length(); j++) {
- strByte[j >> 2] |= str.charAt(j) << ((j % 4) * 8); // 一个整数存储四个字节,小端序
- }
- strByte[j >> 2] |= 0x80 << ((j % 4) * 8); // 尾部添加1
- // 添加原长度,长度指位的长度,所以要乘8,然后是小端序,所以放在倒数第二个,这里长度只用了32位
- strByte[num * 16 - 2] = str.length() * 8;
- return strByte;
- }
- /**
- * 调用函数
- * @param source 原始字符串
- * @return
- */
- public String getMD5(String source) {
- // 初始化
- init();
- int[] strByte = add(source);
- for (int i = 0; i < strByte.length / 16; i += 16) {
- int[] num = new int[16];
- for (int j = 0; j < 16; j++) {
- num[j] = strByte[i * 16 + j];
- }
- mainLoop(num);
- }
- return changeHex(Atemp) + changeHex(Btemp) + changeHex(Ctemp) + changeHex(Dtemp);
- }
- /**
- * 整数变成16进制字符串
- * @param a 整数
- * @return
- */
- private String changeHex(int a) {
- String str="";
- String tmp = "";
- for(int i=0;i<4;i++) {
- tmp = Integer.toHexString(((a >> i * 8) % (1 << 8)) & 0xff);
- if (tmp.length() < 2) {
- tmp = "0" + tmp;
- }
- str += tmp;
- }
- return str;
- }
- /**
- * 测试方法
- * @param args
- */
- public static void main(String[] args) {
- String str = MD5.getInstance().getMD5("");
- String str1 = MD5.getInstance().getMD5("123");
- System.out.println(str);
- System.out.println("d41d8cd98f00b204e9800998ecf8427e");
- System.out.println(str1);
- System.out.println("202cb962ac59075b964b07152d234b70");
- }
- }</span><span style="font-family:微软雅黑;font-size: 14px;">
- </span>
结束语
或许你经常见到 MD5,但你从来没有注意过,到底什么才是 MD5。也或许你知道什么是 MD5,但或许你并不了解 MD5 是作何用的。那么,从今天起,从你读完这篇博客起,我相信,以后再见到 MD5 的时候,你肯定会对它印象深刻,再下载文件的时候,也会对它进行 MD5 的一致性校验。那么,我就可以说,我的这篇文章还是起到了一丁点的作用。
序
这段时间,工作中用到了 Java 的加解密技术,本着学习的态度,打算从这篇文章开始,详细的研究一番 Java 在加解密技术上有什么与众不同,同时,也想为大家或者自己留下点什么,一块分享这其中的“精髓”。需要说明的是,这个系列可能要持续一段时间,因为,加解密的相关技术太多太多了,要搞明白这些着实不是一件容易的事。
背景
说到这个加解密技术,之前一直没有机会研究这个东西,这次公司里的项目需要加解密的支持,因此有机会能够好好研究这一块。刚开始写的时候,还在考虑是写一篇文章还是写一个系列,后来我发现,加解密这块的东西很多,如果一篇文章的话,就我现在这种水平,还远远不能一篇以序之,所以,还是决定采用细嚼慢咽的方式,来一点一点的消化。
正文
废话说了很多,终于要进正题了。
其实,严格来说,BASE64 是一种编码的方式,并不是真正意义上的加解密,不过,从另一个角度来考虑的话,就是把数据变为人不会用肉眼能分辨其真实性的角度来说,BASE64 也是属于加解密范畴的。而且,有的加密技术,也是需要通过 BASE64 来编码转换的。
那么,BASE64 到底是什么呢?下面我来大概的介绍一下 BASE64 的编码原理。
BASE64 的编码都是按字符串长度,以每 3 个 8 bit 的字符为一组,然后针对每组,首先获取每个字符的 ASCII 编码,然后将 ASCII 编码转换成 8 bit 的二进制,得到一组 3*8=24 bit 的字节。然后再将这 24 bit 划分为 4 个 6 bit 的字节,并在每个 6 bit 的字节前面都填两个高位 0,得到 4 个 8 bit 的字节,然后将这 4 个 8 bit 的字节转换成十进制,对照 BASE64 编码表 (下表),得到对应编码后的字符。
注:1. 要求被编码字符是8bit的,所以须在ASCII编码范围内,\u0000-\u00ff,中文就不行。
2. 如果被编码字符长度不是3的倍数的时候,则都用0代替,对应的输出字符为=
原理讲清楚了,下面举两个例子说明一下。
a) 字符长度为能被3整除时:比如“Tom” :
所以,btoa('Tom') = VG9t,也就是说,“Tom”的 BASE64 编码结果为 VG9t。
b) 字符串长度不能被3整除时,比如“Lucy”:
由于 Lucy 只有 4 个字母,所以按 3 个一组的话,第二组还有两个空位,所以需要用 0 来补齐。这里就需要注意,因为是需要补齐而出现的 0,所以转化成十进制的时候就不能按常规用 BASE64 编码表来对应,所以不是 a, 可以理解成为一种特殊的“异常”,编码应该对应“=”。
结束语
通过我上边说的这些,我想你已经了解了 BASE64 编码的原理,当然,如果你现在去看 Java 中有关 BASE64 编码源码的话,我想你一定能很快就搞明白的。不过,在此之前,推荐你还是要亲自试一下,用程序编码一个单词,然后使用上边提到的原理,人工编码相同的单词,你就会更加清楚是怎么回事。
顺带说一句,工作中,不能因为有了计算机,就什么事都要依赖它。有一句话,我想大家都知道,“用进废退”,就是这个道理。核心的东西掌握了,再来借助计算机,那就是事半功倍了,否则只会沦为高科技时代的奴隶。