Unity使用ILRuntime热更之代码安全:分块解密并执行的技术实现

讲到游戏开发,大家耳熟能详的游戏引擎想必有Unity3D的一席之地,而U3D开发手游,我们常会用到热更新技术,达到及时修复Bug与即时新增功能
说到热更新,目前最常见的做法就是在C#内置一个虚拟机,来解释执行不同的语言(如Lua、TypeScript、C#和IL)
最常用的热更方案,目前还是使用Lua进行热更新,所有的游戏代码都写到Lua文件内,再将其打入AB包进行热更。
本篇文章,作者强力推荐C#热更新方案:ILRuntime,该方案可以解释执行C#代码生成的解决方案(DLL)内的IL指令,以达到热更新的需求,非常好用,再也不需要去写Lua了,C#就可以热更了。

话题

那么讲到热更新,大家都会面临一个问题:我的热更新文件是否安全,热更代码丢热更资源里面,是否会造成泄露?
在这个问题上,就仁者见仁智者见智了,有的人可能会说:
“就我这代码,泄露就泄露吧,没必要做什么防护”
有的人可能说:
“魔高一尺道高一丈,没有绝对的安全的,所以没必要进行保护”
那么本篇文章的主要思想,还是 尽全力的提高热更源码泄露的门槛,让人破解起来难度更大 ,如果你觉得代码安全毫无意义,请忽略本文章。

前言

作者在GitHub(全球最大同行社交平台),有一个U3D的热更框架,热更新部分的实现就是基于ILRuntime,所以也曾经进行过一定程度的热更代码保护。
想看看的可以参考: JEngine知乎文章
之前的方案呢,很简单,可以参考下图:
AB热更流程示意图
那么这个逻辑,看似很完善,哪怕AB包被盗了,也解不出加密后的源码(不懂AES的可以自行百度,别说什么穷举法来破解,笑死我,对你没好处)
但是,这个做法还是有遗漏的地方:解密后的源码,明文存入MemoryStream,会导致源码进MemoryStream的内存缓冲区,以至于可以被抓内存来获取源码!!

沟通

意识到了这个问题,作者我联系了ILRuntime的作者,蓝大。
我和蓝大进行了一段时间的沟通,最后得到了解决方案:
ILRuntime获取热更DLL中的指令,会用到Mono.Cecil的 ModuleDefinition.ReadModule方法,而这个方法传的参数,就是之前我的那个MemoryStream。
Mono.Cecil会调用该Stream的 Read 、 ReadByte 和 Seek 方法,来分块读取这个包含了热更代码的文件,所以只需要针对这3个方法,做特殊处理即可。
理论上,我只需要写一个 继承Stream的派生类 ,然后在 重载的Read和ReadByte 方法里,进行解密和返回操作即可, Seek 这个方法暂时不用管,后面会进行解释。

开发

初步构思后,我便自信满满的开始了开发:
我写了个Stream的派生类,然后重载了全部需要重载的方法,其中除了 Read、ReadByte、Seek和Position 之外,我全返回了抛出不支持异常
这里讲一下这4个是干嘛的:
  • Read :读取一段区域的字节,参数是byte[] buffer, int offset, int count,分别是需要Stream把截取的字节段落写入的缓冲区,截取开始的位置和截取的长度, 读完后Position要+=count
  • ReadByte :读取当前位置(Position)的字节,返回这个字节
  • Seek: 偏移Position,这样可以读不同区域的数据,参数是long offset, SeekOrigin loc,显而易见,参数分别是偏移的位置,和偏移方式,SeekOrgin有3个选项:Begin,Current和End,分别是从头开始第offset位,当前开始往后offset位,和从末尾开始往前offset位(这种情况offset是负数)
  • Position :Stream当前读到的位置,用于定位和继续往后读之类的
那么我只需要保证 Seek 和 Position正常 ,就可以正常的Read和ReadByte了。
与此同时,我的JStream还需要一个缓存区,用于存byte[]的数据(叫做buffer,可以随意命名),当然,这里面肯定是要进加密数据的,不然会被抓内存。
紧接着,就是分块解密了。
因为我之前是用的AES加密,而 AES是每16块字节进行加密的 ,所以我将数据按16个16个去处理,解密,再返回就好。
那么有了思路,就好操作了,很快我就把我需要写的方法开了个头:
    
private byte [ ] GetBytesAt ( int start , int length )
这其中, start是截取开始的部分,length是截取长度
在这里我们 假设 ,我们要从 截取第10位开始截取,截取8位,到第18位 。
按 AES加密是16块分块加密 来说,我们需要包含第10位的这一块,以及包含18位的这一块,
10小于16,所以 10在第一块加密区块内
18大于16小于32,所以 18在第二块加密区块内
我们需要 从加密密文的第1位开始,一直读到第32位 ,也就包含了全部需要的密文
紧接着,我们对其进行解密
我这边加密的时候,采用的 PKCS7做Padding对加密的密文 进行填充,但我们 分块解密的时候,是没Padding的 ,所以我们 需要以Padding.None去分块解密 。
如果小伙伴还不知道什么是AES的Padding,可以自行百度,也可以忽略继续往下读。
那么我们现在获得了,一个32位的解密明文。
这个时候呢,我们又要从第10位开始读这个解密明文,读到第18位,然后存到新的byte[]内,进行返回,具体实现如下:
    
