< Summary

Information
Class: LOCKnet.Data.Database
Assembly: LOCKnet.Data
File(s): /home/runner/work/LOCKnet/LOCKnet/src/LOCKnet.Data/Database.cs
Line coverage
100%
Covered lines: 153
Uncovered lines: 0
Coverable lines: 153
Total lines: 211
Line coverage: 100%
Branch coverage
100%
Covered branches: 2
Total branches: 2
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
.ctor(...)100%11100%
.ctor(...)100%11100%
Initialize()100%22100%
TryAddColumn(...)100%11100%

File(s)

/home/runner/work/LOCKnet/LOCKnet/src/LOCKnet.Data/Database.cs

#LineLine coverage
 1using Microsoft.Data.Sqlite;
 2[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("LOCKnet.Data.Tests")]
 3
 4namespace LOCKnet.Data;
 5
 6/// <summary>
 7/// Verwaltet die SQLite-Datenbankverbindung und initialisiert das Schema.
 8/// Erstellt beim ersten Aufruf von <see cref="Initialize"/> alle benötigten Tabellen
 9/// (Credentials, MasterKey, Settings) mit <c>CREATE TABLE IF NOT EXISTS</c>.
 10/// </summary>
 11public class Database
 12{
 13  private readonly ISqliteConnectionFactory _connectionFactory;
 14  private readonly VaultStorageDescriptor _storage;
 15
 16  /// <summary>
 17  /// Initialisiert eine neue Instanz von <see cref="Database"/> mit einem Datei-Pfad.
 18  /// </summary>
 19  /// <param name="databasePath">
 20  /// Pfad zur SQLite-Datenbankdatei (Standard: <c>credentials.db</c> im Arbeitsverzeichnis).
 21  /// </param>
 22  public Database(string databasePath = "credentials.db")
 2423    : this(new PlainSqliteConnectionFactory(databasePath))
 2424  {
 2425  }
 26
 27  /// <summary>
 28  /// Initializes a <see cref="Database"/> with a fully-formed connection string.
 29  /// Use this overload in tests when an in-memory connection string is needed.
 30  /// </summary>
 31  internal Database(string connectionString, bool useConnectionStringDirectly)
 6232    : this(PlainSqliteConnectionFactory.FromConnectionString(connectionString))
 6233  {
 6234    _ = useConnectionStringDirectly;
 6235  }
 36
 15937  internal Database(ISqliteConnectionFactory connectionFactory)
 15938  {
 15939    ArgumentNullException.ThrowIfNull(connectionFactory);
 40
 15941    _connectionFactory = connectionFactory;
 15942    _storage = connectionFactory.Storage;
 15943  }
 44
 45  /// <summary>
 46  /// Erstellt alle Tabellen (Credentials, MasterKey, Settings) via <c>CREATE TABLE IF NOT EXISTS</c>.
 47  /// Kann mehrfach aufgerufen werden — idempotent.
 48  /// </summary>
 49  public void Initialize()
 16050  {
 51    const string kdfParametersDefault = "TEXT NOT NULL DEFAULT '{\"HashAlgorithm\":\"SHA256\",\"Iterations\":600000,\"Ke
 16052    var recovery = StorageRewriteArtifacts.Recover(_storage.DatabasePath);
 53
 16054    using var connection = _connectionFactory.OpenConnection();
 55
 56    // Credentials table
 15957    using (var cmd = connection.CreateCommand())
 15958    {
 15959      cmd.CommandText = @"
 15960                CREATE TABLE IF NOT EXISTS Credentials (
 15961                    Id INTEGER PRIMARY KEY AUTOINCREMENT,
 15962                    Title TEXT NOT NULL,
 15963                    Username TEXT,
 15964                    EncryptedPassword BLOB NOT NULL,
 15965                    EncryptedMetadata BLOB,
 15966                    CredentialUuid TEXT NOT NULL DEFAULT '',
 15967                    SecretFormatVersion INTEGER NOT NULL DEFAULT 0,
 15968                    MetadataFormatVersion INTEGER NOT NULL DEFAULT 0,
 15969                    URL TEXT,
 15970                    Notes TEXT,
 15971                    CreatedAt TEXT DEFAULT CURRENT_TIMESTAMP,
 15972                    UpdatedAt TEXT DEFAULT CURRENT_TIMESTAMP
 15973                );";
 15974      cmd.ExecuteNonQuery();
 15975    }
 76
 77    try
 15978    {
 15979      using var migrationCommand = connection.CreateCommand();
 15980      migrationCommand.CommandText = "ALTER TABLE Credentials ADD COLUMN IconKey TEXT;";
 15981      migrationCommand.ExecuteNonQuery();
 15382    }
 683    catch (SqliteException)
 684    {
 685    }
 86
 87    try
 15988    {
 15989      using var mc = connection.CreateCommand();
 15990      mc.CommandText = "ALTER TABLE Credentials ADD COLUMN CredentialType INTEGER NOT NULL DEFAULT 0;";
 15991      mc.ExecuteNonQuery();
 15392    }
 693    catch (SqliteException)
 694    {
 695    }
 96
 97    // MasterKey table
 15998    using (var cmd = connection.CreateCommand())
 15999    {
 159100      cmd.CommandText = @"
 159101                CREATE TABLE IF NOT EXISTS MasterKey (
 159102                    Id INTEGER PRIMARY KEY CHECK(Id = 1),
 159103                    PasswordHash BLOB NOT NULL,
 159104                    FormatVersion INTEGER NOT NULL DEFAULT 1,
 159105                    KdfIdentifier TEXT NOT NULL DEFAULT 'PBKDF2-SHA256',
 159106                    KdfParameters TEXT NOT NULL DEFAULT '{""HashAlgorithm"":""SHA256"",""Iterations"":600000,""KeyLength
 159107                    Salt BLOB NOT NULL,
 159108                    WrappedVaultKey BLOB,
 159109                    RequiresStorageCompaction INTEGER NOT NULL DEFAULT 0,
 159110                    LastStorageCompactionAttemptUtc TEXT,
 159111                    LastStorageCompactionFailureKind INTEGER NOT NULL DEFAULT 0,
 159112                    LastStorageCompactionError TEXT,
 159113                    CreatedAt TEXT DEFAULT CURRENT_TIMESTAMP,
 159114                    UpdatedAt TEXT DEFAULT CURRENT_TIMESTAMP
 159115                );";
 159116      cmd.ExecuteNonQuery();
 159117    }
 118
 159119    TryAddColumn(connection, "MasterKey", "FormatVersion", "INTEGER NOT NULL DEFAULT 1");
 159120    TryAddColumn(connection, "MasterKey", "KdfIdentifier", "TEXT NOT NULL DEFAULT 'PBKDF2-SHA256'");
 159121    TryAddColumn(connection, "MasterKey", "KdfParameters", kdfParametersDefault);
 159122    TryAddColumn(connection, "MasterKey", "WrappedVaultKey", "BLOB");
 159123    TryAddColumn(connection, "MasterKey", "UsesLegacyKeyMaterial", "INTEGER NOT NULL DEFAULT 0");
 159124    TryAddColumn(connection, "MasterKey", "RequiresStorageCompaction", "INTEGER NOT NULL DEFAULT 0");
 159125    TryAddColumn(connection, "MasterKey", "LastStorageCompactionAttemptUtc", "TEXT");
 159126    TryAddColumn(connection, "MasterKey", "LastStorageCompactionFailureKind", "INTEGER NOT NULL DEFAULT 0");
 159127    TryAddColumn(connection, "MasterKey", "LastStorageCompactionError", "TEXT");
 159128    TryAddColumn(connection, "MasterKey", "StorageMigrationState", "INTEGER NOT NULL DEFAULT 0");
 159129    TryAddColumn(connection, "MasterKey", "StorageMigrationTargetMode", "INTEGER NOT NULL DEFAULT 0");
 159130    TryAddColumn(connection, "MasterKey", "LastStorageMigrationAttemptUtc", "TEXT");
 159131    TryAddColumn(connection, "MasterKey", "LastStorageMigrationError", "TEXT");
 132
 159133    TryAddColumn(connection, "Credentials", "CredentialUuid", "TEXT NOT NULL DEFAULT ''");
 159134    TryAddColumn(connection, "Credentials", "SecretFormatVersion", "INTEGER NOT NULL DEFAULT 0");
 159135    TryAddColumn(connection, "Credentials", "EncryptedMetadata", "BLOB");
 159136    TryAddColumn(connection, "Credentials", "MetadataFormatVersion", "INTEGER NOT NULL DEFAULT 0");
 137
 159138    using (var cmd = connection.CreateCommand())
 159139    {
 159140      cmd.CommandText = @"
 159141                CREATE UNIQUE INDEX IF NOT EXISTS IX_Credentials_CredentialUuid
 159142                ON Credentials(CredentialUuid)
 159143                WHERE CredentialUuid <> '';";
 159144      cmd.ExecuteNonQuery();
 159145    }
 146
 159147    using (var cmd = connection.CreateCommand())
 159148    {
 159149      cmd.CommandText = @"
 159150                CREATE TRIGGER IF NOT EXISTS TRG_Credentials_CurrentMetadata_Insert
 159151                BEFORE INSERT ON Credentials
 159152                WHEN NEW.MetadataFormatVersion = 1 AND (
 159153                    NEW.EncryptedMetadata IS NULL OR length(NEW.EncryptedMetadata) = 0 OR
 159154                    length(NEW.CredentialUuid) <> 32 OR NEW.CredentialUuid GLOB '*[^0-9A-Fa-f]*' OR
 159155                    NEW.Title <> '' OR ifnull(NEW.Username, '') <> '' OR ifnull(NEW.URL, '') <> '' OR
 159156                    ifnull(NEW.Notes, '') <> '' OR ifnull(NEW.IconKey, '') <> '' OR ifnull(NEW.CredentialType, 0) <> 0
 159157                )
 159158                BEGIN
 159159                    SELECT RAISE(ABORT, 'Current metadata records must not persist plaintext metadata.');
 159160                END;";
 159161      cmd.ExecuteNonQuery();
 159162    }
 163
 159164    using (var cmd = connection.CreateCommand())
 159165    {
 159166      cmd.CommandText = @"
 159167                CREATE TRIGGER IF NOT EXISTS TRG_Credentials_CurrentMetadata_Update
 159168                BEFORE UPDATE ON Credentials
 159169                WHEN NEW.MetadataFormatVersion = 1 AND (
 159170                    NEW.EncryptedMetadata IS NULL OR length(NEW.EncryptedMetadata) = 0 OR
 159171                    length(NEW.CredentialUuid) <> 32 OR NEW.CredentialUuid GLOB '*[^0-9A-Fa-f]*' OR
 159172                    NEW.Title <> '' OR ifnull(NEW.Username, '') <> '' OR ifnull(NEW.URL, '') <> '' OR
 159173                    ifnull(NEW.Notes, '') <> '' OR ifnull(NEW.IconKey, '') <> '' OR ifnull(NEW.CredentialType, 0) <> 0
 159174                )
 159175                BEGIN
 159176                    SELECT RAISE(ABORT, 'Current metadata records must not persist plaintext metadata.');
 159177                END;";
 159178      cmd.ExecuteNonQuery();
 159179    }
 180
 181    // Settings table
 159182    using (var cmd = connection.CreateCommand())
 159183    {
 159184      cmd.CommandText = @"
 159185                CREATE TABLE IF NOT EXISTS Settings (
 159186                    Id INTEGER PRIMARY KEY AUTOINCREMENT,
 159187                    Key TEXT NOT NULL UNIQUE,
 159188                    Value TEXT NOT NULL,
 159189                    CreatedAt TEXT DEFAULT CURRENT_TIMESTAMP,
 159190                    UpdatedAt TEXT DEFAULT CURRENT_TIMESTAMP
 159191                );";
 159192      cmd.ExecuteNonQuery();
 159193    }
 194
 159195    if (recovery.ShouldClearPendingState)
 1196      StorageRewriteArtifacts.ClearPendingState(connection);
 318197  }
 198
 199  private static void TryAddColumn(SqliteConnection connection, string table, string column, string definition)
 2703200  {
 201    try
 2703202    {
 2703203      using var command = connection.CreateCommand();
 2703204      command.CommandText = $"ALTER TABLE {table} ADD COLUMN {column} {definition};";
 2703205      command.ExecuteNonQuery();
 765206    }
 1938207    catch (SqliteException)
 1938208    {
 1938209    }
 2703210  }
 211}