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,6 +155,9 @@ public class OuterEncryptionReader : Stream
EraseCurrentChunk(); EraseCurrentChunk();
int decrypted = 0, read = 0; int decrypted = 0, read = 0;
if (isCurrentChunkLast)
currentChunk = new byte[encryptedData.Length + encryptedRemainderLength];
else
currentChunk = new byte[(encryptedData.Length + encryptedRemainderLength) / 16 * 16]; currentChunk = new byte[(encryptedData.Length + encryptedRemainderLength) / 16 * 16];
if (encryptedRemainderLength > 0 && encryptedData.Length >= 16 - encryptedRemainderLength) if (encryptedRemainderLength > 0 && encryptedData.Length >= 16 - encryptedRemainderLength)
{ {

View File

@@ -40,8 +40,11 @@ public class PassStoreContentChunk
try try
{ {
chunkLen = rd.ReadUInt16(); byte[] chunkLenBytes = new byte[3];
chunkLen = chunkLen | (rd.ReadByte() << 16); if (str.Read(chunkLenBytes) < 3)
throw PassStoreFileException.UnexpectedEndOfFile;
chunkLen = BinaryPrimitives.ReadUInt16LittleEndian(new(chunkLenBytes, 0, 2));
chunkLen |= chunkLenBytes[2] << 16;
} }
catch (EndOfStreamException) catch (EndOfStreamException)
{ {
@@ -90,18 +93,17 @@ public class PassStoreContentChunk
{ {
BinaryReader rd = new(s); BinaryReader rd = new(s);
int chunkLen; int chunkLen;
try
{ byte[] chunkLenBytes = new byte[3];
chunkLen = rd.ReadUInt16(); if (s.Read(chunkLenBytes) < 3)
chunkLen = (chunkLen << 8) | rd.ReadByte();
}
catch (EndOfStreamException)
{
throw PassStoreFileException.UnexpectedEndOfFile; throw PassStoreFileException.UnexpectedEndOfFile;
} chunkLen = BinaryPrimitives.ReadUInt16LittleEndian(new(chunkLenBytes, 0, 2));
chunkLen |= chunkLenBytes[2] << 16;
chunkLen &= ~(1 << 23); // 23 бит имеет специальное значение chunkLen &= ~(1 << 23); // 23 бит имеет специальное значение
byte[] chunk = new byte[3 + HMAC_SIZE + chunkLen]; 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; throw PassStoreFileException.UnexpectedEndOfFile;
} }

View File

@@ -1,4 +1,5 @@
using System; using System;
using static KeyKeeper.PasswordStore.FileFormatConstants;
namespace KeyKeeper.PasswordStore; namespace KeyKeeper.PasswordStore;
@@ -7,4 +8,9 @@ public struct LoginField
public byte Type; public byte Type;
public Guid CustomFieldSubtype; public Guid CustomFieldSubtype;
public required string Value; 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;
using System.IO; using System.IO;
using KeyKeeper.PasswordStore.Crypto; using KeyKeeper.PasswordStore.Crypto;
using static KeyKeeper.PasswordStore.FileFormatConstants;
namespace KeyKeeper.PasswordStore; namespace KeyKeeper.PasswordStore;
@@ -33,5 +34,45 @@ public abstract class PassStoreEntry
wr.Write(serializedEntry); 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(); protected abstract byte[] InnerSerialize();
} }

View File

@@ -36,7 +36,7 @@ public class PassStoreEntryGroup : PassStoreEntry, IPassStoreDirectory
try try
{ {
byte groupType = rd.ReadByte(); byte groupType = rd.ReadByte();
byte[] guidBuffer = new byte[8]; byte[] guidBuffer = new byte[16];
Guid? customGroupSubtype = null; Guid? customGroupSubtype = null;
if (groupType == GROUP_TYPE_CUSTOM) if (groupType == GROUP_TYPE_CUSTOM)
{ {
@@ -71,6 +71,13 @@ public class PassStoreEntryGroup : PassStoreEntry, IPassStoreDirectory
return ChildEntries.GetEnumerator(); 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() protected override byte[] InnerSerialize()
{ {
MemoryStream str = new(); MemoryStream str = new();

View File

@@ -23,6 +23,31 @@ public class PassStoreEntryPassword : PassStoreEntry
ExtraFields = extras ?? new(); 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() protected override byte[] InnerSerialize()
{ {
MemoryStream str = new(); MemoryStream str = new();
@@ -36,11 +61,29 @@ public class PassStoreEntryPassword : PassStoreEntry
return str.ToArray(); return str.ToArray();
} }
private void WriteField(Stream str, LoginField field) private static void WriteField(Stream str, LoginField field)
{ {
str.WriteByte(field.Type); str.WriteByte(field.Type);
if (field.Type == LOGIN_FIELD_CUSTOM_ID) if (field.Type == LOGIN_FIELD_CUSTOM_ID)
str.Write(field.CustomFieldSubtype.ToByteArray()); str.Write(field.CustomFieldSubtype.ToByteArray());
FileFormatUtil.WriteU16TaggedString(str, field.Value); 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 string filename;
private byte[]? key; private byte[]? key;
private IPassStoreDirectory? root; private InnerEncryptionInfo? innerCrypto;
private PassStoreEntry? root;
public PassStoreFileAccessor(string filename, bool create, StoreCreationOptions? createOptions) public PassStoreFileAccessor(string filename, bool create, StoreCreationOptions? createOptions)
{ {
@@ -44,7 +45,7 @@ public class PassStoreFileAccessor : IPassStore
{ {
if (Locked) if (Locked)
throw new InvalidOperationException(); throw new InvalidOperationException();
return root!; return (IPassStoreDirectory)root!;
} }
public int GetTotalEntryCount() public int GetTotalEntryCount()
@@ -58,7 +59,50 @@ public class PassStoreFileAccessor : IPassStore
using FileStream file = new(filename, FileMode.Open, FileAccess.Read, FileShare.None); using FileStream file = new(filename, FileMode.Open, FileAccess.Read, FileShare.None);
FileHeader hdr = FileHeader.ReadFrom(file); 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() public void Lock()
@@ -153,7 +197,10 @@ public class PassStoreFileAccessor : IPassStore
wr.Write(FILE_FIELD_CONFIG); wr.Write(FILE_FIELD_CONFIG);
wr.Write(FILE_FIELD_STORE); wr.Write(FILE_FIELD_STORE);
root = (IPassStoreDirectory) WriteInitialStoreTree(cryptoWriter); root = WriteInitialStoreTree(cryptoWriter);
wr.Write(FILE_FIELD_END);
cryptoWriter.Flush(); cryptoWriter.Flush();
cryptoWriter.Dispose(); cryptoWriter.Dispose();
} }
@@ -173,6 +220,17 @@ public class PassStoreFileAccessor : IPassStore
return root; 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 ( record FileHeader (
ushort FileVersionMajor, ushort FileVersionMajor,
ushort FileVersionMinor, ushort FileVersionMinor,
@@ -328,4 +386,10 @@ public class PassStoreFileAccessor : IPassStore
return new AesKdf(Rounds, Seed); 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 UnsupportedVersion = new("unsupported format version");
public static readonly PassStoreFileException InvalidCryptoHeader = new("invalid encryption header"); public static readonly PassStoreFileException InvalidCryptoHeader = new("invalid encryption header");
public static readonly PassStoreFileException ContentHMACMismatch = new("content HMAC mismatch"); 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) public PassStoreFileException(string description): base(description)
{ {