-
2021-11-24 01:24:06
大家好,我是 漫步 ,今天来分享一篇关于文件加密和解密相关的,喜欢我的分享记得关注我并设为星标。
本文作者:coolyuantao[1]
背景
最近团队遇到一个小需求,存在两个系统 A、B,系统 A 支持用户在线制作皮肤包,制作后的皮肤包用户可以下载后,导入到另外的系统 B 上。皮肤包本身的其实就是一个 zip 压缩包,系统 B 接收到压缩包后,解压并做一些常规的校验,比如版本、内容合法性校验等,整体功能也比较简单。
但没想到啊,一帮测试人员对我们开发人员一顿输出,首先绕过系统 A 搞了几个视频文件,把后缀改成
zip
就直接想上传,系统 B 每次都是等到上传完后才发现文件不合法,系统 B 在文件没上传完前又无法解压,也不知道文件内容是不是合法的,就这么消耗了大量带宽、大量时间后才提示用户皮肤包有问题。这里涉及了两个问题,我们来捋一捋:
文件如何做加密,这样用户便无法去逆向,压缩包内部的敏感信息不会泄露出去。
服务端在接收到信息流时,在未传输完时如何去判断压缩包的合法性,提前告知用户。
AES VS RSA
说到加密,自己很多人会想到对称算法 AES[2] 以及非对称算法 RSA[3]。这两种算法按字面意思也较好理解,对称加密技术说白一点就是加密跟解密使用的是同一个密钥,这种加密算法速度极快,安全级别高,加密前后的大小一致;非对称加密技术则有
公钥PK
、私钥SK
,算法的原理在于寻找两个素数,让他们的乘积刚好等于一个约定的数字,非对称算法的安全性是依赖于大数的分解,这个目前没有理论支持可以快速破解,它的安全性完全依赖于这个密钥的长度,一般用 1024 位已经足够使用。但是它的速度相比对称算法慢得多,一般仅用于少量数据的加密,待加密的数据长度不能超过密钥的长度。使用 AES 对文件加密
结合这两种加密方式的优缺点,我们采用 AES 对文件本身做加解密,使用 AES 的原因主要考虑如下:
加解密性能问题,AES 的速度极快,相比 RSA 有 1000 倍以上提升。
RSA 对源文有长度的要求,最大长度仅有密钥长度。
AES 的加密算法 Node.js 的crypto[4]模块中已经有内置,具体的使用可以参考官方文档。
AES 加密逻辑
const crypto = require('crypto'); const algorithm = 'aes-256-gcm'; /** * 对一个buffer进行AES加密 * @param {Buffer} buffer 待加密的内容 * @param {String} key 密钥 * @param {String} iv 初始向量 * @return {{key: string, iv: string, tag: Buffer, context: Buffer}} */ function aesEncrypt (buffer, key, iv) { // 初始化加密算法 const cipher = crypto.createCipheriv(algorithm, key, iv); let encrypted = cipher.update(buffer); let end = cipher.final(); // 生成身份验证标签,用于验证密文的来源 const tag = cipher.getAuthTag(); return { key, iv, tag, buffer: buffer.concat([encrypted, end]); }; }
AES 解密逻辑
解密整体跟加密一样,只是接口换个名字即可:
const crypto = require('crypto'); const algorithm = 'aes-256-gcm'; /** * 对一个buffer进行AES解密 * @param {{key: string, iv: string, tag: Buffer, buffer: Buffer}} ret 待解密的内容 * @param {String} key 密钥 * @param {String} iv 初始向量 * @return {Buffer} */ function aesDecrypt ({key, iv, tag, buffer}) { // 初始化解密算法 const decipher = crypto.createDecipheriv(algorithm, key, iv); // 生成身份验证标签,用于验证密文的来源 decipher.setAuthTag(tag); let decrypted = decipher.update(buffer); let end = decipher.final(); return Buffer.concat([decrypted, end]); }
AES 具体使用
有了上述两个接口后,我们便可实现一个简单的对称加密了:
const key = 'abcdefghijklmnopqrstuvwxyz123456'; // 32 共享密钥,长度跟算法需要匹配上 const iv = 'abcdefghijklmnop'; // 16 初始向量,长度跟算法需要匹配上 let fileBuffer = Buffer.from('abc'); // 加密 let encrypted = aesEncrypt(fileBuffer, key, iv); // 解密 let context = aesDecrypt(encrypted); console.log(context.toString());
一般情况下,这个密钥较为重要,如果发生泄露则加密失去意义,所以
key
、iv
会使用随机数动态生成,比如:const key = crypto.randomBytes(32); const iv = crypto.randomBytes(16);
通过上述的调整后,加解密文件是比较容易的,回到我们的业务系统上面,系统 A 生成的压缩包,最终是需要给系统 B 使用,两个系统是隔离的,那这样
key
、iv
如何传输到系统 B 上面呢,况且还是动态生成的,生成出来key
系统 B 是不知道的。读到这聪明的你可能会想到,在把压缩包给到 B 的时候,顺便把
key
、iv
一同提交过去不就可以了,但细想了下,这个肯定不能明文把这个密钥发送过去,要不这个加密意义何在。这时便需要用上 RSA 非对称加密技术了。
使用 RSA 算法对密钥再次进行非对称加密
RSA 的加密算法 Node.js 的 crypto 模块[5] 中已经有内置,具体的使用可以参考官方文档。
生成 RSA 的公钥与私钥
使用 openssl[6] 组件可以直接生成
RSA
的公钥私钥对,具体的命令可以参考:www.scottbrady91.com/OpenSSL/Cre…[7]。# 生成私钥 openssl genrsa -out private.pem 1024 # 提取公钥 openssl rsa -in private.pem -pubout -out public.pem
这样生成出来的两个文件
private.pem
、public.pem
就可以使用了,下面我们使用 Node.js 实现具体的加解密逻辑。RSA 加密逻辑
const fs = require('fs'); const crypto = require('crypto'); const PK = fs.readFileSync('./public.pem', 'utf-8'); /** * 对一个buffer进行RSA加密 * @param {Buffer} 待加密的内容 * @return {Buffer} */ function rsaEncrypt (buffer) { return crypto.publicEncrypt(PK, buffer); }
RSA 解密逻辑
const fs = require('fs'); const crypto = require('crypto'); const SK = fs.readFileSync('./private.pem', 'utf-8'); /** * 对一个buffer进行RSA解密 * @param {Buffer} 待解密的内容 * @return {Buffer} */ function rsaDecrypt (buffer) { return crypto.privateDecrypt(SK, buffer); }
RSA 具体使用
有了上述接口后,便可对 AES 的密钥进行加密后再传输,服务器 B 保存好
RSA 私钥
,服务器 A 则可以直接用RSA 公钥
对数据加密后再发送,结合刚AES
的逻辑后,如下:/** * 加密文件 * @param {Buffer} fileBuffer * @return {{file: Buffer, key: Buffer}} */ function encrypt (fileBuffer) { const key = crypto.randomBytes(32); const iv = crypto.randomBytes(16); const { tag, file } = aesEncrypt(fileBuffer, key, iv); return { file, key: rsaEncrypt(Buffer.concat([key, iv, tag])); // 由于长度是固定的,直接连在一起即可 }; } /** * 解密文件 * @param {{file: Buffer, key: Buffer}} * @return {Buffer} */ function decrypt ({file, key}) { const source = rsaDecrypt(key).toString(); const k = source.slice(0, 32); const iv = source.slice(32, 48); const tag = source.slice(48); return aesDecrypt({ key: k, iv, tag, buffer: file }) }
这样结合在一起后,服务器 A 生成的压缩包,只要包含好
{file, key}
这两块内容,服务器 B 便可把文件解密出来了,这样基本上实现了我们第一点的目标:1. 文件如何做加密,这样用户便无法去逆向,压缩包内部的敏感信息不会泄露出去但还遗留了另外一个问题需要解决:2. 服务端在接收到信息流时,在未传输完时如何去判断压缩包的合法性,提前告知用户
关于解密的还可以看看:记一次破解前端加密详细过程
优化加密文件
按上面的加密方式,输出的结果是一个
buffer文件
内容,以及一个加密过的key
,除了这些信息外,一般这个buffer文件
压缩包还会有一些额外的信息,比如:版本号、压缩包生成时间,描述信息等。这些信息按常规的方式,可能是分成几个文件,然后再打一个压缩包把文件放在一起,比如:// zip file - pkg manifest.json // 额外的信息 key.json // 保存了加密过的密钥 file.json // 加密过的文件
但如果用这种方式保存,一般情况下还要对这个
zip文件
做下加密,然后改下后缀名,但是服务器 B 在读取这个文件后仍然是需要全部接收,再解压到临时目录,读取内容后才可以做校验,这样问题仍然解决不了。除此之外,还有另外一个常见的需求,产品一般希望在浏览器侧在文件上传时就先做初步的解析,把明显不合法的文件提示到用户,这样用户体验更好。
这个问题的解决方案也不难,这些所有额外的信息都是可以把它当成二进制插入到文件的头部上的,比如:
包字段描述:|----插入的额外信息----|----后面才是真正的文件内容----| 二进制文件:010101010101010101010xxxxxxxxxxxxxxxxxxxxxxxxxxxx
文件头字段设计
我们把这些所有信息,按一定的格式,使用二进制的方式全部串连在一起,最终交付的只有一个组合过的文件,比如:
// theme pkg. 0 8 16 |------flag------|--extra length--| |----------extra data...----------| |-------------data...-------------|
flag
:
固定标识
THEME
,长度:8 byte
,说明该压缩包为一个皮肤包,这样可以快速对压缩包进行识别extra length
:
extra data
的真实长度,这是一个 16 进制的数据,长度:8 byte
,说明插入的数据长度。比如:长度35
的数据,转化为 16 进制后为0x23
,那这字段为00000023
extra data
:
使用
RSA
加密过的数据,我们可以把上述需要用RSA
加密的信息全部放在这里,比如key
字段、版本号、描述信息等data
:
使用
AES
加密过的数据,可以通过extra data
里面保存的key
把真实的数据全部解密出来生成的新的加密文件
有了上面的理论基础后,马上可以实践起来,代码如下:
/** * 加密文件 * @param {Buffer} fileBuffer * @return {Buffer} */ function encrypt (fileBuffer) { const key = crypto.randomBytes(32); const iv = crypto.randomBytes(16); const version = 'v1.1'; // 记录上所有额外的压缩外信息,比如版本号、原始的密钥 const extraJSON = { version, key, iv } // 完成文件的AES加密,并输出身份验证标签 const { tag, file } = aesEncrypt(fileBuffer, key, iv); extraJSON.tag = tag; // 对 extraJSON 整个进行RSA加密 const extraData = rsaEncrypt(Buffer.from(JSON.stringify(extraJSON))); const extraLength = extraData.length; // 最终把所有数据合并在一起 return Buffer.concat([ Buffer.from('THEME'), Buffer.from(Buffer.from(extraLength.toString(16).padStart(8, '0'))), extraData, file ]); }
通过这种加密方式后,相关的信息都放在文件的头部上,我们可以不用对整个文件进行操作的时候,便可以轻松读取出来,对于解密其实就是一个反向的操作。
对新生成的文件进行解密
/** * 解密文件 * @param {Buffer} fileBuffer * @return {Buffer} */ function decrypt (fileBuffer) { const type = fileBuffer.slice(0, 8); // THEME const extraLength = +('0x' + fileBuffer.slice(8, 16).toString()); const extraDataEndIndex = 16 + extraLength; // 对已经被RSA加密过的数据进行解密操作 const extraData = rsaDecrypt(fileBuffer.slice(16, extraDataEndIndex)); const extraJSON = JSON.parse(extraData); // 最终使用AES再对剩下文件进行解密操作,即为最终的文件 return aesDecrypt({ key: extraJSON.key, iv: extraJSON.iv, tag: extraJSON.tag, buffer: Buffer.slice(extraDataEndIndex) }); }
使用这种方式处理后,在
RSA
解密出extraData
的时候,就可以对整个文件进行各种校验,整个过程只要有异常说明文件已经被篡改,用这种方式比用压缩包会好很多,特别是文件体积庞大的时候,可以流式处理,发现不合理时即可马上阻止。浏览器端如何解析该文件
由于现在整个文件格式都是二进制流,现代的浏览器都是有相应的能力去读取并做处理的,这样也可以在用户上传文件时先做一定的初步处理,体验会有比较大的提升
可以使用
DataView
的方式把二进制数据读取出来,详情可以参考:DataView[8],初步的实现如下:/** * 把二进制流转成对应ascii字符 * @param {DataView} dv 二进制数据库 * @param {Number} start 起始位置 * @param {Number} end 结束位置 * @return {String} */ function buffer2Char (dv, start, end) { let ret = []; for (let i = start; i < end; i++) { let charCode = dv.getUint8(i); let code = String.fromCharCode(charCode); ret.push(code); } return ret.join(''); } function test () { let fileDom = document.getElementById('file'); let file = fileDom.files[0]; let reader = new FileReader(); reader.readAsArrayBuffer(file); reader.addEventListener("load", function(e) { let dv = new DataView(buffer); let flag = buffer2Char(dv, 0, 8); // THEME var extraLength = +('0x' + buffer2Char(dv, 8, 16)); var extraData = buffer2Char(dv, 16, extraLength); console.log(flag, extraLength, extraData); }); }
当然用这种方式有一个前提是需要把一部分非敏感的信息放出来,不要加密,这样便可以实现在浏览器端也对文件进行读取。只需要前后端的格式约定做好,都可以采用这种方式对压缩包进行一定的初步校验,当然后端的校验仍然是需要做好的。
至此,我们完成了对文件的加密、解密以及浏览器解析等操作,希望对你们有帮助
结语
文件的加密、解密在后端其实是一个很常规的操作,除了上面聊到的
AES
、RSA
,其实还有其它很多加密方案,具体可以看看 Node.js crypto 模块[9],已经有内置比较多的方案可以直接使用。当然文件的加解密,也可以直接用
zip
、7z
等这些压缩工具,再配合密码的方案,一般情况也是够用的,但是免不了有定制化的需求,一般也都是结合使用,比如上面的fileBuffer
实际内部就是先用这些工具对文件进行了压缩并加密。还是以场景为重,多种方案结合效果更好。文件加解密的就讲到这里吧,还有什么其它问题的可以在评论区讨论,谢谢。
关于本文
作者:IDuxFE
https://juejin.cn/post/6997565255463206925参考资料
[1]
https://juejin.cn/user/1047150053304157
[2]https://en.wikipedia.org/wiki/Advanced_Encryption_Standard
[3]https://en.wikipedia.org/wiki/RSA_(cryptosystem)
[4]http://nodejs.cn/api/crypto.html
[6]https://www.openssl.org/
[7]https://www.scottbrady91.com/OpenSSL/Creating-RSA-Keys-using-OpenSSL
[8]https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/DataView
[9]http://nodejs.cn/api/crypto.html
最后
关注下方「前端开发博客」,回复 “加群”
加入我们一起学习,天天进步
如果觉得这篇文章还不错,来个【分享、点赞、在看】三连吧,让更多的人也看到~
更多相关内容 -
Spring Boot 实现配置文件加解密原理
2020-08-19 03:20:36主要介绍了Spring Boot 实现配置文件加解密原理,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧 -
android中对文件加密解密的实现
2020-09-01 10:02:27本文主要介绍了android中对文件加密解密的实现,文件加密现在的应用很广,有需要的朋友可以了解一下。 -
Spring boot配置文件加解密详解
2020-08-26 06:35:54主要给大家介绍了关于Spring boot配置文件加解密的相关资料,文中通过示例代码以及图文介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧 -
文件加解密软件
2018-06-23 10:09:34加密软件方便给文档、文件夹各类文件快速加密解密,安全可靠管理自己文件。 -
Java文件加解密工具类
2018-09-12 10:48:31Java文件加解密工具类,使用AES算法,256位秘钥。srcFile为输入文件,destFile为输出文件。 -
[C++]AES全文件加解密
2020-05-02 16:30:19C++下的AES加密算法,可以对所有文件进行加解密。 控制台程序,VS2012下开发,不引入额外库,方便移植。 包含加解密程序,丰富注释信息,初学者也能看懂。 包含一个加密例子。 -
华为光猫配置文件加解密工具.zip
2020-05-03 01:02:32华为光猫配置文件加解密工具,包含XML和CFG等多种格式文件加解密,傻瓜式操作,亲测好用,对华为HS8145C,HS8145V以及HGxxxx系列华为光猫均有效,可用于导出配置文件,解密修改后重新加密上传,以达到修改部分选项和... -
华为配置文件加解密.exe
2021-08-08 03:27:28华为配置文件加解密.exe -
Java文件加解密工具
2021-07-12 09:40:22JavaSwing 字符串、文件加解密工具源码。支持字符流,字节流,分段加密。支持对称AES,DES,非对称RSA等加密,输出格式支持HEX,Base64,二进制流等 -
java实现文件加密解密
2016-10-25 10:19:48利用java实现将资源文件(包括图片、动画等类型)进行简单的加密、解密 -
中兴光猫cfg配置文件加密解密工具ctce8_cfg_tool
2021-12-01 10:30:38中兴光猫cfg配置文件加密解密工具 -
C#文件加密解密(完整项目)
2017-11-05 11:28:11实现C#加密各种文件 和解密加密后的文件 完整工程 直接可以编译运行 -
华为ONU配置文件加密解密工具
2020-05-09 14:23:38华为ONU配置文件加密解密工具,XML,CFG,$1,$2,SU解密! -
华为光猫配置文件加解密工具.exe
2020-04-20 15:40:09加密方法: 先用7-zip压缩成gzip格式压缩文件 然后输入文件: 压缩后的文件 输出文件: new.xml 点加密 之后就可以通过导入配置文件导入光猫了。 -
RSA文件加解密(C#源代码)
2018-12-21 17:11:43C#编写的文件加、解密,使用RSA算法实现。选择本地文件后,对文件进行加密,再对加密后的文件进行解密。代码在VS 2017下直接运行。(源代码,C#实现) -
华为光猫配置文件加解密工具
2018-10-15 21:13:33华为光猫配置文件加解密工具(华为光猫加解密工具)是可以帮助用户对华为光猫备份文件进行解密或加密的小工具。解密后可以查看自己路由器的超级密码,从而获取更多实用的使用权限,解决宽带总被人蹭网的问题,欢迎下载... -
Android文件加密解密的实现
2017-12-15 11:41:29最近项目中需要用到加解密功能,言外之意就是不想让人家在反编译后通过不走心就能获取文件里一些看似有用的信息 -
文件加解密
2014-08-21 09:12:28在信息安全技术中,常需要对文件进行加密来实现文件的安全性,这个程序可以实现对文件的加密及解密 -
全能文件加密解密源码.zip
2019-08-01 15:55:35Visual Studio工程,C#,源代码,文件加密码解密,感兴趣的可以下载看一下 -
DES文件加密解密算法实现
2017-07-25 21:36:55DES对于文件的加密解密,采用CBC-DES模式编写。可联系maibox_krj@163.com获取。 -
Spring Boot 在启动时进行配置文件加解密
2020-08-19 01:57:43主要介绍了Spring Boot 在启动时进行配置文件加解密的方法,本文通过实例给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下 -
delphi 文件加密解密(支持大文件)
2014-09-25 14:13:33CSDN也有相同的资源,不过缺少关键性代码,而这份是完整源码,在D7下可直接使用,经过...Delphi实现CryptAPI加密文件、解密文件的示例源码,选择需要加密加密的文件,设置密码,可轻松实现加密,同时程序还实现解密。 -
Qt异或实现文件加解密
2019-01-03 12:04:40这是使用异或实现的简单的文件的加解密,另外还有一个我的资源实现加解密的在这里:https://download.csdn.net/download/lxiao428/10802653 -
C# 文件加解密工具
2018-03-23 09:42:43用C#写的加密工具,支持任意文件加解密,支持注册表,可以双击文件打开程序 -
Qt实现文件的加解密
2018-11-23 11:31:16这是一个文件加解密的demo,亲测可用。Base64是一种基于64个可打印字符来表示二进制数据的表示方法。由于2的6次方等于64,所以每6个比特为一个单元,对应某个可打印字符。三个字节有24个比特,对应于4个Base64单元,... -
华为光猫配置文件加密解密工具
2016-01-18 18:53:39华为光猫备份文件解密 加密 超级密码查看 -
java—DES文件加解密.zip
2021-03-21 15:52:30java语言编写的,通过实现DES算法对文件进行加解密,是单机版的小程序,有简易的图形界面,可以修改密钥,可以对150MB以上的文件进行快速加解密 -
DES文件加密解密系统 密码学课设
2015-07-06 14:30:53基于C语言编写的一个DES文件加密解密系统,可以实现对文件内容进行加密解密,用于密码学课程设计,包含完整可运行代码,以及课设报告。 -
encrypt_AES-CBC_文件加解密_
2021-09-28 21:47:23函数为简单测试函数,实现aes文件加解密