哈希算法总结及案例实现

目录

Hash的概念及其意义

哈希算法特点

哈希碰撞及产生的原因 

常用的哈希算法

Java程序中的使用方法

MD5算法

SHA-1算法 

HashTools工具类

 哈希算法的用途

校验下载文件

存储用户密码

 Hmac算法

使用步骤

Hmac算法的验证 

 总结


Hash的概念及其意义

举个例子,我们每个在世的人,为了能够参与各种社会活动,都需要一个用于识别自己的标志,比如身份证,名字等等。也许你觉得这些东西就足以代表你个人,但是这种代表性非常脆弱,因为重名的人很多,身份证也可以伪造。最可靠的办法是把一个人的所有基因序列记录下来用来代表这个人,但显然这样做并不实际。而指纹看上去是一种不错的选择,虽然一些专业组织仍然可以模拟某个人的指纹,但这种代价实在太高了。

而对于在互联网世界里传送的文件来说,如何标志一个文件的身份同样重要。比如说我们下载一个文件,文件的下载过程中会经过很多网络服务器、路由器的中转,如何保证这个文件就是我们所需要的呢?我们不可能去一一检测这个文件的每个字节,也不能简单地利用文件名、文件大小这些极容易伪装的信息,这时候,我们就需要一种指纹一样的标志来检查文件的可靠性,这种指纹就是我们现在所用的Hash算法(也叫散列算法,摘要算法)

哈希算法(Hash Algorithm),又称摘要算法,是一种从任意文件中创造小的数字"指纹"的方法。与指纹一样,哈希算法就是一种以较短的信息来保证文件唯一性的标志,这种标志与文件的每一个字节都相关,而且难以找到逆向规律。因此,当原有文件发生改变时,其标志值也会发生改变,从而告诉文件使用者当前的文件已经不是你所需求的文件。

这种标志有何意义呢?之前文件下载过程就是一个很好的例子,事实上,现在大部分的网络部署和版本控制工具都在使用散列算法来保证文件可靠性。而另一方面,我们在进行文件系统同步、备份等工具时,使用哈希算法来标志文件唯一性能帮助我们减少系统开销,这一点在很多云存储服务器中都有应用。

当然,作为一种指纹,哈希法最重要的用途在于给证书、文档、密码等高安全系数的内容添加加密保护。这一方面的用途主要是得益于哈希算法的不可逆性,这种不可逆性体现在,你不仅不可能根据一段通过哈希算法得到的指纹来获得原有的文件,也不可能简单地创造一个文件并让它的指纹与一段目标指纹相一致。哈希算法的这种不可逆性维持着很多安全框架的运营。

哈希算法特点

哈希算法的作用是对输入的任意一组数据进行运算,得到固定长度的结果。

其最重要的特点就是:

  • 相同的输入会得到相同的输出
  • 不同的输入大概率会得到不同的输出

一个优秀的 hash 算法,将能实现:

  • 正向快速:给定明文和 hash 算法,在有限时间和有限资源内能计算出 hash 值。
  • 逆向困难:给定(若干) hash 值,在有限时间内很难(基本不可能)逆推出明文。
  • 输入敏感:原始输入信息修改一点信息,产生的 hash 值看起来应该都有很大不同
  • 冲突避免:很难找到两段内容不同的明文,使得它们的 hash 值一致(发生冲突)。即对于任意两个不同的数据块,其hash值相同的可能性极小;对于一个给定的数据块,找到和它hash值相同的数据块极为困难。

但在不同的使用场景中,如数据结构和安全领域里,其中对某一些特点会有所侧重。

哈希碰撞及产生的原因 

在它的特点中说到,不同的输入"大概率会得到不同的输出",那么也就是说存在完全不同的输入,会得到相同的输出,这就是哈希碰撞,比如:

