开发需知的文件加密与解密

背景

最近团队遇到一个小需求,存在两个系统 A、B,系统 A 支持用户在线制作皮肤包,制作后的皮肤包用户可以下载后,导入到另外的系统 B 上。皮肤包本身的其实就是一个 zip 压缩包,系统 B 接收到压缩包后,解压并做一些常规的校验,比如版本、内容合法性校验等,整体功能也比较简单。

但没想到啊,一帮测试人员对我们开发人员一顿输出,首先绕过系统 A 搞了几个视频文件,把后缀改成 zip 就直接想上传,系统 B 每次都是等到上传完后才发现文件不合法,系统 B 在文件没上传完前又无法解压,也不知道文件内容是不是合法的,就这么消耗了大量带宽、大量时间后才提示用户皮肤包有问题。

这里涉及了两个问题,我们来捋一捋:

  1. 文件如何做加密,这样用户便无法去逆向,压缩包内部的敏感信息不会泄露出去。
  2. 服务端在接收到信息流时,在未传输完时如何去判断压缩包的合法性,提前告知用户。

AES VS RSA

说到加密,自己很多人会想到对称算法 AES 以及非对称算法 RSA。这两种算法按字面意思也较好理解,对称加密技术说白一点就是加密跟解密使用的是同一个密钥,这种加密算法速度极快,安全级别高,加密前后的大小一致;非对称加密技术则有公钥PK私钥SK,算法的原理在于寻找两个素数,让他们的乘积刚好等于一个约定的数字,非对称算法的安全性是依赖于大数的分解,这个目前没有理论支持可以快速破解,它的安全性完全依赖于这个密钥的长度,一般用 1024 位已经足够使用。但是它的速度相比对称算法慢得多,一般仅用于少量数据的加密,待加密的数据长度不能超过密钥的长度。

使用 AES 对文件加密

结合这两种加密方式的优缺点,我们采用 AES 对文件本身做加解密,使用 AES 的原因主要考虑如下:

  1. 加解密性能问题,AES 的速度极快,相比 RSA 有 1000 倍以上提升。
  2. RSA 对源文有长度的要求,最大长度仅有密钥长度。

AES 的加密算法 Node.js 的crypto模块中已经有内置,具体的使用可以参考官方文档。

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()); 

一般情况下,这个密钥较为重要,如果发生泄露则加密失去意义,所以keyiv会使用随机数动态生成,比如:

const key = crypto.randomBytes(32);
const iv = crypto.randomBytes(16); 

通过上述的调整后,加解密文件是比较容易的,回到我们的业务系统上面,系统 A 生成的压缩包,最终是需要给系统 B 使用,两个系统是隔离的,那这样 keyiv 如何传输到系统 B 上面呢,况且还是动态生成的,生成出来 key 系统 B 是不知道的。
读到这聪明的你可能会想到,在把压缩包给到 B 的时候,顺便把 keyiv 一同提交过去不就可以了,但细想了下,这个肯定不能明文把这个密钥发送过去,要不这个加密意义何在。这时便需要用上RSA 非对称加密技术了。

使用 RSA 算法对密钥再次进行非对称加密

RSA 的加密算法 Node.js 的 crypto 模块 中已经有内置,具体的使用可以参考官方文档。

生成 RSA 的公钥与私钥

使用 openssl 组件可以直接生成 RSA 的公钥私钥对,具体的命令可以参考:https://www.scottbrady91.com/openssl/creating-rsa-keys-using-openssl。

# 生成私钥
openssl genrsa -out private.pem 1024# 提取公钥
openssl rsa -in private.pem -pubout -out public.pem 

这样生成出来的两个文件 private.pempublic.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
- pkgmanifest.json       // 额外的信息key.json            // 保存了加密过的密钥file.json           // 加密过的文件 

但如果用这种方式保存,一般情况下还要对这个 zip文件 做下加密,然后改下后缀名,但是服务器 B 在读取这个文件后仍然是需要全部接收,再解压到临时目录,读取内容后才可以做校验,这样问题仍然解决不了。

除此之外,还有另外一个常见的需求,产品一般希望在浏览器侧在文件上传时就先做初步的解析,把明显不合法的文件提示到用户,这样用户体验更好。

这个问题的解决方案也不难,这些所有额外的信息都是可以把它当成二进制插入到文件的头部上的,比如:

包字段描述:|----插入的额外信息----|----后面才是真正的文件内容----|  
二进制文件:010101010101010101010xxxxxxxxxxxxxxxxxxxxxxxxxxxx 

文件头字段设计

我们把这些所有信息,按一定的格式,使用二进制的方式全部串连在一起,最终交付的只有一个组合过的文件,比如:

// theme pkg.0                8                16                 
|------flag------|--extra length--|
|----------extra data...----------|
|-------------data...-------------|
  • flag: 固定标识 THEME,长度:8 byte,说明该压缩包为一个皮肤包,这样可以快速对压缩包进行识别
  • extra lengthextra 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);    // THEMEconst 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,初步的实现如下:

