使用Aead加密支持随机读写的文件

关联数据的认证加密Aead(authenticated encryption with associated data),是一种同时具备保密性,完整性和可认证性的加密形式,加密过程采用数据分组形式,对同一个密钥,每次加密需要使用不重复的Nonce(Number used only Once),加密后生成验证数据标签(Tag)用于解密时验证,并且可以附加一段明文数据(associated data)作为额外验证数据。

Aead加密模式的这些特征,非常适合用于随机读写文件的加密,待加密文件数据按固定大小分组,分组序号+随机初始化向量IV作为每个分组加密的Nonce,分组序号作为附加验证数据(associated data),加密文件中只需要保存块大小(BlockSize), 随机初始化向量(IV),加密数据块(Block Cipher)即可。

在块大小和初始化向量已知的情况下,任何文件位置的数据分组序号都能被计算出来,而每次加密最小单元是一个数据块,所以可以定位文件的任何位置进行读写,即文件的随机读写。

使用流读写方法实现AeadStream,写入数据:

public override void Write(byte[] src, int offset, int count)
        {
            actionPos = streamPos;
            var actionLen = streamLen;

            initBuff(count);
            int writeLen;
            while (count > 0)
            {
                writeLen = count.min(blockSize - blockOff);
                if (blockOff == 0
                    && (writeLen == blockSize // overwrite whole block
                        || writeLen >= actionLen - actionPos)) // overwrite exist block
                {
                    encryptPack(src, offset, writeLen);
                }
                else
                {
                    var dataLen = readFile(pack, pack.Length, blockIdx) - tagSize;
                    if (dataLen != (actionLen - blockIdx * blockSize).min(blockSize))
                        throw new Error(this, "DataError", 
                                    dataLen, 
                                    actionLen - blockIdx * blockSize);

                    packEnc.decrypt(pack, 0, dataLen + tagSize, blockIdx, block);
                    Buffer.BlockCopy(src, offset, block, blockOff, writeLen);

                    encryptPack(block, 0, dataLen.max(blockOff + writeLen));
                }

                offset += writeLen;
                count -= writeLen;

                actionPos += writeLen;
                actionLen = actionLen.max(actionPos);
            }
            writeBuff();

            streamPos = actionPos;
            streamLen = actionLen;
        }

读取数据:

public override int Read(byte[] dst, int offset, int count)
        {
            actionPos = streamPos;

            readBuff(count);
            int remain = count, dataLen, readLen;
            while (remain > 0)
            {
                if (actionPos >= streamLen)
                    break;
                dataLen = seekPack() - tagSize;
                readLen = remain.min(dataLen - blockOff);
                if (readLen <= 0)
                    throw new Error(this, "DataShort", remain, dataLen, blockOff);

                if (blockOff == 0 && dataLen <= remain)
                    decryptPack(dataLen, dst, offset);
                else
                {
                    decryptPack(dataLen, block);
                    Buffer.BlockCopy(block, blockOff, dst, offset, readLen);
                }

                offset += readLen;
                remain -= readLen;

                actionPos += readLen;
            }

            streamPos = actionPos;
            return count - remain;
        }

定位文件位置:

long streamPos = 0;
        public override long Position
        {
            get => streamPos;
            set
            {
                if (value < 0 || value > Length)
                    throw new IOException("pos out of range!");
                streamPos = value;
            }
        }

public override long Seek(long offset, SeekOrigin origin)
        {
            var pos = streamPos;
            switch (origin)
            {
                case SeekOrigin.Begin:
                    pos = offset;
                    break;
                case SeekOrigin.Current:
                    pos += offset;
                    break;
                case SeekOrigin.End:
                    pos = streamLen - offset;
                    break;
            }
            return Position = pos;
        }

AeadStream使用方法跟FileStream类似,都是Stream的标准读写方法。

完整代码:AeadStream.cs

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using util.crypt;
using util.ext;

namespace util.rep.aead
{
    public class AeadStream : Stream
    {
        public const string Type = "aead";
        public const short Version = 1;
        public static byte[] KeyDomain = "file-key".utf8();

        public Stream fs;
        public AeadConf conf;

        public static long dataSize(long fileSize, AeadConf conf)
        {
            var packTotal = fileSize - headSize(conf);
            return (packTotal / conf.packSize()) * conf.BlockSize
                    + ((packTotal % conf.packSize()) - conf.tagSize()).max(0);
        }

        public static int headSize(AeadConf conf)
            => Type.Length + 2 + conf.FileIdSize + conf.nonceSize();

        byte[] fileId;
        byte[] nonce;
        public AeadStream create()
        {
            fileId = conf.FileIdSize.aesRnd();
            nonce = aeadEnc.NonceSize.aesRnd();

            var header = Type.utf8().merge(Version.bytes(), fileId, nonce);
            fs.write(header);

            return this;
        }

        public AeadStream open()
        {
            var header = new byte[prefix];
            if (fs.readFull(header) != header.Length
                || header.utf8(0, Type.Length) != Type)
                throw new Error(this, "InvalidType");
            var ver = header.i16(Type.Length);
            if (ver > Version)
                throw new Error(this, "InvalidVersion", ver);

            fileId = header.sub(Type.Length + 2, conf.FileIdSize);
            nonce = header.tail(aeadEnc.NonceSize);
            streamLen = dataSize(fs.Length, conf);

            return this;
        }

        int? pre;
        int prefix => (int)(pre ?? (pre = headSize(conf)));
        int packSize => blockSize + tagSize;
        int blockSize => conf.BlockSize;
        int tagSize => aeadEnc.TagSize;

