密码加密方案

使用openssl生成RSA的公钥和私钥

1
2
openssl genrsa -out rsa_1024_priv.pem 1024
openssl rsa -pubout -in rsa_1024_priv.pem -out rsa_1024_pub.pem

服务启动时加载pem文件,以eggjs为例,在app.js中

1
2
3
4
5
6
7
8
9
10
const fs = require('fs');

module.exports = app => {
app.beforeStart(async () => {
app.RSA = {
privateKey: fs.readFileSync('./pem/rsa_1024_priv.pem'),
publicKey: fs.readFileSync('./pem/rsa_1024_pub.pem'),
};
});
}

客户端登录时(启动时)先向服务器请求公钥

1
2
3
4
5
6
Axios.get('/secretKey'))

{
"secretKey":
"LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlHZk1BMEdDU3FHU0liM0RRRUJBUVVBQTRHTkFEQ0JpUUtCZ1FESW5ZanJsMHdjUDhveHBzc2I0eWhuMTRKaAptQnIzbEl3ZzFNbjRXUXVSTkkrNmxXVTI1U25NaGx3ajVjcXVJdkZkRk5PbVFpWnpoMGk4aVZDMEFGR2pxVFRFCjZ6VXN4dTRmL3phdnZPQ3hJUDZrV1ZjVXloNTFFTjV4V3VaRGpFTkFEQzA4ZllQTkx4RlZ0aFVNS2tHLzg1WnIKd2RFRjhzVGVLLzY2ZDlHaEF3SURBUUFCCi0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQo="
}

客户端生成 AES 的 key 与 iv 以及当前的时间戳
使用 RSA 加密 AES 的 key 与 iv 以及时间戳 生成 TOKEN
使用 AES 加密用户密码
将 TOKEN 和时间戳加入 HTTP Header
使用用户名与加密后的密码发起登录请求

1
2
3
4
5
6
Axios.request({
url: '/login',
method: 'POST',
headers: { timestamp, token },
data: { email, password: passwordEncrypted }
}))

服务器接收到登录请求时,使用 RSA 私钥解密 TOKEN ,从中分离出 AES key、 iv以及时间戳
判断请求头中的时间戳与解密后的时间戳是否相等以及当前时间戳(服务器端)与解密后的时间戳间隔是否在两秒内,如不符合条件则是非法请求
完成判断后使用解密后的 key 与 iv 解密密码

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
async login() {
const key = new NodeRSA();
key.setOptions({ encryptionScheme: 'pkcs1' });
key.importKey(this.ctx.app.RSA.privateKey, 'private');
const { email, password } = this.ctx.request.body;
const { token, timestamp } = this.ctx.request.header;
const tokenDecrypted = key.decrypt(token, 'utf8');
const tokenArray = tokenDecrypted.split(',');
const AESKey = tokenArray[0];
const iv = tokenArray[1];
const timestampInToken = tokenArray[2];
if (timestamp !== timestampInToken || new Date().getTime() - timestampInToken > 2000) {
this.ctx.status = 401;
this.ctx.body = {
message: '非法登录',
};
return;
}
const decrypted = CryptoJS.AES.decrypt(
password,
CryptoJS.enc.Base64.parse(AESKey),
{
iv: CryptoJS.enc.Base64.parse(iv),
});
this.ctx.body = {
timestamp,
email,
password: decrypted.toString(CryptoJS.enc.Utf8),
};
}

客户端详细代码如下

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
const CryptoJS = require('crypto-js')
const jsrsasign = require('jsrsasign')
const axios = require('axios')

/*
使用服务器端生成的公钥加密 AES 使用的key,iv 以及当前时间戳/
@param AESKey
@param iv
@param timestamp
@param publicKey
@returns { |string}
*/
const createToken = (AESKey, iv, timestamp, publicKey) => {
const keyString = CryptoJS.enc.Base64.stringify(AESKey)
const ivString = CryptoJS.enc.Base64.stringify(iv)
const text = `${keyString},${ivString},${timestamp}`
const encrypted = jsrsasign.KJUR.crypto.Cipher.encrypt(text, publicKey)
return jsrsasign.hextob64(encrypted)
}

/*
使用AES加密登录使用的密码
@param password
@param AESKey
@param iv
@returns {string}
*/
const encryptPassword = (password, AESKey, iv) => {
const encrypted = CryptoJS.AES.encrypt(password, AESKey, {iv})
return encrypted.toString()
}

const Axios = axios.create({
baseURL: 'http://127.0.0.1:7001',
});

const login = async () => {
const email = 'changjunhao@meimiao.net'
const password = 'Life.like.summer.flowers'

// 获取公钥
const { secretKey }= ( await Axios.get('/secretKey')).data

// 根据公钥pem字符串获取对应的publicKey
const publicKey = jsrsasign.KEYUTIL.getKey(jsrsasign.b64toutf8(secretKey))

// 生成AES使用的盐值
const salt = CryptoJS.lib.WordArray.random(128/8)

// 根据盐值与密码短语生成AES KEY
const AESKey = CryptoJS.PBKDF2('Life like summer flowers', salt, {keySize: 256/32})

// 生成AES使用的iv
const iv = CryptoJS.lib.WordArray.random(16)

// 校验时效的时间戳
const timestamp = new Date().getTime()

// 使用RSA公钥加密生成 token
const token = createToken(AESKey, iv, timestamp, publicKey)

// 使用AES加密密码
const passwordEncrypted = encryptPassword(password, AESKey, iv)
return(await Axios.request({
url: '/login',
method: 'POST',
headers: {
timestamp,
// timestamp: timestamp + 100,
token,
},
data: {
email,
password: passwordEncrypted
}
})).data
}