merge branch feature/pass-store-api

This commit is contained in:
2025-12-04 21:13:28 +03:00
21 changed files with 1524 additions and 3 deletions

View File

@@ -0,0 +1,51 @@
using System;
using System.Security.Cryptography;
using System.Text;
namespace KeyKeeper.PasswordStore.Crypto;
public class CompositeKey
{
public string Password { get; }
public byte[]? Salt
{
get { return salt; }
set
{
if (salt == null)
salt = value;
}
}
private byte[]? salt;
public bool CanComputeHash
{
get { return salt != null; }
}
public CompositeKey(string password, byte[]? salt)
{
if (password == null)
throw new ArgumentNullException("password");
Password = password;
if (salt != null && (salt.Length < FileFormatConstants.MIN_MASTER_SALT_LEN ||
salt.Length > FileFormatConstants.MAX_MASTER_SALT_LEN))
throw new ArgumentException("salt");
this.salt = salt;
}
public byte[] Hash()
{
if (!CanComputeHash)
throw new InvalidOperationException("salt is not set");
byte[] passwordBytes = Encoding.UTF8.GetBytes(Password);
byte[] hashedString = new byte[passwordBytes.Length + Salt.Length * 2];
Salt.CopyTo(hashedString, 0);
passwordBytes.CopyTo(hashedString, Salt.Length);
Salt.CopyTo(hashedString, Salt.Length + passwordBytes.Length);
return SHA256.HashData(hashedString);
}
}

View File

@@ -0,0 +1,42 @@
using System;
using System.Security.Cryptography;
namespace KeyKeeper.PasswordStore.Crypto.KeyDerivation;
public class AesKdf : MasterKeyDerivationFunction
{
public const int MIN_ROUNDS = 10;
public const int MAX_ROUNDS = 25_000_000;
public const int SEED_LENGTH = 32;
private int rounds;
private byte[] seed;
public AesKdf(int rounds, byte[] seed)
{
if (rounds < MIN_ROUNDS || rounds > MAX_ROUNDS)
throw new ArgumentOutOfRangeException(nameof(rounds));
if (seed.Length != SEED_LENGTH)
throw new ArgumentException("seed length must be " + SEED_LENGTH);
this.rounds = rounds;
this.seed = seed;
}
public override byte[] Derive(CompositeKey source, int keySizeBytes)
{
if (keySizeBytes > SEED_LENGTH)
throw new ArgumentOutOfRangeException(nameof(keySizeBytes));
byte[] key = source.Hash()[..SEED_LENGTH];
byte[] nextKey = new byte[SEED_LENGTH];
Aes cipher = Aes.Create();
cipher.KeySize = SEED_LENGTH * 8;
for (int i = 0; i < rounds; ++i)
{
cipher.Key = key;
cipher.EncryptEcb(seed, nextKey, PaddingMode.None);
(nextKey, key) = (key, nextKey);
}
return key;
}
}

View File

@@ -0,0 +1,6 @@
namespace KeyKeeper.PasswordStore.Crypto;
public abstract class MasterKeyDerivationFunction
{
public abstract byte[] Derive(CompositeKey source, int keySizeBytes);
}

View File