/// <summary> /// 获取特定位置的真实数据(包含解密过程) /// </summary> /// <param name="start"></param> /// <param name="length"></param> /// <returns></returns> private byte [ ] GetBytesAt ( int start , int length ) { int remainder = start % 16 ; // 余数 int offset = start - remainder ; // 偏移值,截取开始的地方,比如 67 变 64 int count = length + ( 16 - length % 16 ) ; //获得需要切割的数组的长度,比如 77 变 80,77+ (16- 77/16的余数) = 77 + (16-13) = 77 + 3 = 80 var result = new byte [ length ] ; //返回值,长度为length // Log.Print($"start:{start}, length:{length}, remainder:{remainder}"); //现在需要将buffer切割,从offset开始,到count为止 var encryptedData = new byte [ count ] ; //创建加密数据数组 Buffer . BlockCopy ( _buffer , offset , encryptedData , 0 , count ) ; //从原始数据里分割出来 // Log.Print("获取到的密文:"+string.Join(", ", encryptedData)); //给encryptedData解密 var decrypt = CryptoHelper . AesDecryptWithNoPadding ( encryptedData , _key ) ; //截取decrypt,从remainder开始,到length为止,比如余数是3,那么从3-1的元素开始 offset = remainder ; // Log.Print($"copy from offset:{offset}, result.length:{length}, decrypt.length:{decrypt.Length}"); //这里有个问题,比如decrypt有16字节,而result是12字节,offset是8,那么12+8 > 16,就会出现错误 //所以这里要改一下 var total = offset + length ; var dLength = decrypt . Length ; if ( total > dLength ) { Array . Resize ( ref decrypt , total ) ; } Buffer . BlockCopy ( decrypt , offset , result , 0 , length ) ; // Log.Print("解密结果:"+string.Join(", ", decrypt)); # if UNITY_EDITOR EncryptedCounts ++ ; # endif return result ; }
注意,上面的代码直接抄会有问题,因为你没有很多参数,你需要实现自己的Stream才行,当然,最底部会附带完整代码。
通过这个代码,很明显, 技术点在于求Start除以16的余数,然后与Start相减得到需要开始读加密区块的地方 , 然后需要读的长度为传入的长度参数的下一个可以被16整除的数字 。
紧接着,就是上面所阐述的了,进行 NoPadding的解密 ,就可以取出了。
这里有一个小坑,就是在复制数据到数组的时候,如果数据长度不够,会导致报错,所以这里有个小判断。
好,现在分块解密 “算是” 初步解决,接下来再修改一下Read和ReadByte就可以开始测试了
    
