Java代码实现加密算法实践
在平常开发中经常需要考虑到各种安全问题,所以常常会用到各种加密算法,包括对称加密算法和非对称加密算法,如MD5、SHA256、DES、3DES以及AES等。本文并不涉及到各种加密算法的详细介绍,仅仅是介绍如何使用Java语言实现加密的逻辑。
MD5和SHA算法都是单向的不可逆哈希加密算法,意味着使用明文加密后的密文是无法解密出明文的,当然了这并不表示密文就无法破解,仍然可以借助哈希碰撞的方式破解出明文,只是比较耗时一些。就目前而言MD5和SHA1算法都已经被认为不安全的加密算法了。DES、3DES以及AES都是对称加密算法,只要知道加密的密钥都可以破解出明文。还有一种比较常用的非对称加密算法RSA,该算法的公钥和私钥是不同的,在Java中实现RSA加密跟对称加密实现逻辑类似。
- MD5消息摘要算法(MD5 Message-Digest Algorithm),一种被广泛使用的密码散列函数,可以产生出一个128位(16字节)的散列值(hash value),用于确保信息传输完整一致。在Java语言中,经过MD5加密后密文长度是32位的长度,也有少数使用的是16位长度的密文,其实16位密文是从32位密文中截取的一部分。
- SHA安全散列算法(Secure Hash Algorithm,缩写为SHA)是一个密码散列函数家族,是FIPS所认证的安全散列算法。能计算出一个数字消息所对应到的,长度固定的字符串(又称消息摘要)的算法。SHA有许多种类,包括SHA、SHA-256、SHA-512等。不同加密算法加密后的密文长度也是不同的。
- DES数据加密标准算法(Data Encryption Standard),速度较快,适用于加密大量数据的场合。DES加密算法已经被认为是不安全的加密算法了。
- 3DES(Triple DES):是基于DES,对一块数据用三个不同的密钥进行三次加密,强度更高。该算法是AES算法出现之前的一个过渡加密算法。
- AES高级加密标准(Advanced Encryption Standard),是下一代的加密算法标准,速度快,安全级别高。
MD5与SHA加密
MD5加密
MD5加密算法应该是多数开发者接触最多的一种加密算法,标准的MD5加密算法是32位长度,也有部分使用的是16位长度。Java语言使用MD5加密实现如下:
public static String getMD5(String src) { String result = null; try { MessageDigest md = MessageDigest.getInstance("MD5"); md.update(src.getBytes()); result = new BigInteger(1, md.digest()).toString(16); for (int i = 0; i < 32 - result.length(); i++) { result = "0" + result; } } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } return result; }
在上述算法实现上面使用了BigInteger转换成16位哈希值,由于BigInteger会把0省略掉,需补全至32位,所以又使用了一个for循环进行补全处理。当然了哈希算法不止一种,上面使用的是最简单的一种转换方式。由于MD5加密后标准输出时32位的,如果是SHA算法,加密后的密文长度就不局限于32位长度了,上面使用BigInteger方式就有些欠妥了,当然了只要可以确定是哪种加密算法,即可以确定加密后的密文长度,BigInteger仍然可以直接使用。
如下是其它两种比较常见的哈希处理。
private static final char[] DIGITS_HEX = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; public static char[] toHex(byte[] data) { char[] toDigits = DIGITS_HEX; int len = data.length; char[] out = new char[len << 1]; // two characters form the hex value. for (int i = 0, j = 0; i < len; i++) { out[j++] = toDigits[(0xf0 & data[i]) >>> 4]; out[j++] = toDigits[0x0f & data[i]]; } return out; } public static String getHexString(byte[] data) { StringBuffer strHexString = new StringBuffer(); for (int i = 0; i < data.length; i++) { String hex = Integer.toHexString(0xff & data[i]); if (hex.length() == 1) { strHexString.append('0'); } strHexString.append(hex); } return strHexString.toString(); }
标准的MD5加密输出时是32位长度的,如果使用的是16位长度的,只要将上述加密后的输出结果使用substring(8,24)截取一下即可。
在上述MD5算法实现中使用了MessageDigest.getInstance("MD5")方法,方法中传入了一个MD5
字符串,当使用SHA算法时又应该传入什么字符串呢?我们可以参考 Java Cryptography Architecture Standard Algorithm Name Documentation 。
MessageDigest的getInstantce()方法的入参algorithm大小写是不敏感的,这里我们可以使用如下Java代码拿到所有的String类型入参。
for (String str : Security.getAlgorithms("MessageDigest")) { System.out.println(str); } //SHA-384 SHA-224 SHA-256 MD2 SHA SHA-512 MD5
SHA加密
由于SHA算法有多种类型,因此在设计使用SHA加密算法时我们可以传入两个入参,一个是原字符串src,一个是算法名称algorithm。
public static String getSha(String src, String algorithm) { String result = null; try { MessageDigest md = MessageDigest.getInstance(algorithm); md.update(src.getBytes()); result = Utils.getHexString(md.digest()); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } return result; }
DES、3DES和AES算法
这三种算法都是对称加密算法,因此在实现上既需要加密算法,也需要解密算法,同时需要传入对应的加解密的秘钥。
先上一下代码实现,三种加密算法的实现类似。
DES加密
public class DesUtils { private static final String CIPHER_ALGORITHM="DES/ECB/PKCS5Padding"; private static final String ALGORITHM="DES"; public static String encrypt(String key, String src) { String result = null; try { SecureRandom random = new SecureRandom(); DESKeySpec desKey = new DESKeySpec(key.getBytes()); // 创建一个密匙工厂,然后用它把DESKeySpec转换成 SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(ALGORITHM); SecretKey securekey = keyFactory.generateSecret(desKey); // Cipher对象实际完成加密操作 Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM); // 用密匙初始化Cipher对象 cipher.init(Cipher.ENCRYPT_MODE, securekey, random); result = Base64.getEncoder().encodeToString(cipher.doFinal(src.getBytes())); } catch (Exception e) { e.printStackTrace(); } return result; } public static String decrypt(String key, String src) { String result = null; try { SecureRandom random = new SecureRandom(); DESKeySpec desKey = new DESKeySpec(key.getBytes()); // 创建一个密匙工厂,然后用它把DESKeySpec转换成 SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(ALGORITHM); SecretKey securekey = keyFactory.generateSecret(desKey); // Cipher对象实际完成加密操作 Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM); // 用密匙初始化Cipher对象 cipher.init(Cipher.DECRYPT_MODE, securekey, random); result = new String(cipher.doFinal(Base64.getDecoder().decode(src))); } catch (Exception e) { e.printStackTrace(); } return result; } }
SecretKeyFactory的创建也是使用了一个单例模式,需要传入对应的算法名称,可以传入的所有算法名称仍然可以根据如下实现获取。
for (String str : Security.getAlgorithms("SecretKeyFactory")) { System.out.println(str); }
这里重点看一下Cipher实例的创建,该类的getInstance(String transformation)方法传入的是一个transformation,transformation的可以使用两种类型,一种是只需要传入一段参数,直接传入算法名称即可,如DES,另外一种需要传入三段参数,三段参数的组成为算法名称/加密模式/填充模式,如DES/CBC/PKCS5Padding。
有关Cipher类的transformation可以参考https://docs.oracle.com/javase/8/docs/api/javax/crypto/Cipher.html。在开发中一般填充模式很少使用NoPadding,这种方式相当于不填充,根据不同的算法,使用了该模式需要我们明文必须为8的倍数。
上述示例Cipher使用的是init(int opmode, Key key, SecureRandom random)
三个参数的方法,还有一个比较常用的方法init(int opmode, Key key, AlgorithmParameterSpec params)
,在github或者其他第三方开源框架中经常可以看到传入了一个IvParameterSpec类型的参数,这样可以增加加密算法的强度,IvParameterSpec其实是AlgorithmParameterSpec接口的子类型。一般IvParameterSpec都是结合CBS加密模式实现加密算法。有关IvParameterSpec的示例在本文就不贴出来了,有兴趣可以在网上查看一些相关示例。
在使用DESKeySpec需要注意传入的key的长度要至少8位,否则则会抛出InvalidKeyException异常,如果key超过8位,则会截取使用前8位。
public static final int DES_KEY_LEN = 8; public DESKeySpec(byte[] key, int offset) throws InvalidKeyException { if (key.length - offset < DES_KEY_LEN) { throw new InvalidKeyException("Wrong key size"); } this.key = new byte[DES_KEY_LEN]; System.arraycopy(key, offset, this.key, 0, DES_KEY_LEN); }
3DES加密
3DES算法在使用上面跟DES算法类似,只需要将DESKeySpec换成DESedeKeySpec,CIPHER_ALGORITHM替换为DESede/ECB/PKCS5Padding,ALGORITHM替换为DESede,其余的地方代码逻辑一样。由于使用了3DES算法,key的长度这里至少需要24位,否则也会抛出类似DES中InvalidKeyException。在使用3DES算法时,如果密钥key是类似8位一个循环的话,其加密结果跟DES一样。
System.out.println(DESedeUtils.encrypt("123456781234567812345678", "123456")); System.out.println(DesUtils.encrypt("12345678", "123456")); //ED5wLgc3Mnw= //ED5wLgc3Mnw=
AES加密
由于Java并没有提供类似DES和3DES秘钥实现类,因此在使用上跟前面两个加密算法还是有差异性的。有关AES加密算法的Java实现如下:
private static final String CIPHER_ALGORITHM = "AES/ECB/PKCS5Padding"; private static final String ALGORITHM = "AES"; public static String encrypt(String key, String src) { String result = null; try { SecureRandom random = new SecureRandom(); SecretKeySpec securekey=new SecretKeySpec(key.getBytes(), ALGORITHM); // Cipher对象实际完成加密操作 Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM); // 用密匙初始化Cipher对象 cipher.init(Cipher.ENCRYPT_MODE, securekey, random); result = Base64.getEncoder().encodeToString(cipher.doFinal(src.getBytes())); } catch (Exception e) { e.printStackTrace(); } return result; } public static String decrypt(String key, String src) { String result = null; try { SecureRandom random = new SecureRandom(); SecretKeySpec securekey=new SecretKeySpec(key.getBytes(), ALGORITHM); // Cipher对象实际完成加密操作 Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM); // 用密匙初始化Cipher对象 cipher.init(Cipher.DECRYPT_MODE, securekey, random); result = new String(cipher.doFinal(Base64.getDecoder().decode(src))); } catch (Exception e) { e.printStackTrace(); } return result; }
不同于DES(3DES)至少8(24)位,超过8(24)位则会截取前面8(24)位作为加密的key,通过上面代码实现AES加密算法时需要确保秘钥key必须是16位,无论是低于还是高于16位都会抛出InvalidKeyException异常。
有关RSA加密实现逻辑本文不再罗列出来了,由于RSA加密算法时非对称加密算法,因此在实现时它的key需要两个,其中一个作为私钥,另外一个作为公钥。