@@ -0,0 +1,204 @@
using System;
using System.IO;
using System.Security.Cryptography;
namespace KeyKeeper.PasswordStore.Crypto;
public class OuterEncryptionReader : Stream
{
public override bool CanRead => true;
public override bool CanWrite => false;
public override bool CanSeek => false;
public override bool CanTimeout => false;
public override int ReadTimeout
{
get { throw new InvalidOperationException(); }
set { throw new InvalidOperationException(); }
}
public override int WriteTimeout
{
get { throw new InvalidOperationException(); }
set { throw new InvalidOperationException(); }
}
public override long Position
{
get => position;
set { throw new NotSupportedException(); }
}
public override long Length => throw new NotSupportedException();
private FileStream file;
private byte[] key;
private Aes aes;
private ICryptoTransform decryptor;
/// <summary>
/// Последний считанный из файла расшифрованный чанк. Первые
/// <see cref="chunkPosition"/> байт - уже отданные при вызовах Read,
/// остальные - еще не отданные.
/// </summary>
private byte[]? currentChunk;
private int chunkPosition = 0;
/// <summary>
/// Порядковый номер чанка, лежащего в <see cref="currentChunk"/>.
/// </summary>
private int nextChunkOrdinal = 0;
private bool isCurrentChunkLast;
private long position = 0;
/// <summary>
/// Ещё не расшифрованные байты, которые были считаны из файла, но их
/// оказалось меньше, чем вмещает блок AES (менее 16 байт).
/// </summary>
private byte[] encryptedRemainder;
/// <summary>
/// Количество полезных байт в <see cref="encryptedRemainder"/>.
/// </summary>
private int encryptedRemainderLength;
/// <summary>
/// Создаёт экземпляр reader, использующий файловый поток для чтения.
/// </summary>
/// <param name="file">Файловый поток, указатель которого должен стоять на
/// первом content chunk.</param>
/// <param name="key">Ключ, который будет использован для проверки HMAC
/// и расшифровки содержимого.</param>
public OuterEncryptionReader(FileStream file, byte[] key, byte[] iv)
{
aes = Aes.Create();
aes.KeySize = 256;
aes.Key = key;
aes.IV = iv;
aes.Mode = CipherMode.CFB;
aes.Padding = PaddingMode.None;
decryptor = aes.CreateDecryptor();
this.file = file;
this.key = key;
currentChunk = null;
encryptedRemainder = new byte[16];
encryptedRemainderLength = 0;
LoadAndDecryptNextChunk();
}
public override void SetLength(long value)
=> throw new NotSupportedException();
public override IAsyncResult BeginRead(byte[] buffer, int offset, int count,
AsyncCallback? callback, object? state)
=> throw new NotSupportedException();
public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count,
AsyncCallback? callback, object? state)
=> throw new NotSupportedException();
public override void Flush()
{}
public override int EndRead(IAsyncResult asyncResult)
=> throw new NotSupportedException();
public override void EndWrite(IAsyncResult asyncResult)
=> throw new NotSupportedException();
public override long Seek(long offset, SeekOrigin origin)
=> throw new NotSupportedException();
public override int Read(byte[] buffer, int offset, int count)
=> Read(new Span<byte>(buffer, offset, count));
public override void Write(byte[] buffer, int offset, int count)
=> throw new NotSupportedException();
public override void Write(ReadOnlySpan<byte> buffer)
=> throw new NotSupportedException();
public override int Read(Span<byte> buffer)
{
Console.WriteLine("OE read " + buffer.Length);
int toRead = buffer.Length;
int read = 0;
while (toRead > 0)
{
if (currentChunk == null || currentChunk.Length - chunkPosition == 0)
{
if (!isCurrentChunkLast)
{
Console.WriteLine("OER: reading next chunk");
LoadAndDecryptNextChunk();
}
else
{
Console.WriteLine("OER: read " + read + " bytes before EOF");
break;
}
}
byte[] chunk = currentChunk!;
int n = Math.Min(toRead, chunk.Length - chunkPosition);
Console.WriteLine("OER: copy " + n + " bytes chunk+" + chunkPosition + " -> buffer+" + read);
new Span<byte>(chunk, chunkPosition, n).CopyTo(buffer.Slice(read));
read += n;
toRead -= n;
chunkPosition += n;
position += n;
Console.WriteLine(string.Format("read={0} toread={1} pos={2}", read, toRead, chunkPosition));
}
return read;
}
private void LoadAndDecryptNextChunk()
{
if (isCurrentChunkLast)
return;
var chunk = PassStoreContentChunk.GetFromStream(file, key, nextChunkOrdinal);
nextChunkOrdinal += 1;
isCurrentChunkLast = chunk.IsLast;
var encryptedData = chunk.GetContent();
EraseCurrentChunk();
int decrypted = 0, read = 0;
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)
.CopyTo(new Span<byte>(encryptedRemainder, encryptedRemainderLength, 16 - encryptedRemainderLength));
decryptor.TransformBlock(encryptedRemainder, 0, 16, currentChunk, 0);
decrypted = 16;
read = 16 - encryptedRemainderLength;
encryptedRemainderLength = 0;
}
if (!isCurrentChunkLast)
{
int wholeBlocksLen = (encryptedData.Length - decrypted) / 16 * 16;
if (wholeBlocksLen > 0)
{
byte[] blocks = new byte[wholeBlocksLen];
encryptedData.Slice(read, wholeBlocksLen).CopyTo(blocks);
decryptor.TransformBlock(blocks, 0, wholeBlocksLen, currentChunk, decrypted);
decrypted += wholeBlocksLen;
read += wholeBlocksLen;
}
if (read < encryptedData.Length)
{
encryptedRemainderLength = encryptedData.Length - read;
encryptedData.Slice(read, encryptedRemainderLength).CopyTo(encryptedRemainder);
}
} else
{
byte[] finalData = new byte[encryptedData.Length - read];
encryptedData.Slice(read).CopyTo(finalData);
byte[] decryptedFinalData = decryptor.TransformFinalBlock(finalData, 0, finalData.Length);
decryptedFinalData.CopyTo(currentChunk, decrypted);
Array.Fill<byte>(decryptedFinalData, 0);
}
chunkPosition = 0;
}
private void EraseCurrentChunk()
{
if (currentChunk == null) return;
Array.Fill<byte>(currentChunk, 0);
currentChunk = null;
}
}

View File

@@ -0,0 +1,70 @@
using System;
using System.IO;
using static KeyKeeper.PasswordStore.FileFormatConstants;
namespace KeyKeeper.PasswordStore.Crypto;
public static class OuterEncryptionUtil
{
/// <summary>
/// Проверяет корректность заголовка внешнего шифрования,
/// который содержит соль для мастер-ключа и параметры шифрования +
/// генерации ключа. Сдвигает указатель потока f на первый байт после
/// заголовка.
/// </summary>
/// <param name="f">Поток, указатель которого стоит на начале заголовка
/// внешнего шифрования.</param>
/// <exception cref="PassStoreFileException">Если заголовок содержит некорректные поля или неполный</exception>
public static void CheckOuterEncryptionHeader(FileStream f)
{
BinaryReader rd = new(f);
try
{
byte masterSaltLen = rd.ReadByte();
if (masterSaltLen < MIN_MASTER_SALT_LEN || masterSaltLen > MAX_MASTER_SALT_LEN)
{
throw PassStoreFileException.InvalidCryptoHeader;
}
f.Seek(masterSaltLen, SeekOrigin.Current);
byte encryptAlgo = rd.ReadByte();
if (encryptAlgo == ENCRYPT_ALGO_AES)
{
// пропустить 16 байт вектора инициализации AES
f.Seek(16, SeekOrigin.Current);
}
else
{
throw PassStoreFileException.InvalidCryptoHeader;
}
byte keyDerivationFunctionType = rd.ReadByte();
if (keyDerivationFunctionType == KDF_TYPE_AESKDF)
{
int nRounds;
try
{
nRounds = rd.Read7BitEncodedInt();
}
catch (FormatException)
{
throw PassStoreFileException.InvalidCryptoHeader;
}
if (nRounds < MIN_AESKDF_ROUNDS || nRounds > MAX_AESKDF_ROUNDS)
{
throw PassStoreFileException.InvalidCryptoHeader;
}
// пропустить 32 байта сида AES-KDF
f.Seek(32, SeekOrigin.Current);
}
}
catch (EndOfStreamException)
{
throw PassStoreFileException.UnexpectedEndOfFile;
}
}
}

