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需要两个,其中一个作为私钥,另外一个作为公钥。

评论

您确定要删除吗?删除之后不可恢复