mirror of
https://github.com/KeyKeeperApp/KeyKeeper.git
synced 2026-04-21 15:06:29 +03:00
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:
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
{}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user