View File

@@ -0,0 +1,131 @@
using System;
using System.IO;
using System.Security.Cryptography;
namespace KeyKeeper.PasswordStore.Crypto;
public class OuterEncryptionWriter : Stream
{
public override bool CanRead => false;
public override bool CanWrite => true;
public override bool CanSeek => false;
public override bool CanTimeout => false;
public override int ReadTimeout
{
get { throw new InvalidOperationException(); }
set { throw new InvalidOperationException(); }
}
public override int WriteTimeout
{
get { throw new InvalidOperationException(); }
set { throw new InvalidOperationException(); }
}
public override long Position
{
get => position;
set { throw new NotSupportedException(); }
}
public override long Length => throw new NotSupportedException();
private FileStream file;
private byte[] key;
private Aes aes;
private ICryptoTransform encryptor;
private byte[] currentChunk;
private int currentChunkOrdinal = 0;
private int chunkPosition = 0;
private long position = 0;
public OuterEncryptionWriter(FileStream file, byte[] key, byte[] iv)
{
if (!file.CanWrite)
throw new ArgumentException("file must be writeable");
this.file = file;
this.key = key;
currentChunk = new byte[524288];
aes = Aes.Create();
aes.KeySize = 256;
aes.Key = key;
aes.IV = iv;
aes.Mode = CipherMode.CFB;
aes.Padding = PaddingMode.None;
encryptor = aes.CreateEncryptor();
}
public override void SetLength(long value)
=> throw new NotSupportedException();
public override IAsyncResult BeginRead(byte[] buffer, int offset, int count,
AsyncCallback? callback, object? state)
=> throw new NotSupportedException();
public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count,
AsyncCallback? callback, object? state)
=> throw new NotSupportedException();
public override int EndRead(IAsyncResult asyncResult)
=> throw new NotSupportedException();
public override void EndWrite(IAsyncResult asyncResult)
=> throw new NotSupportedException();
public override long Seek(long offset, SeekOrigin origin)
=> throw new NotSupportedException();
public override int Read(Span<byte> buffer)
=> throw new NotSupportedException();
public override int Read(byte[] buffer, int offset, int count)
=> throw new NotImplementedException();
public override void Write(byte[] buffer, int offset, int count)
=> Write(new ReadOnlySpan<byte>(buffer, offset, count));
public override void Write(ReadOnlySpan<byte> buffer)
{
int written = 0;
while (written < buffer.Length)
{
if (chunkPosition == currentChunk.Length)
EncryptAndStoreCurrentFullChunk();
int n = Math.Min(buffer.Length, currentChunk.Length - chunkPosition);
buffer.Slice(written, n).CopyTo(new Span<byte>(currentChunk, chunkPosition, n));
written += n;
chunkPosition += n;
position += n;
}
}
/// <summary>
/// Шифрует оставшиеся данные и добавляет к файлу чанк с пометкой о том
/// что он последний. Вызов после завершения записи полезных данных
/// обязателен, в противном случае потеря данных неизбежна.
/// </summary>
public override void Flush()
{
byte[] encryptedData = encryptor.TransformFinalBlock(currentChunk, 0, chunkPosition);
PassStoreContentChunk chunk = PassStoreContentChunk.FromEncryptedContent(encryptedData, key, currentChunkOrdinal, true);
file.Write(chunk.Chunk);
EraseCurrentChunk();
chunkPosition = 0;
}
private void EncryptAndStoreCurrentFullChunk()
{
byte[] encryptedData = new byte[currentChunk.Length];
encryptor.TransformBlock(currentChunk, 0, currentChunk.Length, encryptedData, 0);
PassStoreContentChunk chunk = PassStoreContentChunk.FromEncryptedContent(encryptedData, key, currentChunkOrdinal, false);
currentChunkOrdinal += 1;
file.Write(chunk.Chunk);
EraseCurrentChunk();
chunkPosition = 0;
}
private void EraseCurrentChunk()
{
if (currentChunk == null) return;
Array.Fill<byte>(currentChunk, 0);
}
}

View File