        AeadCrypt ae;
        AeadCrypt aeadEnc => ae ?? (ae = conf.newCrypt());
        PackCrypt pke;
        PackCrypt packEnc => pke ?? (pke = new PackCrypt(aeadEnc.setKey(conf.deriveKey(KeyDomain.merge(fileId), aeadEnc.KeySize)), nonce));

        long actionPos;

        byte[] pk;
        byte[] pack => pk ?? (pk = new byte[packSize]);
        
        byte[] blk;
        byte[] block => blk ?? (blk = new byte[blockSize]);

        long blockIdx => actionPos / blockSize;
        int blockOff => (int)(actionPos % blockSize);

        byte[] buff;
        int buffLen;
        int buffPos;
        long buffIdx;
        void initBuff(int dataLen)
        {
            buffLen = ((blockOff + dataLen - 1) / blockSize + 1) * packSize;
            if (buff == null || buff.Length < buffLen)
                buff = new byte[buffLen];
            buffPos = 0;
            buffIdx = blockIdx;
        }

        void readBuff(int dataLen)
        {
            initBuff(dataLen);
            buffLen = readFile(buff, buffLen, buffIdx);
        }

        void writeBuff()
        {
            if (buffPos <= 0)
                return;
            seekFile(buffIdx);
            fs.Write(buff, 0, buffPos);
        }

        void seekFile(long packIdx)
            => fs.Position = prefix + packIdx * packSize;

        int readFile(byte[] dst, int dstLen, long packIdx)
        {
            seekFile(packIdx);
            return fs.readFull(dst, 0, dstLen);
        }

        int seekPack()
        {
            buffPos = (int)((blockIdx - buffIdx) * packSize);
            return packSize.min(buffLen - buffPos);
        }

        void decryptPack(int dataSize, byte[] dst, int dstOff = 0)
            => packEnc.decrypt(buff, buffPos, dataSize + tagSize, blockIdx, dst, dstOff);

        void encryptPack(byte[] src, int srcOff, int srcLen)
        {
            packEnc.encrypt(src, srcOff, srcLen, blockIdx, buff, buffPos);
            buffPos += srcLen + tagSize;
        }

        public override int Read(byte[] dst, int offset, int count)
        {
            actionPos = streamPos;

            readBuff(count);
            int remain = count, dataLen, readLen;
            while (remain > 0)
            {
                if (actionPos >= streamLen)
                    break;
                dataLen = seekPack() - tagSize;
                readLen = remain.min(dataLen - blockOff);
                if (readLen <= 0)
                    throw new Error(this, "DataShort", remain, dataLen, blockOff);

                if (blockOff == 0 && dataLen <= remain)
                    decryptPack(dataLen, dst, offset);
                else
                {
                    decryptPack(dataLen, block);
                    Buffer.BlockCopy(block, blockOff, dst, offset, readLen);
                }

                offset += readLen;
                remain -= readLen;

                actionPos += readLen;
            }

            streamPos = actionPos;
            return count - remain;
        }

        public override void Write(byte[] src, int offset, int count)
        {
            actionPos = streamPos;
            var actionLen = streamLen;

            initBuff(count);
            int writeLen;
            while (count > 0)
            {
                writeLen = count.min(blockSize - blockOff);
                if (blockOff == 0
                    && (writeLen == blockSize // overwrite whole block
                        || writeLen >= actionLen - actionPos)) // overwrite exist block
                {
                    encryptPack(src, offset, writeLen);
                }
                else
                {
                    var dataLen = readFile(pack, pack.Length, blockIdx) - tagSize;
                    if (dataLen != (actionLen - blockIdx * blockSize).min(blockSize))
                        throw new Error(this, "DataError", 
                                    dataLen, 
                                    actionLen - blockIdx * blockSize);

                    packEnc.decrypt(pack, 0, dataLen + tagSize, blockIdx, block);
                    Buffer.BlockCopy(src, offset, block, blockOff, writeLen);

                    encryptPack(block, 0, dataLen.max(blockOff + writeLen));
                }

                offset += writeLen;
                count -= writeLen;

                actionPos += writeLen;
                actionLen = actionLen.max(actionPos);
            }
            writeBuff();

            streamPos = actionPos;
            streamLen = actionLen;
        }

        public override long Seek(long offset, SeekOrigin origin)
        {
            var pos = streamPos;
            switch (origin)
            {
                case SeekOrigin.Begin:
                    pos = offset;
                    break;
                case SeekOrigin.Current:
                    pos += offset;
                    break;
                case SeekOrigin.End:
                    pos = streamLen - offset;
                    break;
            }
            return Position = pos;
        }

        public override void SetLength(long value)
        {
            throw new NotSupportedException();
        }

        public override bool CanRead => fs.CanRead;
        public override bool CanSeek => fs.CanSeek;
        public override bool CanWrite => fs.CanWrite;
        long streamLen = 0;
        public override long Length => streamLen;
        long streamPos = 0;
        public override long Position
        {
            get => streamPos;
            set
            {
                if (value < 0 || value > Length)
                    throw new IOException("pos out of range!");
                streamPos = value;
            }
        }
        public override void Flush() => fs.Flush();
        public override void Close()
        {
            fs?.Close();
            fs = null;
        }
    }
}
View Code

Github链接:

https://github.com/bsmith-zhao/vfs/blob/main/util/rep/aead/AeadStream.cs

其他参考:

https://github.com/bsmith-zhao/vfs/blob/main/util/rep/aead/AeadConf.cs

https://github.com/bsmith-zhao/vfs/tree/main/util/crypt

https://github.com/bsmith-zhao/vfs/tree/main/util/crypt/sodium

posted @ 2023-10-15 22:00  bsmith  阅读(216)  评论(0)    收藏  举报