"AaAaAa".hashCode(); // 0x7460e8c0
"BBAaBB".hashCode(); // 0x7460e8c0
"通话".hashCode(); // 0x11ff03
"重地".hashCode(); // 0x11ff03

哈希碰撞是不可避免的,因为输出的字节长度是固定的,例如String类的hashcode()方法输出的类型为int型,它只能表示-2147483648~2147483647这个范围的数,可是有无数种输入,范围却是有限的,那么必定就会导致冲突。

常用的哈希算法

根据哈希算法的碰撞概率,算法输出的长度越长,它产生碰撞的概率就越小,也就越安全

常用的哈希算法有:

 Java的标准库中为我们提供了常用的哈希算法,通过统一的接口进行调用。

Java程序中的使用方法

首先创建MessageDigest对应的子类对象。通过该类的getInstance()方法,传入算法的字符串名称,然后决定其具体的子类实例对象。

然后反复调用实例对象的update()方法输入数据,输入结束后调用digest()方法获得byte[]数组表示的摘要,也就是哈希值,然后通过逻辑代码转换成为16进制的字符串。

下面是常用算法的案例实现: 

MD5算法

  使用MD5算法加密一个字符串:

//MD5加密
public class Demo06 {
	public static void main(String[] args) throws NoSuchAlgorithmException {
		// 	创建基于MD5算法的消息摘要对象
		MessageDigest md5 = MessageDigest.getInstance("MD5");
		// 更新原始数据
		md5.update("gypp".getBytes());
		// 获取加密后的结果
		byte[] retArr = md5.digest();
        // 输出加密结果数组的内容: [29, 20, -72, -57, -91, 99, -80, 24, -50, 71, -25, -36, -18, 25, 3, -15]
		System.out.println(Arrays.toString(retArr));
        // 将字节数组转换为16进制的字符串并打印: 1d14b8c7a563b018ce47e7dcee1903f1
		System.out.println(HashTools.bytesToHex(retArr));
        // 输出加密结果数组的长度: 16
		System.out.println(retArr.length);
	}
}

使用MD5算法加密一张本地的图片:

// 按照MD5算法对图片进行加密
public class Demo07 {
	public static void main(String[] args) throws IOException, NoSuchAlgorithmException {
		// 图片的原始字节内容
		byte[] imgByteArr = Files.readAllBytes(Paths.get("d:\\file\\twistzz.jpg"));
		// 创建基于MD5算法的消息摘要对象
		MessageDigest md5 = MessageDigest.getInstance("MD5");
		// 输入加密内容
		md5.update(imgByteArr);
		
		// 获取加密摘要
		byte[] md5ImgByteArr = md5.digest();
		// [-10, 7, 56, -81, 56, -65, -44, -101, -8, 64, -74, 110, 102, 101, 38, 112]
		System.out.println(Arrays.toString(md5ImgByteArr));
		
		// f60738af38bfd49bf840b66e66652670
		System.out.println(HashTools.bytesToHex(md5ImgByteArr));
		
		// 固定长度16
		System.out.println(md5ImgByteArr.length);
	}
}

SHA-1算法 

SHA-1 也是一种哈希算法,它的输出是 160 bits ,即 20 字节。 SHA-1 是由美国国家安全局开发 的, SHA 算法实际上是一个系列,包括 SHA-0 (已废弃)、 SHA-1 、 SHA-256 、 SHA-512 等。 在 Java 中使用 SHA-1 ,和 MD5 完全一样,只需要把算法名称改为" SHA-1 " :

// SHA-1加密
public class Demo06 {
	public static void main(String[] args) throws NoSuchAlgorithmException {
		// 	创建基于SHA-1算法的消息摘要对象
		MessageDigest md5 = MessageDigest.getInstance("SHA-1");
		// 更新原始数据
		md5.update("gypp".getBytes());
		// 获取加密后的结果
		byte[] retArr = md5.digest();
        // 20 bytes: [-103, 124, 77, 54, 2, -53, 37, 73, -110, -57, -11, 38, -33, -100, 69, 19, 23, -15, -108, -20]
		System.out.println(Arrays.toString(retArr));
        // 997c4d3602cb254992c7f526df9c451317f194ec
		System.out.println(HashTools.bytesToHex(retArr));
        // 20
		System.out.println(retArr.length);
	}
}