@@ -0,0 +1,156 @@
using System;
using System.Linq;
using System.Buffers.Binary;
using System.IO;
using System.Security.Cryptography;
using static KeyKeeper.PasswordStore.FileFormatConstants;
namespace KeyKeeper.PasswordStore.Crypto;
/// <summary>
/// Класс, представляющий собой обертку над content chunkом, считанным из файла
/// хранилища. Не расшифровывает содержимое, но проверяет целостность и
/// подлинность при создании объекта.
/// </summary>
public class PassStoreContentChunk
{
public bool IsLast { get; }
public byte[] Chunk { get { return chunk; } }
private byte[] chunk;
private int chunkLen;
/// <summary>
/// Создаёт объект content chunk, считывая массив байт. Бросает исключение
/// в случае, если массив не содержит корректный content chunk или не
/// совпадает HMAC
/// </summary>
/// <param name="chunk">Массив байт, содержащий весь content chunk, включая
/// длину и HMAC</param>
/// <param name="key">Ключ от хранилища. Используется только для
/// проверки HMAC и не хранится в объекте</param>
/// <param name="chunkOrdinal">Порядковый номер content chunk'а, начиная
/// с 0</param>
public PassStoreContentChunk(byte[] chunk, byte[] key, int chunkOrdinal)
{
this.chunk = chunk;
MemoryStream str = new(chunk);
BinaryReader rd = new(str);
try
{
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)
{
throw PassStoreFileException.UnexpectedEndOfFile;
}
IsLast = (chunkLen & (1 << 23)) != 0;
chunkLen &= ~(1 << 23);
if (chunk.Length != chunkLen + 3 + HMAC_SIZE)
{
throw PassStoreFileException.UnexpectedEndOfFile;
}
byte[] storedHmac = new byte[HMAC_SIZE];
str.Read(storedHmac, 0, HMAC_SIZE);
byte[] dataToHash = chunk[(int)str.Position..];
byte[] actualHmac = ComputeHmac(dataToHash, key, chunkOrdinal);
if (!storedHmac.SequenceEqual(actualHmac))
{
throw PassStoreFileException.ContentHMACMismatch;
}
}
private PassStoreContentChunk(byte[] chunk, bool isLast)
{
this.chunk = chunk;
this.chunkLen = chunk.Length;
this.IsLast = isLast;
}
/// <summary>
/// Создаёт объект content chunk, считывая байты из потока. Бросает
/// исключение в случае, если массив не содержит корректный content chunk
/// или не совпадает HMAC
/// </summary>
/// <param name="chunk">Массив байт, содержащий весь content chunk, включая
/// длину и HMAC</param>
/// <param name="key">Ключ от хранилища. Используется только для
/// проверки HMAC и не хранится в объекте</param>
/// <param name="chunkOrdinal">Порядковый номер content chunk'а, начиная
/// с 0</param>
public static PassStoreContentChunk GetFromStream(Stream s, byte[] key, int chunkOrdinal)
{
BinaryReader rd = new(s);
int chunkLen;
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];
Array.Copy(chunkLenBytes, chunk, 3);
if (s.Read(chunk, 3, HMAC_SIZE + chunkLen) < HMAC_SIZE + chunkLen)
{
throw PassStoreFileException.UnexpectedEndOfFile;
}
return new PassStoreContentChunk(chunk, key, chunkOrdinal);
}
public static PassStoreContentChunk FromEncryptedContent(byte[] content, byte[] key, int chunkOrdinal, bool isLast)
{
int chunkLen = content.Length;
byte[] chunk = new byte[chunkLen + HMAC_SIZE + 3];
BinaryPrimitives.WriteUInt16LittleEndian(new Span<byte>(chunk, 0, 2), (ushort)(chunkLen & 0xffff));
chunk[2] = (byte)(chunkLen >> 16);
if (isLast) chunk[2] |= 1 << 7;
ComputeHmac(content, key, chunkOrdinal).CopyTo(chunk, 3);
content.CopyTo(chunk, 3 + HMAC_SIZE);
return new PassStoreContentChunk(chunk, isLast);
}
public ReadOnlySpan<byte> GetContent()
{
return new ReadOnlySpan<byte>(chunk, 3 + HMAC_SIZE, chunkLen);
}
private static byte[] ComputeHmac(byte[] data, byte[] key, int chunkOrdinal)
{
SHA3_512 hasher = SHA3_512.Create();
byte[] innerKey = key.Select(x => (byte)(x ^ 0x36)).ToArray();
byte[] outerKey = key.Select(x => (byte)(x ^ 0x5c)).ToArray();
hasher.TransformBlock(innerKey, 0, innerKey.Length, null, 0);
Array.Fill<byte>(innerKey, 0); // erase key after use
hasher.TransformBlock(data, 0, data.Length, null, 0);
byte[] encodedOrdinal = new byte[sizeof(int)];
BinaryPrimitives.WriteInt32LittleEndian(new Span<byte>(encodedOrdinal), chunkOrdinal);
hasher.TransformFinalBlock(encodedOrdinal, 0, encodedOrdinal.Length);
byte[] innerHash = hasher.Hash!;
hasher.Clear();
hasher = SHA3_512.Create();
hasher.TransformBlock(outerKey, 0, outerKey.Length, null, 0);
Array.Fill<byte>(outerKey, 0);
hasher.TransformFinalBlock(innerHash, 0, innerHash.Length);
byte[] hmac = hasher.Hash!;
hasher.Clear();
return hmac;
}
}

View File

