0%

敏感数据加密

目前的情况

这一模块目前在项目中主要有两个地方用到:

  1. 二维码
  2. 用户密码

二维码目前未加密,目前的解决方案是采用 Base64 行 “加密”。

用户密码目前使用的是 散列算法-Md5 进行的加密(前端处理,未加 salt)。

问题

Base64Md5 都不能称得上是加密算法。 以 Base64 为例,它的应用场景并不是加密,而是 让数据符合传输协议的要求

Base64是一种数据编码方式,目的是让数据符合传输协议的要求。标准Base64编码解码无需额外信息即完全可逆,即使你自己自定义字符集设计一种类Base64的编码方式用于数据加密,在多数场景下也较容易破解。 —Base64编码原理与应用

至于 Md5 ,它是一种 散列算法, 类似的还有 SHA1,其主要应场景是 校验数据的完整性。链接:安全算法 - 摘要算法

之前提到过已经有一些网站可以暴力破解,它们利用的是 彩虹表:拿到 md5 字符后,在后台利用穷举法进行反向破解,然后将映射保存至 彩虹表,之后就可以直接查询到。加了 salt 虽然可以增加破解难度,但是如果是固定 salt,仍然免除不了被破解的风险。所以直接将 md5 值作为密码存储到数据库,当发生拖库时(数据泄漏),用户的密码有泄漏风险。

解决办法

下面直接引用 Base64编码原理与应用 这篇文章给出的建议。

针对数据加密:

对于数据加密应该使用专门的目前还没有有效方式快速破解的加密算法。比如:对称加密算法AES-128-CBC,对称加密需要密钥,只要密钥没有泄露,通常难以破解;也可以使用非对称加密算法,如 RSA,利用极大整数因数分解的计算量极大这一特点,使得使用公钥加密的数据,只有使用私钥才能快速解密。

对于数据校验,也应该使用专门的消息认证码生成算法,如 HMAC - 一种使用单向散列函数构造消息认证码的方法,其过程是不可逆的、唯一确定的,并且使用密钥来生成认证码,其目的是防止数据在传输过程中被篡改或伪造。将原始数据与认证码一起传输,数据接收端将原始数据使用相同密钥和相同算法再次生成认证码,与原有认证码进行比对,校验数据的合法性。

针对用户密码的安全性:

针对各大网站被脱库的问题,请问应该怎么存储用户的登录密码?

答案是:在注册时,根据用户设置的登录密码,生成其消息认证码,然后存储用户名和消息认证码,不存储原始密码。每次用户登录时,根据登录密码,生成消息认证码,与数据库中存储的消息认证码进行比对,以确认是否为有效用户,这样即使网站被脱库,用户的原始密码也不会泄露,不会为用户使用的其他网站带来账号风险。

当然,使用的消息认证码算法其哈希碰撞的概率应该极低才行,目前一般在HMAC算法中使用SHA256。对于这种方式需要注意一点:防止用户使用弱密码,否则也可能会被暴力破解。现在的网站一般要求用户密码6个字符以上,并且同时有数字和大小写字母,甚至要求有特殊字符。

另外,也可以使用加入随机salt的哈希算法来存储校验用户密码。


后端实现(二维码)

目前先处理了二维码。利用 AES 对称加密实现,秘钥直接硬编码写死了。优点是 App 端扫码时无需请求服务器,并且二维码内容安全性也得到了保证。但是缺点也很明显:秘钥是固定的,会有泄漏风险。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
public class AESUtil {
/**
* 算法/加密模式/填充方式
*/
private static final String AES_PKCS5P = "AES/ECB/PKCS5Padding";

/**
* 加密
*
* @param str 需要加密的字符串
* @param key 密钥(必须为16位,超出或少于返回内容都为null)
*/
public static String encrypt(String str, String key) {
if (StringUtils.isEmpty(key)) {
throw new RuntimeException("key不能为空!");
}
if (key.length() != 16) {
throw new RuntimeException("秘钥长度为16位!");
}
if (StringUtils.isEmpty(str)) {
return null;
}
try {
byte[] raw = key.getBytes(StandardCharsets.UTF_8);
SecretKeySpec secretKeySpec = new SecretKeySpec(raw, "AES");
// "算法/模式/补码方式"
Cipher cipher = Cipher.getInstance(AES_PKCS5P);
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec);
byte[] encrypted = cipher.doFinal(str.getBytes(StandardCharsets.UTF_8));
// 此处使用Base64作为转码功能
return new BASE64Encoder().encode(encrypted);
} catch (Exception ex) {
return null;
}
}

/**
* 解密
*
* @param str 需要解密的字符串
* @param key 密钥
*/
public static String decrypt(String str, String key) {
if (StringUtils.isEmpty(key)) {
throw new RuntimeException("key不能为空!");
}
if (key.length() != 16) {
throw new RuntimeException("秘钥长度为16位!");
}
if (StringUtils.isEmpty(str)) {
return null;
}
try {
byte[] raw = key.getBytes(StandardCharsets.UTF_8);
SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
Cipher cipher = Cipher.getInstance(AES_PKCS5P);
cipher.init(Cipher.DECRYPT_MODE, skeySpec);
// 先用base64解密
byte[] encrypted = new BASE64Decoder().decodeBuffer(str);
byte[] original = cipher.doFinal(encrypted);
return new String(original, StandardCharsets.UTF_8);
} catch (Exception ex) {
return null;
}
}

public static void main(String[] args) {
String key = "4%YkW!@g5LGcf9Ut";
String str = "Hello World!";
String encrypt = encrypt(str, key);
String decrypt = decrypt(encrypt, key);

System.out.println("字符:" + str + ",秘钥:" + key);
System.out.println("加密后:" + encrypt);
System.out.println("解密后:" + decrypt);
}
}

注:真正的秘钥没有放上来,App端可以联系后端获取。


效果展示

16877470867344459111c4bd051186e133ac331fa6e81.png


更加优化的方案(二维码)

上面的实现只是利用了对称加密,还可以结合非对称加密实现更优化的解决方案:

将对称加密的密钥使用非对称加密的公钥进行加密,然后发送出去,接收方使用私钥进行解密得到对称加密的密钥,然后双方可以使用对称加密来进行沟通。

当然,这么做在扫码时就需要请求一次服务器,以获取被公钥加密后的对称秘钥。但是相比之下安全性更高。


参考文章

Base64编码原理与应用

关于加密算法的讨论 -v2ex

对称 和 非对称 密钥 结合使用