类似的,计算 SHA-256 ,我们需要传入名称" SHA-256 ",计算 SHA-512 ,我们需要传入名称" SHA-512 "。 Java 标准库支持的所有哈希算法都可以在这里查到。 

HashTools工具类

所以这里将一些通用的方法整理成了一个工具类,以便于更便捷的使用:

  • digestByMD5和digestBySHA1的不同仅仅在于基于的算法不同,故实例化的对象也就不一样。
  • 两个方法进行加密时调用的方法是一样的,所以将该过程封装成了handler()方法。
  • 最后handler()方法会调用bytesToHex()方法将加密后的字节数组转换为十六进制的字符串,将每个字节数转换为两位16进制的字符。
// Hash算法工具类
public class HashTools {
	// 消息摘要对象
	private static MessageDigest digest;
	
	// 私有构造方法
	private HashTools() {
		
	}
	
	// 使用MD5算法进行哈希计算
	public static String digestByMD5(String source) throws NoSuchAlgorithmException {
		digest = MessageDigest.getInstance("MD5");
		return handler(source);
	}
	
	// 使用SHA-1算法进行哈希计算
	public static String digestBySHA1(String source) throws NoSuchAlgorithmException {
		digest = MessageDigest.getInstance("SHA-1");
		return handler(source);
	}
	
	// 通过消息摘要对象加密内容并处理
	private static String handler(String source) {
		digest.update(source.getBytes());
		byte[] bytes = digest.digest();
		return bytesToHex(bytes);
	}
	
	//将字节数组处理为十六进制的字符串
	public static String bytesToHex(byte[]bytes) {
		StringBuilder ret = new StringBuilder();
		for(byte b: bytes) {
			ret.append(String.format("%02x", b));
		}
		return ret.toString();
	}
}

 哈希算法的用途

校验下载文件

哈希算法中相同的输入永远会得到相同的输出,那么如果输入被修改,输出结果也会大不相同。我们在网站上下载软件的时候,经常看到下载页显示的MD5哈希值:

 我们只需计算自己本地文件的哈希值,与官网公开的对应文件的哈希值作对比: 如果相同,那么文件下载正确;否则文件有被篡改过。

存储用户密码

哈希算法的另一个重要用途是存储用户口令。如果直接将用户的原始口令存放到数据库中,会产生极大的安全风险:

  • 数据库管理员可以看到用户明文口令。
  • 数据库一旦泄露,黑客即可获取所有用户明文口令。

我们可以通过哈希算法存储用户口令的哈希值,比如MD5。在用户输入原始口令后,系统计算用户输入的原始口令的 MD5 并与数据库存储的 MD5 对比,如果一致,说明口令正确,否则,口令错误。就如下图所示:

但同时我们还要注意防止彩虹表攻击。

如果从MD5反推明文口令,只能使用暴力穷举的办法,但是会消耗大量的算力和时间。但是,如果有一个预先计算好的常用口令和它们的 MD5 的对照表,这个表就是彩虹表。如果用户使用了常用口令,黑客从 MD5 一下就能反查到原始口令: 

所以尽量不要使用含有个人相关信息的字符串作为密码。

为了抵御彩虹表攻击,也可以采取特殊的措施: 对每个口令额外添加随机数,这个方法称之为加盐( salt ): digest = md5(salt + inputPassword) 

