保密
保密是有成本的,追求越高的安全等级,我们就要付出越多的工作量与算力消耗。
就连国家保密法都会把秘密信息划分为秘密、机密、绝密三级来区别对待,可见即使是信息安全,也应该有所取舍。
1、Hash加密
以摘要代替明文如果密码本身比较复杂,那么一次简单的哈希摘要就至少可以保证,即使在传输过程中有信息泄露,也不会被逆推出原信息;
即使密码在一个系统中泄露了,也不至于威胁到其他系统的使用。但这种处理不能防止弱密码被彩虹表攻击所破解。
2、先加盐值再做哈希
盐值可以替弱密码建立一道防御屏障,在一定程度上可以防御已有的彩虹表攻击。
但它并不能阻止加密结果被监听、窃取后,攻击者直接发送加密结果给服务端进行冒认。
3、将盐值变为动态值能有效防止冒认
如果每次向服务端传输时,密码都掺入了动态的盐值,让每次加密的结果都不一样,那么即使传输给服务端的加密结果被窃取了,攻击者也不能冒用来进行另一次调用。
不过,尽管在双方通讯均可能泄露的前提下,协商出只有通讯双方才知道的保密信息是完全可行的(后面两讲介绍“传输安全层”时会提到),但这样协商出盐值的过程将变得极为复杂,而且每次协商只能保护一次操作,因而也很难阻止攻击者对其他服务的重放攻击。
4、加入动态令牌防止重放攻击
我们可以给服务加入动态令牌,这样在网关或其他流量公共位置建立校验逻辑,服务端愿意付出在集群中分发令牌信息等代价的前提下,就可以做到防止重放攻击。
但这种手段的弱点是,依然不能抵御传输过程中被嗅探而泄露信息的问题。
5、启用 HTTPS 来应对因嗅探而导致的信息泄露
启用 HTTPS 可以防御链路上的恶意嗅探,也能在通讯层面解决重放攻击的问题。
但是它依然有因客户端被攻破而产生伪造根证书的风险、因服务端被攻破产生证书泄露被中间人冒认的风险、因CRL更新不及时或者OCSP Soft-fail 产生吊销证书被冒用的风险,以及因 TLS 的版本过低或密码学套件选用不当产生加密强度不足的风险。
进一步提升保密强度的不同手段为了抵御前面提到的这种种风险,我们还要进一步提升保密强度。比如说,银行会使用独立于客户端的存储证书的物理设备(俗称的 U 盾),来避免根证书被客户端中的恶意程序窃取伪造;当大型网站涉及到账号、金钱等操作时,会使用双重验证开辟出一条独立于网络的信息通道(如手机验证码、电子邮件),来显著提高冒认的难度;甚至一些关键企业(如国家电网)或机构(如军事机构),会专门建设遍布全国各地的、与公网物理隔离的专用内部网络,来保障通讯安全。
通过了解以上这些逐步升级的保密措施,你应该能对“更高的安全强度同时也意味着要付出更多的代价”,有更加具体的理解了,并不是任何一个网站、系统、服务都需要无限拔高的安全性。
也许这个时候,你还会好奇另一个问题:安全的强度有尽头吗?存不存在某种绝对安全的保密方式?答案可能会出乎你的意料,确实是有的。信息论之父香农就严格证明了一次性密码(One Time Password)的绝对安全性。
但是使用一次性密码必须有个前提,就是我们已经提前安全地把密码或密码列表传达给了对方。比如说,你给朋友人肉送去一本存储了完全随机密码的密码本,然后每次使用其中一条密码来进行加密通讯,用完一条丢弃一条。
这样理论上可以做到绝对的安全,但显然这种绝对安全对于互联网来说没有任何的可行性。
6、什么程度加密是合适的,可以接受的
int password = 123456;
client_hash = MD5(password) // e10adc3949ba59abbe56e057f20f883e
//MD5Hash 有高效碰撞的问题,加盐
client_hash = MD5(MD5(password) + salt) // SALT = $2a$10$o5L.dWYEjZjaejOmN3x4Qu
//上述可以彩虹表破解
//使用慢哈希函数来解决暴力破解问题,加密客户端的hash
client_hash = BCrypt(MD5(password) + salt) // MFfTW3uNI4eqhwDkG7HP9p2mzEUu/r2
//服务端生产动态盐
SecureRandom random = new SecureRandom();
byte server_salt[] = new byte[36];
random.nextBytes(server_salt); // tq2pdxrblkbgp8vt8kbdpmzdh1w8bex
server_hash = SHA256(client_hash + server_salt); // 55b4b5815c216cf80599990e781cd8974a1e384d49fbde7776d096e1dd436f67
DB.save(server_hash, server_salt);
authentication_hash = MFfTW3uNI4eqhwDkG7HP9p2mzEUu/r2
result = SHA256(authentication_hash + server_salt); // 55b4b5815c216cf80599990e781cd8974a1e384d49fbde7776d096e1dd436f67
authentication = compare(result, server_hash) // yes
7、加密代码的实现
package com.lf.java.architecture;
import org.mindrot.jbcrypt.BCrypt;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
public class PasswordHashingUtil {
// 生成随机盐值
public static String generateSalt() {
SecureRandom random = new SecureRandom();
byte[] saltBytes = new byte[16];
random.nextBytes(saltBytes);
return bytesToHex(saltBytes);
}
// 使用MD5对密码进行散列
public static String hashWithMD5(String data) throws NoSuchAlgorithmException {
MessageDigest digest = MessageDigest.getInstance("MD5");
byte[] hashBytes = digest.digest(data.getBytes());
return bytesToHex(hashBytes);
}
// 使用BCrypt对密码进行散列
public static String hashPasswordWithBCrypt(String password, String salt) {
return BCrypt.hashpw(password + salt, BCrypt.gensalt());
}
// 使用SHA-256对散列值和盐值进行散列
public static String hashWithSHA256(String data) throws NoSuchAlgorithmException {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hashBytes = digest.digest(data.getBytes());
return bytesToHex(hashBytes);
}
// 将字节数组转换为十六进制字符串
public static String bytesToHex(byte[] bytes) {
StringBuilder hexString = new StringBuilder();
for (byte b : bytes) {
hexString.append(String.format("%02x", b));
}
return hexString.toString();
}
public static void main(String[] args) throws NoSuchAlgorithmException {
String password = "user123";
String clientSalt = " $2a$10$o5L.dWYEjZjaejOmN3x4Qu";
//使用慢哈希函数来解决彩虹表暴力破解问题,生成客户端的hash
// 使用BCrypt对密码进行散列(需要客户端使用慢哈希方法生成client,增加暴力破解时间成本)
String clientHash = hashPasswordWithBCrypt(hashWithMD5(password), clientSalt);
// MD5Hash 有高效碰撞的问题,使用SHA-256对散列值和盐值进行散列
try {
//生成服务端动态盐
String salt = generateSalt();
String serverHash = hashWithSHA256(clientHash + salt);
System.out.println("Client Hash (BCrypt): " + clientHash);
System.out.println("Server Hash (SHA-256): " + serverHash);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
}
}