首页 > 代码库 > 【Java编码准则】の #13使用散列函数保存密码

【Java编码准则】の #13使用散列函数保存密码

     明文保存密码的程序在很多方面容易造成密码的泄漏。虽然用户输入的密码一般时明文形式,但是应用程序必须保证密码不是以明文形式存储的。

     限制密码泄漏危险的一个有效的方法是使用散列函数,它使得程序中可以间接的对用户输入的密码和原来的密码进行比较,而不需要保存明文或者对密码进行解密后比较。这个方法使密码泄漏的风险降到最低,同时没有引入其他缺点。


[加密散列函数]

     散列函数产生的值称为哈希值或者消息散列,散列函数是计算可行函数,但反过来是计算不可行的。事实上,密码可以被编码为一个哈希值,但哈希值不能被解码成对应的密码。两个密码是否相同,可以通过判断它们的哈希值是否相等。

     一个好的实践是对明文加盐之后再进行哈希值的计算。盐是一个唯一的序列或者随机生成的数据片段,通常和哈希值一起存放。盐的作用是防止哈希值的暴力破解,前提是盐足够长以产生足够的熵(长度较短的盐值不足以降低暴力攻击的速度)。每个密码必须有自己的唯一的盐,如果多个密码共用同一个盐的话,两个用户可能会有相同的密码。

     散列函数和盐的长度的选择是安全和性能的一个均衡。选择强度高的散列函数以增强暴力破解的难度的同时,也会增加密码验证的时间。增强盐的长度可以增大暴力破解的难度,但同时会增加额外的存储空间。

     Java的MessageDigest类提供了多种加密散列函数的实现,但要避免使用有缺陷的散列函数,例如MD5,而像SHA-1和SHA-2之类由美国国家安全局维护的散列函数,目前是安全的。实践中,很多应用使用SHA-256散列函数,因为这个函数兼顾了性能和安全。


[不符合安全要求的代码示例]

     下面的代码使用对称加密算法对存储在password.bin文件中的密码进行加密和解密操作。

public final class Password {

	private void setPassword(byte[] pass) throws Exception {
		// arbitrary encryption scheme
		byte[] encryted = encrypt(pass);
		clearArray(pass);
		
		// encrypted password to password.bin
		saveBytes(encrypted, "password.bin");
		clearArray(encrypted);
	}
	
	boolean checkPassword(byte[] pass) throws Exception {
		// load the encrypted password
		byte[] encrypted = loadBytes("password.bin");
		byte[] decrpyted = decrypt(encrypted);
		boolean arraysEqual = Arrays.equals(decrypted, pass);
		clearArray(decrypted);
		clearArray(pass);
		return arraysEqual;
	}
	
	private void clearArray(byte[] a) {
		for (int i=0; i<a.length; ++i) {
			a[i] = 0;
		}
	}
}

     攻击者可能对上述bin文件进行解密,然后获得密码,尤其当攻击者知道程序中使用的密钥和加密方式时。密码甚至必须不让系统管理员或者特权用户知道。因此,使用加密方式对防止密码泄漏危险的作用有限。


[不符合安全的代码示例]

     下面代码基于MessageDigest类使用SHA-256散列函数来对比字符串,而不是使用明文,但它使用的是String对象来存储密码。

public final class Password {

	private void setPassword(String pass) throws Exception {
		byte[] salt = generateSalt(12);
		MessageDigest msgDigest = MessageDigest.getInstance("SHA-256");
		// encode the string and salt
		byte[] hashVal = msgDigest.digest((pass + salt).getBytes());
		saveBytes(salt, "salt.bin");
		// save the hash value to password.bin
		saveBytes(hashVal, "password.bin");
	}
	
	boolean checkPassword(String pass) throws Exception {
		byte[] salt = loadBytes("salt.bin");
		MessageDigest msgDigest = MessageDigest.getInstance("SHA-256");
		// encode the string and salt
		byte[] hashVal1 = msgDigest.digest((pass + salt).getBytes());
		// load the hash value stored in password.bin
		byte[] hashVal2 = loadBytes("password.bin");
		return Arrays.equals(hashVal1, hashVal2);
	}
	
	private byte[] generateSalt(int n) {
		// generate a random byte array of length n
	}
}

     即使攻击者知道程序使用SHA-256散列函数和12字节的盐,他还是不能从password.bin和salt.bin中获取获取真实的密码。

     虽然上面的代码解决了密码被解密的问题,但是这段代码使用String对象来存放明文的密码,因此,可参见“#00限制敏感数据的生命周期”进行改进。


[符合安全要求的解决方案]

     下面的代码使用byte数组来存储明文密码。

public final class Password {

	private void setPassword(byte[] pass) throws Exception {
		byte[] salt = generateSalt(12);
		byte[] input = appendArrays(pass, salt);
		MessageDigest msgDigest = MessageDigest.getInstance("SHA-256");
		// encode the string and salt
		byte[] hashVal = msgDigest.digest(input);
		clearArray(pass);
		clearArray(input);
		saveBytes(salt, "salt.bin");
		
		// save the hash value to password.bin
		saveBytes(hashVal, "password.bin");
		clearArray(salt);
		clearArray(hashVal);
	}
	
	boolean checkPassword(byte[] pass) throws Exception {
		byte[] salt = loadBytes("salt.bin");
		byte[] input = appendArrays(pass, salt);
		MessageDigest msgDigest = MessageDigest.getInstance("SHA-256");
		// encode the string and salt
		byte[] hashVal1 = msgDigest.digest(input);
		clearArray(pass);
		clearArray(input);
		
		// load the hash value stored in password.bin
		byte[] hashVal2 = loadBytes("password.bin");
		boolean arraysEqual = Arrays.equals(hashVal1, hashVal2);
		clearArray(hashVal1);
		clearArray(hashVal2);
		return arraysEqual;
	}
	
	private byte[] generateSalt(int n) {
		// generate a random byte array of length n
	}
	
	private byte[] appendArray(byte[] a, byte[] b) {
		// return a new array of a[] appended to b[]
	}
	
	private void clearArray(byte[] a) {
		for (int i=0; i<a.length; ++i) {
			a[i] = 0;
		}
	}
}

     在setPassword()和checkPassword()函数中,明文密码在使用后立即清空。因此,攻击者在明文密码清空后必须花费更多精力才能获取明文密码。提供有保证的密码清空是极具挑战的,它可能是平台相关的,甚至可能由于复制垃圾回收/动态分页/其他运行在Java语言之下的平台机制而变得不可行。


[适用性]

     没有经过安全散列运算就进行存储的密码容易被非法的用户获取到,违背本条约将导致程序容易被非法利用。

像密码管理器这样的应用程序可能需要取得原始密码并将其填写到第三方应用中。这一点是允许的,即使它违背了本条约。密码管理器是由单一用户访问的,而且一般是经过用户许可的才可以保存用户的密码,同时根据要求进行密码的展示。因此,安全的决定因素取决于用户个人的能力而非程序的功能。


——欢迎转载,请注明出处 http://blog.csdn.net/asce1885 ,未经本人同意请勿用于商业用途,谢谢——