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
This commit is contained in:
2025-12-04 21:10:04 +03:00
parent 5c8a63f56a
commit 33e31cb070
8 changed files with 186 additions and 18 deletions

View File

@@ -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)

View File

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

View File

@@ -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);
}
}

View File

@@ -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();
}

View File

@@ -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();

View File

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

View File

@@ -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<byte> 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
)
{}
}

View File

@@ -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)
{