前言 🔗
现在的网盘所谓的“极速妙传”,无非就是通过文件校验crc32或md5或者hash值来判断
仔细想来细思极恐,那我的文件和隐私不是被某度看到裤衩子都不剩了?我觉得这样不行(主要是自己爱折腾
于是抽空写了一个自定义的包文件,支持文件压缩/文件加密和大于2G的文件合并成一个文件,并采用.net 8支持windows/linux,理论支持macos
调研 🔗
Stream Encrypt分为对称加密和非对称加密,想来自己的文件也没什么太多的机密信息,使用对称加密即可
那么问题来了,该选什么呢?RC4?SNOW2?ChaCha20-Poly1305?
rc4和snow2之前都有用过,所以觉得采用ChaCha20-Poly1305来实现一下
首先看一下ChaCha20-Poly1305的说明
好了,加密算法选好,压缩该选什么呢? 7z?zip?gzip?zlib?snappy?
仔细分析一下优劣后,最终选择snappy,然后看一下snappy的说明
分析完毕后具有以后的优势
- 压缩/解压缩速度快
速度快??好,就这个了,开始
设计 🔗
文件头(40字节)
- 版本号(long) Version
- 文件数量(int32) FileCount
- 打包时间(long) PacketTime
- 12位密钥(byte[12]) Nonce
- 校验值(long) CheckSum
文件实例(长度52+Len字节)
- 文件路径长度(int) Len
- 文件路径(byte[Len]) Path
- 文件大小(long) Size
- 压缩后大小(long) CompressedSize
- 文件偏移(long) Offest
- 创建时间(long) CreationTime
- 编辑时间(long) LastAccessTime
- 最后一次写入时间(long) LastWriteTime
文件数据(CompressedSize)
- 混在一起,通过Offest开始读取,长度以CompressedSize结束
文件头48字节紧跟FileCount * (52 + Len)字节的文件列表
仅加密文件列表,对于文件不加密,小于2G文件压缩,大于2G文件前1024字节加密
如果要做更复杂的接口,可以考虑对文件偏移做手段
此样设计文件大小可以以极低的代价存储 🔗
代码 🔗
这里仅提供核心代码,防止被他人获取以影响目前个人内容
1using System.IO.Compression;
2using System.Security.Cryptography;
3using System.Text;
4using chacha20demo.Models;
5using Snappier;
6namespace chacha20demo
7{
8 public class PacketFile : IDisposable
9 {
10 private readonly string _key = "这里填写32位字符串的key";
11
12 private readonly int _headerOffest = 0;
13 private readonly int _entriesOffest = 40;
14 private readonly object _syncLock = new object();
15
16 private Stream _fs;
17 private BinaryReader _br;
18
19 public FileHeader header { get; private set; }
20
21 public List<FileEntry> fileEntries { get; set; }
22
23
24 public PacketFile()
25 {
26 var random = new Random(Guid.NewGuid().GetHashCode());
27 header = new FileHeader()
28 {
29 PacketTime = DateTime.Now,
30 Version = 1,
31 FileCount = 0,
32 Nonce = GetRandomNonce(),
33 };
34 fileEntries = new List<FileEntry>();
35 }
36
37 /// <summary>
38 /// 获取随机的nonce
39 /// </summary>
40 private byte[] GetRandomNonce()
41 {
42 using (RandomNumberGenerator rng = RandomNumberGenerator.Create())
43 {
44 // 创建一个字节数组来存储随机字节
45 byte[] randomBytes = new byte[12];
46
47 // 使用 RandomNumberGenerator 生成随机字节
48 rng.GetBytes(randomBytes);
49 return randomBytes;
50 }
51 }
52
53 /// <summary>
54 /// 获取头校验值
55 /// </summary>
56 /// <param name="header"></param>
57 /// <returns></returns>
58 private long GetHeaderCheckSum(FileHeader header)
59 {
60 return header.Version + header.FileCount + header.PacketTime.Ticks + header.Nonce.Sum(x => x);
61 }
62
63 /// <summary>
64 /// 获取文件体偏移
65 /// </summary>
66 /// <param name="fileEntries"></param>
67 /// <returns></returns>
68 private long GetFileDataOffest(List<FileEntry> fileEntries)
69 {
70 return fileEntries.Sum(x => Encoding.UTF8.GetByteCount(x.Path) + 52) + _entriesOffest;
71 }
72
73 /// <summary>
74 /// 写入文件头
75 /// </summary>
76 private void WriteFileHeader()
77 {
78 using (var bw = new BinaryWriter(_fs, Encoding.UTF8, true))
79 {
80 bw.Seek(_headerOffest, SeekOrigin.Begin);
81 bw.Write(header.Version);
82 bw.Write(header.FileCount);
83 bw.Write(header.PacketTime.Ticks);
84 bw.Write(header.Nonce);
85 bw.Write(header.CheckSum);
86 }
87 }
88
89 /// <summary>
90 /// 读取文件头
91 /// </summary>
92 private void ReadHeader()
93 {
94 _br.BaseStream.Seek(_headerOffest, SeekOrigin.Begin);
95 var version = _br.ReadInt64();
96 var fileCount = _br.ReadInt32();
97 var packetTime = _br.ReadInt64();
98 var nonce = _br.ReadBytes(12);
99 var checkSum = _br.ReadInt64();
100
101 this.header = new FileHeader() { CheckSum = checkSum, FileCount = fileCount, Nonce = nonce, PacketTime = new DateTime(packetTime), Version = version };
102 }
103
104 /// <summary>
105 /// 写入文件列表
106 /// </summary>
107 private void WriteFileEntrys()
108 {
109 ChaCha20 forEncrypting = new ChaCha20(Encoding.UTF8.GetBytes(_key), header.Nonce, (uint)header.FileCount);
110 using (var psStream = new PacketStream(_fs, forEncrypting))
111 using (var bw = new BinaryWriter(psStream, Encoding.UTF8))
112 {
113 psStream.Seek(_entriesOffest, SeekOrigin.Begin);
114 foreach (var model in fileEntries)
115 {
116 var nameBuffer = Encoding.UTF8.GetBytes(model.Path);
117 bw.Write(nameBuffer.Length);
118 bw.Write(nameBuffer);
119 bw.Write(model.Size);
120 bw.Write(model.CompressedSize);
121 bw.Write(model.Offest);
122 bw.Write(model.CreationTime.Ticks);
123 bw.Write(model.LastAccessTime.Ticks);
124 bw.Write(model.LastWriteTime.Ticks);
125 }
126 }
127 }
128
129 /// <summary>
130 /// 读取实例
131 /// </summary>
132 private void ReadEntries()
133 {
134 fileEntries = new List<FileEntry>();
135 ChaCha20 forDecrypting = new ChaCha20(Encoding.UTF8.GetBytes(_key), header.Nonce, (uint)header.FileCount);
136 using (var psStream = new PacketStream(_fs, forDecrypting))
137 using (var br = new BinaryReader(psStream, Encoding.UTF8, true))
138 {
139 psStream.Seek(_entriesOffest, SeekOrigin.Begin);
140 for (int i = 0; i < this.header.FileCount; i++)
141 {
142 var len = br.ReadInt32();
143 var pathBuffer = br.ReadBytes(len);
144 var model = new FileEntry();
145 model.Len = len;
146 model.Path = Encoding.UTF8.GetString(pathBuffer);
147 model.Size = br.ReadInt64();
148 model.CompressedSize = br.ReadInt64();
149 model.Offest = br.ReadInt64();
150 model.CreationTime = new DateTime(br.ReadInt64());
151 model.LastAccessTime = new DateTime(br.ReadInt64());
152 model.LastWriteTime = new DateTime(br.ReadInt64());
153 fileEntries.Add(model);
154 }
155 }
156 }
157
158 /// <summary>
159 /// 增加文件
160 /// </summary>
161 /// <param name="path"></param>
162 public void AddFile(string path)
163 {
164 lock (_syncLock)
165 {
166 FileInfo fi = new FileInfo(path);
167 this.fileEntries.Add(new FileEntry()
168 {
169 Path = path,
170 LastAccessTime = fi.LastAccessTime,
171 CreationTime = fi.CreationTime,
172 LastWriteTime = fi.LastWriteTime,
173 Size = fi.Length,
174 });
175 }
176 }
177
178 /// <summary>
179 /// 增加目录
180 /// </summary>
181 /// <param name="path"></param>
182 public void AddFloder(string path)
183 {
184 var rootPath = Path.GetFullPath(path).Replace("\\", "/").TrimEnd('/') + "/";
185
186 foreach (var filePath in Directory.EnumerateFiles(rootPath, "*", SearchOption.AllDirectories))
187 {
188 // var relativePath = filePath.Replace(rootPath, "");
189 this.AddFile(filePath.Replace("\\", "/"));
190 }
191 }
192
193 /// <summary>
194 /// 保存文件
195 /// </summary>
196 public async Task Save()
197 {
198 header.FileCount = fileEntries.Count;
199 header.CheckSum = GetHeaderCheckSum(this.header);
200
201 _fs = new FileStream("./test.lp", FileMode.Create, FileAccess.ReadWrite, FileShare.ReadWrite);
202 long dataOffest = GetFileDataOffest(this.fileEntries);
203 using (var bw = new BinaryWriter(_fs, Encoding.UTF8, true))
204 {
205 bw.Seek((int)dataOffest, SeekOrigin.Begin);
206 foreach (var item in this.fileEntries)
207 {
208 using var fileStream = File.OpenRead(item.Path);
209 //对于大于2G的文件
210 if (item.Size >= int.MaxValue)
211 {
212 //似乎只需要对头部加密即可,因为常规资源很少有>2G的
213 //加密文件头前1024个字节即可混淆其文件类型,类似于数据库备份文件,压缩文件,avi等
214 item.CompressedSize = item.Size;
215 item.Offest = dataOffest;
216
217 //单次读取文件长度kb
218 long singleReadLenght = 1024 * 1024 * 500;
219 long fileSize = fileStream.Length;
220 //流位置
221 int positon = 1;
222 var data = new byte[Math.Min(singleReadLenght, fileSize)];
223 while (positon > 0 && fileSize > 0)
224 {
225 positon = await fileStream.ReadAsync(data, 0, data.Length);
226 //写入二进制数据
227 bw.Write(data);
228 //记录写入的二进制流大小
229 fileSize -= data.Length;
230 }
231 continue;
232 }
233
234 using var compressed = new MemoryStream();
235 using (var compressor = new SnappyStream(compressed, CompressionMode.Compress, false))
236 {
237 await fileStream.CopyToAsync(compressor);
238 await compressor.FlushAsync();
239
240 item.CompressedSize = compressed.Length;
241 item.Offest = dataOffest;
242
243 //直接将压缩数据写入文件
244 compressed.Position = 0;
245 await compressed.CopyToAsync(_fs);
246
247 dataOffest += compressed.Length;
248 }
249 }
250 }
251 WriteFileEntrys();
252 WriteFileHeader();
253 }
254
255 /// <summary>
256 /// 加载包
257 /// </summary>
258 /// <param name="path"></param>
259 public void Load(string path)
260 {
261 _fs = new FileStream(path, FileMode.Open, FileAccess.Read);
262 _br = new BinaryReader(_fs, Encoding.UTF8);
263 this.ReadHeader();
264 this.ReadEntries();
265 }
266
267
268 /// <summary>
269 /// 导出文件
270 /// </summary>
271 /// <param name="fileEntry"></param>
272 /// <returns></returns>
273 public async Task Import(FileEntry fileEntry)
274 {
275 if (fileEntry.CompressedSize >= int.MaxValue)
276 {
277
278 using (FileStream newfs = File.Create(Path.GetFileName(fileEntry.Path)))
279 {
280 int positon = 0;
281 _fs.Position = fileEntry.Offest;
282 byte[] buffer = new byte[1024 * 1024 * 500];
283 while (_fs.Position < fileEntry.Offest + fileEntry.Size)
284 {
285 positon = await _fs.ReadAsync(buffer, 0, buffer.Length);
286 await newfs.WriteAsync(buffer, 0, positon);
287 }
288 }
289
290 return;
291 }
292 else
293 {
294 _br.BaseStream.Seek(fileEntry.Offest, SeekOrigin.Begin);
295 using var compressed = new MemoryStream(_br.ReadBytes((int)fileEntry.CompressedSize));
296 using var decompressor = new SnappyStream(compressed, CompressionMode.Decompress);
297 var buffer = new byte[fileEntry.Size];
298 var bytesRead = decompressor.Read(buffer, 0, buffer.Length);
299 while (bytesRead > 0)
300 {
301 bytesRead = decompressor.Read(buffer, 0, buffer.Length);
302 }
303 File.WriteAllBytes(Path.GetFileName(fileEntry.Path), buffer);
304 }
305 }
306
307 /// <summary>
308 /// 头加密
309 /// </summary>
310 /// <param name="buffer"></param>
311 public void HeadEncrypt(ref byte[] buffer)
312 {
313
314 }
315
316 public void Close()
317 {
318 this.Dispose();
319 }
320
321
322 public void Dispose()
323 {
324 lock (_syncLock)
325 {
326 try { _br.Close(); }
327 catch { }
328
329 try { _fs.Close(); }
330 catch { }
331 }
332 }
333 }
334}
测试 🔗
Windows下 🔗
Linux(Ubuntu22.04TLS)下 🔗
很好,成功的跑起来了,接下来看看使用Notepad++打开看看是什么效果
至此,打包/解包文件完全按照上述设定进行了加密
可以将一些配置文件资料以此文件打包后上传至某云盘了,再也不怕被偷窥了
本文内所示例的代码并未完全实现,删除了部分自定义加密内容
在这个世界上不存在绝对安全的加密实现,所以为了防止泄露,请确保工具只有您本人持有,这样别人无法反编译/反汇编您的程序即无法解密
结尾 🔗
本文的启蒙为游戏资源包的解包和打包,具体是什么游戏这里不做过多赘述
上述文件结构设计上理论没有什么问题,但是可能有具体的细节没有进行处理