< Summary

Information
Class: LOCKnet.Data.SqlCipherEncryptedVaultMigrationExporter
Assembly: LOCKnet.Data
File(s): /home/runner/work/LOCKnet/LOCKnet/src/LOCKnet.Data/SqlCipherEncryptedVaultMigrationExporter.cs
Line coverage
90%
Covered lines: 299
Uncovered lines: 31
Coverable lines: 330
Total lines: 489
Line coverage: 90.6%
Branch coverage
71%
Covered branches: 74
Total branches: 104
Branch coverage: 71.1%
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%
get_TargetMode()100%11100%
ProbeRuntime()66.66%10653.12%
ExportPlaintextVault(...)50%6685.71%
ValidateExportedVault(...)80%101088.88%
PersistMigratedHeader(...)77.77%191888%
TryOpenEncryptedVault(...)83.33%66100%
EnsureRuntimeAvailable()75%44100%
OpenEncryptedConnection(...)100%22100%
RequireCipherVersion(...)50%4487.5%
RequireQuickCheck(...)50%4485.71%
RequireTable(...)75%44100%
CopyMasterKey(...)64.28%1414100%
CopyCredentials(...)58.33%1212100%
CopySettings(...)100%22100%
ClassifyOpenFailure(...)90%1010100%
GetInnermostMessage(...)50%2283.33%
GetConfiguredProviderPath()100%11100%
.ctor(...)100%11100%
get_Storage()100%11100%
OpenConnection()100%11100%

File(s)

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

