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;
+ }
+}