public override int Read ( byte [ ] buffer , int offset , int count ) { if ( buffer == null ) throw new ArgumentNullException ( nameof ( buffer ) , "buffer == null" ) ; if ( offset < 0 ) throw new ArgumentOutOfRangeException ( nameof ( offset ) , "offset < 0" ) ; if ( count < 0 ) throw new ArgumentOutOfRangeException ( nameof ( count ) , "count < 0" ) ; if ( buffer . Length - offset < count ) throw new ArgumentException ( "invalid buffer length" ) ; if ( ! _isOpen ) Log . PrintError ( "stream is closed" ) ; int n = _length - _position ; if ( n > count ) n = count ; if ( n <= 0 ) return 0 ; /* MemoryStream源码是这样的,我也不知道为什么要把8个长度的单独拉出来 if (n <= 8) { int byteCount = n; while (--byteCount >= 0) buffer[offset + byteCount] = _buffer[_position + byteCount]; } else Buffer.BlockCopy(_buffer, _position, buffer, offset, n); */ /* * JEngine的分块解密 * 理论上,aes是 每16字节为单位 加密 * 所以只需给buffer以16位单位切割即可 */ if ( _encrypted ) { try { Buffer . BlockCopy ( GetBytesAt ( _position , count ) , 0 , buffer , offset , n ) ; //复制过去 //这个用来做log的,去掉注释就可以调试 //这边的result是缓存,把log给打出来,不然分配buffer后,如果出错了没办法还原解密结果 // var result = new byte[buffer.Length]; // Buffer.BlockCopy(buffer, 0, result, 0, buffer.Length); // Buffer.BlockCopy(src, 0, result, offset, n); // Log.Print("解密结果:" + string.Join(", ", result)); // Buffer.BlockCopy(result, 0, buffer, offset, n); } catch ( Exception ex ) { Log . PrintError ( ex ) ; throw ; } } else { //没加密的直接读就好 Buffer . BlockCopy ( _buffer , _position , buffer , offset , n ) ; //对比无加密的:加密的字节,用于测试(可能会有不一样的地方) // Log.Print("原文结果:" + string.Join(", ", buffer)); // var src = GetBytesAt(_position, n); // var result = new byte[buffer.Length]; // Buffer.BlockCopy(buffer, 0, result, 0, buffer.Length); // Buffer.BlockCopy(src, 0, result, offset, n); // Log.Print("解密结果:" + string.Join(", ", result)); // var equal = CompareArray(buffer,result); // var en = CryptoHelper.AesEncryptWithNoPadding(buffer, _key); // Log.Print("原文加密结果:" + string.Join(", ", en)); // Log.Print($"pos: {_position}, count: {count}, decrypt equals original: {equal}"); // Log.Print($"======================="); } _position += n ; return n ; } public override int ReadByte ( ) { if ( ! _isOpen ) Log . PrintError ( "stream is closed" ) ; if ( _position >= _length ) return - 1 ; return _encrypted ? GetBytesAt ( _position ++ , 1 ) [ 0 ] : _buffer [ _position ++ ] ; }
这段代码注释很足,没必要解释了

调试

我自信满满的开始进行测试,结果布满棘刺
写了丰富的Log,然后我自己的JStream在那边分块解密着热更代码。
结果读到 第四百多位,它就出错了 ,于是我又拿了个 MemoryStream 进行对比
错误一
对比后发现,莫名其妙的,也 没Seek也没Read也没ReadByte , MemoryStream的Position比我自己实现 的, 多了十几二十位 ,这还得了!
再到后面,更是离谱,多了 六万位 !!
错误二
最后为了省事,我下载了一份 MemoryStream的源码 ,把它的 Position机制复制了过来 ,这个坑总算解决了!
    
public override long Position { get { if ( ! _isOpen ) Log . PrintError ( "stream is closed" ) ; return _position - _origin ; } set { if ( value < 0 ) throw new ArgumentOutOfRangeException ( nameof ( value ) , "value < 0 is invalid" ) ; if ( ! _isOpen ) Log . PrintError ( "stream is closed" ) ; if ( value > MemStreamMaxLength ) throw new ArgumentOutOfRangeException ( nameof ( value ) , "value > stream length is invalid" ) ; _position = _origin + ( int ) value ; } }
很难相信, MemoryStream自己的Position机制,竟然还带了个origin参数 ,这也就是导致我 自己写的Position不对的罪魁祸首 !
好, 这个问题解决了,新的问题又来了 。
读到第4万位,出现了我之前提到的, 复制数据到数组长度不够 的坑。
错误三
还好,最后发现并修复了。
这还不错, 好歹读了上万位了哈哈。
然而,我 还是太天真 了, 新的问题又来了 。
读到 第10万 位, Mono.Cecil那边出现了空错误,这不科学啊,我解密的代码没问题啊!
错误四
然后我就开始了漫长的排查,
错误五
错误六
通过对比,我发现, 这俩玩意儿没区别啊,怎么就空了呢?
于是,我写了个代码,对比每一次读取的字节是否一样:
    
/// <summary> /// 数组比较是否相等 /// </summary> /// <param name="bt1">数组1</param> /// <param name="bt2">数组2</param> /// <returns>true:相等,false:不相等</returns> public bool CompareArray ( byte [ ] bt1 , byte [ ] bt2 ) { var len1 = bt1 . Length ; var len2 = bt2 . Length ; if ( len1 != len2 ) { return false ; } for ( var i = 0 ; i < len1 ; i ++ ) { if ( bt1 [ i ] != bt2 [ i ] ) { // Log.PrintError($"original: {string.Join(",",bt1)}, decrypt: {string.Join(",",bt2)}\n" + // $"{bt1[i]} != {bt2[i]}"); return false ; } } return true ; }
然后在读取时:
    
//对比无加密的:加密的字节,用于测试(可能会有不一样的地方) Log . Print ( "原文结果:" + string . Join ( ", " , buffer ) ) ; var src = GetBytesAt ( _position , n ) ; var result = new byte [ buffer . Length ] ; Buffer . BlockCopy ( buffer , 0 , result , 0 , buffer . Length ) ; Buffer . BlockCopy ( src , 0 , result , offset , n ) ; Log . Print ( "解密结果:" + string . Join ( ", " , result ) ) ; var equal = CompareArray ( buffer , result ) ; var en = CryptoHelper . AesEncryptWithNoPadding ( buffer , _key ) ; Log . Print ( "原文加密结果:" + string . Join ( ", " , en ) ) ; Log . Print ( $"pos: { _position } , count: { count } , decrypt equals original: { equal } " ) ; Log . Print ( $"=======================" ) ;
通过对比, 我发现,每次返回16位的buffer,解密的对比未加密的数据,总会多几个0,诡异的是读取也没报错,到后来才爆发空错误
例如:
本应返回:[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16],解密后返回了[1,2,3,4,5,6,7,8,9,10,0,0,0,0,0,0],后面的东西没了!
然后我进行了 大量的StackOverFlow和谷歌 ,发现,这还是 AES解密的锅 ,这玩意儿 解密的是会丢失最后几位!
解决方法就是, 扩大解密范围!
还记的之前, 是取截取末尾之后的那个可以整除16的位数吗,现在我们再+16位,扩大范围!
    
/// <summary> /// 获取特定位置的真实数据(包含解密过程) /// </summary> /// <param name="start"></param> /// <param name="length"></param> /// <returns></returns> private byte [ ] GetBytesAt ( int start , int length ) { int remainder = start % 16 ; // 余数 int offset = start - remainder ; // 偏移值,截取开始的地方,比如 67 变 64 int count = length + ( 32 - length % 16 ) ; //获得需要切割的数组的长度,比如 77 变 96(80+16),77+ (32- 77/16的余数) = 77 + (32-13) = 77 + 19 = 96,多16位确保不丢东西 var result = new byte [ length ] ; //返回值,长度为length // Log.Print($"start:{start}, length:{length}, remainder:{remainder}"); //现在需要将buffer切割,从offset开始,到count为止 var encryptedData = new byte [ count ] ; //创建加密数据数组 Buffer . BlockCopy ( _buffer , offset , encryptedData , 0 , count ) ; //从原始数据里分割出来 // Log.Print("获取到的密文:"+string.Join(", ", encryptedData)); //给encryptedData解密 var decrypt = CryptoHelper . AesDecryptWithNoPadding ( encryptedData , _key ) ; //截取decrypt,从remainder开始,到length为止,比如余数是3,那么从3-1的元素开始 offset = remainder ; // Log.Print($"copy from offset:{offset}, result.length:{length}, decrypt.length:{decrypt.Length}"); //这里有个问题,比如decrypt有16字节,而result是12字节,offset是8,那么12+8 > 16,就会出现错误 //所以这里要改一下 var total = offset + length ; var dLength = decrypt . Length ; if ( total > dLength ) { Array . Resize ( ref decrypt , total ) ; } Buffer . BlockCopy ( decrypt , offset , result , 0 , length ) ; // Log.Print("解密结果:"+string.Join(", ", decrypt)); # if UNITY_EDITOR EncryptedCounts ++ ; # endif return result ; }
16 - length % 16 变为了 32 - length % 16
再次运行,终于成功!
终于成功
然后测了下 耗时,很奈斯!
分块解密初始化热更DLL耗时
才 48ms ,毫无问题,同时 返回了128块代码 块给ILRuntime

总结

这个分块出来的代码,是明文,但我和ILRuntime作者确定了, ILRuntime不会缓存 ,但我不能保证Mono.Cecil是否会缓存。
然而,你的 代码被切成几万份 ,那不得 内存抓个几万次 ?所以啊,没啥毛病!
性能 这块, 多几ms 而已, 无GC ,没啥毛病, 不会卡顿 !
然后,为什么我要做这个,主要总有人说我是缝合怪,那我不得证明一下我自己吗,当然,也欢迎各位大佬指出本文存在的任何错误,我会虚心接受的!

结尾

注,Log.Print是Debug.Log,Log.PrintError是Debug.LogError,Log.PrintWarning是Debug.LogWarning
以下是JStream源码 :
    
using System ; using System . IO ; namespace JEngine . Core { public class JStream : Stream { private byte [ ] _buffer ; // Either allocated internally or externally. private readonly int _origin ; // For user-provided arrays, start at this origin private int _position ; // read/write head. private int _length ; // Number of bytes within the memory stream private int _capacity ; // length of usable portion of buffer for stream private string _key ; //解密密码 private string _defaultKey = "hello_JEngine_!_" ; # if UNITY_EDITOR public int EncryptedCounts { get ; set ; } # endif // Note that _capacity == _buffer.Length for non-user-provided byte[]'s private bool _encrypted = true ; //是否aes加密了 private bool _expandable ; // User-provided buffers aren't expandable. private readonly bool _exposable ; // Whether the array can be returned to the user. private bool _isOpen ; // Is this stream open or closed? private readonly uint maxLength = 2147483648 ; private const int MemStreamMaxLength = Int32 . MaxValue ; public bool Encrypted { get => _encrypted ; set => _encrypted = value ; } public JStream ( byte [ ] buffer , string key ) : this ( buffer , key , true ) { } public JStream ( byte [ ] buffer , string key , bool writable ) { _buffer = buffer ?? throw new ArgumentNullException ( nameof ( buffer ) , "buffer == null" ) ; _length = _capacity = buffer . Length ; _exposable = false ; _origin = 0 ; _isOpen = true ; _key = key ; if ( _key . Length < 16 ) { _key = Init . Instance . Key . Length < 16 ? _defaultKey : Init . Instance . Key ; } } public JStream ( byte [ ] buffer , string key , int index , int count ) : this ( buffer , key , index , count , false ) { } public JStream ( byte [ ] buffer , string key , int index , int count , bool publiclyVisible ) { if ( buffer == null ) throw new ArgumentNullException ( nameof ( buffer ) , "buffer == null" ) ; if ( index < 0 ) throw new ArgumentOutOfRangeException ( nameof ( index ) , "index < 0" ) ; if ( count < 0 ) throw new ArgumentOutOfRangeException ( nameof ( count ) , "count < 0" ) ; if ( buffer . Length - index < count ) throw new ArgumentException ( "invalid length of buffer" ) ; _buffer = buffer ; _origin = _position = index ; _length = _capacity = index + count ; _exposable = publiclyVisible ; // Can TryGetBuffer/GetBuffer return the array? _expandable = false ; _isOpen = true ; _key = key ; if ( _key . Length < 16 ) { _key = Init . Instance . Key . Length < 16 ? _defaultKey : Init . Instance . Key ; } } public override bool CanRead => _isOpen ; public override bool CanSeek => _isOpen ; public override bool CanWrite => false ; protected override void Dispose ( bool disposing ) { try { if ( disposing ) { _isOpen = false ; _expandable = false ; // Don't set buffer to null - allow TryGetBuffer, GetBuffer & ToArray to work. } } finally { // Call base.Close() to cleanup async IO resources base . Dispose ( disposing ) ; } } // returns a bool saying whether we allocated a new array. private bool EnsureCapacity ( int value ) { // Check for overflow if ( value < 0 ) throw new IOException ( "Stream too long, value < capacity of stream is invalid" ) ; if ( value > _capacity ) { int newCapacity = value ; if ( newCapacity < 256 ) newCapacity = 256 ; // We are ok with this overflowing since the next statement will deal // with the cases where _capacity*2 overflows. if ( newCapacity < _capacity * 2 ) newCapacity = _capacity * 2 ; // We want to expand the array up to Array.MaxArrayLengthOneDimensional // And we want to give the user the value that they asked for if ( ( uint ) ( _capacity * 2 ) > maxLength ) newCapacity = value < maxLength ? value : ( int ) ( maxLength / 2 ) ; Capacity = newCapacity ; return true ; } return false ; } public override void Flush ( ) { } public virtual byte [ ] GetBuffer ( ) { if ( ! _exposable ) throw new UnauthorizedAccessException ( "UnauthorizedAccess to get member buffer" ) ; return _buffer ; } public virtual bool TryGetBuffer ( out ArraySegment < byte > buffer ) { if ( ! _exposable ) { buffer = default ( ArraySegment < byte > ) ; return false ; } buffer = new ArraySegment < byte > ( _buffer , offset : _origin , count : ( _length - _origin ) ) ; return true ; } // -------------- PERF: Internal functions for fast direct access of JStream buffer (cf. BinaryReader for usage) --------------- // PERF: Internal sibling of GetBuffer, always returns a buffer (cf. GetBuffer()) internal byte [ ] InternalGetBuffer ( ) { return _buffer ; } // PERF: Get origin and length - used in ResourceWriter. internal void InternalGetOriginAndLength ( out int origin , out int length ) { if ( ! _isOpen ) Log . PrintError ( "stream is closed" ) ; origin = _origin ; length = _length ; } // PERF: True cursor position, we don't need _origin for direct access internal int InternalGetPosition ( ) { if ( ! _isOpen ) Log . PrintError ( "stream is closed" ) ; return _position ; } // PERF: Takes out Int32 as fast as possible internal int InternalReadInt32 ( ) { if ( ! _isOpen ) Log . PrintError ( "stream is closed" ) ; int pos = ( _position += 4 ) ; // use temp to avoid ---- if ( pos > _length ) { _position = _length ; Log . PrintError ( "end of file" ) ; } return _buffer [ pos - 4 ] | _buffer [ pos - 3 ] << 8 | _buffer [ pos - 2 ] << 16 | _buffer [ pos - 1 ] << 24 ; } // PERF: Get actual length of bytes available for read; do sanity checks; shift position - i.e. everything except actual copying bytes internal int InternalEmulateRead ( int count ) { if ( ! _isOpen ) Log . PrintError ( "stream is closed" ) ; int n = _length - _position ; if ( n > count ) n = count ; if ( n < 0 ) n = 0 ; _position += n ; return n ; } // Gets & sets the capacity (number of bytes allocated) for this stream. // The capacity cannot be set to a value less than the current length // of the stream. // public virtual int Capacity { get { if ( ! _isOpen ) Log . PrintError ( "stream is closed" ) ; return _capacity - _origin ; } set { // Only update the capacity if the MS is expandable and the value is different than the current capacity. // Special behavior if the MS isn't expandable: we don't throw if value is the same as the current capacity if ( value < Length ) throw new ArgumentOutOfRangeException ( nameof ( value ) , "value < capcacity is invalid" ) ; if ( ! _isOpen ) Log . PrintError ( "stream is closed" ) ; if ( ! _expandable && ( value != Capacity ) ) Log . PrintError ( "JStream is not expandable" ) ; // JStream has this invariant: _origin > 0 => !expandable (see ctors) if ( _expandable && value != _capacity ) { if ( value > 0 ) { byte [ ] newBuffer = new byte [ value ] ; if ( _length > 0 ) Buffer . BlockCopy ( _buffer , 0 , newBuffer , 0 , _length ) ; _buffer = newBuffer ; } else { _buffer = null ; } _capacity = value ; } } } public override long Length { get { if ( ! _isOpen ) Log . PrintError ( "stream is closed" ) ; return _length - _origin ; } } public override long Position { get { if ( ! _isOpen ) Log . PrintError ( "stream is closed" ) ; return _position - _origin ; } set { if ( value < 0 ) throw new ArgumentOutOfRangeException ( nameof ( value ) , "value < 0 is invalid" ) ; if ( ! _isOpen ) Log . PrintError ( "stream is closed" ) ; if ( value > MemStreamMaxLength ) throw new ArgumentOutOfRangeException ( nameof ( value ) , "value > stream length is invalid" ) ; _position = _origin + ( int ) value ; } } public override int Read ( byte [ ] buffer , int offset , int count ) { if ( buffer == null ) throw new ArgumentNullException ( nameof ( buffer ) , "buffer == null" ) ; if ( offset < 0 ) throw new ArgumentOutOfRangeException ( nameof ( offset ) , "offset < 0" ) ; if ( count < 0 ) throw new ArgumentOutOfRangeException ( nameof ( count ) , "count < 0" ) ; if ( buffer . Length - offset < count ) throw new ArgumentException ( "invalid buffer length" ) ; if ( ! _isOpen ) Log . PrintError ( "stream is closed" ) ; int n = _length - _position ; if ( n > count ) n = count ; if ( n <= 0 ) return 0 ; /* MemoryStream源码是这样的,我也不知道为什么要把8个长度的单独拉出来 if (n <= 8) { int byteCount = n; while (--byteCount >= 0) buffer[offset + byteCount] = _buffer[_position + byteCount]; } else Buffer.BlockCopy(_buffer, _position, buffer, offset, n); */ /* * JEngine的分块解密 * 理论上,aes是 每16字节为单位 加密 * 所以只需给buffer以16位单位切割即可 */ if ( _encrypted ) { try { Buffer . BlockCopy ( GetBytesAt ( _position , count ) , 0 , buffer , offset , n ) ; //复制过去 //这个用来做log的,去掉注释就可以调试 //这边的result是缓存,把log给打出来,不然分配buffer后,如果出错了没办法还原解密结果 // var result = new byte[buffer.Length]; // Buffer.BlockCopy(buffer, 0, result, 0, buffer.Length); // Buffer.BlockCopy(src, 0, result, offset, n); // Log.Print("解密结果:" + string.Join(", ", result)); // Buffer.BlockCopy(result, 0, buffer, offset, n); } catch ( Exception ex ) { Log . PrintError ( ex ) ; throw ; } } else { //没加密的直接读就好 Buffer . BlockCopy ( _buffer , _position , buffer , offset , n ) ; //对比无加密的:加密的字节,用于测试(可能会有不一样的地方) // Log.Print("原文结果:" + string.Join(", ", buffer)); // var src = GetBytesAt(_position, n); // var result = new byte[buffer.Length]; // Buffer.BlockCopy(buffer, 0, result, 0, buffer.Length); // Buffer.BlockCopy(src, 0, result, offset, n); // Log.Print("解密结果:" + string.Join(", ", result)); // var equal = CompareArray(buffer,result); // var en = CryptoHelper.AesEncryptWithNoPadding(buffer, _key); // Log.Print("原文加密结果:" + string.Join(", ", en)); // Log.Print($"pos: {_position}, count: {count}, decrypt equals original: {equal}"); // Log.Print($"======================="); } _position += n ; return n ; } /// <summary> /// 数组比较是否相等 /// </summary> /// <param name="bt1">数组1</param> /// <param name="bt2">数组2</param> /// <returns>true:相等,false:不相等</returns> public bool CompareArray ( byte [ ] bt1 , byte [ ] bt2 ) { var len1 = bt1 . Length ; var len2 = bt2 . Length ; if ( len1 != len2 ) { return false ; } for ( var i = 0 ; i < len1 ; i ++ ) { if ( bt1 [ i ] != bt2 [ i ] ) { // Log.PrintError($"original: {string.Join(",",bt1)}, decrypt: {string.Join(",",bt2)}\n" + // $"{bt1[i]} != {bt2[i]}"); return false ; } } return true ; } public override int ReadByte ( ) { if ( ! _isOpen ) Log . PrintError ( "stream is closed" ) ; if ( _position >= _length ) return - 1 ; return _encrypted ? GetBytesAt ( _position ++ , 1 ) [ 0 ] : _buffer [ _position ++ ] ; } /// <summary> /// 获取特定位置的真实数据(包含解密过程) /// </summary> /// <param name="start"></param> /// <param name="length"></param> /// <returns></returns> private byte [ ] GetBytesAt ( int start , int length ) { int remainder = start % 16 ; // 余数 int offset = start - remainder ; // 偏移值,截取开始的地方,比如 67 变 64 int count = length + ( 32 - length % 16 ) ; //获得需要切割的数组的长度,比如 77 变 96(80+16),77+ (32- 77/16的余数) = 77 + (32-13) = 77 + 19 = 96,多16位确保不丢东西 var result = new byte [ length ] ; //返回值,长度为length // Log.Print($"start:{start}, length:{length}, remainder:{remainder}"); //现在需要将buffer切割,从offset开始,到count为止 var encryptedData = new byte [ count ] ; //创建加密数据数组 Buffer . BlockCopy ( _buffer , offset , encryptedData , 0 , count ) ; //从原始数据里分割出来 // Log.Print("获取到的密文:"+string.Join(", ", encryptedData)); //给encryptedData解密 var decrypt = CryptoHelper . AesDecryptWithNoPadding ( encryptedData , _key ) ; //截取decrypt,从remainder开始,到length为止,比如余数是3,那么从3-1的元素开始 offset = remainder ; // Log.Print($"copy from offset:{offset}, result.length:{length}, decrypt.length:{decrypt.Length}"); //这里有个问题,比如decrypt有16字节,而result是12字节,offset是8,那么12+8 > 16,就会出现错误 //所以这里要改一下 var total = offset + length ; var dLength = decrypt . Length ; if ( total > dLength ) { Array . Resize ( ref decrypt , total ) ; } Buffer . BlockCopy ( decrypt , offset , result , 0 , length ) ; // Log.Print("解密结果:"+string.Join(", ", decrypt)); # if UNITY_EDITOR EncryptedCounts ++ ; # endif return result ; } public override long Seek ( long offset , SeekOrigin loc ) { if ( ! _isOpen ) Log . PrintError ( "stream is closed" ) ; if ( offset > MemStreamMaxLength ) throw new ArgumentOutOfRangeException ( nameof ( offset ) , "offset > stream length is invalid" ) ; switch ( loc ) { case SeekOrigin . Begin : { int tempPosition = unchecked ( _origin + ( int ) offset ) ; if ( offset < 0 || tempPosition < _origin ) throw new IOException ( "offset < 0 from the beginning of stream is invalid" ) ; _position = tempPosition ; break ; } case SeekOrigin . Current : { int tempPosition = unchecked ( _position + ( int ) offset ) ; if ( unchecked ( _position + offset ) < _origin || tempPosition < _origin ) throw new IOException ( "offset is before the stream which is invalid" ) ; _position = tempPosition ; break ; } case SeekOrigin . End : { int tempPosition = unchecked ( _length + ( int ) offset ) ; if ( unchecked ( _length + offset ) < _origin || tempPosition < _origin ) throw new IOException ( "offset is before the stream which is invalid" ) ; _position = tempPosition ; break ; } default : throw new ArgumentException ( "invalid seek origin" ) ; } return _position ; } // Sets the length of the stream to a given value. The new // value must be nonnegative and less than the space remaining in // the array, Int32.MaxValue - origin // Origin is 0 in all cases other than a JStream created on // top of an existing array and a specific starting offset was passed // into the JStream constructor. The upper bounds prevents any // situations where a stream may be created on top of an array then // the stream is made longer than the maximum possible length of the // array (Int32.MaxValue). // public override void SetLength ( long value ) { if ( value < 0 || value > Int32 . MaxValue ) { throw new ArgumentOutOfRangeException ( nameof ( value ) , "value does not fit the length (out of range)" ) ; } // Origin wasn't publicly exposed above. if ( value > ( Int32 . MaxValue - _origin ) ) { throw new ArgumentOutOfRangeException ( nameof ( value ) , "value is too big" ) ; } int newLength = _origin + ( int ) value ; bool allocatedNewArray = EnsureCapacity ( newLength ) ; if ( ! allocatedNewArray && newLength > _length ) Array . Clear ( _buffer , _length , newLength - _length ) ; _length = newLength ; if ( _position > newLength ) _position = newLength ; } public virtual byte [ ] ToArray ( ) { byte [ ] copy = new byte [ _length - _origin ] ; Buffer . BlockCopy ( _buffer , _origin , copy , 0 , _length - _origin ) ; return copy ; } public override void Write ( byte [ ] buffer , int offset , int count ) { throw new NotSupportedException ( "JStream does not support write method!" ) ; } } }
以下是CryptoHelper,加密类源码:
    
// // CryptoHelper.cs // // Author: // JasonXuDeveloper(傑) <jasonxudeveloper@gmail.com> // // Copyright (c) 2020 JEngine // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. using System ; using System . Security . Cryptography ; using System . Text ; using UnityEngine ; namespace JEngine . Core { public class CryptoHelper { /// <summary> /// 加密字符串 /// </summary> /// <param name="value"></param> /// <param name="key"></param> /// <returns></returns> /// <exception cref="Exception"></exception> public static string EncryptStr ( string value , string key ) { try { Byte [ ] keyArray = System . Text . Encoding . UTF8 . GetBytes ( key ) ; Byte [ ] toEncryptArray = System . Text . Encoding . UTF8 . GetBytes ( value ) ; var rijndael = new System . Security . Cryptography . RijndaelManaged ( ) ; rijndael . Key = keyArray ; rijndael . Mode = System . Security . Cryptography . CipherMode . ECB ; rijndael . Padding = System . Security . Cryptography . PaddingMode . PKCS7 ; System . Security . Cryptography . ICryptoTransform cTransform = rijndael . CreateEncryptor ( ) ; Byte [ ] resultArray = cTransform . TransformFinalBlock ( toEncryptArray , 0 , toEncryptArray . Length ) ; return Convert . ToBase64String ( resultArray , 0 , resultArray . Length ) ; } catch ( Exception ex ) { Log . PrintError ( ex ) ; return null ; } } /// <summary> /// 解密字符串 /// </summary> /// <param name="value"></param> /// <param name="key"></param> /// <returns></returns> /// <exception cref="Exception"></exception> public static string DecryptStr ( string value , string key ) { try { Byte [ ] keyArray = System . Text . Encoding . UTF8 . GetBytes ( key ) ; Byte [ ] toEncryptArray = Convert . FromBase64String ( value ) ; var rijndael = new System . Security . Cryptography . RijndaelManaged ( ) ; rijndael . Key = keyArray ; rijndael . Mode = System . Security . Cryptography . CipherMode . ECB ; rijndael . Padding = System . Security . Cryptography . PaddingMode . PKCS7 ; System . Security . Cryptography . ICryptoTransform cTransform = rijndael . CreateDecryptor ( ) ; Byte [ ] resultArray = cTransform . TransformFinalBlock ( toEncryptArray , 0 , toEncryptArray . Length ) ; return System . Text . Encoding . UTF8 . GetString ( resultArray ) ; } catch ( Exception ex ) { Log . PrintError ( ex ) ; return null ; } } /// <summary> /// AES 算法加密(ECB模式) 将明文加密 /// </summary> /// <param name="toEncryptArray,">明文</param> /// <param name="Key">密钥</param> /// <returns>加密后base64编码的密文</returns> public static byte [ ] AesEncrypt ( byte [ ] toEncryptArray , string Key ) { try { byte [ ] keyArray = Encoding . UTF8 . GetBytes ( Key ) ; RijndaelManaged rDel = new RijndaelManaged ( ) ; rDel . Key = keyArray ; rDel . Mode = CipherMode . ECB ; rDel . Padding = PaddingMode . PKCS7 ; ICryptoTransform cTransform = rDel . CreateEncryptor ( ) ; byte [ ] resultArray = cTransform . TransformFinalBlock ( toEncryptArray , 0 , toEncryptArray . Length ) ; return resultArray ; } catch ( Exception ex ) { Log . PrintError ( ex ) ; return null ; } } /// <summary> /// AES 算法解密(ECB模式) 将密文base64解码进行解密,返回明文 /// </summary> /// <param name="toDecryptArray">密文</param> /// <param name="Key">密钥</param> /// <returns>明文</returns> public static byte [ ] AesDecrypt ( byte [ ] toDecryptArray , string Key ) { try { byte [ ] keyArray = Encoding . UTF8 . GetBytes ( Key ) ; RijndaelManaged rDel = new RijndaelManaged ( ) ; rDel . Key = keyArray ; rDel . Mode = CipherMode . ECB ; rDel . Padding = PaddingMode . PKCS7 ; ICryptoTransform cTransform = rDel . CreateDecryptor ( ) ; byte [ ] resultArray = cTransform . TransformFinalBlock ( toDecryptArray , 0 , toDecryptArray . Length ) ; return resultArray ; } catch ( Exception ex ) { Log . PrintError ( ex ) ; return null ; } } /// <summary> /// AES 算法加密(ECB模式) 无padding填充,用于分块解密 /// </summary> /// <param name="toEncryptArray,">明文</param> /// <param name="Key">密钥</param> /// <returns>加密后base64编码的密文</returns> public static byte [ ] AesEncryptWithNoPadding ( byte [ ] toEncryptArray , string Key ) { try { byte [ ] keyArray = Encoding . UTF8 . GetBytes ( Key ) ; RijndaelManaged rDel = new RijndaelManaged ( ) ; rDel . Key = keyArray ; rDel . Mode = CipherMode . ECB ; rDel . Padding = PaddingMode . None ; ICryptoTransform cTransform = rDel . CreateEncryptor ( ) ; byte [ ] resultArray = cTransform . TransformFinalBlock ( toEncryptArray , 0 , toEncryptArray . Length ) ; return resultArray ; } catch ( Exception ex ) { Log . PrintError ( ex ) ; return null ; } } /// <summary> /// AES 算法解密(ECB模式) 无padding填充,用于分块解密 /// </summary> /// <param name="toDecryptArray">密文</param> /// <param name="Key">密钥</param> /// <returns>明文</returns> public static byte [ ] AesDecryptWithNoPadding ( byte [ ] toDecryptArray , string Key ) { try { byte [ ] keyArray = Encoding . UTF8 . GetBytes ( Key ) ; RijndaelManaged rDel = new RijndaelManaged ( ) ; rDel . Key = keyArray ; rDel . Mode = CipherMode . ECB ; rDel . Padding = PaddingMode . None ; ICryptoTransform cTransform = rDel . CreateDecryptor ( ) ; byte [ ] resultArray = cTransform . TransformFinalBlock ( toDecryptArray , 0 , toDecryptArray . Length ) ; return resultArray ; } catch ( Exception ex ) { Log . PrintError ( ex ) ; return null ; } } } }
感谢大家的阅读!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值