<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          前端開發(fā)者也需要了解的文件加解密知識(shí)

          共 7692字,需瀏覽 16分鐘

           ·

          2021-12-01 11:48

          背景

          最近團(tuán)隊(duì)遇到一個(gè)小需求,存在兩個(gè)系統(tǒng) A、B,系統(tǒng) A 支持用戶在線制作皮膚包,制作后的皮膚包用戶可以下載后,導(dǎo)入到另外的系統(tǒng) B 上。皮膚包本身的其實(shí)就是一個(gè) zip 壓縮包,系統(tǒng) B 接收到壓縮包后,解壓并做一些常規(guī)的校驗(yàn),比如版本、內(nèi)容合法性校驗(yàn)等,整體功能也比較簡(jiǎn)單。

          但沒想到啊,一幫測(cè)試人員對(duì)我們開發(fā)人員一頓輸出,首先繞過系統(tǒng) A 搞了幾個(gè)視頻文件,把后綴改成?zip?就直接想上傳,系統(tǒng) B 每次都是等到上傳完后才發(fā)現(xiàn)文件不合法,系統(tǒng) B 在文件沒上傳完前又無(wú)法解壓,也不知道文件內(nèi)容是不是合法的,就這么消耗了大量帶寬、大量時(shí)間后才提示用戶皮膚包有問題。

          這里涉及了兩個(gè)問題,我們來捋一捋:

          1. 文件如何做加密,這樣用戶便無(wú)法去逆向,壓縮包內(nèi)部的敏感信息不會(huì)泄露出去。
          2. 服務(wù)端在接收到信息流時(shí),在未傳輸完時(shí)如何去判斷壓縮包的合法性,提前告知用戶。

          AES VS RSA

          說到加密,自己很多人會(huì)想到對(duì)稱算法 AES[2]?以及非對(duì)稱算法 RSA[3]。這兩種算法按字面意思也較好理解,對(duì)稱加密技術(shù)說白一點(diǎn)就是加密跟解密使用的是同一個(gè)密鑰,這種加密算法速度極快,安全級(jí)別高,加密前后的大小一致;非對(duì)稱加密技術(shù)則有公鑰PK私鑰SK,算法的原理在于尋找兩個(gè)素?cái)?shù),讓他們的乘積剛好等于一個(gè)約定的數(shù)字,非對(duì)稱算法的安全性是依賴于大數(shù)的分解,這個(gè)目前沒有理論支持可以快速破解,它的安全性完全依賴于這個(gè)密鑰的長(zhǎng)度,一般用 1024 位已經(jīng)足夠使用。但是它的速度相比對(duì)稱算法慢得多,一般僅用于少量數(shù)據(jù)的加密,待加密的數(shù)據(jù)長(zhǎng)度不能超過密鑰的長(zhǎng)度

          使用 AES 對(duì)文件加密

          結(jié)合這兩種加密方式的優(yōu)缺點(diǎn),我們采用 AES 對(duì)文件本身做加解密,使用 AES 的原因主要考慮如下:

          1. 加解密性能問題,AES 的速度極快,相比 RSA 有 1000 倍以上提升。
          2. RSA 對(duì)源文有長(zhǎng)度的要求,最大長(zhǎng)度僅有密鑰長(zhǎng)度。

          AES 的加密算法 Node.js 的crypto[4]模塊中已經(jīng)有內(nèi)置,具體的使用可以參考官方文檔。

          AES 加密邏輯

          const?crypto?=?require('crypto');
          const?algorithm?=?'aes-256-gcm';

          /**
          ?*?對(duì)一個(gè)buffer進(jìn)行AES加密
          ?*?@param?{Buffer}?buffer???待加密的內(nèi)容
          ?*?@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();
          ????//?生成身份驗(yàn)證標(biāo)簽,用于驗(yàn)證密文的來源
          ????const?tag?=?cipher.getAuthTag();
          ????return?{
          ????????key,
          ????????iv,
          ????????tag,
          ????????buffer:?buffer.concat([encrypted,?end]);
          ????};
          }

          AES 解密邏輯

          解密整體跟加密一樣,只是接口換個(gè)名字即可:

          const?crypto?=?require('crypto');
          const?algorithm?=?'aes-256-gcm';

          /**
          ?*?對(duì)一個(gè)buffer進(jìn)行AES解密
          ?*?@param?{{key:?string,?iv:?string,?tag:?Buffer,?buffer:?Buffer}}?ret???待解密的內(nèi)容
          ?*?@param?{String}?key??????密鑰
          ?*?@param?{String}?iv???????初始向量
          ?*?@return?{Buffer}
          ?*/
          function?aesDecrypt?({key,?iv,?tag,?buffer})?{
          ????//?初始化解密算法
          ????const?decipher?=?crypto.createDecipheriv(algorithm,?key,?iv);
          ????//?生成身份驗(yàn)證標(biāo)簽,用于驗(yàn)證密文的來源
          ????decipher.setAuthTag(tag);
          ????let?decrypted?=?decipher.update(buffer);
          ????let?end?=?decipher.final();
          ????return?Buffer.concat([decrypted,?end]);
          }

          AES 具體使用

          有了上述兩個(gè)接口后,我們便可實(shí)現(xiàn)一個(gè)簡(jiǎn)單的對(duì)稱加密了:

          const?key?=?'abcdefghijklmnopqrstuvwxyz123456';?//?32?共享密鑰,長(zhǎng)度跟算法需要匹配上
          const?iv?=?'abcdefghijklmnop';??//?16?初始向量,長(zhǎng)度跟算法需要匹配上
          let?fileBuffer?=?Buffer.from('abc');

          //?加密
          let?encrypted?=?aesEncrypt(fileBuffer,?key,?iv);

          //?解密
          let?context?=?aesDecrypt(encrypted);
          console.log(context.toString());

          一般情況下,這個(gè)密鑰較為重要,如果發(fā)生泄露則加密失去意義,所以keyiv會(huì)使用隨機(jī)數(shù)動(dòng)態(tài)生成,比如:

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

          通過上述的調(diào)整后,加解密文件是比較容易的,回到我們的業(yè)務(wù)系統(tǒng)上面,系統(tǒng) A 生成的壓縮包,最終是需要給系統(tǒng) B 使用,兩個(gè)系統(tǒng)是隔離的,那這樣?keyiv?如何傳輸?shù)较到y(tǒng) B 上面呢,況且還是動(dòng)態(tài)生成的,生成出來?key?系統(tǒng) B 是不知道的。

          讀到這聰明的你可能會(huì)想到,在把壓縮包給到 B 的時(shí)候,順便把?keyiv?一同提交過去不就可以了,但細(xì)想了下,這個(gè)肯定不能明文把這個(gè)密鑰發(fā)送過去,要不這個(gè)加密意義何在。

          這時(shí)便需要用上?RSA 非對(duì)稱加密技術(shù)了。

          使用 RSA 算法對(duì)密鑰再次進(jìn)行非對(duì)稱加密

          RSA 的加密算法 Node.js 的?crypto 模塊[5]?中已經(jīng)有內(nèi)置,具體的使用可以參考官方文檔。

          生成 RSA 的公鑰與私鑰

          使用?openssl[6]?組件可以直接生成?RSA?的公鑰私鑰對(duì),具體的命令可以參考:www.scottbrady91.com/OpenSSL/Cre…[7]

          #?生成私鑰
          openssl?genrsa?-out?private.pem?1024

          #?提取公鑰
          openssl?rsa?-in?private.pem?-pubout?-out?public.pem

          這樣生成出來的兩個(gè)文件?private.pempublic.pem?就可以使用了,下面我們使用 Node.js 實(shí)現(xiàn)具體的加解密邏輯。

          RSA 加密邏輯

          const?fs?=?require('fs');
          const?crypto?=?require('crypto');
          const?PK?=?fs.readFileSync('./public.pem',?'utf-8');

          /**
          ?*?對(duì)一個(gè)buffer進(jìn)行RSA加密
          ?*?@param?{Buffer}?待加密的內(nèi)容
          ?*?@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');

          /**
          ?*?對(duì)一個(gè)buffer進(jìn)行RSA解密
          ?*?@param?{Buffer}?待解密的內(nèi)容
          ?*?@return?{Buffer}
          ?*/
          function?rsaDecrypt?(buffer)?{
          ????return?crypto.privateDecrypt(SK,?buffer);
          }

          RSA 具體使用

          有了上述接口后,便可對(duì) AES 的密鑰進(jìn)行加密后再傳輸,服務(wù)器 B 保存好?RSA 私鑰?,服務(wù)器 A 則可以直接用?RSA 公鑰?對(duì)數(shù)據(jù)加密后再發(fā)送,結(jié)合剛?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]));?????//?由于長(zhǎng)度是固定的,直接連在一起即可
          ????};
          }

          /**
          ?*?解密文件
          ?*?@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
          ????})
          }

          這樣結(jié)合在一起后,服務(wù)器 A 生成的壓縮包,只要包含好?{file, key}?這兩塊內(nèi)容,服務(wù)器 B 便可把文件解密出來了,這樣基本上實(shí)現(xiàn)了我們第一點(diǎn)的目標(biāo):1. 文件如何做加密,這樣用戶便無(wú)法去逆向,壓縮包內(nèi)部的敏感信息不會(huì)泄露出去

          但還遺留了另外一個(gè)問題需要解決:2. 服務(wù)端在接收到信息流時(shí),在未傳輸完時(shí)如何去判斷壓縮包的合法性,提前告知用戶

          關(guān)于解密的還可以看看:記一次破解前端加密詳細(xì)過程

          優(yōu)化加密文件

          按上面的加密方式,輸出的結(jié)果是一個(gè)?buffer文件?內(nèi)容,以及一個(gè)?加密過的key,除了這些信息外,一般這個(gè)?buffer文件?壓縮包還會(huì)有一些額外的信息,比如:版本號(hào)、壓縮包生成時(shí)間,描述信息等。這些信息按常規(guī)的方式,可能是分成幾個(gè)文件,然后再打一個(gè)壓縮包把文件放在一起,比如:

          //?zip?file
          -?pkg
          ????manifest.json???????//?額外的信息
          ????key.json????????????//?保存了加密過的密鑰
          ????file.json???????????//?加密過的文件

          但如果用這種方式保存,一般情況下還要對(duì)這個(gè)?zip文件?做下加密,然后改下后綴名,但是服務(wù)器 B 在讀取這個(gè)文件后仍然是需要全部接收,再解壓到臨時(shí)目錄,讀取內(nèi)容后才可以做校驗(yàn),這樣問題仍然解決不了。

          除此之外,還有另外一個(gè)常見的需求,產(chǎn)品一般希望在瀏覽器側(cè)在文件上傳時(shí)就先做初步的解析,把明顯不合法的文件提示到用戶,這樣用戶體驗(yàn)更好。

          這個(gè)問題的解決方案也不難,這些所有額外的信息都是可以把它當(dāng)成二進(jìn)制插入到文件的頭部上的,比如:

          包字段描述:|----插入的額外信息----|----后面才是真正的文件內(nèi)容----|??
          二進(jìn)制文件:010101010101010101010xxxxxxxxxxxxxxxxxxxxxxxxxxxx

          文件頭字段設(shè)計(jì)

          我們把這些所有信息,按一定的格式,使用二進(jìn)制的方式全部串連在一起,最終交付的只有一個(gè)組合過的文件,比如:

          //?theme?pkg.

          0????????????????8????????????????16?????????????????
          |------flag------|--extra?length--|
          |----------extra?data...----------|
          |-------------data...-------------|
          • flag

          固定標(biāo)識(shí)?THEME,長(zhǎng)度:8 byte,說明該壓縮包為一個(gè)皮膚包,這樣可以快速對(duì)壓縮包進(jìn)行識(shí)別

          • extra length

          extra data?的真實(shí)長(zhǎng)度,這是一個(gè) 16 進(jìn)制的數(shù)據(jù),長(zhǎng)度:8 byte,說明插入的數(shù)據(jù)長(zhǎng)度。比如:長(zhǎng)度?35?的數(shù)據(jù),轉(zhuǎn)化為 16 進(jìn)制后為?0x23,那這字段為?00000023

          • extra data

          使用?RSA?加密過的數(shù)據(jù),我們可以把上述需要用?RSA?加密的信息全部放在這里,比如?key?字段、版本號(hào)、描述信息等

          • data

          使用?AES?加密過的數(shù)據(jù),可以通過?extra data?里面保存的?key?把真實(shí)的數(shù)據(jù)全部解密出來

          生成的新的加密文件

          有了上面的理論基礎(chǔ)后,馬上可以實(shí)踐起來,代碼如下:

          /**
          ?*?加密文件
          ?*?@param?{Buffer}?fileBuffer
          ?*?@return?{Buffer}
          ?*/
          function?encrypt?(fileBuffer)?{
          ????const?key?=?crypto.randomBytes(32);
          ????const?iv?=?crypto.randomBytes(16);
          ????const?version?=?'v1.1';

          ????//?記錄上所有額外的壓縮外信息,比如版本號(hào)、原始的密鑰
          ????const?extraJSON?=?{
          ????????version,
          ????????key,
          ????????iv
          ????}
          ????//?完成文件的AES加密,并輸出身份驗(yàn)證標(biāo)簽
          ????const?{?tag,?file?}?=?aesEncrypt(fileBuffer,?key,?iv);
          ????extraJSON.tag?=?tag;

          ????//?對(duì)?extraJSON?整個(gè)進(jìn)行RSA加密
          ????const?extraData?=?rsaEncrypt(Buffer.from(JSON.stringify(extraJSON)));
          ????const?extraLength?=?extraData.length;

          ????//?最終把所有數(shù)據(jù)合并在一起
          ????return?Buffer.concat([
          ????????Buffer.from('THEME'),
          ????????Buffer.from(Buffer.from(extraLength.toString(16).padStart(8,?'0'))),
          ????????extraData,
          ????????file
          ????]);
          }

          通過這種加密方式后,相關(guān)的信息都放在文件的頭部上,我們可以不用對(duì)整個(gè)文件進(jìn)行操作的時(shí)候,便可以輕松讀取出來,對(duì)于解密其實(shí)就是一個(gè)反向的操作。

          對(duì)新生成的文件進(jìn)行解密

          /**
          ?*?解密文件
          ?*?@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;

          ????//?對(duì)已經(jīng)被RSA加密過的數(shù)據(jù)進(jìn)行解密操作
          ????const?extraData?=?rsaDecrypt(fileBuffer.slice(16,?extraDataEndIndex));
          ????const?extraJSON?=?JSON.parse(extraData);
          ????//?最終使用AES再對(duì)剩下文件進(jìn)行解密操作,即為最終的文件
          ????return?aesDecrypt({
          ????????key:?extraJSON.key,
          ????????iv:?extraJSON.iv,
          ????????tag:?extraJSON.tag,
          ????????buffer:?Buffer.slice(extraDataEndIndex)
          ????});
          }

          使用這種方式處理后,在?RSA?解密出?extraData?的時(shí)候,就可以對(duì)整個(gè)文件進(jìn)行各種校驗(yàn),整個(gè)過程只要有異常說明文件已經(jīng)被篡改,用這種方式比用壓縮包會(huì)好很多,特別是文件體積龐大的時(shí)候,可以流式處理,發(fā)現(xiàn)不合理時(shí)即可馬上阻止。

          瀏覽器端如何解析該文件

          由于現(xiàn)在整個(gè)文件格式都是二進(jìn)制流,現(xiàn)代的瀏覽器都是有相應(yīng)的能力去讀取并做處理的,這樣也可以在用戶上傳文件時(shí)先做一定的初步處理,體驗(yàn)會(huì)有比較大的提升

          可以使用?DataView?的方式把二進(jìn)制數(shù)據(jù)讀取出來,詳情可以參考:DataView[8],初步的實(shí)現(xiàn)如下:

          /**
          ?*?把二進(jìn)制流轉(zhuǎn)成對(duì)應(yīng)ascii字符
          ?*?@param?{DataView}?dv?????????二進(jìn)制數(shù)據(jù)庫(kù)
          ?*?@param?{Number}???start??????起始位置
          ?*?@param?{Number}???end????????結(jié)束位置
          ?*?@return?{String}
          ?*/
          function?buffer2Char?(dv,?start,?end)?{
          ????let?ret?=?[];
          ????for?(let?i?=?start;?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);
          ????});
          }

          當(dāng)然用這種方式有一個(gè)前提是需要把一部分非敏感的信息放出來,不要加密,這樣便可以實(shí)現(xiàn)在瀏覽器端也對(duì)文件進(jìn)行讀取。只需要前后端的格式約定做好,都可以采用這種方式對(duì)壓縮包進(jìn)行一定的初步校驗(yàn),當(dāng)然后端的校驗(yàn)仍然是需要做好的。

          至此,我們完成了對(duì)文件的加密、解密以及瀏覽器解析等操作,希望對(duì)你們有幫助

          結(jié)語(yǔ)

          文件的加密、解密在后端其實(shí)是一個(gè)很常規(guī)的操作,除了上面聊到的?AESRSA,其實(shí)還有其它很多加密方案,具體可以看看?Node.js crypto 模塊[9],已經(jīng)有內(nèi)置比較多的方案可以直接使用。

          當(dāng)然文件的加解密,也可以直接用?zip7z?等這些壓縮工具,再配合密碼的方案,一般情況也是夠用的,但是免不了有定制化的需求,一般也都是結(jié)合使用,比如上面的?fileBuffer?實(shí)際內(nèi)部就是先用這些工具對(duì)文件進(jìn)行了壓縮并加密。還是以場(chǎng)景為重,多種方案結(jié)合效果更好。

          文件加解密的就講到這里吧,還有什么其它問題的可以在評(píng)論區(qū)討論,謝謝。

          關(guān)于本文

          作者: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


          瀏覽 57
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  欧美日本黄色 | 91看成品人视频 | av噢美| 亚洲欧洲日本视频 | 亚洲香蕉|