@@ -0,0 +1,34 @@
using System.Security.Cryptography;
using KeyKeeper.PasswordStore.Crypto.KeyDerivation;
namespace KeyKeeper.PasswordStore;
static class FileFormatConstants
{
public const int MIN_MASTER_SALT_LEN = 8;
public const int MAX_MASTER_SALT_LEN = 40;
public const int MIN_AESKDF_ROUNDS = AesKdf.MIN_ROUNDS;
public const int MAX_AESKDF_ROUNDS = AesKdf.MAX_ROUNDS;
public const byte ENCRYPT_ALGO_AES = 14;
public const byte KDF_TYPE_AESKDF = 195;
public const int HMAC_SIZE = HMACSHA3_512.HashSizeInBytes;
public const uint FILE_FIELD_BEGIN = 0x7853dbd5;
public const uint FILE_FIELD_INNER_CRYPTO = 0x613e91e4;
public const uint FILE_FIELD_CONFIG = 0xd36a53c0;
public const uint FILE_FIELD_STORE = 0x981f9bc8;
public const uint FILE_FIELD_END = 0x010ba81a;
public const byte ENTRY_PASS_ID = 0x00;
public const byte ENTRY_GROUP_ID = 0x01;
public const byte LOGIN_FIELD_PASSWORD_ID = 0x00;
public const byte LOGIN_FIELD_USERNAME_ID = 0x01;
public const byte LOGIN_FIELD_EMAIL_ID = 0x02;
public const byte LOGIN_FIELD_ACCOUNT_NUMBER_ID = 0x03;
public const byte LOGIN_FIELD_NOTES_ID = 0x04;
public const byte LOGIN_FIELD_CUSTOM_ID = 0xff; // пока не используется
public const byte GROUP_TYPE_ROOT = 0x00;
public const byte GROUP_TYPE_DEFAULT = 0x01;
public const byte GROUP_TYPE_FAVOURITES = 0x02;
public const byte GROUP_TYPE_SIMPLE = 0x03;
public const byte GROUP_TYPE_CUSTOM = 0xff; // пока не используется
public static readonly byte[] BEGIN_MARKER = [0x5f, 0x4f, 0xcf, 0x67, 0xc0, 0x90, 0xd0, 0xe5];
}

View File

@@ -0,0 +1,80 @@
using System;
using System.Buffers.Binary;
using System.IO;
using System.Text;
namespace KeyKeeper.PasswordStore;
static class FileFormatUtil
{
public static int WriteVarUint16(Stream str, ulong number)
{
int size = 0;
do {
ushort portion = (ushort)(number & 0x7fff);
number >>= 15;
if (number != 0)
portion |= 1 << 15;
size += 2;
str.WriteByte((byte)(portion & 0xff));
str.WriteByte((byte)(portion >> 8));
}
while (number != 0);
return size;
}
public static ulong ReadVarUint16(Stream str)
{
ulong number = 0;
int i = 0;
while (true)
{
int b = str.ReadByte();
if (b == -1)
throw PassStoreFileException.UnexpectedEndOfFile;
number |= (ulong)b << i;
i += 8;
b = str.ReadByte();
if (b == -1)
throw PassStoreFileException.UnexpectedEndOfFile;
number |= (ulong)(b & 0x7f) << i;
if ((b & 0x80) == 0)
break;
i += 7;
}
return number;
}
public static int WriteU8TaggedString(Stream str, string s)
{
byte[] b = Encoding.UTF8.GetBytes(s);
if (b.Length > 255)
throw new ArgumentException("string too long");
str.WriteByte((byte)b.Length);
str.Write(b);
return b.Length + 1;
}
public static int WriteU16TaggedString(Stream str, string s)
{
byte[] b = Encoding.UTF8.GetBytes(s);
if (b.Length > 65535)
throw new ArgumentException("string too long");
str.WriteByte((byte)(b.Length & 0xff));
str.WriteByte((byte)(b.Length >> 8));
str.Write(b);
return b.Length + 2;
}
public static string ReadU16TaggedString(Stream str)
{
byte[] lenBytes = new byte[2];
if (str.Read(lenBytes) < 2)
throw PassStoreFileException.UnexpectedEndOfFile;
int len = BinaryPrimitives.ReadUInt16LittleEndian(lenBytes);
byte[] s = new byte[len];
if (str.Read(s) < len)
throw PassStoreFileException.UnexpectedEndOfFile;
return Encoding.UTF8.GetString(s);
}
}

View File

@@ -0,0 +1,13 @@
using KeyKeeper.PasswordStore.Crypto;
namespace KeyKeeper.PasswordStore;
public interface IPassStore
{
bool Locked { get; }
IPassStoreDirectory GetRootDirectory();
int GetTotalEntryCount();
void Unlock(CompositeKey key);
void Lock();
}

View File

@@ -0,0 +1,7 @@
using System.Collections.Generic;
namespace KeyKeeper.PasswordStore;
public interface IPassStoreDirectory : IEnumerable<PassStoreEntry>
{
}

View File

@@ -0,0 +1,16 @@
using System;
using static KeyKeeper.PasswordStore.FileFormatConstants;
namespace KeyKeeper.PasswordStore;
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