//通过随机加盐,解决彩虹表攻击问题
public class Demo08 {
	public static void main(String[] args) throws NoSuchAlgorithmException {
		// 原始密码
		String pwd = "password";
		// 产生随机的盐值
		String salt = UUID.randomUUID().toString().substring(0,4);
		MessageDigest md5 = MessageDigest.getInstance("MD5");
		md5.update(pwd.getBytes());
		//加入随机盐值
		md5.update(salt.getBytes());
		
		String digestHexStr = HashTools.bytesToHex(md5.digest());
		System.out.println(digestHexStr);
		System.out.println();
		System.out.println(HashTools.digestByMD5("password"));
	}
}

 Hmac算法

Hmac 算法就是一种基于密钥的消息认证码算法,它的全称是 Hash-based Message Authenticati on Code ,是一种更安全的消息摘要算法。 Hmac 算法总是和某种哈希算法配合起来用的。例如,我们使用 MD5 算法,对应的就是 Hmac MD5 算 法,它相当于“加盐”的 MD5 : HmacMD5 ≈ md5(secure_random_key, input) 因此, HmacMD5 可以看作带有一个安全的 key 的 MD5 。使用 HmacMD5 而不是用 MD5 加 salt , 有如下好处:

  • HmacMD5 使用的 key 长度是 64 字节,更安全;
  • Hmac 是标准算法,同样适用于 SHA-1 等其他哈希算法;
  • Hmac 输出和原有的哈希算法长度一致。

可见, Hmac 本质上就是把 key 混入摘要的算法。验证此哈希时,除了原始的输入数据,还要提供 key 。为了保证安全,我们不会自己指定 key ,而是通过 Java 标准库的 KeyGenerator 生成一个安全的随机的 key 。

下面是HmacMD5算法的案例实现:

// Hmac算法
public class Demo09 {
	public static void main(String[] args) throws NoSuchAlgorithmException, InvalidKeyException {
		
		String pwd = "password";
		
		// 获取HmacMD5密钥生成器
		KeyGenerator keyGen = KeyGenerator.getInstance("HmacMD5");
		
		// 生成密钥
		SecretKey key = keyGen.generateKey();
		System.out.println("密钥: "+Arrays.toString(key.getEncoded()));
		System.out.println("密钥长度: "+key.getEncoded().length);
		System.out.println("密钥字符串: "+HashTools.bytesToHex(key.getEncoded()));
		
		// 获取Hmac加密算法对象
		Mac mac = Mac.getInstance("HmacMD5");
		mac.init(key); // 初始化密钥
		mac.update(pwd.getBytes()); // 更新加密内容
		byte[] bytes = mac.doFinal(); // 加密处理,并返回加密结果(字节数组)
		String ret = HashTools.bytesToHex(bytes); // 将加密结果处理为16进制字符串
		System.out.println("加密结果16进制字符串: "+ret);
		System.out.println("加密后的字节数组: "+Arrays.toString(bytes));
		System.out.println("字节数组的长度: "+bytes.length);
		System.out.println("16进制字符串长度: "+ret.length());
	}
}

使用步骤

和 MD5 相比,使用 HmacMD5 的步骤是:

  1. 通过名称HmacMD5获取 KeyGenerator 实例;
  2. 通过KeyGenerator创建一个 SecretKey 实例;
  3. 通过名称HmacMD5 获取 Mac 实例;
  4. 用SecretKey初始化Mac实例;
  5. 通过Mac实例对象调用init()方法初始化密钥
  6. 对 Mac 实例反复调用 update(byte[]) 输入数据;
  7. 调用 Mac 实例的 doFinal() 获取最终的哈希值。

Hmac算法的验证 

有了 Hmac 计算的哈希值和 SecretKey ,我们想要验证怎么办?这时, SecretKey 不能从 KeyGenerator 生 成,而是从一个 byte[] 数组恢复:

恢复 SecretKey 的语句就是 new SecretKeySpec(hkey, "HmacMD5") ,也就是根据密钥的字节数组和使用的算法,重新创建出一个SecretKey实例对象

