1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        前端開(kāi)發(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)單。

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

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

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

        AES VS RSA

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

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

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

        1. 加解密性能問(wèn)題,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)證密文的來(lái)源
        ????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)證密文的來(lái)源
        ????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ì)稱(chēng)加密了:

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

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

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

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

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

        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

        這樣生成出來(lái)的兩個(gè)文件?private.pem、public.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 便可把文件解密出來(lái)了,這樣基本上實(shí)現(xiàn)了我們第一點(diǎn)的目標(biāo):1. 文件如何做加密,這樣用戶便無(wú)法去逆向,壓縮包內(nèi)部的敏感信息不會(huì)泄露出去

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

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

        優(yōu)化加密文件

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

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

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

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

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

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

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

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

        //?theme?pkg.

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

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

        • extra length

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

        • extra data

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

        • data

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

        生成的新的加密文件

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

        /**
        ?*?加密文件
        ?*?@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ò)這種加密方式后,相關(guān)的信息都放在文件的頭部上,我們可以不用對(duì)整個(gè)文件進(jìn)行操作的時(shí)候,便可以輕松讀取出來(lái),對(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加密過(guò)的數(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è)過(guò)程只要有異常說(shuō)明文件已經(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ù)讀取出來(lái),詳情可以參考: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è)前提是需要把一部分非敏感的信息放出來(lái),不要加密,這樣便可以實(shí)現(xiàn)在瀏覽器端也對(duì)文件進(jìn)行讀取。只需要前后端的格式約定做好,都可以采用這種方式對(duì)壓縮包進(jìn)行一定的初步校驗(yàn),當(dāng)然后端的校驗(yàn)仍然是需要做好的。

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

        結(jié)語(yǔ)

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

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

        文件加解密的就講到這里吧,還有什么其它問(wèn)題的可以在評(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)
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            大乳护士喂奶三级hd | 啊灬啊灬啊灬快好喷水液视频 | 亚洲一二三菠萝蜜 | 韩国成人无码视频 | 影音先锋 一区二区三区 | 亚洲黄片在线看 | 天堂视频在线 | 色综合天天色 | 国产 激情 在线 | 无码一区二区波多野结衣播放搜索 |