From 33e31cb0703aaf3ba01fb2314d908e02489e2600 Mon Sep 17 00:00:00 2001 From: Slavasil Date: Thu, 4 Dec 2025 21:10:04 +0300 Subject: [PATCH] implement file reading (unlocking the database) - implement ReadField in PassStoreEntryPassword - make WriteField static - add ToString for LoginField, PassStoreEntryPassword & PassStoreEntryGroup - fix bugs in OuterEncryptionReader and PassStoreFileAccessor - change PassStoreFileAccessor.root type to PassStoreEntry --- .../Crypto/OuterEncryptionReader.cs | 5 +- .../Crypto/PassStoreContentChunk.cs | 24 ++++--- src/KeyKeeper/PasswordStore/LoginField.cs | 6 ++ src/KeyKeeper/PasswordStore/PassStoreEntry.cs | 41 +++++++++++ .../PasswordStore/PassStoreEntryGroup.cs | 9 ++- .../PasswordStore/PassStoreEntryPassword.cs | 45 +++++++++++- .../PasswordStore/PassStoreFileAccessor.cs | 72 +++++++++++++++++-- .../PasswordStore/PassStoreFileException.cs | 2 + 8 files changed, 186 insertions(+), 18 deletions(-) diff --git a/src/KeyKeeper/PasswordStore/Crypto/OuterEncryptionReader.cs b/src/KeyKeeper/PasswordStore/Crypto/OuterEncryptionReader.cs index a479e83..035215b 100644 --- a/src/KeyKeeper/PasswordStore/Crypto/OuterEncryptionReader.cs +++ b/src/KeyKeeper/PasswordStore/Crypto/OuterEncryptionReader.cs @@ -155,7 +155,10 @@ public class OuterEncryptionReader : Stream EraseCurrentChunk(); int decrypted = 0, read = 0; - currentChunk = new byte[(encryptedData.Length + encryptedRemainderLength) / 16 * 16]; + if (isCurrentChunkLast) + currentChunk = new byte[encryptedData.Length + encryptedRemainderLength]; + else + currentChunk = new byte[(encryptedData.Length + encryptedRemainderLength) / 16 * 16]; if (encryptedRemainderLength > 0 && encryptedData.Length >= 16 - encryptedRemainderLength) { encryptedData.Slice(0, 16 - encryptedRemainderLength) diff --git a/src/KeyKeeper/PasswordStore/Crypto/PassStoreContentChunk.cs b/src/KeyKeeper/PasswordStore/Crypto/PassStoreContentChunk.cs index b58c94a..30efa6d 100644 --- a/src/KeyKeeper/PasswordStore/Crypto/PassStoreContentChunk.cs +++ b/src/KeyKeeper/PasswordStore/Crypto/PassStoreContentChunk.cs @@ -40,8 +40,11 @@ public class PassStoreContentChunk try { - chunkLen = rd.ReadUInt16(); - chunkLen = chunkLen | (rd.ReadByte() << 16); + byte[] chunkLenBytes = new byte[3]; + if (str.Read(chunkLenBytes) < 3) + throw PassStoreFileException.UnexpectedEndOfFile; + chunkLen = BinaryPrimitives.ReadUInt16LittleEndian(new(chunkLenBytes, 0, 2)); + chunkLen |= chunkLenBytes[2] << 16; } catch (EndOfStreamException) { @@ -90,18 +93,17 @@ public class PassStoreContentChunk { BinaryReader rd = new(s); int chunkLen; - try - { - chunkLen = rd.ReadUInt16(); - chunkLen = (chunkLen << 8) | rd.ReadByte(); - } - catch (EndOfStreamException) - { + + byte[] chunkLenBytes = new byte[3]; + if (s.Read(chunkLenBytes) < 3) throw PassStoreFileException.UnexpectedEndOfFile; - } + chunkLen = BinaryPrimitives.ReadUInt16LittleEndian(new(chunkLenBytes, 0, 2)); + chunkLen |= chunkLenBytes[2] << 16; + chunkLen &= ~(1 << 23); // 23 бит имеет специальное значение byte[] chunk = new byte[3 + HMAC_SIZE + chunkLen]; - if (s.Read(chunk) < chunk.Length) + Array.Copy(chunkLenBytes, chunk, 3); + if (s.Read(chunk, 3, HMAC_SIZE + chunkLen) < HMAC_SIZE + chunkLen) { throw PassStoreFileException.UnexpectedEndOfFile; } diff --git a/src/KeyKeeper/PasswordStore/LoginField.cs b/src/KeyKeeper/PasswordStore/LoginField.cs index d2067f0..e4973d9 100644 --- a/src/KeyKeeper/PasswordStore/LoginField.cs +++ b/src/KeyKeeper/PasswordStore/LoginField.cs @@ -1,4 +1,5 @@ using System; +using static KeyKeeper.PasswordStore.FileFormatConstants; namespace KeyKeeper.PasswordStore; @@ -7,4 +8,9 @@ public struct LoginField public byte Type; public Guid CustomFieldSubtype; public required string Value; + + public override string ToString() + { + return string.Format("LoginField(type={0} {1} value={2})", Type, Type == LOGIN_FIELD_CUSTOM_ID ? "customtype=" + CustomFieldSubtype : "", Value); + } } \ No newline at end of file diff --git a/src/KeyKeeper/PasswordStore/PassStoreEntry.cs b/src/KeyKeeper/PasswordStore/PassStoreEntry.cs index eee882e..f9588bf 100644 --- a/src/KeyKeeper/PasswordStore/PassStoreEntry.cs +++ b/src/KeyKeeper/PasswordStore/PassStoreEntry.cs @@ -1,6 +1,7 @@ using System; using System.IO; using KeyKeeper.PasswordStore.Crypto; +using static KeyKeeper.PasswordStore.FileFormatConstants; namespace KeyKeeper.PasswordStore; @@ -33,5 +34,45 @@ public abstract class PassStoreEntry wr.Write(serializedEntry); } + public static PassStoreEntry ReadFromStream(Stream str) + { + BinaryReader rd = new(str); + try + { + rd.Read7BitEncodedInt(); + + byte[] uuidBuffer = new byte[16]; + if (rd.Read(uuidBuffer) < 16) + throw PassStoreFileException.UnexpectedEndOfFile; + Guid id = new Guid(uuidBuffer); + + ulong timestamp = FileFormatUtil.ReadVarUint16(str); + DateTime createdAt = DateTimeOffset.FromUnixTimeSeconds((long)timestamp).UtcDateTime; + timestamp = FileFormatUtil.ReadVarUint16(str); + DateTime modifiedAt = DateTimeOffset.FromUnixTimeSeconds((long)timestamp).UtcDateTime; + + if (rd.Read(uuidBuffer) < 16) + throw PassStoreFileException.UnexpectedEndOfFile; + Guid iconType = new Guid(uuidBuffer); + + string name = FileFormatUtil.ReadU16TaggedString(str); + + byte entryType = rd.ReadByte(); + if (entryType == ENTRY_GROUP_ID) + { + return PassStoreEntryGroup.ReadFromStream(str, id, createdAt, modifiedAt, iconType, name); + } else if (entryType == ENTRY_PASS_ID) + { + return PassStoreEntryPassword.ReadFromStream(str, id, createdAt, modifiedAt, iconType, name); + } else + { + throw PassStoreFileException.InvalidPassStoreEntry; + } + } catch (EndOfStreamException) + { + throw PassStoreFileException.UnexpectedEndOfFile; + } + } + protected abstract byte[] InnerSerialize(); } diff --git a/src/KeyKeeper/PasswordStore/PassStoreEntryGroup.cs b/src/KeyKeeper/PasswordStore/PassStoreEntryGroup.cs index 0d75a41..9624cec 100644 --- a/src/KeyKeeper/PasswordStore/PassStoreEntryGroup.cs +++ b/src/KeyKeeper/PasswordStore/PassStoreEntryGroup.cs @@ -36,7 +36,7 @@ public class PassStoreEntryGroup : PassStoreEntry, IPassStoreDirectory try { byte groupType = rd.ReadByte(); - byte[] guidBuffer = new byte[8]; + byte[] guidBuffer = new byte[16]; Guid? customGroupSubtype = null; if (groupType == GROUP_TYPE_CUSTOM) { @@ -71,6 +71,13 @@ public class PassStoreEntryGroup : PassStoreEntry, IPassStoreDirectory return ChildEntries.GetEnumerator(); } + public override string ToString() + { + return string.Format( + "EntryGroup (id={0} name={1} chilren=[{2}])", + Id, Name, string.Join(", ", ChildEntries)); + } + protected override byte[] InnerSerialize() { MemoryStream str = new(); diff --git a/src/KeyKeeper/PasswordStore/PassStoreEntryPassword.cs b/src/KeyKeeper/PasswordStore/PassStoreEntryPassword.cs index 56481c2..09cece9 100644 --- a/src/KeyKeeper/PasswordStore/PassStoreEntryPassword.cs +++ b/src/KeyKeeper/PasswordStore/PassStoreEntryPassword.cs @@ -23,6 +23,31 @@ public class PassStoreEntryPassword : PassStoreEntry ExtraFields = extras ?? new(); } + public static PassStoreEntry ReadFromStream(Stream str, Guid id, DateTime createdAt, DateTime modifiedAt, Guid iconType, string name) + { + BinaryReader rd = new(str); + try + { + LoginField username = ReadField(str); + LoginField password = ReadField(str); + PassStoreEntryPassword entry = new(id, createdAt, modifiedAt, iconType, name, username, password); + int extraFieldCount = rd.Read7BitEncodedInt(); + for (; extraFieldCount > 0; extraFieldCount--) + entry.ExtraFields.Add(ReadField(str)); + return entry; + } catch (EndOfStreamException) + { + throw PassStoreFileException.UnexpectedEndOfFile; + } + } + + public override string ToString() + { + return string.Format( + "EntryPassword(id={0} name={1} fields=[first={2} second={3} extra={4}])", + Id, Name, Username, Password, string.Join(", ", ExtraFields)); + } + protected override byte[] InnerSerialize() { MemoryStream str = new(); @@ -36,11 +61,29 @@ public class PassStoreEntryPassword : PassStoreEntry return str.ToArray(); } - private void WriteField(Stream str, LoginField field) + private static void WriteField(Stream str, LoginField field) { str.WriteByte(field.Type); if (field.Type == LOGIN_FIELD_CUSTOM_ID) str.Write(field.CustomFieldSubtype.ToByteArray()); FileFormatUtil.WriteU16TaggedString(str, field.Value); } + + private static LoginField ReadField(Stream str) + { + int t = str.ReadByte(); + if (t == -1) + throw PassStoreFileException.UnexpectedEndOfFile; + LoginField field = new() { Value = "" }; + field.Type = (byte)t; + if (t == LOGIN_FIELD_CUSTOM_ID) + { + byte[] uuidBuffer = new byte[16]; + if (str.Read(uuidBuffer) < 16) + throw PassStoreFileException.UnexpectedEndOfFile; + field.CustomFieldSubtype = new Guid(uuidBuffer); + } + field.Value = FileFormatUtil.ReadU16TaggedString(str); + return field; + } } \ No newline at end of file diff --git a/src/KeyKeeper/PasswordStore/PassStoreFileAccessor.cs b/src/KeyKeeper/PasswordStore/PassStoreFileAccessor.cs index 3876aeb..8169c34 100644 --- a/src/KeyKeeper/PasswordStore/PassStoreFileAccessor.cs +++ b/src/KeyKeeper/PasswordStore/PassStoreFileAccessor.cs @@ -20,7 +20,8 @@ public class PassStoreFileAccessor : IPassStore private string filename; private byte[]? key; - private IPassStoreDirectory? root; + private InnerEncryptionInfo? innerCrypto; + private PassStoreEntry? root; public PassStoreFileAccessor(string filename, bool create, StoreCreationOptions? createOptions) { @@ -44,7 +45,7 @@ public class PassStoreFileAccessor : IPassStore { if (Locked) throw new InvalidOperationException(); - return root!; + return (IPassStoreDirectory)root!; } public int GetTotalEntryCount() @@ -58,7 +59,50 @@ public class PassStoreFileAccessor : IPassStore using FileStream file = new(filename, FileMode.Open, FileAccess.Read, FileShare.None); FileHeader hdr = FileHeader.ReadFrom(file); - Console.WriteLine(hdr); // debug + + file.Seek((file.Position + 4096 - 1) / 4096 * 4096, SeekOrigin.Begin); + + key.Salt = hdr.PreSalt; + this.key = hdr.KdfInfo.GetKdf().Derive(key, 32); + using OuterEncryptionReader cryptoReader = new(file, this.key, ((OuterAesHeader)hdr.OuterCryptoHeader).InitVector); + using BinaryReader rd = new(cryptoReader); + + { + if (rd.ReadUInt32() != FILE_FIELD_BEGIN) + throw PassStoreFileException.InvalidBeginMarker; + Span marker = stackalloc byte[8]; + if (rd.Read(marker) < 8) + throw PassStoreFileException.UnexpectedEndOfFile; + if (!marker.SequenceEqual(BEGIN_MARKER)) + throw PassStoreFileException.InvalidBeginMarker; + } + + while (true) + { + try + { + uint fileField = rd.ReadUInt32(); + bool end = false; + switch (fileField) + { + case FILE_FIELD_INNER_CRYPTO: + ReadInnerCryptoInfo(cryptoReader); + break; + case FILE_FIELD_CONFIG: + break; + case FILE_FIELD_STORE: + this.root = PassStoreEntry.ReadFromStream(cryptoReader); + break; + case FILE_FIELD_END: + end = true; + break; + } + if (end) break; + } catch (EndOfStreamException) + { + throw PassStoreFileException.UnexpectedEndOfFile; + } + } } public void Lock() @@ -153,7 +197,10 @@ public class PassStoreFileAccessor : IPassStore wr.Write(FILE_FIELD_CONFIG); wr.Write(FILE_FIELD_STORE); - root = (IPassStoreDirectory) WriteInitialStoreTree(cryptoWriter); + root = WriteInitialStoreTree(cryptoWriter); + + wr.Write(FILE_FIELD_END); + cryptoWriter.Flush(); cryptoWriter.Dispose(); } @@ -173,6 +220,17 @@ public class PassStoreFileAccessor : IPassStore return root; } + private InnerEncryptionInfo ReadInnerCryptoInfo(Stream str) + { + byte[] key = new byte[32]; + byte[] iv = new byte[16]; + if (str.Read(key) < 32) + throw PassStoreFileException.UnexpectedEndOfFile; + if (str.Read(iv) < 16) + throw PassStoreFileException.UnexpectedEndOfFile; + return new(key, iv); + } + record FileHeader ( ushort FileVersionMajor, ushort FileVersionMinor, @@ -328,4 +386,10 @@ public class PassStoreFileAccessor : IPassStore return new AesKdf(Rounds, Seed); } } + + record struct InnerEncryptionInfo( + byte[] Key, + byte[] Iv + ) + {} } diff --git a/src/KeyKeeper/PasswordStore/PassStoreFileException.cs b/src/KeyKeeper/PasswordStore/PassStoreFileException.cs index ad858c0..35d5a03 100644 --- a/src/KeyKeeper/PasswordStore/PassStoreFileException.cs +++ b/src/KeyKeeper/PasswordStore/PassStoreFileException.cs @@ -9,6 +9,8 @@ public class PassStoreFileException : Exception public static readonly PassStoreFileException UnsupportedVersion = new("unsupported format version"); public static readonly PassStoreFileException InvalidCryptoHeader = new("invalid encryption header"); public static readonly PassStoreFileException ContentHMACMismatch = new("content HMAC mismatch"); + public static readonly PassStoreFileException InvalidPassStoreEntry = new("invalid store entry"); + public static readonly PassStoreFileException InvalidBeginMarker = new("invalid marker of the beginning of data"); public PassStoreFileException(string description): base(description) {