diff --git a/src/KeyKeeper/PasswordStore/Crypto/PassStoreContentChunk.cs b/src/KeyKeeper/PasswordStore/Crypto/PassStoreContentChunk.cs new file mode 100644 index 0000000..bab004a --- /dev/null +++ b/src/KeyKeeper/PasswordStore/Crypto/PassStoreContentChunk.cs @@ -0,0 +1,107 @@ +using System; +using System.Buffers.Binary; +using System.IO; +using System.Security.Cryptography; +using KeyKeeper.PasswordStore; +using static KeyKeeper.PasswordStore.FileFormatConstants; + +namespace KeyKeeper.Crypto; + +/// +/// Класс, представляющий собой обертку над content chunkом, считанным из файла +/// хранилища. Не расшифровывает содержимое, но проверяет целостность и +/// подлинность при создании объекта. +/// +public class PassStoreContentChunk +{ + private byte[] chunk; + private int chunkLen; + + /// + /// Создаёт объект content chunk, считывая массив байт. Бросает исключение + /// в случае, если массив не содержит корректный content chunk или не + /// совпадает HMAC + /// + /// Массив байт, содержащий весь content chunk, включая + /// длину и HMAC + /// Ключ от хранилища. Используется только для + /// проверки HMAC и не хранится в объекте + /// Порядковый номер content chunk'а, начиная + /// с 0 + public PassStoreContentChunk(byte[] chunk, byte[] key, int chunkOrdinal) + { + this.chunk = chunk; + + MemoryStream str = new(chunk); + BinaryReader rd = new(str); + + try + { + chunkLen = rd.ReadUInt16(); + chunkLen = (chunkLen << 8) | rd.ReadByte(); + } + catch (EndOfStreamException) + { + throw PassStoreFileException.UnexpectedEndOfFile; + } + + if (chunk.Length != chunkLen + 3 + HMAC_SIZE) + { + throw PassStoreFileException.UnexpectedEndOfFile; + } + + byte[] storedHmac = new byte[HMAC_SIZE]; + str.Read(storedHmac, 0, HMAC_SIZE); + + HMACSHA3_512 hmac = new(key); + hmac.TransformBlock(chunk, (int)str.Position, Math.Min(chunkLen, chunk.Length - (int)str.Position), null, 0); + + byte[] encodedOrdinal = new byte[sizeof(int)]; + BinaryPrimitives.WriteInt32LittleEndian(new Span(encodedOrdinal), chunkOrdinal); + hmac.TransformBlock(encodedOrdinal, 0, encodedOrdinal.Length, null, 0); + + byte[] actualHmac = hmac.Hash!; + + if (!storedHmac.Equals(actualHmac)) + { + throw PassStoreFileException.ContentHMACMismatch; + } + } + + /// + /// Создаёт объект content chunk, считывая байты из потока. Бросает + /// исключение в случае, если массив не содержит корректный content chunk + /// или не совпадает HMAC + /// + /// Массив байт, содержащий весь content chunk, включая + /// длину и HMAC + /// Ключ от хранилища. Используется только для + /// проверки HMAC и не хранится в объекте + /// Порядковый номер content chunk'а, начиная + /// с 0 + public static PassStoreContentChunk GetFromStream(Stream s, byte[] key, int chunkOrdinal) + { + BinaryReader rd = new(s); + int chunkLen; + try + { + chunkLen = rd.ReadUInt16(); + chunkLen = (chunkLen << 8) | rd.ReadByte(); + } + catch (EndOfStreamException) + { + throw PassStoreFileException.UnexpectedEndOfFile; + } + byte[] chunk = new byte[3 + HMAC_SIZE + chunkLen]; + if (s.Read(chunk) < chunk.Length) + { + throw PassStoreFileException.UnexpectedEndOfFile; + } + return new PassStoreContentChunk(chunk, key, chunkOrdinal); + } + + public ReadOnlySpan GetContent() + { + return new ReadOnlySpan(chunk, 3 + HMAC_SIZE, chunkLen); + } +}