diff --git a/src/KeyKeeper/PasswordStore/Crypto/OuterEncryptionReader.cs b/src/KeyKeeper/PasswordStore/Crypto/OuterEncryptionReader.cs new file mode 100644 index 0000000..cc05baa --- /dev/null +++ b/src/KeyKeeper/PasswordStore/Crypto/OuterEncryptionReader.cs @@ -0,0 +1,200 @@ +using System; +using System.IO; +using System.Security.Cryptography; + +namespace KeyKeeper.PasswordStore.Crypto; + +public class OuterEncryptionReader : Stream +{ + public override bool CanRead => true; + public override bool CanWrite => false; + public override bool CanSeek => false; + public override bool CanTimeout => false; + public override int ReadTimeout + { + get { throw new InvalidOperationException(); } + set { throw new InvalidOperationException(); } + } + public override int WriteTimeout + { + get { throw new InvalidOperationException(); } + set { throw new InvalidOperationException(); } + } + public override long Position + { + get => position; + set => position = value; + } + public override long Length => throw new NotSupportedException(); + + private FileStream file; + private byte[] key; + private Aes aes; + private ICryptoTransform decryptor; + + /// + /// Последний считанный из файла расшифрованный чанк. Первые + /// байт - уже отданные при вызовах Read, + /// остальные - еще не отданные. + /// + private byte[]? currentChunk; + private int chunkPosition = 0; + /// + /// Порядковый номер чанка, лежащего в . + /// + private int currentChunkOrdinal = 0; + private bool isCurrentChunkLast; + private long position = 0; + + /// + /// Ещё не расшифрованные байты, которые были считаны из файла, но их + /// оказалось меньше, чем вмещает блок AES (менее 16 байт). + /// + private byte[] encryptedRemainder; + /// + /// Количество полезных байт в . + /// + private int encryptedRemainderLength; + + /// + /// Создаёт экземпляр reader, использующий файловый поток для чтения. + /// + /// Файловый поток, указатель которого должен стоять на + /// первом content chunk. + /// Ключ, который будет использован для проверки HMAC + /// и расшифровки содержимого. + public OuterEncryptionReader(FileStream file, byte[] key, byte[] iv) + { + aes = Aes.Create(); + aes.KeySize = 256; + aes.Key = key; + aes.IV = iv; + aes.Mode = CipherMode.CFB; + aes.Padding = PaddingMode.None; + decryptor = aes.CreateDecryptor(); + + this.file = file; + this.key = key; + currentChunk = null; + encryptedRemainder = new byte[16]; + encryptedRemainderLength = 0; + LoadAndDecryptNextChunk(); + } + + public override void SetLength(long value) + => throw new NotSupportedException(); + + public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, + AsyncCallback? callback, object? state) + => throw new NotSupportedException(); + public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, + AsyncCallback? callback, object? state) + => throw new NotSupportedException(); + + public override void Flush() + {} + + public override int EndRead(IAsyncResult asyncResult) + => throw new NotSupportedException(); + public override void EndWrite(IAsyncResult asyncResult) + => throw new NotSupportedException(); + + + public override long Seek(long offset, SeekOrigin origin) + => throw new NotSupportedException(); + + public override int Read(byte[] buffer, int offset, int count) + => Read(new Span(buffer, offset, count)); + + public override void Write(byte[] buffer, int offset, int count) + => throw new NotSupportedException(); + public override void Write(ReadOnlySpan buffer) + => throw new NotSupportedException(); + + public override int Read(Span buffer) + { + Console.WriteLine("OE read " + buffer.Length); + int toRead = buffer.Length; + int read = 0; + while (toRead > 0) + { + if (currentChunk == null || currentChunk.Length - chunkPosition == 0) + { + if (!isCurrentChunkLast) + { + Console.WriteLine("OER: reading next chunk"); + LoadAndDecryptNextChunk(); + } + else + { + Console.WriteLine("OER: read " + read + " bytes before EOF"); + break; + } + } + byte[] chunk = currentChunk!; + int n = Math.Min(toRead, chunk.Length - chunkPosition); + Console.WriteLine("OER: copy " + n + " bytes chunk+" + chunkPosition + " -> buffer+" + read); + new Span(chunk, chunkPosition, n).CopyTo(buffer.Slice(read)); + read += n; + toRead -= n; + chunkPosition += n; + position += n; + Console.WriteLine(string.Format("read={} toread={} pos={}", read, toRead, chunkPosition)); + } + return read; + } + + private void LoadAndDecryptNextChunk() + { + if (isCurrentChunkLast) + return; + var chunk = PassStoreContentChunk.GetFromStream(file, key, currentChunkOrdinal); + isCurrentChunkLast = chunk.IsLast; + var encryptedData = chunk.GetContent(); + EraseCurrentChunk(); + + int decrypted = 0, read = 0; + currentChunk = new byte[(encryptedData.Length + encryptedRemainderLength) / 16 * 16]; + if (encryptedRemainderLength > 0 && encryptedData.Length >= 16 - encryptedRemainderLength) + { + encryptedData.Slice(0, 16 - encryptedRemainderLength) + .CopyTo(new Span(encryptedRemainder, encryptedRemainderLength, 16 - encryptedRemainderLength)); + decryptor.TransformBlock(encryptedRemainder, 0, 16, currentChunk, 0); + decrypted = 16; + read = 16 - encryptedRemainderLength; + encryptedRemainderLength = 0; + } + if (!isCurrentChunkLast) + { + int wholeBlocksLen = (encryptedData.Length - decrypted) / 16 * 16; + if (wholeBlocksLen > 0) + { + byte[] blocks = new byte[wholeBlocksLen]; + encryptedData.Slice(read, wholeBlocksLen).CopyTo(blocks); + decryptor.TransformBlock(blocks, 0, wholeBlocksLen, currentChunk, decrypted); + decrypted += wholeBlocksLen; + read += wholeBlocksLen; + } + if (read < encryptedData.Length) + { + encryptedRemainderLength = encryptedData.Length - read; + encryptedData.Slice(read, encryptedRemainderLength).CopyTo(encryptedRemainder); + } + } else + { + byte[] finalData = new byte[encryptedData.Length - read]; + encryptedData.Slice(read).CopyTo(finalData); + byte[] decryptedFinalData = decryptor.TransformFinalBlock(finalData, 0, finalData.Length); + decryptedFinalData.CopyTo(currentChunk, decrypted); + Array.Fill(decryptedFinalData, 0); + } + chunkPosition = 0; + } + + private void EraseCurrentChunk() + { + if (currentChunk == null) return; + Array.Fill(currentChunk, 0); + currentChunk = null; + } +}