梦想博客

自定义结构体文件

calendar 2023/12/16
refresh-cw 2023/12/19
2484字,5分钟

前言 🔗

现在的网盘所谓的“极速妙传”,无非就是通过文件校验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++打开看看是什么效果


至此,打包/解包文件完全按照上述设定进行了加密

可以将一些配置文件资料以此文件打包后上传至某云盘了,再也不怕被偷窥了

本文内所示例的代码并未完全实现,删除了部分自定义加密内容

在这个世界上不存在绝对安全的加密实现,所以为了防止泄露,请确保工具只有您本人持有,这样别人无法反编译/反汇编您的程序即无法解密

结尾 🔗

本文的启蒙为游戏资源包的解包和打包,具体是什么游戏这里不做过多赘述

上述文件结构设计上理论没有什么问题,但是可能有具体的细节没有进行处理

分类