mirror of
https://github.com/KeyKeeperApp/KeyKeeper.git
synced 2026-05-01 20:36:32 +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();
|
EraseCurrentChunk();
|
||||||
|
|
||||||
int decrypted = 0, read = 0;
|
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)
|
if (encryptedRemainderLength > 0 && encryptedData.Length >= 16 - encryptedRemainderLength)
|
||||||
{
|
{
|
||||||
encryptedData.Slice(0, 16 - encryptedRemainderLength)
|
encryptedData.Slice(0, 16 - encryptedRemainderLength)
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
)
|
||||||
|
{}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user