#LineLine coverage
 1using LOCKnet.Core.DataAbstractions;
 2using LOCKnet.Data.Repositories;
 3using Microsoft.Data.Sqlite;
 4
 5namespace LOCKnet.Data;
 6
 7internal sealed class SqlCipherEncryptedVaultMigrationExporter : IEncryptedVaultMigrationExporter
 8{
 9  private readonly string _password;
 10  private readonly Func<SqlCipherRuntimeProbeResult>? _runtimeProbeOverride;
 11
 12  internal SqlCipherEncryptedVaultMigrationExporter(string password)
 1213    : this(password, null)
 1214  {
 1215  }
 16
 1517  internal SqlCipherEncryptedVaultMigrationExporter(string password, Func<SqlCipherRuntimeProbeResult>? runtimeProbeOver
 1518  {
 1519    ArgumentException.ThrowIfNullOrWhiteSpace(password);
 1520    _password = password;
 1521    _runtimeProbeOverride = runtimeProbeOverride;
 1522  }
 23
 324  public VaultStorageMigrationTargetMode TargetMode => VaultStorageMigrationTargetMode.EncryptedSqlite;
 25
 26  internal SqlCipherRuntimeProbeResult ProbeRuntime()
 3427  {
 3428    if (_runtimeProbeOverride is not null)
 329      return _runtimeProbeOverride();
 30
 3131    var providerPath = GetConfiguredProviderPath();
 3132    var productionCrediblePath = providerPath is SqlCipherProviderPackagingPath.OfficialZetetic;
 33
 34    try
 3135    {
 36#if USE_OFFICIAL_SQLCIPHER_PATH
 37      if (providerPath is SqlCipherProviderPackagingPath.BundleZeteticWithProviderSqlcipher)
 38        SQLitePCL.raw.SetProvider(new SQLitePCL.SQLite3Provider_sqlcipher());
 39#endif
 3140    }
 041    catch (Exception ex)
 042    {
 043      return new SqlCipherRuntimeProbeResult(
 044        false,
 045        null,
 046        null,
 047        providerPath,
 048        productionCrediblePath,
 049        SqlCipherExporterFailureKind.NativeProviderLoadFailure,
 050        $"SQLCipher provider activation failed: {GetInnermostMessage(ex)}");
 51    }
 52
 3153    SQLitePCL.Batteries_V2.Init();
 54
 55    try
 3156    {
 3157      using var connection = new SqliteConnection("Data Source=:memory:");
 3158      connection.Open();
 3159      using var versionCommand = connection.CreateCommand();
 3160      versionCommand.CommandText = "PRAGMA cipher_version;";
 3161      var cipherVersion = Convert.ToString(versionCommand.ExecuteScalar()) ?? string.Empty;
 3162      if (string.IsNullOrWhiteSpace(cipherVersion))
 063        return new SqlCipherRuntimeProbeResult(false, null, connection.ServerVersion, providerPath, productionCrediblePa
 64
 3165      return new SqlCipherRuntimeProbeResult(true, cipherVersion, connection.ServerVersion, providerPath, productionCred
 66    }
 067    catch (Exception ex)
 068    {
 069      var message = GetInnermostMessage(ex);
 070      return new SqlCipherRuntimeProbeResult(false, null, null, providerPath, productionCrediblePath, SqlCipherExporterF
 71    }
 3472  }
 73
 74  public void ExportPlaintextVault(string sourceConnectionString, string destinationPath)
 1075  {
 1076    ArgumentException.ThrowIfNullOrWhiteSpace(sourceConnectionString);
 1077    ArgumentException.ThrowIfNullOrWhiteSpace(destinationPath);
 78
 1079    EnsureRuntimeAvailable();
 80
 981    var sourcePath = StorageRewriteArtifacts.TryResolveDatabasePath(sourceConnectionString)
 982      ?? throw new SqlCipherEncryptedVaultMigrationException(SqlCipherExporterFailureKind.MigrationExportFailure, "SQLCi
 83
 984    if (File.Exists(destinationPath) && !StorageRewriteArtifacts.TryDeleteFile(destinationPath))
 085      throw new SqlCipherEncryptedVaultMigrationException(SqlCipherExporterFailureKind.MigrationExportFailure, "Vorhande
 86
 87    try
 988    {
 989      var destinationFactory = new SqlCipherConnectionFactory(destinationPath, _password);
 990      new Database(destinationFactory).Initialize();
 91
 892      using var sourceConnection = new SqliteConnection(sourceConnectionString);
 893      sourceConnection.Open();
 894      RepositoryBase.ConfigureConnection(sourceConnection);
 95
 896      using var destinationConnection = destinationFactory.OpenConnection();
 897      using var transaction = destinationConnection.BeginTransaction();
 98
 899      CopyMasterKey(sourceConnection, destinationConnection, transaction);
 8100      CopyCredentials(sourceConnection, destinationConnection, transaction);
 8101      CopySettings(sourceConnection, destinationConnection, transaction);
 102
 8103      transaction.Commit();
 8104    }
 0105    catch (SqlCipherEncryptedVaultMigrationException)
 0106    {
 0107      throw;
 108    }
 1109    catch (Exception ex) when (ex is SqliteException or IOException or InvalidOperationException)
 1110    {
 1111      throw new SqlCipherEncryptedVaultMigrationException(SqlCipherExporterFailureKind.MigrationExportFailure, GetInnerm
 112    }
 8113  }
 114
 115  public void ValidateExportedVault(string destinationPath)
 7116  {
 7117    ArgumentException.ThrowIfNullOrWhiteSpace(destinationPath);
 7118    EnsureRuntimeAvailable();
 119
 7120    if (!File.Exists(destinationPath))
 1121      throw new SqlCipherEncryptedVaultMigrationException(SqlCipherExporterFailureKind.InvalidTarget, "SQLCipher-Zielart
 122
 6123    var openResult = TryOpenEncryptedVault(destinationPath, _password);
 6124    if (!openResult.Success)
 1125      throw new SqlCipherEncryptedVaultMigrationException(openResult.FailureKind, openResult.Message ?? "SQLCipher-Ziela
 126
 127    try
 5128    {
 5129      using var connection = OpenEncryptedConnection(destinationPath, _password, SqliteOpenMode.ReadOnly);
 5130      RequireCipherVersion(connection);
 5131      RequireQuickCheck(connection);
 5132      RequireTable(connection, "MasterKey");
 5133      RequireTable(connection, "Credentials");
 5134      RequireTable(connection, "Settings");
 135
 4136      using var masterKeyCount = connection.CreateCommand();
 4137      masterKeyCount.CommandText = "SELECT COUNT(*) FROM MasterKey;";
 4138      if (Convert.ToInt64(masterKeyCount.ExecuteScalar() ?? 0L) != 1)
 1139        throw new SqlCipherEncryptedVaultMigrationException(SqlCipherExporterFailureKind.ValidationFailure, "SQLCipher-Z
 3140    }
 2141    catch (SqlCipherEncryptedVaultMigrationException)
 2142    {
 2143      throw;
 144    }
 0145    catch (Exception ex) when (ex is SqliteException or IOException or InvalidOperationException)
 0146    {
 0147      throw new SqlCipherEncryptedVaultMigrationException(SqlCipherExporterFailureKind.OperationalFailure, GetInnermostM
 148    }
 3149  }
 150
 151  public void PersistMigratedHeader(string databasePath, VaultHeader header)
 4152  {
 4153    ArgumentException.ThrowIfNullOrWhiteSpace(databasePath);
 4154    ArgumentNullException.ThrowIfNull(header);
 4155    EnsureRuntimeAvailable();
 156
 157    try
 4158    {
 4159      using var connection = OpenEncryptedConnection(databasePath, _password, SqliteOpenMode.ReadWrite);
 4160      using var command = connection.CreateCommand();
 4161      command.CommandText = @"
 4162        UPDATE MasterKey
 4163        SET PasswordHash = $hash,
 4164          FormatVersion = $formatVersion,
 4165          KdfIdentifier = $kdfIdentifier,
 4166          KdfParameters = $kdfParameters,
 4167          Salt = $salt,
 4168          WrappedVaultKey = $wrappedVaultKey,
 4169          UsesLegacyKeyMaterial = $usesLegacyKeyMaterial,
 4170          RequiresStorageCompaction = $requiresStorageCompaction,
 4171          LastStorageCompactionAttemptUtc = $lastStorageCompactionAttemptUtc,
 4172          LastStorageCompactionFailureKind = $lastStorageCompactionFailureKind,
 4173          LastStorageCompactionError = $lastStorageCompactionError,
 4174          StorageMigrationState = $storageMigrationState,
 4175          StorageMigrationTargetMode = $storageMigrationTargetMode,
 4176          LastStorageMigrationAttemptUtc = $lastStorageMigrationAttemptUtc,
 4177          LastStorageMigrationError = $lastStorageMigrationError,
 4178          UpdatedAt = CURRENT_TIMESTAMP
 4179        WHERE Id = 1;";
 4180      command.Parameters.AddWithValue("$hash", header.LegacyPasswordHash);
 4181      command.Parameters.AddWithValue("$formatVersion", header.FormatVersion);
 4182      command.Parameters.AddWithValue("$kdfIdentifier", header.KdfIdentifier);
 4183      command.Parameters.AddWithValue("$kdfParameters", header.KdfParameters.Serialize());
 4184      command.Parameters.AddWithValue("$salt", header.Salt);
 4185      command.Parameters.AddWithValue("$wrappedVaultKey", (object?)header.WrappedVaultKey ?? DBNull.Value);
 4186      command.Parameters.AddWithValue("$usesLegacyKeyMaterial", header.UsesLegacyKeyMaterial ? 1 : 0);
 4187      command.Parameters.AddWithValue("$requiresStorageCompaction", header.RequiresStorageCompaction ? 1 : 0);
 4188      command.Parameters.AddWithValue("$lastStorageCompactionAttemptUtc", (object?)header.LastStorageCompactionAttemptUt
 4189      command.Parameters.AddWithValue("$lastStorageCompactionFailureKind", (int)header.LastStorageCompactionFailureKind)
 4190      command.Parameters.AddWithValue("$lastStorageCompactionError", (object?)header.LastStorageCompactionError ?? DBNul
 4191      command.Parameters.AddWithValue("$storageMigrationState", (int)header.StorageMigrationState);
 4192      command.Parameters.AddWithValue("$storageMigrationTargetMode", (int)header.StorageMigrationTargetMode);
 4193      command.Parameters.AddWithValue("$lastStorageMigrationAttemptUtc", (object?)header.LastStorageMigrationAttemptUtc?
 4194      command.Parameters.AddWithValue("$lastStorageMigrationError", (object?)header.LastStorageMigrationError ?? DBNull.
 4195      command.ExecuteNonQuery();
 4196    }
 0197    catch (SqlCipherEncryptedVaultMigrationException)
 0198    {
 0199      throw;
 200    }
 0201    catch (Exception ex) when (ex is SqliteException or IOException or InvalidOperationException)
 0202    {
 0203      throw new SqlCipherEncryptedVaultMigrationException(SqlCipherExporterFailureKind.OperationalFailure, GetInnermostM
 204    }
 4205  }
 206
 207  internal SqlCipherVaultOpenResult TryOpenEncryptedVault(string destinationPath, string password)
 11208  {
 11209    ArgumentException.ThrowIfNullOrWhiteSpace(destinationPath);
 11210    ArgumentException.ThrowIfNullOrWhiteSpace(password);
 211
 11212    var runtime = ProbeRuntime();
 11213    if (!runtime.IsAvailable)
 1214      return new SqlCipherVaultOpenResult(false, runtime.CipherVersion, runtime.ProviderPath, runtime.IsProductionCredib
 215
 10216    if (!File.Exists(destinationPath))
 1217      return new SqlCipherVaultOpenResult(false, runtime.CipherVersion, runtime.ProviderPath, runtime.IsProductionCredib
 218
 219    try
 9220    {
 9221      using var connection = OpenEncryptedConnection(destinationPath, password, SqliteOpenMode.ReadOnly);
 7222      var cipherVersion = RequireCipherVersion(connection);
 7223      using var command = connection.CreateCommand();
 7224      command.CommandText = "SELECT COUNT(*) FROM sqlite_master;";
 7225      _ = Convert.ToInt64(command.ExecuteScalar() ?? 0L);
 7226      return new SqlCipherVaultOpenResult(true, cipherVersion, runtime.ProviderPath, runtime.IsProductionCrediblePath, S
 227    }
 2228    catch (Exception ex) when (ex is SqliteException or InvalidOperationException)
 2229    {
 2230      var kind = ClassifyOpenFailure(ex);
 2231      return new SqlCipherVaultOpenResult(false, runtime.CipherVersion, runtime.ProviderPath, runtime.IsProductionCredib
 232    }
 11233  }
 234
 235  private void EnsureRuntimeAvailable()
 21236  {
 21237    var runtime = ProbeRuntime();
 21238    if (!runtime.IsAvailable)
 1239      throw new SqlCipherEncryptedVaultMigrationException(runtime.FailureKind, runtime.Message ?? "SQLCipher-Laufzeit ko
 20240  }
 241
 242  private static SqliteConnection OpenEncryptedConnection(string databasePath, string password, SqliteOpenMode mode)
 35243  {
 35244    var builder = new SqliteConnectionStringBuilder
 35245    {
 35246      DataSource = databasePath,
 35247      Mode = mode,
 35248      Password = password,
 35249    };
 250
 35251    var connection = new SqliteConnection(builder.ToString());
 35252    connection.Open();
 32253    if (mode != SqliteOpenMode.ReadOnly)
 20254      RepositoryBase.ConfigureConnection(connection);
 32255    return connection;
 32256  }
 257
 258  private static string RequireCipherVersion(SqliteConnection connection)
 12259  {
 12260    using var command = connection.CreateCommand();
 12261    command.CommandText = "PRAGMA cipher_version;";
 12262    var cipherVersion = Convert.ToString(command.ExecuteScalar()) ?? string.Empty;
 12263    if (string.IsNullOrWhiteSpace(cipherVersion))
 0264      throw new SqlCipherEncryptedVaultMigrationException(SqlCipherExporterFailureKind.NativeProviderLoadFailure, "PRAGM
 265
 12266    return cipherVersion;
 12267  }
 268
 269  private static void RequireQuickCheck(SqliteConnection connection)
 5270  {
 5271    using var command = connection.CreateCommand();
 5272    command.CommandText = "PRAGMA quick_check(1);";
 5273    var result = Convert.ToString(command.ExecuteScalar()) ?? string.Empty;
 5274    if (!string.Equals(result, "ok", StringComparison.OrdinalIgnoreCase))
 0275      throw new SqlCipherEncryptedVaultMigrationException(SqlCipherExporterFailureKind.ValidationFailure, "SQLCipher qui
 10276  }
 277
 278  private static void RequireTable(SqliteConnection connection, string tableName)
 15279  {
 15280    using var command = connection.CreateCommand();
 15281    command.CommandText = "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name=$name;";
 15282    command.Parameters.AddWithValue("$name", tableName);
 15283    if (Convert.ToInt64(command.ExecuteScalar() ?? 0L) <= 0)
 1284      throw new SqlCipherEncryptedVaultMigrationException(SqlCipherExporterFailureKind.ValidationFailure, $"SQLCipher-Zi
 28285  }
 286
 287  private static void CopyMasterKey(SqliteConnection sourceConnection, SqliteConnection destinationConnection, SqliteTra
 8288  {
 8289    using var sourceCommand = sourceConnection.CreateCommand();
 8290    sourceCommand.CommandText = @"
 8291      SELECT Id, PasswordHash, FormatVersion, KdfIdentifier, KdfParameters, Salt, WrappedVaultKey, UsesLegacyKeyMaterial
 8292             RequiresStorageCompaction, LastStorageCompactionAttemptUtc, LastStorageCompactionFailureKind, LastStorageCo
 8293             StorageMigrationState, StorageMigrationTargetMode, LastStorageMigrationAttemptUtc, LastStorageMigrationErro
 8294      FROM MasterKey;";
 8295    using var reader = sourceCommand.ExecuteReader();
 16296    while (reader.Read())
 8297    {
 8298      using var insert = destinationConnection.CreateCommand();
 8299      insert.Transaction = transaction;
 8300      insert.CommandText = @"
 8301        INSERT INTO MasterKey (Id, PasswordHash, FormatVersion, KdfIdentifier, KdfParameters, Salt, WrappedVaultKey, Use
 8302          RequiresStorageCompaction, LastStorageCompactionAttemptUtc, LastStorageCompactionFailureKind, LastStorageCompa
 8303          StorageMigrationState, StorageMigrationTargetMode, LastStorageMigrationAttemptUtc, LastStorageMigrationError, 
 8304        VALUES ($id, $passwordHash, $formatVersion, $kdfIdentifier, $kdfParameters, $salt, $wrappedVaultKey, $usesLegacy
 8305          $requiresStorageCompaction, $lastStorageCompactionAttemptUtc, $lastStorageCompactionFailureKind, $lastStorageC
 8306          $storageMigrationState, $storageMigrationTargetMode, $lastStorageMigrationAttemptUtc, $lastStorageMigrationErr
 8307      insert.Parameters.AddWithValue("$id", reader.GetInt64(0));
 8308      insert.Parameters.AddWithValue("$passwordHash", reader.IsDBNull(1) ? [] : (byte[])reader[1]);
 8309      insert.Parameters.AddWithValue("$formatVersion", reader.GetInt32(2));
 8310      insert.Parameters.AddWithValue("$kdfIdentifier", reader.GetString(3));
 8311      insert.Parameters.AddWithValue("$kdfParameters", reader.GetString(4));
 8312      insert.Parameters.AddWithValue("$salt", (byte[])reader[5]);
 8313      insert.Parameters.AddWithValue("$wrappedVaultKey", reader.IsDBNull(6) ? DBNull.Value : reader[6]);
 8314      insert.Parameters.AddWithValue("$usesLegacyKeyMaterial", reader.GetInt32(7));
 8315      insert.Parameters.AddWithValue("$requiresStorageCompaction", reader.GetInt32(8));
 8316      insert.Parameters.AddWithValue("$lastStorageCompactionAttemptUtc", reader.IsDBNull(9) ? DBNull.Value : reader.GetS
 8317      insert.Parameters.AddWithValue("$lastStorageCompactionFailureKind", reader.GetInt32(10));
 8318      insert.Parameters.AddWithValue("$lastStorageCompactionError", reader.IsDBNull(11) ? DBNull.Value : reader.GetStrin
 8319      insert.Parameters.AddWithValue("$storageMigrationState", reader.GetInt32(12));
 8320      insert.Parameters.AddWithValue("$storageMigrationTargetMode", reader.GetInt32(13));
 8321      insert.Parameters.AddWithValue("$lastStorageMigrationAttemptUtc", reader.IsDBNull(14) ? DBNull.Value : reader.GetS
 8322      insert.Parameters.AddWithValue("$lastStorageMigrationError", reader.IsDBNull(15) ? DBNull.Value : reader.GetString
 8323      insert.Parameters.AddWithValue("$createdAt", reader.GetDateTime(16));
 8324      insert.Parameters.AddWithValue("$updatedAt", reader.GetDateTime(17));
 8325      insert.ExecuteNonQuery();
 8326    }
 16327  }
 328
 329  private static void CopyCredentials(SqliteConnection sourceConnection, SqliteConnection destinationConnection, SqliteT
 8330  {
 8331    using var sourceCommand = sourceConnection.CreateCommand();
 8332    sourceCommand.CommandText = @"
 8333      SELECT Id, Title, Username, EncryptedPassword, EncryptedMetadata, CredentialUuid, SecretFormatVersion, MetadataFor
 8334             URL, Notes, CreatedAt, UpdatedAt, IconKey, CredentialType
 8335      FROM Credentials;";
 8336    using var reader = sourceCommand.ExecuteReader();
 16337    while (reader.Read())
 8338    {
 8339      using var insert = destinationConnection.CreateCommand();
 8340      insert.Transaction = transaction;
 8341      insert.CommandText = @"
 8342        INSERT INTO Credentials (Id, Title, Username, EncryptedPassword, EncryptedMetadata, CredentialUuid, SecretFormat
 8343          URL, Notes, CreatedAt, UpdatedAt, IconKey, CredentialType)
 8344        VALUES ($id, $title, $username, $encryptedPassword, $encryptedMetadata, $credentialUuid, $secretFormatVersion, $
 8345          $url, $notes, $createdAt, $updatedAt, $iconKey, $credentialType);";
 8346      insert.Parameters.AddWithValue("$id", reader.GetInt64(0));
 8347      insert.Parameters.AddWithValue("$title", reader.GetString(1));
 8348      insert.Parameters.AddWithValue("$username", reader.IsDBNull(2) ? DBNull.Value : reader.GetString(2));
 8349      insert.Parameters.AddWithValue("$encryptedPassword", (byte[])reader[3]);
 8350      insert.Parameters.AddWithValue("$encryptedMetadata", reader.IsDBNull(4) ? DBNull.Value : reader[4]);
 8351      insert.Parameters.AddWithValue("$credentialUuid", reader.GetString(5));
 8352      insert.Parameters.AddWithValue("$secretFormatVersion", reader.GetInt32(6));
 8353      insert.Parameters.AddWithValue("$metadataFormatVersion", reader.GetInt32(7));
 8354      insert.Parameters.AddWithValue("$url", reader.IsDBNull(8) ? DBNull.Value : reader.GetString(8));
 8355      insert.Parameters.AddWithValue("$notes", reader.IsDBNull(9) ? DBNull.Value : reader.GetString(9));
 8356      insert.Parameters.AddWithValue("$createdAt", reader.GetDateTime(10));
 8357      insert.Parameters.AddWithValue("$updatedAt", reader.GetDateTime(11));
 8358      insert.Parameters.AddWithValue("$iconKey", reader.IsDBNull(12) ? DBNull.Value : reader.GetString(12));
 8359      insert.Parameters.AddWithValue("$credentialType", reader.GetInt32(13));
 8360      insert.ExecuteNonQuery();
 8361    }
 16362  }
 363
 364  private static void CopySettings(SqliteConnection sourceConnection, SqliteConnection destinationConnection, SqliteTran
 8365  {
 8366    using var sourceCommand = sourceConnection.CreateCommand();
 8367    sourceCommand.CommandText = "SELECT Id, Key, Value, CreatedAt, UpdatedAt FROM Settings;";
 8368    using var reader = sourceCommand.ExecuteReader();
 16369    while (reader.Read())
 8370    {
 8371      using var insert = destinationConnection.CreateCommand();
 8372      insert.Transaction = transaction;
 8373      insert.CommandText = @"
 8374        INSERT INTO Settings (Id, Key, Value, CreatedAt, UpdatedAt)
 8375        VALUES ($id, $key, $value, $createdAt, $updatedAt);";
 8376      insert.Parameters.AddWithValue("$id", reader.GetInt64(0));
 8377      insert.Parameters.AddWithValue("$key", reader.GetString(1));
 8378      insert.Parameters.AddWithValue("$value", reader.GetString(2));
 8379      insert.Parameters.AddWithValue("$createdAt", reader.GetDateTime(3));
 8380      insert.Parameters.AddWithValue("$updatedAt", reader.GetDateTime(4));
 8381      insert.ExecuteNonQuery();
 8382    }
 16383  }
 384
 385  private static SqlCipherExporterFailureKind ClassifyOpenFailure(Exception exception)
 7386  {
 7387    if (exception is SqliteException sqliteException)
 5388    {
 5389      if (sqliteException.SqliteErrorCode == 26)
 3390        return SqlCipherExporterFailureKind.WrongKey;
 391
 2392      if (sqliteException.Message.Contains("file is not a database", StringComparison.OrdinalIgnoreCase) ||
 2393        sqliteException.Message.Contains("not a database", StringComparison.OrdinalIgnoreCase))
 1394      {
 1395        return SqlCipherExporterFailureKind.WrongKey;
 396      }
 397
 1398      return SqlCipherExporterFailureKind.InvalidTarget;
 399    }
 400
 2401    if (exception.InnerException is not null)
 1402      return ClassifyOpenFailure(exception.InnerException);
 403
 1404    return SqlCipherExporterFailureKind.OperationalFailure;
 7405  }
 406
 407  private static string GetInnermostMessage(Exception exception)
 3408  {
 3409    var current = exception;
 3410    while (current.InnerException is not null)
 0411      current = current.InnerException;
 412
 3413    return current.Message;
 3414  }
 415
 416  private static SqlCipherProviderPackagingPath GetConfiguredProviderPath()
 31417  {
 418#if USE_OFFICIAL_SQLCIPHER_PATH
 419    return SqlCipherProviderPackagingPath.BundleZeteticWithProviderSqlcipher;
 420#else
 31421    return SqlCipherProviderPackagingPath.LegacyBundleESqlCipher;
 422#endif
 31423  }
 424
 425  private sealed class SqlCipherConnectionFactory : ISqliteConnectionFactory
 426  {
 427    private readonly string _databasePath;
 428    private readonly string _password;
 429
 9430    public SqlCipherConnectionFactory(string databasePath, string password)
 9431    {
 9432      _databasePath = Path.GetFullPath(databasePath);
 9433      _password = password;
 9434      Storage = new VaultStorageDescriptor(VaultStorageMode.EncryptedSqlite, $"Data Source={_databasePath}", _databasePa
 9435    }
 436
 9437    public VaultStorageDescriptor Storage { get; }
 438
 439    public SqliteConnection OpenConnection()
 17440      => OpenEncryptedConnection(_databasePath, _password, SqliteOpenMode.ReadWriteCreate);
 441  }
 442}
 443
 444internal enum SqlCipherExporterFailureKind
 445{
 446  None = 0,
 447  NativeProviderLoadFailure = 1,
 448  CipherSupportUnavailable = 2,
 449  WrongKey = 3,
 450  InvalidTarget = 4,
 451  MigrationExportFailure = 5,
 452  ValidationFailure = 6,
 453  OperationalFailure = 7,
 454}
 455
 456internal enum SqlCipherProviderPackagingPath
 457{
 458  LegacyBundleESqlCipher = 0,
 459  BundleZeteticWithProviderSqlcipher = 1,
 460  OfficialZetetic = 2,
 461}
 462
 463internal sealed class SqlCipherEncryptedVaultMigrationException : InvalidOperationException
 464{
 465  internal SqlCipherEncryptedVaultMigrationException(SqlCipherExporterFailureKind failureKind, string message, Exception
 466    : base(message, innerException)
 467  {
 468    FailureKind = failureKind;
 469  }
 470
 471  internal SqlCipherExporterFailureKind FailureKind { get; }
 472}
 473
 474internal sealed record SqlCipherRuntimeProbeResult(
 475  bool IsAvailable,
 476  string? CipherVersion,
 477  string? SqliteVersion,
 478  SqlCipherProviderPackagingPath ProviderPath,
 479  bool IsProductionCrediblePath,
 480  SqlCipherExporterFailureKind FailureKind,
 481  string? Message);
 482
 483internal sealed record SqlCipherVaultOpenResult(
 484  bool Success,
 485  string? CipherVersion,
 486  SqlCipherProviderPackagingPath ProviderPath,
 487  bool IsProductionCrediblePath,
 488  SqlCipherExporterFailureKind FailureKind,
 489  string? Message);