// 按照密钥的字节数组,恢复Hmac密钥
public class Demo10 {
	public static void main(String[] args) throws NoSuchAlgorithmException, InvalidKeyException {

		// 密钥(字节数组)
		byte[] keybyteArr = {-119, 57, -22, 20, -96, -105, 46, 28, 53, 23, -75, 30, -20, -107, 58, -39, -100, 14, 91, 114, -5, 0, -89, 71, 89, 74, -82, -87, 116, -75, -117, -12, 55, 98, 60, 83, 122, -126, -102, 23, -88, 24, -65, 94, -2, -69, -58, 54, -117, 21, 58, 16, 4, -86, 5, 114, 73, -30, 2, 90, 6, 125, 105, 3};
	
		// 恢复密钥
		SecretKey key = new SecretKeySpec(keybyteArr, "HmacMD5");
		
		Mac mac = Mac.getInstance("HmacMD5");
		mac.init(key);
		mac.update("password".getBytes());
		
		String ret = HashTools.bytesToHex(mac.doFinal());
		System.out.println(ret);
	}
}

那么如果存储的是密钥的十六进制字符串形式,怎样进行验证呢?

只需要对字符串做一些逻辑处理,将它处理为字节数组,再按照刚才的步骤即可成功验证。

        // 密钥(字符串)
		String keyStr = "此处为密钥的字符串形式";

		byte[] keybyteArr= new byte[64];
		for(int i=0,k=0;i<keyStr.length();i+=2,k++) {
			keybyteArr[k] = (byte)Integer.parseInt(keyStr.substring(i,i+2),16);
		}

 总结

  • 哈希算法可用于验证数据完整性,具有防篡改检测的功能;
  • 常用的哈希算法有 MD5 、 SHA-1 等;
  • 用哈希存储口令时要考虑彩虹表攻击。
  • Hmac 算法是一种标准的基于密钥的哈希算法,可以配合 MD5 、 SHA-1 等哈希算法,计算的摘要长度和原摘要算法长度相同。
  • BouncyCastle 是一个开源的第三方算法提供商;
  • BouncyCastle 提供了很多 Java 标准库没有提供的哈希算法和加密算法;

 

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
可以使用哈希函数对文件进行计算,得到一个固定长度的哈希值,然后将该哈希值与预先计算好的正确哈希值进行比较,如果相同,则说明文件完整性验证通过。在C++中,可以使用标准库中的哈希函数,如SHA-1、SHA-256等。具体实现可以参考以下代码: ```cpp #include <iostream> #include <fstream> #include <sstream> #include <iomanip> #include <openssl/sha.h> std::string sha256(std::string filename) { std::ifstream file(filename, std::ios::binary); if (!file) { throw std::runtime_error("Failed to open file"); } std::stringstream buffer; buffer << file.rdbuf(); file.close(); std::string data = buffer.str(); unsigned char hash[SHA256_DIGEST_LENGTH]; SHA256_CTX sha256; SHA256_Init(&sha256); SHA256_Update(&sha256, data.c_str(), data.size()); SHA256_Final(hash, &sha256); std::stringstream ss; for (int i = 0; i < SHA256_DIGEST_LENGTH; i++) { ss << std::hex << std::setw(2) << std::setfill('0') << (int)hash[i]; } return ss.str(); } int main() { std::string filename = "test.txt"; std::string correct_hash = "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3"; std::string hash = sha256(filename); if (hash == correct_hash) { std::cout << "File integrity verified" << std::endl; } else { std::cout << "File integrity verification failed" << std::endl; } return 0; } ``` 其中,`sha256`函数使用了OpenSSL库中的SHA-256哈希函数,计算文件的哈希值,并将结果转换为字符串返回。在`main`函数中,我们可以指定要验证的文件名和正确的哈希值,然后调用`sha256`函数计算文件的哈希值,并与正确的哈希值进行比较,从而验证文件的完整性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值