从业务思考再次邂逅加密算法

从业务思考再次邂逅加密算法

七月 17, 2021 本文共计: 1.4k 字 预计阅读时长: 5分钟

在介绍骚操作的时候,先:

着重介绍下常见的加密摘要函数:

首先可以先介绍下盐的用途。

盐(Salt),在密码学中,是指在散列之前将散列内容(例如:密码)的任意固定位置插入特定的字符串。这个在散列中加入字符串的方式称为“加盐”。其作用是让加盐后的散列结果和没有加盐的结果不相同,在不同的应用情景中,这个处理可以增加额外的安全性。

为什么要加盐?

通常情况下,当字段经过散列处理(如SHA-1),会生成一段散列值,而散列后的值一般是无法通过特定算法得到原始字段的。但是某些情况,比如一个大型的彩虹表,通过在表中搜索该SHA-1值,很有可能在极短的时间内找到该散列值对应的真实字段内容。

加盐后的散列值,可以极大的降低由于用户数据被盗而带来的密码泄漏风险,即使通过彩虹表寻找到了散列后的数值所对应的原始内容,但是由于经过了加盐,插入的字符串扰乱了真正的密码,使得获得真实密码的概率大大降低。

MD5

下面的解释来自知乎 : https://zhuanlan.zhihu.com/p/191446610

MD5(信息-摘要算法5):MD5将任意长度的“字节串”映射为一个128bit的大整数。MD5以512位分组来处理输入的信息,且每一分组又被划分为16个32位子分组,经过了一系列的处理后,算法的输出由四个32位分组组成,将这四个32位分组级联后将生成一个128位散列值。

SHA

SHA(安全哈希算法):该算法的思想是接收一段明文,然后以一种不可逆的方式将它转换成一段(通常更小)密文,也可以简单的理解为取一串输入码(称为预映射或信息),并把它们转化为长度较短、位数固定的输出序列即散列值(也称为信息摘要或信息认证代码)的过程。可以对任意长度的数据运算生成一个160位的数值。SHA将输入流按照每块512位(64个字节)进行分块,并产生20个字节的被称为信息认证代码或信息摘要的输出。SHA-1是不可逆的、防冲突,并具有良好的雪崩效应。

数据传输加密算法流程

前后端约定加密算法,在后端生成公钥&私钥

前端请求接口拿到公钥加密后,给后端:

1
2
3
4
5
6
7
8
9
10
11
12
// 加盐加密
import JsSha from 'jssha';

const encryptPwdWithSalt = (pwd, salt) => {
const JsShaObj = new JsSha('SHA_256', 'TEXT');

// 加密加盐,必要时可以二次加密再加盐
JsShaObj.update(`${pwd}${salt}`);

// 返回 16 进制的 hash 散列值
return JsShaObj.getHash('HEX');
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

// 公钥再次加密
import JsEncrypt from 'jsencrypt';

const setRSAPKAndEncrypt = (publicKey, pwd, salt) => {

const encrypt = new JSEncrypt();

// 设置RSA公钥
encrypt.setPublicKey(publicKey);

return JsEncrypt.encrypt(encryptPwdWithSalt(pwd, salt))
}

后端拿到前端的加密结果后,根据服务端存储的私钥解密。

对比数据库密文,判断hash是否一致。

Case:需要注意的几点

  • 盐通常加在前面,不过也无所谓
  • 在使用客户端请求盐这种操作的时候,一定要注意不要直接用用户名去做请求,因为,恶意攻击者可能会通过脚本拿我们的有效用户名。

另外的用途

需求盘点

在修改数据流图时,且切换所在tab时,检测是否有改动,弹出二次确认弹窗。

拆分逻辑

  • 切换 tab 拦截
  • 改动校验

方案

  • tab切换使用Antd tabs组件的受控模式(注入onChange属性与activeKey
  • 改动校验,使用MD5加密序列化字符串做对比

开发过程中的坑

  • 组件封装独立性过高,借助PubSub消息通知,唤起二次确认,与确认完毕后,tab切换操作续命。
  • MD5相同的内容,但是多一个空格,hash值都会不同,加密前先过滤空格
  • 存储初始MD5
    • 考虑多个tab打开相同的页面
    • 生命周期

解决多tab问题

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
import { nanoid } from 'nanoid';

const pageM: Partial<Record<string, any>> = {};


const gNanoId = (key: string) => {
if (!pageModal[key]) {
pageModal[key] = nanoid();
}
};


function plantStore(key: string, value: string): boolean {
gNanoId(key);
sessionStorage.setItem(`${pageModal[key]}_${key}`, value);
return true;
}

function getStore(key: string): string {
return sessionStorage.getItem(`${pageModal[key]}_${key}`);
}

function rmStore(key: string): string {
sessionStorage.removeItem(`${pageModal.id}_${key}`);
return `${pageModal.id}_${key}`;
}

function clearStore(key: string) {
Object.keys(sessionStorage).forEach(item => {
if (item.includes(key)) {
sessionStorage.removeItem(item);
}
});
}

export {
plantStore,
rmStore,
getStore,
clearStore
};

End

在最后提到的,脚本伪造请求的时候,其实我们目前的请求都有Referer,但是要在 Nginx 层配置请求:

这里暂时列举图片请求的防盗规则

1
2
3
4
5
6
7
8

location ~* \.(gif|jpg|png)$ {
valid_referers none blocked *.phptest.com;
if ($invalid_referer) {
return 403;
}
}

最后

  • 感谢我的现任同事龙哥(javaer)给予的耐心解释~
  • 感谢我的现任同事圈圈(jser)在周会过程中分享的nanoid

参考文章