try to implement PassStoreFileAccessor with file creation functionality

This commit is contained in:
2025-12-03 17:18:10 +03:00
parent a27d9b34fb
commit 4ca776b54e

View File

@@ -0,0 +1,258 @@
using System;
using System.Collections.Generic;
using System.IO;
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 IPassStoreDirectory? 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()
{
return root!;
}
public int GetTotalEntryCount()
{
throw new NotImplementedException();
}
public void Unlock(CompositeKey key)
{
if (!Locked) return;
}
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 != 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);
}
byte[] masterKey = newHeader.KdfInfo.GetKdf().Derive(options.Key, 32);
// пока предполагаем что везде используется AES
OuterEncryptionWriter cryptoWriter = new(file, masterKey, ((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);
WriteInitialStoreTree(cryptoWriter);
cryptoWriter.Flush();
cryptoWriter.Dispose();
}
private void WriteInitialStoreTree(OuterEncryptionWriter w)
{
PassStoreEntry root =
new PassStoreEntryGroup(
Guid.NewGuid(),
DateTime.UtcNow,
DateTime.UtcNow,
Guid.Empty,
"",
GROUP_TYPE_ROOT
);
root.WriteToStream(w);
}
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
(
MAX_AESKDF_ROUNDS,
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;
}
};
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);
}
}
}