@@ -0,0 +1,78 @@
using System;
using System.IO;
using KeyKeeper.PasswordStore.Crypto;
using static KeyKeeper.PasswordStore.FileFormatConstants;
namespace KeyKeeper.PasswordStore;
public abstract class PassStoreEntry
{
public Guid Id { get; set; }
public DateTime CreationDate { get; protected set; }
public DateTime ModificationDate { get; set; }
public Guid IconType { get; set; }
public string Name { get; set; }
public PassStoreEntryType Type { get; set; }
public void WriteToStream(Stream str)
{
MemoryStream tmp = new();
BinaryWriter wr = new(tmp);
wr.Write(Id.ToByteArray());
ulong timestamp = (ulong) new DateTimeOffset(CreationDate.ToUniversalTime()).ToUnixTimeSeconds();
FileFormatUtil.WriteVarUint16(tmp, timestamp);
timestamp = (ulong) new DateTimeOffset(ModificationDate.ToUniversalTime()).ToUnixTimeSeconds();
FileFormatUtil.WriteVarUint16(tmp, timestamp);
wr.Write(IconType.ToByteArray());
FileFormatUtil.WriteU16TaggedString(tmp, Name);
wr.Write(InnerSerialize());
byte[] serializedEntry = tmp.ToArray();
tmp.Dispose();
wr = new(str);
wr.Write7BitEncodedInt(serializedEntry.Length);
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

@@ -0,0 +1,97 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using static KeyKeeper.PasswordStore.FileFormatConstants;
namespace KeyKeeper.PasswordStore;
public class PassStoreEntryGroup : PassStoreEntry, IPassStoreDirectory
{
public byte GroupType { get; set; }
public Guid? CustomGroupSubtype { get; set; }
public List<PassStoreEntry> ChildEntries { get; set; }
public PassStoreEntryGroup(Guid id, DateTime createdAt, DateTime modifiedAt,
Guid iconType, string name, byte groupType,
List<PassStoreEntry>? children = null,
Guid? customGroupSubtype = null)
{
Id = id;
CreationDate = createdAt;
ModificationDate = modifiedAt;
IconType = iconType;
Name = name;
GroupType = groupType;
if (GroupType == GROUP_TYPE_CUSTOM && customGroupSubtype == null)
throw new ArgumentNullException("custom group type");
CustomGroupSubtype = customGroupSubtype;
ChildEntries = children ?? new();
}
public static PassStoreEntry ReadFromStream(Stream str, Guid id, DateTime createdAt, DateTime modifiedAt, Guid iconType, string name)
{
BinaryReader rd = new(str);
try
{
byte groupType = rd.ReadByte();
byte[] guidBuffer = new byte[16];
Guid? customGroupSubtype = null;
if (groupType == GROUP_TYPE_CUSTOM)
{
if (rd.Read(guidBuffer) < 16)
throw PassStoreFileException.UnexpectedEndOfFile;
customGroupSubtype = new Guid(guidBuffer);
}
int entryCount = rd.Read7BitEncodedInt();
List<PassStoreEntry> children = new();
for (int i = 0; i < entryCount; i++)
children.Add(PassStoreEntry.ReadFromStream(str));
return new PassStoreEntryGroup(
id, createdAt, modifiedAt,
iconType, name, groupType, children,
customGroupSubtype
);
} catch (EndOfStreamException)
{
throw PassStoreFileException.UnexpectedEndOfFile;
}
}
IEnumerator<PassStoreEntry> IEnumerable<PassStoreEntry>.GetEnumerator()
{
return ChildEntries.GetEnumerator();
}
IEnumerator IEnumerable.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()
{
MemoryStream str = new();
str.WriteByte(ENTRY_GROUP_ID);
str.WriteByte(GroupType);
if (GroupType == GROUP_TYPE_CUSTOM)
str.Write(CustomGroupSubtype!.Value.ToByteArray());
BinaryWriter wr = new(str);
wr.Write7BitEncodedInt(ChildEntries.Count);
foreach (PassStoreEntry entry in ChildEntries)
entry.WriteToStream(str);
return str.ToArray();
}
}

View File

@@ -0,0 +1,89 @@
using System;
using System.Collections.Generic;
using System.IO;
using static KeyKeeper.PasswordStore.FileFormatConstants;
namespace KeyKeeper.PasswordStore;
public class PassStoreEntryPassword : PassStoreEntry
{
public LoginField Username { get; set; }
public LoginField Password { get; set; }
public List<LoginField> ExtraFields { get; set; }
public PassStoreEntryPassword(Guid id, DateTime createdAt, DateTime modifiedAt, Guid iconType, string name, LoginField username, LoginField password, List<LoginField>? extras = null)
{
Id = id;
CreationDate = createdAt;
ModificationDate = modifiedAt;
IconType = iconType;
Name = name;
Username = username;
Password = password;
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();
str.WriteByte(ENTRY_PASS_ID);
WriteField(str, Username);
WriteField(str, Password);
BinaryWriter wr = new(str);
wr.Write7BitEncodedInt(ExtraFields.Count);
foreach (LoginField field in ExtraFields)
WriteField(str, field);
return str.ToArray();
}
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

@@ -0,0 +1,7 @@
namespace KeyKeeper.PasswordStore;
public enum PassStoreEntryType
{
Password,
Directory,
}

View File

@@ -0,0 +1,395 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using KeyKeeper.PasswordStore.Crypto;
using KeyKeeper.PasswordStore.Crypto.KeyDerivation;
using static KeyKeeper.PasswordStore.FileFormatConstants;
namespace KeyKeeper.PasswordStore;
/// <summary>
/// Класс, содержащий реализацию доступа к хранилищу паролей
/// </summary>
public class PassStoreFileAccessor : IPassStore
{
private const ushort FORMAT_VERSION_MAJOR = 0;
private const ushort FORMAT_VERSION_MINOR = 0;
private static readonly byte[] FORMAT_MAGIC = [0xf5, 0x3a, 0xa4, 0xb7, 0xeb, 0xd9, 0xc2, 0x12];
private string filename;
private byte[]? key;
private InnerEncryptionInfo? innerCrypto;
private PassStoreEntry? root;
public PassStoreFileAccessor(string filename, bool create, StoreCreationOptions? createOptions)
{
this.filename = filename;
this.key = null;
if (!create)
{
CheckStoreFile();
} else if (createOptions != null)
{
CreateNewAndUnlock(createOptions);
} else throw new ArgumentException("createOptions must not be null when creating a new store");
}
public bool Locked
{
get { return key == null; }
}
public IPassStoreDirectory GetRootDirectory()
{
if (Locked)
throw new InvalidOperationException();
return (IPassStoreDirectory)root!;
}
public int GetTotalEntryCount()
{
throw new NotImplementedException();
}
public void Unlock(CompositeKey key)
{
if (!Locked) return;
using FileStream file = new(filename, FileMode.Open, FileAccess.Read, FileShare.None);
FileHeader hdr = FileHeader.ReadFrom(file);
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()
{
if (Locked) return;
}
/// <summary>
/// Проверяет внешнюю целостность файла хранилища, то есть:
/// 1. совпадает сигнатура (magic number)
/// 2. совпадает версия формата
/// 3. поля криптозаголовка корректны
/// </summary>
void CheckStoreFile()
{
using FileStream file = File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.Read);
BinaryReader f = new(file);
byte[] magic = new byte[8];
if (f.Read(magic, 0, 8) < 8)
{
throw PassStoreFileException.UnexpectedEndOfFile;
}
if (!magic.SequenceEqual(FORMAT_MAGIC))
{
throw PassStoreFileException.IncorrectMagicNumber;
}
ushort version;
try
{
version = f.ReadUInt16();
}
catch (EndOfStreamException)
{
throw PassStoreFileException.UnexpectedEndOfFile;
}
if (version != FORMAT_VERSION_MAJOR)
{
throw PassStoreFileException.UnsupportedVersion;
}
try
{
version = f.ReadUInt16();
}
catch (EndOfStreamException)
{
throw PassStoreFileException.UnexpectedEndOfFile;
}
if (version != FORMAT_VERSION_MINOR)
{
throw PassStoreFileException.UnsupportedVersion;
}
OuterEncryptionUtil.CheckOuterEncryptionHeader(file);
}
private void CreateNewAndUnlock(StoreCreationOptions options)
{
using FileStream file = File.Open(filename, FileMode.Create, FileAccess.Write, FileShare.None);
FileHeader newHeader = FileHeader.Default();
newHeader.WriteTo(file);
options.Key.Salt = newHeader.PreSalt;
int randomPaddingLen = 4096 - (int)(file.Position % 4096);
if (randomPaddingLen > 0)
{
byte[] randomPadding = new byte[randomPaddingLen];
RandomNumberGenerator.Fill(randomPadding);
file.Write(randomPadding);
}
key = newHeader.KdfInfo.GetKdf().Derive(options.Key, 32);
// пока предполагаем что везде используется AES
OuterEncryptionWriter cryptoWriter = new(file, key, ((OuterAesHeader)newHeader.OuterCryptoHeader).InitVector);
BinaryWriter wr = new(cryptoWriter);
wr.Write(FILE_FIELD_BEGIN);
cryptoWriter.Write(BEGIN_MARKER);
byte[] innerKey = new byte[32];
RandomNumberGenerator.Fill(innerKey);
byte[] innerIv = new byte[16];
RandomNumberGenerator.Fill(innerIv);
wr.Write(FILE_FIELD_INNER_CRYPTO);
cryptoWriter.Write(innerKey);
cryptoWriter.Write(innerIv);
wr.Write(FILE_FIELD_CONFIG);
wr.Write(FILE_FIELD_STORE);
root = WriteInitialStoreTree(cryptoWriter);
wr.Write(FILE_FIELD_END);
cryptoWriter.Flush();
cryptoWriter.Dispose();
}
private PassStoreEntry WriteInitialStoreTree(OuterEncryptionWriter w)
{
PassStoreEntry root =
new PassStoreEntryGroup(
Guid.NewGuid(),
DateTime.UtcNow,
DateTime.UtcNow,
Guid.Empty,
"",
GROUP_TYPE_ROOT
);
root.WriteToStream(w);
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,
byte[] PreSalt,
OuterEncryptionHeader OuterCryptoHeader,
KdfHeader KdfInfo
)
{
public static FileHeader Default()
{
int saltLen = (MIN_MASTER_SALT_LEN + MAX_MASTER_SALT_LEN) / 2;
byte[] preSalt = new byte[saltLen];
RandomNumberGenerator.Fill(preSalt);
byte[] iv = new byte[16];
RandomNumberGenerator.Fill(iv);
byte[] aesKdfSeed = new byte[32];
RandomNumberGenerator.Fill(aesKdfSeed);
return new FileHeader
(
FORMAT_VERSION_MAJOR,
FORMAT_VERSION_MINOR,
preSalt,
new OuterAesHeader
(
iv
),
new AesKdfHeader
(
200000,
aesKdfSeed
)
);
}
public int WriteTo(Stream s)
{
int written = 0;
s.Write(FORMAT_MAGIC);
written += FORMAT_MAGIC.Length;
BinaryWriter wr = new(s);
wr.Write(FORMAT_VERSION_MAJOR);
wr.Write(FORMAT_VERSION_MINOR);
written += 4;
wr.Write((byte)PreSalt.Length);
s.Write(PreSalt);
written += 1 + PreSalt.Length;
if (OuterCryptoHeader is OuterAesHeader aes)
{
wr.Write(ENCRYPT_ALGO_AES);
s.Write(aes.InitVector);
written += 1 + aes.InitVector.Length;
}
if (KdfInfo is AesKdfHeader aesKdf)
{
long pos = s.Position;
wr.Write(KDF_TYPE_AESKDF);
wr.Write7BitEncodedInt(aesKdf.Rounds);
wr.Write(aesKdf.Seed);
written += (int)(s.Position - pos);
}
return written;
}
public static FileHeader ReadFrom(Stream s)
{
BinaryReader rd = new(s);
{
byte[] magic = new byte[8];
if (rd.Read(magic, 0, 8) < 8)
throw PassStoreFileException.UnexpectedEndOfFile;
if (!magic.SequenceEqual(FORMAT_MAGIC))
throw PassStoreFileException.IncorrectMagicNumber;
}
try
{
ushort major, minor;
major = rd.ReadUInt16();
minor = rd.ReadUInt16();
if (major != FORMAT_VERSION_MAJOR || minor != FORMAT_VERSION_MINOR)
throw PassStoreFileException.UnsupportedVersion;
byte saltLen = rd.ReadByte();
if (saltLen < MIN_MASTER_SALT_LEN || saltLen > MAX_MASTER_SALT_LEN)
throw PassStoreFileException.InvalidCryptoHeader;
byte[] salt = new byte[saltLen];
if (rd.Read(salt) < saltLen)
throw PassStoreFileException.UnexpectedEndOfFile;
byte typeDiscrim = rd.ReadByte();
OuterEncryptionHeader outerEncrHdr;
if (typeDiscrim == ENCRYPT_ALGO_AES)
{
byte[] iv = new byte[16];
if (rd.Read(iv) < 16)
throw PassStoreFileException.UnexpectedEndOfFile;
outerEncrHdr = new OuterAesHeader(iv);
} else
{
throw PassStoreFileException.InvalidCryptoHeader;
}
typeDiscrim = rd.ReadByte();
KdfHeader kdfHdr;
if (typeDiscrim == KDF_TYPE_AESKDF)
{
int rounds = rd.Read7BitEncodedInt();
byte[] seed = new byte[32];
if (rd.Read(seed) < 32)
throw PassStoreFileException.UnexpectedEndOfFile;
kdfHdr = new AesKdfHeader(rounds, seed);
} else
{
throw PassStoreFileException.InvalidCryptoHeader;
}
return new FileHeader(
major, minor, salt,
outerEncrHdr, kdfHdr
);
}
catch (EndOfStreamException)
{
throw PassStoreFileException.UnexpectedEndOfFile;
}
}
};
record OuterEncryptionHeader {}
record OuterAesHeader(
byte[] InitVector
) : OuterEncryptionHeader;
abstract record KdfHeader
{
public abstract MasterKeyDerivationFunction GetKdf();
}
record AesKdfHeader(
int Rounds,
byte[] Seed
) : KdfHeader
{
public override MasterKeyDerivationFunction GetKdf()
{
return new AesKdf(Rounds, Seed);
}
}
record struct InnerEncryptionInfo(
byte[] Key,
byte[] Iv
)
{}
}

View File

@@ -0,0 +1,18 @@
using System;
namespace KeyKeeper.PasswordStore;
public class PassStoreFileException : Exception
{
public static readonly PassStoreFileException UnexpectedEndOfFile = new("unexpected EOF");
public static readonly PassStoreFileException IncorrectMagicNumber = new("incorrect signature (magic number)");
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)
{
}
}

View File

@@ -0,0 +1,9 @@
using KeyKeeper.PasswordStore.Crypto;
namespace KeyKeeper.PasswordStore;
public record StoreCreationOptions
{
public int LockTimeoutSeconds { get; init; }
public CompositeKey Key { get; init; }
}

View File

@@ -1,13 +1,25 @@
using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using KeyKeeper.PasswordStore;
using KeyKeeper.PasswordStore.Crypto;
namespace KeyKeeper;
public partial class RepositoryWindow: Window
{
public IPassStore? PassStore { private get; init; }
public RepositoryWindow()
{
InitializeComponent();
}
protected override void OnOpened(EventArgs e)
{
base.OnOpened(e);
if (PassStore!.Locked)
PassStore.Unlock(new CompositeKey("blablabla", null));
}
}

View File

@@ -3,6 +3,7 @@ using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Platform.Storage;
using KeyKeeper.PasswordStore;
using KeyKeeper.ViewModels;
using System;
using System.Collections.Generic;
@@ -39,7 +40,11 @@ namespace KeyKeeper.Views
if (file.TryGetLocalPath() is string path)
{
(DataContext as MainWindowViewModel)!.CreateVault(path);
OpenRepositoryWindow();
OpenRepositoryWindow(new PassStoreFileAccessor(path, true, new StoreCreationOptions()
{
Key = new PasswordStore.Crypto.CompositeKey("blablabla", null),
LockTimeoutSeconds = 800,
}));
}
}
}
@@ -70,16 +75,17 @@ namespace KeyKeeper.Views
if (file.TryGetLocalPath() is string path)
{
(DataContext as MainWindowViewModel)!.OpenVault(path);
OpenRepositoryWindow();
OpenRepositoryWindow(new PassStoreFileAccessor(path, false, null));
}
}
}
private void OpenRepositoryWindow()
private void OpenRepositoryWindow(IPassStore store)
{
var repositoryWindow = new RepositoryWindow()
{
DataContext = this.DataContext,
PassStore = store,
WindowStartupLocation = WindowStartupLocation.CenterScreen
};
repositoryWindow.Closed += (s, e) => this.Show();