通常,我們會遇到項目中的一些數據、模型、資源文件保護問題,以防被人挪用,或者泄露流重要、敏感信息。本文即討論這種外部資源文件的保護,以Windows環境下的模型文件的保護爲例:(如果是linux,方法一不可行,只能將方法一用字節數組加載,參考https://blog.csdn.net/flyingleo1981/article/details/8193964)
主要考慮兩種方法,一種是將外部資源文件嵌入二進制文件中(exe、dll、lib),然後將二進制文件加殼保護,這種方法開發量小,僅需要將資源文件嵌入並在運行時加載。
二是將外部文件加密,加密方法可以自定義,在加載模型文件前解密,考慮到安全性,防止解密後的模型文件暴露於內存被輕易dump,考慮使用流式加密的方法進行加解密,由此相對安全一點。
由於項目加載模型文件採用的是c++的文件流進行,而加載模型文件的方法封裝成了庫,所以這兩種問題中都涉及一個問題是繼承實現一個輸入流,無論是從內存中加載還是解密後加載。其中嵌入資源文件的方法有個簡單但安全性不高效率略低的做法可以避開繼承輸入流,即直接將嵌入資源重新加載到內存後將內存中的數據先寫入某個流再從中讀取。
繼承iostream實現自定義都流輸入輸出,重點在於實現自定義的streambuf管理用於輸入輸出的緩衝區,因爲根據繼承關係,fstream、stringstream都是從源(file、string)處讀取/寫入至streambuf中,當streambuf已全部讀取/寫滿溢出,則完成緩衝區的更新/序列化,因而關鍵是實現streambuf中的buf管理。有幾篇博文講的比較清楚,這裏不再複述:
最全面的資料當然是官方流的說明文檔:
http://www.cplusplus.com/reference/streambuf/streambuf/
https://izualzhy.cn/stream-buffer 這篇代碼相對規範,更規範的參考南加大的例子:http://ilab.usc.edu/rjpeters/groovx/stdiobuf_8h-source.html
http://kaiyuan.me/2017/06/22/custom-streambuf/ 這篇講的通俗易懂
https://wizmann.tk/phxrpc-1.html 這篇稍微深入點,並可以對照實現 putback 功能
所以,當採用資源嵌入的方式的時候,在windows下:
1 2 3 4 5 6 |
#include "resource.h" #include <Windows.h>
HRSRC hRes = ::FindResourceA(NULL, MAKEINTRESOURCEA(IDR_MODEL_CRY), "MODEL"); HGLOBAL hMem = ::LoadResource(NULL, hRes); DWORD dwSize = ::SizeofResource(NULL, hRes); |
然後將hMem所指向的內存作爲只讀緩衝區,用於streambuf的輸入流緩衝區,dwSize爲緩衝區大小,則可以直接從該內存區域使用流的操作進行讀取,從而避免了內容在內存中的反覆拷貝。
參考實現爲 https://wizmann.tk/phxrpc-1.html 的 char_array_buffer
第二中實現的加解密流,重點是在繼承實現的streambuf中完成對加解密流和文件指針的管理。文件的讀取對應文件讀指針fin_,輸入緩衝區gbuf_,當輸入緩衝區空的時候調用underflow,此時從文件讀取並解密填充輸入緩衝區,而文件寫入對應寫文件指針fout_, 以及輸出緩衝區pbuf_,當輸出緩衝區滿時調用overflow,其內部調用sync將緩衝區內容加密並寫入文件。
這裏加解密算法選擇的aes-ofb,其實是有講究的,也可以選擇aes-cfb,但是aes-ecb和aes-cbc(aes-pcbc)是塊加密方式,涉及填充,稍微複雜些,應用於流式讀寫的時候需要設計合理的填充方式,當追加寫入或者非整塊刷入磁盤再次寫入的時候要解除填充,不能隨時刷入磁盤,隨時再從磁盤讀取並立即寫入。其餘的aes-cfb、aes-ofb、aes-ctr(aes-icm/aes-sic)均爲流加解密模式,其中aes-ofb爲流密鑰異或明/密文模式,相對簡單,aes-cfb在aes-ofb基礎上引入和前述密文的關聯,所以相對安全一些(密文部分串改將影響後續所有解密過程),而aes-ctr模式則更巧妙些,支持隨機訪問模式,因而可以更靈活的完成加解密功能,但是由於我們此處是對流進行操作,所以並不需要如此。關於aes加解密內容,參考如下鏈接:
https://blog.csdn.net/jerry81333/article/details/78336616
具體的代碼:
加密的輸出流實現:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
int CryptoStreamBuf::sync() { int encrypted = 0; int toEncrypt = pptr() - pbase(); // data that can be flushed char buf[MAX_BUFF_SIZE]; AesOfbXor(&aesOutCtx_,pbase(),buf,toEncrypt);
int ret = fwrite(buf,1,toEncrypt,fout_); if (ret == toEncrypt){ fflush(fout_); } else { return -1; } setp(pbase(), pbase() + bufSize_); // reset the buffer pbump(0); // reset pptr to buffer head return 0; }
int CryptoStreamBuf::overflow(int c) { if (-1 == sync()) { return traits_type::eof(); } else { // put c into buffer after successful sync if (!traits_type::eq_int_type(c, traits_type::eof())) { sputc(traits_type::to_char_type(c)); }
// return eq_int_type(c, eof()) ? eof():c; return traits_type::not_eof(c); } } |
解密的輸入流實現:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
int CryptoStreamBuf::underflow() { char buf[MAX_BUFF_SIZE]; int ret = fread(buf,1,bufSize_, fin_); if (ret > 0) { char bufDecrypt[MAX_BUFF_SIZE]; AesOfbXor(&aesInCtx_,buf,bufDecrypt,ret); memcpy(eback(),bufDecrypt,ret); setg(eback(), eback(), eback() + ret); return traits_type::to_int_type(*gptr()); } else { return traits_type::eof(); } } |
此外,實際應用還應該注意的是,加解密的密鑰的保護,不要靜態編譯於代碼中,而應該通過一定的運算生成,否則很容易反編譯獲得,同時應該加入一定的反調試手段,防止直接調試獲取到密鑰,簡單一些的方法直接用工具軟件加殼保護等等。
源碼下載:
https://download.csdn.net/download/atp1992/10963458
原文參見:
http://www.straka.cn/blog/general-resources-file-protect-encrypted-file-stream/