/*** 把二进制流转成对应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);   // THEMEvar extraLength = +('0x' + buffer2Char(dv, 8, 16));var extraData = buffer2Char(dv, 16, extraLength);console.log(flag, extraLength, extraData);});
} 

当然用这种方式有一个前提是需要把一部分非敏感的信息放出来,不要加密,这样便可以实现在浏览器端也对文件进行读取。只需要前后端的格式约定做好,都可以采用这种方式对压缩包进行一定的初步校验,当然后端的校验仍然是需要做好的。

至此,我们完成了对文件的加密、解密以及浏览器解析等操作,希望对你们有帮助

结语

文件的加密、解密在后端其实是一个很常规的操作,除了上面聊到的 AESRSA,其实还有其它很多加密方案,具体可以看看 Node.js crypto 模块,已经有内置比较多的方案可以直接使用。

当然文件的加解密,也可以直接用 zip7z 等这些压缩工具,再配合密码的方案,一般情况也是够用的,但是免不了有定制化的需求,一般也都是结合使用,比如上面的 fileBuffer 实际内部就是先用这些工具对文件进行了压缩并加密。还是以场景为重,多种方案结合效果更好。

文件加解密的就讲到这里吧,还有什么其它问题的可以在评论区讨论,谢谢。

网络安全学习资料

下面我准备了很多网络安全学习资料,包括视频教程+学习路线+必读电子书+相关工具安装包+面试题等欢迎大家来白嫖
在这里插入图片描述
vx扫描下方二维码即可免费领取哦~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/137811.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

dvwa靶场通关(十二)

第十二关&#xff1a;Stored Cross Site Scripting (XSS)&#xff08;存储型xss&#xff09; low 这一关没有任何防护&#xff0c;直接输入弹窗代码 弹窗成功 medium 先试试上面的代码看看&#xff0c;有没有什么防护 发现我们的script标签不见了&#xff0c;应该是被过滤掉…

跑腿系统开发:构建实时任务分配算法的技术挑战

在跑腿系统中&#xff0c;实时任务分配算法是确保任务快速高效完成的关键因素之一。本文将介绍构建实时任务分配算法时可能面临的技术挑战&#xff0c;并提供一个简单的Python示例来解决这些挑战。 技术挑战&#xff1a; 实时数据处理&#xff1a; 跑腿系统需要处理大量的实时任…

Windows Server 2008安装.NET Framework 3.5

安装.NET Framework 3.5一、打开服务器管理器 在开始菜单中搜索“服务器管理器” 二、添加.NET Framework 3.5.1功能 &#xff08;一&#xff09;功能-》添加功能 &#xff08;二&#xff09;选择功能“.NET Framework 3.51” 1.点击“NET Framework 3.5.1”勾选框 2.点击“添…

C#小知识

项目编译后复制文件到生成目录 方法1 对于单个文件&#xff0c;可以点击属性。输出目录里选择始终复制。 方法2 把项目中的ServerScripts复制到输出目录。 在项目设置中&#xff0c;生成事件里添加批处理 xcopy $(ProjectDir)ServerScripts\*.* $(TargetDir)ServerScrip…

本地搭建CFimagehost私人图床——“cpolar内网穿透”

文章目录 1.前言2. CFImagehost网站搭建2.1 CFImagehost下载和安装2.2 CFImagehost网页测试2.3 cpolar的安装和注册 3.本地网页发布3.1 Cpolar临时数据隧道3.2 Cpolar稳定隧道&#xff08;云端设置&#xff09;3.3.Cpolar稳定隧道&#xff08;本地设置&#xff09; 4.公网访问测…

【JVM】Java类的加载机制!

一、类的生命周期 类加载过程包含&#xff1a;加载、验证、准备、解析和初始化 &#xff0c;一共包括5 个阶段。 &#xff08;1&#xff09;加载&#xff1a; 简单来说就是将java类的字节码文件加载到机器内存中。在加载类时&#xff0c;Java虚拟机必须完成以下3件事情&…

[Linux打怪升级之路]-缓冲区

前言 作者&#xff1a;小蜗牛向前冲 名言&#xff1a;我可以接受失败&#xff0c;但我不能接受放弃 如果觉的博主的文章还不错的话&#xff0c;还请点赞&#xff0c;收藏&#xff0c;关注&#x1f440;支持博主。如果发现有问题的地方欢迎❀大家在评论区指正 本期学习目标&…

AI助力安全监管:TSINGSEE视频智能分析系统烟火识别算法

水火无情人有情&#xff0c;火灾一旦发生没有被及时发现&#xff0c;就能在极短的时间内酿成无法挽回的大祸&#xff0c;所以烟火的监管与处理极为重要。为了让火患在刚发生时就能得到扼制&#xff0c;TSINGSEE青犀AI智能分析网关烟火识别算法具有重要意义。 TSINGSEE青犀AI智能…

Arcgis栅格转点时ERROR 999999: 执行函数时出错。 无法创建要素数据集。 执行(RasterToPoint)失败

Arcgis栅格转点时ERROR 999999: 执行函数时出错。 无法创建要素数据集。 执行(RasterToPoint)失败。 问题描述 原因 输出点要素的位置不对 解决方案 点击新建文件地理数据库 然后在该文件地理数据库下输出

.Net IDE智能提示汉化(.Net6、AspNetCore)

先上现成的.net6汉化文件&#xff0c;可以手动下载后参照 如何为 .NET 安装本地化的 IntelliSense 文件 进行安装。或者使用后文的工具进行自动安装。 无对照英文在前中文在前 汉化内容来自 官方在线文档 &#xff0c;某些内容可能存在明显的机翻痕迹。 上一些效果图&#x…

UINT64整型数据在格式化时使用了不匹配的格式化符%d导致其他参数无法打印的问题排查

目录 1、问题描述 2、格式化函数内部解析待格式化参数的完整机制说明 2.1、传递给被调用函数的参数是通过栈传递的 2.2、格式化函数是如何从栈上找到待格式化的参数值&#xff0c;并完成格式化的&#xff1f; 2.3、字符串格式化符%s对应的异常问题场景说明 2.4、为了方便…

项目实战— pytorch搭建CNN处理MNIST数据集

项目文件夹介绍 项目文件夹 CNN_MNIST_practice文件夹是整个项目的文件夹&#xff0c;里面存放了六个子文件夹以及四个 .py 程序&#xff0c;接下来我们分别来介绍这些文件的内容。 其中 minist_all_CPU.py 是CPU版本的模型训练&#xff0b;测试程序&#xff0c;而 min…

【Redis】Redis的特性和应用场景 · 数据类型 · 持久化 · 数据淘汰 · 事务 · 多机部署

【Redis】Redis常见面试题&#xff08;3&#xff09; 文章目录 【Redis】Redis常见面试题&#xff08;3&#xff09;1. 特性&应用场景1.1 Redis能实现什么功能1.2 Redis支持分布式的原理1.3 为什么Redis这么快1.4 Redis实现分布式锁1.5 Redis作为缓存 2. 数据类型2.1 Redis…

03MyBatis-Plus中的常用注解

常用注解 TableName MyBatis-Plus根据BaseMapper中指定的泛型(实体类型名)确定数据库中操作的表,如果根据实体类型名找不到数据库中对应的表则会报表不存在异常 //向表中插入一条数据 Test public void testInsert(){User user new User(null, "张三", 23, "…

Python编辑器和Pycharm的傻瓜式安装部署

给我家憨憨写的python教程 有惊喜等你找噢 ——雁丘 Python解释器Pycharm的安装部署 关于本专栏一 Python编辑器1.1 使用命令提示符编写Python程序1.2 用记事本编写Python程序 二 Pycharm的安装三 Pycharm的部署四 Pycharm基础使用技巧4.1 修改主题颜色4.2 修改字体4.3 快速修…

mysql中update更新时加条件和不加条件速度对比

测试时有时需要执行更新操作&#xff0c;想知道大量数据update时加where条件和不加where条件速度差异如何&#xff0c;正好有条件测试&#xff0c;记录一下。 数据&#xff1a;9张表&#xff0c;每张表300w条数据 一、对9张表进行单字段更新时不加条件(如&#xff1a;update …

【UE虚幻引擎】UE源码版编译、Andorid配置、打包

首先是要下载源码版的UE&#xff0c;我这里下载的是5.2.1 首先要安装Git 在你准备放代码的文件夹下右键点击Git Bash Here 然后可以直接git clone https://github.com/EpicGames/UnrealEngine 不行的话可以直接去官方的Github上下载Zip压缩包后解压 运行里面的Setup.bat&a…

【新书推荐】大模型赛道如何实现华丽的弯道超车 —— 《分布式统一大数据虚拟文件系统 Alluxio原理、技术与实践》

文章目录 大模型赛道如何实现华丽的弯道超车 —— AI/ML训练赋能解决方案01 具备对海量小文件的频繁数据访问的 I/O 效率02 提高 GPU 利用率&#xff0c;降低成本并提高投资回报率03 支持各种存储系统的原生接口04 支持单云、混合云和多云部署01 通过数据抽象化统一数据孤岛02 …

ros2学习笔记:shell环境变量脚本setup.bash[-z][-n][-f]参数作用

-n作用 [ -n 字符串 ] or [ 字符串 ] 字符串的长度为非零&#xff08;有内容&#xff09;则为真。加-n与不加-n结果相同。 -z作用 [ -z 字符串 ] 字符串的长度为零则为真。 字符串为空即NULL时为真&#xff0c;与上面的-n相反。 -f作用 [ -f FILE ] 如果 FILE 存在且是一…

地牢大师问题(bfs提高训练 + 免去边界处理的特殊方法)

地牢大师问题 文章目录 地牢大师问题前言题目描述题目分析输入处理移动方式【和二维的对比】边界判断问题的解决 代码总结 前言 在之前的博客里面&#xff0c;我们介绍了bfs 基础算法的模版和应用,这里我们再挑战一下自己&#xff0c;尝试一个更高水平的题目&#xff0c;加深一…