AES可以对字符串和字节流进行加密,我们加密ab包主要使用字节流加密的方式。

AES工具类

首先在ResFrame——Frames——ResourceFrm文件夹内新建AES文件,这是一个AES工具类。

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;

public class AES
{
private static string AESHead = "AESEncrypt";
/// <summary>
/// AES文件加密
/// </summary>
/// <param name="path">文件路径</param>
/// <param name="encryptKey">密码</param>
public static void AESFileEncrypt(string path, string encryptKey)
{
if (!File.Exists(path))
{
return;
}
try
{
using (FileStream fs = new FileStream(path, FileMode.OpenOrCreate, FileAccess.ReadWrite))
{
if (fs != null)
{
//读取字节头,判断是否已经加密过了
byte[] headByte = new byte[10];
fs.Read(headByte, 0, 10);
string headTag = Encoding.UTF8.GetString(headByte);
if (headTag == AESHead)
{
#if UNITY_EDITOR
UnityEngine.Debug.Log(path + "已经加密过了");
#endif
return;
}
//加密并且写入字节头
fs.Seek(0, SeekOrigin.Begin);//上方fs.Read(headByte, 0, 10);已经读取了10个字节,我们需要回到开始
byte[] buffer = new byte[fs.Length];//根据文件的长度生成缓冲区
fs.Read(buffer, 0, Convert.ToInt32(fs.Length));//将文件读入缓冲区
fs.Seek(0, SeekOrigin.Begin);//将光标设在文件起始位置
fs.SetLength(0);//将流清空
byte[] headBuffer = Encoding.UTF8.GetBytes(AESHead);
fs.Write(headBuffer, 0, 10);//"AESEncrypt"一共十个字母,在UTF8编码中,每个字母占1个字节(1个byte)
byte[] EncBuffer = AESEncrypt(buffer, encryptKey);//加密缓冲区的文件
fs.Write(EncBuffer, 0, EncBuffer.Length);//在字节头的后面将加密的文件字节写进去
}
}
}
catch(Exception e)
{
UnityEngine.Debug.LogException(e);
}
}
/// <summary>
/// AES文件解密(会改动加密文件,不在运行时使用)
/// </summary>
/// <param name="path">文件路径</param>
/// <param name="encryptKey">密码</param>
public static void AESFileDecrypt(string path,string encryptKey)
{
if(!File.Exists(path)) { return; }
try
{
using (FileStream fs = new FileStream(path, FileMode.OpenOrCreate, FileAccess.ReadWrite))
{
if (fs != null)
{
byte[] headByte = new byte[10];
fs.Read(headByte, 0, 10);
string headTag = Encoding.UTF8.GetString(headByte);
if (headTag == AESHead)
{
byte[] buffer = new byte[fs.Length - headByte.Length];
fs.Read(buffer, 0, Convert.ToInt32(fs.Length - headByte.Length));
fs.Seek(0, SeekOrigin.Begin);
fs.SetLength(0);
byte[] decBuffer = AESDecrypt(buffer, encryptKey);
fs.Write(decBuffer, 0, decBuffer.Length);
}
}
}
}
catch(Exception e)
{
UnityEngine.Debug.LogException(e);
}
}
/// <summary>
/// AES文件解密,在运行时使用
/// </summary>
/// <param name="path">文件路径</param>
/// <param name="encryptKey">密码</param>
/// <returns>解密后的字节</returns>
public static byte[] AESFileByteDecrypt(string path,string encryptKey)
{
if (!File.Exists(path)) { return null; }

byte[] decBuffer = null;
try
{
using (FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
{
if (fs != null)
{
byte[] headByte = new byte[10];
fs.Read(headByte, 0, 10);
string headTag = Encoding.UTF8.GetString(headByte);
if (headTag == AESHead)
{
byte[] buffer = new byte[fs.Length - headByte.Length];
fs.Read(buffer, 0, buffer.Length);
decBuffer = AESDecrypt(buffer, encryptKey);
}
}
}
}
catch (Exception e)
{
UnityEngine.Debug.LogException(e);
}

return decBuffer;
}
/// <summary>
/// AES加密字符串
/// </summary>
/// <param name="encryptString">待加密密文</param>
/// <param name="encryptKey">密码</param>
/// <returns>加密后的字符串</returns>
public static string AESEncrypt(string encryptString, string encryptKey)
{
return Convert.ToBase64String(AESEncrypt(Encoding.Default.GetBytes(encryptString),encryptKey));
}
/// <summary>
/// AES加密字节
/// </summary>
/// <param name="encryptByte">待加密字节</param>
/// <param name="encryptKey">密码</param>
/// <returns>加密后的字节</returns>
public static byte[] AESEncrypt(byte[] encryptByte, string encryptKey)
{
if(encryptByte.Length == 0)
{
throw new Exception("明文不能为空");
}
if (string.IsNullOrEmpty(encryptKey))
{
throw new Exception("密钥不能为空");
}
byte[] m_strEncrypt;
byte[] m_btIV = Convert.FromBase64String("Rkb4jvUy/ye7Cd7k89QQgQ==");//初始化向量,用于生成对称加密器
byte[] m_salt = Convert.FromBase64String("gsf4jvkyhye5/d7k8OrLgM==");//固定盐值,防止使用彩虹表暴力破解密钥

Rijndael m_AESProvider = Rijndael.Create();
try
{
MemoryStream m_stream = new MemoryStream();
PasswordDeriveBytes pdb = new PasswordDeriveBytes(encryptKey, m_salt);//根据指定的密码和salt值创建密钥
ICryptoTransform transform = m_AESProvider.CreateEncryptor(pdb.GetBytes(32), m_btIV);//根据生成的密钥和初始化向量生成对称加密器
CryptoStream m_csstream = new CryptoStream(m_stream, transform, CryptoStreamMode.Write);//使用对称加密器生成加密字节流
m_csstream.Write(encryptByte, 0, encryptByte.Length);//将需要加密的字节写入加密字节流中
m_csstream.FlushFinalBlock();//刷新加密字节流并清除缓冲区
m_strEncrypt = m_stream.ToArray();//将内存中加密好的字节赋给需要返回的临时对象
m_stream.Close();
m_stream.Dispose();
m_csstream.Close();
m_csstream.Dispose();
}
catch (IOException ex)
{
throw ex;
}
catch (CryptographicException ex)
{
throw ex;
}
catch (ArgumentException ex)
{
throw ex;
}
catch (Exception ex)
{
throw ex;
}
finally
{
m_AESProvider.Clear();
}
return m_strEncrypt;
}
/// <summary>
/// AES解密字符串
/// </summary>
/// <param name="decryptString">待解密密文</param>
/// <param name="decryptKey">密码</param>
/// <returns>解密后的字符串</returns>
public static string AESDecrypt(string decryptString, string decryptKey)
{
return Convert.ToBase64String(AESDecrypt(Encoding.Default.GetBytes(decryptString),decryptKey));
}
/// <summary>
/// AES解密字节
/// </summary>
/// <param name="decryptByte">待解密字节</param>
/// <param name="decryptKey">密码</param>
/// <returns>解密后的字节</returns>
public static byte[] AESDecrypt(byte[] decryptByte, string decryptKey)
{
if (decryptByte.Length == 0)
{
throw new Exception("明文不能为空");
}
if (string.IsNullOrEmpty(decryptKey))
{
throw new Exception("密钥不能为空");
}
byte[] m_strDecrypt;
byte[] m_btIV = Convert.FromBase64String("Rkb4jvUy/ye7Cd7k89QQgQ==");
byte[] m_salt = Convert.FromBase64String("gsf4jvkyhye5/d7k8OrLgM==");

Rijndael m_AESProvider = Rijndael.Create();
try
{
MemoryStream m_stream = new MemoryStream();
PasswordDeriveBytes pdb = new PasswordDeriveBytes(decryptKey, m_salt);
ICryptoTransform transform = m_AESProvider.CreateDecryptor(pdb.GetBytes(32), m_btIV);
CryptoStream m_csstream = new CryptoStream(m_stream, transform, CryptoStreamMode.Write);
m_csstream.Write(decryptByte, 0, decryptByte.Length);
m_csstream.FlushFinalBlock();
m_strDecrypt = m_stream.ToArray();
m_stream.Close();
m_stream.Dispose();
m_csstream.Close();
m_csstream.Dispose();
}
catch (IOException ex)
{
throw ex;
}
catch (CryptographicException ex)
{
throw ex;
}
catch (ArgumentException ex)
{
throw ex;
}
catch (Exception ex)
{
throw ex;
}
finally
{
m_AESProvider.Clear();
}
return m_strDecrypt;
}
}

打包时加密

BundleEditor脚本内新建两个方法

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
[MenuItem("Tools/加密AB包")]
public static void EncryptAB()
{
DirectoryInfo directoryInfo = new DirectoryInfo(m_BundleTargetPath);
FileInfo[] fileInfos = directoryInfo.GetFiles("*",SearchOption.AllDirectories);
for (int i = 0; i < fileInfos.Length; i++)
{
if (!fileInfos[i].Name.EndsWith(".meta") && !fileInfos[i].Name.EndsWith(".manifest"))
{
AES.AESFileEncrypt(fileInfos[i].FullName, "ATAO2017");
}
}
Debug.Log("加密完成");
}
[MenuItem("Tools/解密AB包")]
public static void DecrytAB()
{
DirectoryInfo directoryInfo = new DirectoryInfo(m_BundleTargetPath);
FileInfo[] fileInfos = directoryInfo.GetFiles("*", SearchOption.AllDirectories);
for (int i = 0; i < fileInfos.Length; i++)
{
if (!fileInfos[i].Name.EndsWith(".meta") && !fileInfos[i].Name.EndsWith(".manifest"))
{
AES.AESFileDecrypt(fileInfos[i].FullName, "ATAO2017");
}
}
Debug.Log("解密完成");
}

然后再在BundleEditorBuildAssetBundle方法最后调用EncryptAB方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
  static void BuildAssetBundle()
{
//...
if (manifest == null)
{
Debug.LogError("AssetBundle 打包失败");
}else
{
Debug.Log("打包完毕");
}
DeleteManifest();
EncryptAB();//+++
}

AB包加载解密

注意事项

运行时使用解密后的ab包,需要将包全部加载到内存中,消耗一定的内存。这里将修改前后的代码都贴上,根据项目需要决定是否加密ab包。

AssetBundle.LoadFromFile不消耗内存,它只是加载了一个文件路径

AssetBundle.LoadFromMemory消耗内存,将整个AB包都加载到内存里

修改AssetBundleManagerLoadAssetBundleConfig的方法

修改前

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    /// <summary>
/// 加载AB配置表
/// </summary>
public bool LoadAssetBundleConfig()
{
#if UNITY_EDITOR
if(!ResourceManager.Instance.m_LoadFromAssetBundle)
return false;
#endif
m_ResourceItemDic.Clear();
string configPath = ABLoadPath + m_ABConfigABName;
string hotABPath = HotPatchManager.Instance.ComputeABPath(m_ABConfigABName);
configPath = string.IsNullOrEmpty(hotABPath) ? configPath : hotABPath;
AssetBundle configAB = AssetBundle.LoadFromFile(configPath);
TextAsset textAsset = configAB.LoadAsset<TextAsset>(m_ABConfigABName);//??
//...
}

这里一定要分清楚,我们在AssetBundleManager里面加载AB包的操作都是同步加载。这里加载的都是AB包

我们在ResourceManager里面有同步加载和异步加载,加载的是AB包里面具体的Assets

修改后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
    /// <summary>
/// 加载AB配置表
/// </summary>
public bool LoadAssetBundleConfig()
{
#if UNITY_EDITOR
if(!ResourceManager.Instance.m_LoadFromAssetBundle)
return false;
#endif
m_ResourceItemDic.Clear();
string configPath = ABLoadPath + m_ABConfigABName;
string hotABPath = HotPatchManager.Instance.ComputeABPath(m_ABConfigABName);
configPath = string.IsNullOrEmpty(hotABPath) ? configPath : hotABPath;
byte[] bytes = AES.AESFileByteDecrypt(configPath, "ATAO2017");//+++
AssetBundle configAB = AssetBundle.LoadFromMemory(bytes);//+++
TextAsset textAsset = configAB.LoadAsset<TextAsset>(m_ABConfigABName);//??
//...
}

修改AssetBundleManagerLoadAssetBundle的方法

修改前

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
    /// <summary>
/// 加载AssetBundle并维护AssetBundleItem中间类
/// </summary>
private AssetBundle LoadAssetBundle(string abName)
{
AssetBundleItem abItem = null;
uint abCrc = CRC32.GetCRC32(abName);
if(!m_AssetBundleItemDic.TryGetValue(abCrc, out abItem))
{
AssetBundle assetBundle = null;
string hotABPath = HotPatchManager.Instance.ComputeABPath(abName);
string fullPath = string.IsNullOrEmpty(hotABPath)? ABLoadPath + abName : hotABPath;

#if UNITY_STANDALONE || UNITY_EDITOR
if(File.Exists(fullPath))//在安卓和WebGL平台,fullPath是一个URL,这个判断是false
{
assetBundle = AssetBundle.LoadFromFile(fullPath);
}
#if UNITY_ANDROID || UNITY_IPHONE
assetBundle = AssetBundle.LoadFromFile(fullPath);
#endif
//...
}
//...
}

修改后

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
    /// <summary>
/// 加载AssetBundle并维护AssetBundleItem中间类
/// </summary>
private AssetBundle LoadAssetBundle(string abName)
{
AssetBundleItem abItem = null;
uint abCrc = CRC32.GetCRC32(abName);
if(!m_AssetBundleItemDic.TryGetValue(abCrc, out abItem))
{
AssetBundle assetBundle = null;
string hotABPath = HotPatchManager.Instance.ComputeABPath(abName);
string fullPath = string.IsNullOrEmpty(hotABPath)? ABLoadPath + abName : hotABPath;
byte[] bytes = AES.AESFileByteDecrypt(fullPath,"ATAO2017");//+++

#if UNITY_STANDALONE || UNITY_EDITOR
if(File.Exists(fullPath))//在安卓和WebGL平台,fullPath是一个URL,这个判断是false
{
assetBundle = AssetBundle.LoadFromMemory(bytes);//+++
}
#endif
#if UNITY_ANDROID || UNITY_IPHONE
assetBundle = AssetBundle.LoadFromMemory(bytes);//+++
#endif
//...
}
//...
}

安卓平台的处理

如果我们使用了加密的AB包,那么在安卓平台的streamAssets目录下是无法进行读取并解密的,安卓平台的streamAssets目录只能通过www或者UnityWebRequest加载。我们在AES工具类中使用的是FileStream加载的文件,所以无法加载。

在安卓平台,我们需要将streamAsset目录下压缩成jar包的ab资源包解压出来,放到presidentDatapath中。