一个简单的对数据有效性进行验证的办法。
最近在一个项目中跟某金融行业的公司有些合作,他们除了要求通讯使用 HTTPS 之外,还对数据的绝对安全性有一定要求,安全性要求有两方面:
- 数据本身不要求特别的加密措施,只是必须确认提交数据的真实性。
- 数据来往的双方服务器身份必须能够得到有效识别(https 依然有中间人攻击风险)。
- 单个请求的数据在特定时间(目前暂定10秒)之后,就必须失效。
背景知识
- 非对称加密
对称加密指的是双方知道密码就能够解开密文,得到真实信息。而与之对应的非对称加密,即使知道密码也未必能解开密文,它由两部分组成,一部分是用于加密数据的公钥,另外一部分是用于解密数据的私钥,加密数据的公钥可以随意传播给任何一个人,但是解密数据的私钥只能由所有者持有,别人使用公钥加密的数据,只能由所有者通过私钥解密。
- 数据散列(hash)
将一段任意长度的内容,经过某种算法,转换成一个固定长度的字符串,但是这种转换是有损的,转换后的字符串没有办法再转回原始内容,这种算法有一个特征是内容中发生任意的细微改变,都将导致结果的巨大不同,通常用于验证数据的真实性。
实现方案
现有业务可以完全不动,最终决定在数据上增加一个签名字段,该签名整合了对方的名称、当前时间戳、和使用 SHA1 256 算法生成的消息内容散列数据,还有一个增加破译复杂度的盐,然后用 RSA 非对称算法进行签名。
具体代码如下,只使用了 openssl 生成公私钥,以及 node 的 crypto 模块:
#!/bin/env node
const crypto = require('crypto');
// 私钥内容,用于解密签名
// 使用 openssl genrsa -out rsa.private 1024 生成
const PRIVATE_KEY = `-----BEGIN RSA PRIVATE KEY-----
MIICXAIBAAKBgQDdAwx2Srt4i9363PL+RK6o/LF58cbAwX9+IVoQGmBvf8jDJZqA
CkOBwdRo0LSeTQRBhe5HuuFS3VpatFIoghq069RfeQfOCxH+qzNqYQ+nI5Er3+FK
9rZFqfbWjZt7GxSfCjZkcikC1z2mgetleUMow0Pi7SuV0PadbqYt4cC16QIDAQAB
AoGBAKWGEgBKKiu3PRIUBp0uXU1Mq7Lzw/I7OTwCyIwE5TK8lmSpNhQtG7ADtgym
Oo/QiJ52KyZnrTe9dl02bc3O2yY+lG1gFeP52zYq0dcAucJqHwyIMPhPsxANgZHX
w+8Y3F5L124MWnZ1YOL9ckNDa7sni8OqZNKgQtu8fAmdTPQBAkEA8xtK571bZ5XO
3HTqOcbGFy3xe+MHndG24QgNdTeedX1zNOnfVtSOEQa53AK9ImKjMZBd3VlXq+2o
GMNa2gWYaQJBAOi7xZg560iDPk/qPscJRgDTyfibAFaV5YNXnPgV4iPWlmn8/Kvr
vLmd79fyTYaps8SZ65Rz7DjQwSQBMyXSgYECQDDB0IwZ1jM4QHzGlhNwYlpTxJLs
PaLRZLRNQSW5Ofamamy6Wyi3CKcxiiUuB3DWB5TxN2IlgQfiakxNIfOIG8ECQHVT
zD583Hd26qABGFrg+vCJ1KVHBvmfodAACDstVQ76LGQMTRkiw8bTr0kvdxPvU5hG
fHQfqLPP0b6j+DQWFoECQCKLSQIGoJIJ+BQPcWpsO7T4zJxSd2gbvMIsKydtn52q
c44/dX59oXkDt2gvDSrGY4Lj/RswNK652ymBIfgKfkI=
-----END RSA PRIVATE KEY-----`;
// 公钥内容,用于加密签名
// 使用 openssl rsa -in rsa.private -pubout -out rsa.pub 生成
const PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDdAwx2Srt4i9363PL+RK6o/LF5
8cbAwX9+IVoQGmBvf8jDJZqACkOBwdRo0LSeTQRBhe5HuuFS3VpatFIoghq069Rf
eQfOCxH+qzNqYQ+nI5Er3+FK9rZFqfbWjZt7GxSfCjZkcikC1z2mgetleUMow0Pi
7SuV0PadbqYt4cC16QIDAQAB
-----END PUBLIC KEY-----`;
// 加密的信息
const MESSAGE = { message: 'Hello World' };
// 信息散列方式
const MESSAGE_HASH_TYPE = 'sha256';
// 应用名称,长度不定,双方知道即可
const APP_NAME = 'testApp';
// Salt,盐,双方约定增加破解难度的随机数,为 32 字节长度
const SALT = 'd41d8cd98f00b204e9800998ecf8427e';
// 签名超时时间,按秒计算,
const TIMEOUT = 10;
// 签名类型
const SIGNATURE_TYPE = 'RSA-SHA256';
// 签名输出类型
// see https://nodejs.org/dist/latest-v7.x/docs/api/crypto.html#crypto_sign_sign_private_key_output_format
const SIGNATURE_OUTPUT_TYPE = 'base64';
/**
* 签名函数
*
* @param {privateKey} string 加密用私钥
* @param {appName} string 加密内容
* @param {salt} string 盐
* @param {message} string | object | array 消息内容
* @returns string 加密后的签名字符串
*/
function sign(privateKey, appName, salt, message = '') {
// 当前时间的 unix 时间戳
const timestamp = parseInt(new Date().getTime()/1000);
// 生成信息散列
const json = JSON.stringify(message);
const hash = crypto.createHash(MESSAGE_HASH_TYPE);
hash.update(json);
const messageHash = hash.digest('hex');
// 生成加密的字符串
const signStr = appName + timestamp + salt + messageHash;
console.log(`String will sign is - ${signStr}`);
// 加密内容
const signer = crypto.createSign(SIGNATURE_TYPE); // 默认通过 RSA-SHA256 对称算法进行加密
signer.update(signStr);
return signer.sign(privateKey, SIGNATURE_OUTPUT_TYPE)
}
/**
* 签名验证函数
* 原理是根据加密后的签名,倒推10秒钟,其中一次的签名如果对上了,就算验证成功。
* 中途最多可能需要生成 10 次加密后的签名,进行对比。
*
* @param {publicKey} string 解密用公钥
* @param {privateKey} string 生成加密用私钥
* @param {appName} string 应用名称
* @param {salt} string 盐
* @param {signature} string 用于验证的签名
* @param {message} string | object | array 消息内容
* @returns boolean 成功为真,失败为假
*/
function validate(publicKey, privateKey, appName, salt, signature, message) {
// 获取当前时间戳
const now = parseInt(new Date().getTime()/1000);
// 生成信息散列
const json = JSON.stringify(message);
const hash = crypto.createHash(MESSAGE_HASH_TYPE);
hash.update(json);
const messageHash = hash.digest('hex');
// 从当前时间戳往前推 10 秒,检查签名字符串是否合法
let count = 0;
let timestamp = now;
while(count < TIMEOUT) {
const signStr = appName + timestamp + salt + messageHash;
// 生成签名
const signer = crypto.createSign(SIGNATURE_TYPE);
signer.update(signStr);
const newSignature = signer.sign(privateKey, SIGNATURE_OUTPUT_TYPE);
// 首先检查签名是否正确
const verifier = crypto.createVerify(SIGNATURE_TYPE);
verifier.update(signStr);
const result = verifier.verify(publicKey, newSignature, SIGNATURE_OUTPUT_TYPE)
// 签名正确后,检查加密后内容是否一致
if (result) {
// Signature correct
if (newSignature === signature) {
// Content correct
return true
}
}
count += 1;
timestamp -=1;
}
return false
}
const signature = sign(PRIVATE_KEY, APP_NAME, SALT, MESSAGE);
console.log(`Signature is - ${signature}`);
// 2 秒内签名可用,应该返回 true
setTimeout(() => {
const result = validate(PUBLIC_KEY, PRIVATE_KEY, APP_NAME, SALT, signature, MESSAGE);
console.log(`Validate should be correct in 2 seconds - ${result}`);
}, 2000);
// 10 秒后签名失效,应该返回 false
setTimeout(() => {
const result = validate(PUBLIC_KEY, PRIVATE_KEY, APP_NAME, SALT, signature, MESSAGE);
console.log(`Validate should be incorrect after 10 seconds - ${result}`);
}, 10000);
执行结果如下:
$ node ./sign.js
String will sign is - testApp1484759320d41d8cd98f00b204e9800998ecf8427ea5759911f3c834348667ca417f6c8bb4484b58f9af27e91e7c95b208e43761fa
Signature is - uDQgfC2oy+4vndp5/PZA+6vnp4q6iicTFIHDVh6gJZ5Ufkd0/c75Oj3xK/jcKthAOkbNnd3A+CcVzo6aFdKmtPAmsR71ZgoVpJ+5g+kAzz5Gruzt499i9IYxhTQKK5Vo2/swF02n6ShOwWvwVeERUfzzYT9AIuUNhmQJD3+egm8=
Validate should be correct in 2 seconds - true
Validate should be incorrect after 10 seconds - false
可以看到该文本被序列化成 testApp1484759320d41d8cd98f00b...
这样的字符串,进而加密成了一串不可识别的代码,接收方拿到这串代码之后即可验证数据的真实性,当超时时则验证失效,这样做还有个好处是将数据失效的时间交由接收方决定,消息发送方只需要增加签名即可。
这段代码只是阐述一个思考过程,并未充分优化,其实可以异步化将每次验证过程放入消息队列中执行。
欢迎交流。;-)
版权所有丨转载请注明出处:https://kxq.io/archives/一个简单的对数据有效性进行验证的办法