From fc8ce0a62f6cef70ce98e91ce5ee25d0eff1e21f Mon Sep 17 00:00:00 2001
From: d2dyno <53011783+d2dyno1@users.noreply.github.com>
Date: Sun, 22 Mar 2026 15:22:28 +0100
Subject: [PATCH] Enable Vault V4
---
src/Core/SecureFolderFS.Core/Constants.cs | 2 +-
.../V4VaultConfigurationDataModel.cs | 46 ++++++++++------
.../DataModels/V4VaultKeystoreDataModel.cs | 9 ++-
.../DataModels/VaultConfigurationDataModel.cs | 9 +--
.../Models/SecurityWrapper.cs | 6 +-
.../Routines/IModifyCredentialsRoutine.cs | 6 +-
.../Routines/Operational/CreationRoutine.cs | 52 +++---------------
.../Operational/ModifyCredentialsRoutine.cs | 55 +++++++------------
.../Routines/Operational/RecoverRoutine.cs | 22 +-------
.../Routines/Operational/UnlockRoutine.cs | 50 +++--------------
.../Validators/ConfigurationValidator.cs | 28 ++--------
.../Validators/VersionValidator.cs | 2 +-
.../VaultAccess/VaultParser.cs | 9 +--
.../VaultManagerService.cs | 12 ++++
.../Services/IVaultManagerService.cs | 13 +++++
.../CredentialsConfirmationViewModel.cs | 16 ++++--
.../CredentialsSelectionViewModel.cs | 5 ++
.../Overlays/CredentialsOverlayViewModel.cs | 21 +++++--
18 files changed, 153 insertions(+), 210 deletions(-)
diff --git a/src/Core/SecureFolderFS.Core/Constants.cs b/src/Core/SecureFolderFS.Core/Constants.cs
index 1dfe5bdee..6b9b7f4ed 100644
--- a/src/Core/SecureFolderFS.Core/Constants.cs
+++ b/src/Core/SecureFolderFS.Core/Constants.cs
@@ -57,7 +57,7 @@ public static class Versions
public const int V2 = 2;
public const int V3 = 3;
public const int V4 = 4;
- public const int LATEST_VERSION = V3;
+ public const int LATEST_VERSION = V4;
}
}
diff --git a/src/Core/SecureFolderFS.Core/DataModels/V4VaultConfigurationDataModel.cs b/src/Core/SecureFolderFS.Core/DataModels/V4VaultConfigurationDataModel.cs
index 65fca374a..38ea79dbd 100644
--- a/src/Core/SecureFolderFS.Core/DataModels/V4VaultConfigurationDataModel.cs
+++ b/src/Core/SecureFolderFS.Core/DataModels/V4VaultConfigurationDataModel.cs
@@ -10,33 +10,62 @@ namespace SecureFolderFS.Core.DataModels
[Serializable]
public sealed record class V4VaultConfigurationDataModel : VersionDataModel
{
+ ///
+ /// Gets the ID for content encryption.
+ ///
[JsonPropertyName(Associations.ASSOC_CONTENT_CIPHER_ID)]
[DefaultValue("")]
public required string ContentCipherId { get; init; }
+ ///
+ /// Gets the ID for file name encryption.
+ ///
[JsonPropertyName(Associations.ASSOC_FILENAME_CIPHER_ID)]
[DefaultValue("")]
public required string FileNameCipherId { get; init; }
+ ///
+ /// Gets the ID for file name encoding.
+ ///
[JsonPropertyName(Associations.ASSOC_FILENAME_ENCODING_ID)]
[DefaultValue("")]
public string FileNameEncodingId { get; set; } = Cryptography.Constants.CipherId.ENCODING_BASE64URL;
+ ///
+ /// Gets the size of the recycle bin.
+ ///
+ ///
+ /// If the size is zero, the recycle bin is disabled.
+ /// If the size is any value smaller than zero, the recycle bin has unlimited size capacity.
+ /// Any values above zero indicate the maximum capacity in bytes that is allowed for the recycling operation to proceed.
+ ///
[JsonPropertyName(Associations.ASSOC_RECYCLE_SIZE)]
[DefaultValue(0L)]
- public long RecycleBinSize { get; set; } = 0L;
+ public long RecycleBinSize { get; set; }
+ ///
+ /// Gets the information about the authentication method used for this vault.
+ ///
[JsonPropertyName(Associations.ASSOC_AUTHENTICATION)]
[DefaultValue("")]
public required string AuthenticationMethod { get; set; } = string.Empty;
+ ///
+ /// Gets the unique identifier of the vault represented by a GUID.
+ ///
[JsonPropertyName(Associations.ASSOC_VAULT_ID)]
[DefaultValue("")]
public required string Uid { get; init; } = string.Empty;
+ ///
+ /// Gets the App Platform used by this vault.
+ ///
[JsonPropertyName(Associations.ASSOC_APP_PLATFORM)]
public AppPlatformVaultOptions? AppPlatform { get; init; }
+ ///
+ /// Gets the HMAC-SHA256 hash of the payload.
+ ///
[JsonPropertyName("hmacsha256mac")]
public byte[]? PayloadMac { get; set; }
@@ -55,21 +84,6 @@ public static V4VaultConfigurationDataModel V4FromVaultOptions(VaultOptions vaul
PayloadMac = new byte[HMACSHA256.HashSizeInBytes]
};
}
-
- public VaultConfigurationDataModel ToVaultConfigurationDataModel()
- {
- return new VaultConfigurationDataModel
- {
- Version = Version,
- ContentCipherId = ContentCipherId,
- FileNameCipherId = FileNameCipherId,
- FileNameEncodingId = FileNameEncodingId,
- AuthenticationMethod = AuthenticationMethod,
- RecycleBinSize = RecycleBinSize,
- Uid = Uid,
- PayloadMac = PayloadMac
- };
- }
}
}
diff --git a/src/Core/SecureFolderFS.Core/DataModels/V4VaultKeystoreDataModel.cs b/src/Core/SecureFolderFS.Core/DataModels/V4VaultKeystoreDataModel.cs
index 7b527fb7b..55167a7c5 100644
--- a/src/Core/SecureFolderFS.Core/DataModels/V4VaultKeystoreDataModel.cs
+++ b/src/Core/SecureFolderFS.Core/DataModels/V4VaultKeystoreDataModel.cs
@@ -26,11 +26,14 @@ public sealed record class V4VaultKeystoreDataModel
///
/// Gets the AES-256-GCM ciphertext of the 256-bit SoftwareEntropy value.
- /// SoftwareEntropy is a CSPRNG secret generated at vault creation that is mixed
- /// into the Argon2id input via HKDF-Extract, raising the quantum security floor
- /// of all authentication methods to 256 bits regardless of auth factor entropy.
+ /// SoftwareEntropy is a CSPRNG secret mixed into Argon2id input via HKDF-Extract,
+ /// raising the quantum security floor of all authentication methods to 256 bits
+ /// regardless of auth factor entropy.
/// It is encrypted under a key derived from the passkey so all active auth
/// factors are required to recover it.
+ ///
+ /// The value is generated at vault creation and can also be rotated during
+ /// credential changes when rebuilding the V4 keystore.
///
[JsonPropertyName("c_softwareEntropy")]
public byte[]? EncryptedSoftwareEntropy { get; init; }
diff --git a/src/Core/SecureFolderFS.Core/DataModels/VaultConfigurationDataModel.cs b/src/Core/SecureFolderFS.Core/DataModels/VaultConfigurationDataModel.cs
index 01e951d7c..d87137739 100644
--- a/src/Core/SecureFolderFS.Core/DataModels/VaultConfigurationDataModel.cs
+++ b/src/Core/SecureFolderFS.Core/DataModels/VaultConfigurationDataModel.cs
@@ -41,14 +41,7 @@ public sealed record class VaultConfigurationDataModel : VersionDataModel
///
[JsonPropertyName(Associations.ASSOC_RECYCLE_SIZE)]
[DefaultValue(0L)]
- public long RecycleBinSize { get; set; } = 0L;
-
- /////
- ///// Gets the specialization of the vault that hints how the user data should be handled.
- /////
- //[JsonPropertyName(Associations.ASSOC_SPECIALIZATION)]
- //[DefaultValue("")]
- //public required string Specialization { get; init; } = string.Empty;
+ public long RecycleBinSize { get; set; }
///
/// Gets the information about the authentication method used for this vault.
diff --git a/src/Core/SecureFolderFS.Core/Models/SecurityWrapper.cs b/src/Core/SecureFolderFS.Core/Models/SecurityWrapper.cs
index 2fb1d4fa4..da6d573e7 100644
--- a/src/Core/SecureFolderFS.Core/Models/SecurityWrapper.cs
+++ b/src/Core/SecureFolderFS.Core/Models/SecurityWrapper.cs
@@ -12,7 +12,7 @@ namespace SecureFolderFS.Core.Models
internal sealed class SecurityWrapper : IWrapper, IEnumerable>, IDisposable
{
private readonly KeyPair _keyPair;
- private readonly VaultConfigurationDataModel _configDataModel;
+ private readonly V4VaultConfigurationDataModel _configDataModel;
private Security? _security;
///
@@ -22,10 +22,10 @@ internal sealed class SecurityWrapper : IWrapper, IEnumerable
diff --git a/src/Core/SecureFolderFS.Core/Routines/IModifyCredentialsRoutine.cs b/src/Core/SecureFolderFS.Core/Routines/IModifyCredentialsRoutine.cs
index 952708b96..d2ac57f49 100644
--- a/src/Core/SecureFolderFS.Core/Routines/IModifyCredentialsRoutine.cs
+++ b/src/Core/SecureFolderFS.Core/Routines/IModifyCredentialsRoutine.cs
@@ -1,6 +1,10 @@
-namespace SecureFolderFS.Core.Routines
+using SecureFolderFS.Shared.ComponentModel;
+using System.Threading;
+
+namespace SecureFolderFS.Core.Routines
{
public interface IModifyCredentialsRoutine : ICredentialsRoutine, IContractRoutine, IOptionsRoutine
{
+ void SetCredentials(IKeyUsage oldPasskey, IKeyUsage newPasskey, CancellationToken cancellationToken = default);
}
}
diff --git a/src/Core/SecureFolderFS.Core/Routines/Operational/CreationRoutine.cs b/src/Core/SecureFolderFS.Core/Routines/Operational/CreationRoutine.cs
index 8a739ea05..f795e771b 100644
--- a/src/Core/SecureFolderFS.Core/Routines/Operational/CreationRoutine.cs
+++ b/src/Core/SecureFolderFS.Core/Routines/Operational/CreationRoutine.cs
@@ -19,10 +19,8 @@ internal sealed class CreationRoutine : ICreationRoutine
{
private readonly IFolder _vaultFolder;
private readonly VaultWriter _vaultWriter;
- private V3VaultKeystoreDataModel? _keystoreDataModel;
- private V4VaultKeystoreDataModel? _v4KeystoreDataModel;
- private VaultConfigurationDataModel? _configDataModel;
- private V4VaultConfigurationDataModel? _v4ConfigDataModel;
+ private V4VaultKeystoreDataModel? _keystoreDataModel;
+ private V4VaultConfigurationDataModel? _configDataModel;
private IKeyUsage? _dekKey;
private IKeyUsage? _macKey;
@@ -46,33 +44,13 @@ public void SetCredentials(IKeyUsage passkey)
var macKey = new byte[KeyTraits.MAC_KEY_LENGTH];
var salt = new byte[KeyTraits.SALT_LENGTH];
- // Fill keys
- RandomNumberGenerator.Fill(dekKey);
- RandomNumberGenerator.Fill(macKey);
- RandomNumberGenerator.Fill(salt);
-
- // Generate keystore
- _keystoreDataModel = passkey.UseKey(key => VaultParser.V3EncryptKeystore(key, dekKey, macKey, salt));
-
- // Create key copies for later use
- _dekKey = SecureKey.TakeOwnership(dekKey);
- _macKey = SecureKey.TakeOwnership(macKey);
- }
-
- public void V4SetCredentials(IKeyUsage passkey)
- {
- // Allocate keys for later use
- var dekKey = new byte[KeyTraits.DEK_KEY_LENGTH];
- var macKey = new byte[KeyTraits.MAC_KEY_LENGTH];
- var salt = new byte[KeyTraits.SALT_LENGTH];
-
// Fill keys and salt
RandomNumberGenerator.Fill(dekKey);
RandomNumberGenerator.Fill(macKey);
RandomNumberGenerator.Fill(salt);
- // Generate V4 keystore — SoftwareEntropy is generated internally by V4EncryptKeystore
- _v4KeystoreDataModel = passkey.UseKey(key => VaultParser.V4EncryptKeystore(key, dekKey, macKey, salt));
+ // Generate V4 keystore
+ _keystoreDataModel = passkey.UseKey(key => VaultParser.V4EncryptKeystore(key, dekKey, macKey, salt));
// Create key copies for later use
_dekKey = SecureKey.TakeOwnership(dekKey);
@@ -82,16 +60,7 @@ public void V4SetCredentials(IKeyUsage passkey)
///
public void SetOptions(VaultOptions vaultOptions)
{
- if (vaultOptions.AppPlatform is null)
- {
- _configDataModel = VaultConfigurationDataModel.FromVaultOptions(vaultOptions);
- _v4ConfigDataModel = null;
- }
- else
- {
- _v4ConfigDataModel = V4VaultConfigurationDataModel.V4FromVaultOptions(vaultOptions);
- _configDataModel = _v4ConfigDataModel.ToVaultConfigurationDataModel();
- }
+ _configDataModel = V4VaultConfigurationDataModel.V4FromVaultOptions(vaultOptions);
}
///
@@ -105,19 +74,12 @@ public async Task FinalizeAsync(CancellationToken cancellationToken
// First, we need to fill in the PayloadMac of the content
_macKey.UseKey(macKey =>
{
- if (_v4ConfigDataModel is not null)
- VaultParser.V4CalculateConfigMac(_v4ConfigDataModel, macKey, _v4ConfigDataModel.PayloadMac);
- else
- VaultParser.CalculateConfigMac(_configDataModel, macKey, _configDataModel.PayloadMac);
+ VaultParser.V4CalculateConfigMac(_configDataModel, macKey, _configDataModel.PayloadMac);
});
// Write the whole configuration
await _vaultWriter.WriteKeystoreAsync(_keystoreDataModel, cancellationToken);
- //await _vaultWriter.WriteV4KeystoreAsync(_v4KeystoreDataModel, cancellationToken);
- if (_v4ConfigDataModel is not null)
- await _vaultWriter.WriteV4ConfigurationAsync(_v4ConfigDataModel, cancellationToken);
- else
- await _vaultWriter.WriteConfigurationAsync(_configDataModel, cancellationToken);
+ await _vaultWriter.WriteV4ConfigurationAsync(_configDataModel, cancellationToken);
// Create the content folder
if (_vaultFolder is IModifiableFolder modifiableFolder)
diff --git a/src/Core/SecureFolderFS.Core/Routines/Operational/ModifyCredentialsRoutine.cs b/src/Core/SecureFolderFS.Core/Routines/Operational/ModifyCredentialsRoutine.cs
index eb03e2336..616f34b28 100644
--- a/src/Core/SecureFolderFS.Core/Routines/Operational/ModifyCredentialsRoutine.cs
+++ b/src/Core/SecureFolderFS.Core/Routines/Operational/ModifyCredentialsRoutine.cs
@@ -9,6 +9,7 @@
using SecureFolderFS.Core.Models;
using SecureFolderFS.Core.VaultAccess;
using SecureFolderFS.Shared.ComponentModel;
+using SecureFolderFS.Shared.Extensions;
using SecureFolderFS.Shared.Models;
namespace SecureFolderFS.Core.Routines.Operational
@@ -20,10 +21,8 @@ internal sealed class ModifyCredentialsRoutine : IModifyCredentialsRoutine
private readonly VaultWriter _vaultWriter;
private KeyPair? _keyPair;
private V4VaultKeystoreDataModel? _existingV4KeystoreDataModel;
- private V3VaultKeystoreDataModel? _keystoreDataModel;
- private V4VaultKeystoreDataModel? _v4KeystoreDataModel;
- private VaultConfigurationDataModel? _configDataModel;
- private V4VaultConfigurationDataModel? _v4ConfigDataModel;
+ private V4VaultKeystoreDataModel? _keystoreDataModel;
+ private V4VaultConfigurationDataModel? _configDataModel;
public ModifyCredentialsRoutine(VaultReader vaultReader, VaultWriter vaultWriter)
{
@@ -34,8 +33,7 @@ public ModifyCredentialsRoutine(VaultReader vaultReader, VaultWriter vaultWriter
///
public async Task InitAsync(CancellationToken cancellationToken = default)
{
- await Task.CompletedTask;
- //_existingV4KeystoreDataModel = await _vaultReader.ReadKeystoreAsync(cancellationToken);
+ _existingV4KeystoreDataModel = await _vaultReader.ReadKeystoreAsync(cancellationToken);
}
///
@@ -50,16 +48,7 @@ public void SetUnlockContract(IDisposable unlockContract)
///
public void SetOptions(VaultOptions vaultOptions)
{
- if (vaultOptions.AppPlatform is null)
- {
- _configDataModel = VaultConfigurationDataModel.FromVaultOptions(vaultOptions);
- _v4ConfigDataModel = null;
- }
- else
- {
- _v4ConfigDataModel = V4VaultConfigurationDataModel.V4FromVaultOptions(vaultOptions);
- _configDataModel = _v4ConfigDataModel.ToVaultConfigurationDataModel();
- }
+ _configDataModel = V4VaultConfigurationDataModel.V4FromVaultOptions(vaultOptions);
}
///
@@ -67,11 +56,10 @@ public unsafe void SetCredentials(IKeyUsage passkey)
{
ArgumentNullException.ThrowIfNull(_keyPair);
- // Generate new salt
+ // Recovery/unlock-contract flow: rotate to a fresh entropy value under the new passkey.
var salt = new byte[Cryptography.Constants.KeyTraits.SALT_LENGTH];
RandomNumberGenerator.Fill(salt);
- // Encrypt a new keystore
passkey.UseKey(key =>
{
fixed (byte* keyPtr = key)
@@ -80,26 +68,26 @@ public unsafe void SetCredentials(IKeyUsage passkey)
_keyPair.UseKeys(state, (dekKey, macKey, s) =>
{
var k = new ReadOnlySpan((byte*)s.keyPtr, s.keyLen);
- _keystoreDataModel = VaultParser.V3EncryptKeystore(k, dekKey, macKey, salt);
+ _keystoreDataModel = VaultParser.V4EncryptKeystore(k, dekKey, macKey, salt);
});
}
});
}
+ ///
[SkipLocalsInit]
- public unsafe void V4SetCredentials(IKeyUsage oldPasskey, IKeyUsage newPasskey, CancellationToken cancellationToken = default)
+ public unsafe void SetCredentials(IKeyUsage oldPasskey, IKeyUsage newPasskey, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(_keyPair);
ArgumentNullException.ThrowIfNull(_existingV4KeystoreDataModel);
- // Generate new salt for the re-encrypted keystore
var salt = new byte[Cryptography.Constants.KeyTraits.SALT_LENGTH];
RandomNumberGenerator.Fill(salt);
- // Decrypt existing SoftwareEntropy using the old passkey, then re-encrypt
- // it under the new passkey alongside the (unchanged) DEK and MAC keys.
- // SoftwareEntropy must be preserved - regenerating it would change the KEK
- // derivation and make the vault permanently unreadable.
+ // Optional step-up flow: preserve existing entropy by decrypting it with the old passkey
+ // and re-encrypting it under the new passkey next to unchanged DEK and MAC keys.
+ // If old passkey material is unavailable (for example recovery-key driven rotation),
+ // the single-passkey overload rotates to fresh entropy and still yields a valid keystore.
Span softwareEntropy = stackalloc byte[32];
try
{
@@ -113,6 +101,9 @@ public unsafe void V4SetCredentials(IKeyUsage oldPasskey, IKeyUsage newPasskey,
});
}
+ if (softwareEntropy.IsAllZeros())
+ throw new CryptographicException("The old passkey material is unavailable.");
+
fixed (byte* softwareEntropyPtr = softwareEntropy)
{
var state = (sePtr: (nint)softwareEntropyPtr, seLen: softwareEntropy.Length);
@@ -126,7 +117,7 @@ public unsafe void V4SetCredentials(IKeyUsage oldPasskey, IKeyUsage newPasskey,
var nk = new ReadOnlySpan((byte*)s2.nkPtr, s2.nkLen);
var se = new Span((byte*)s2.outerState.sePtr, s2.outerState.seLen);
- _v4KeystoreDataModel = VaultParser.V4ReEncryptKeystore(nk, dekKey, macKey, salt, se);
+ _keystoreDataModel = VaultParser.V4ReEncryptKeystore(nk, dekKey, macKey, salt, se);
});
}
});
@@ -142,24 +133,18 @@ public unsafe void V4SetCredentials(IKeyUsage oldPasskey, IKeyUsage newPasskey,
public async Task FinalizeAsync(CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(_keyPair);
+ ArgumentNullException.ThrowIfNull(_keystoreDataModel);
ArgumentNullException.ThrowIfNull(_configDataModel);
// First, we need to fill in the PayloadMac of the content
_keyPair.MacKey.UseKey(macKey =>
{
- if (_v4ConfigDataModel is not null)
- VaultParser.V4CalculateConfigMac(_v4ConfigDataModel, macKey, _v4ConfigDataModel.PayloadMac);
- else
- VaultParser.CalculateConfigMac(_configDataModel, macKey, _configDataModel.PayloadMac);
+ VaultParser.V4CalculateConfigMac(_configDataModel, macKey, _configDataModel.PayloadMac);
});
// Write the whole configuration
await _vaultWriter.WriteKeystoreAsync(_keystoreDataModel, cancellationToken);
- //await _vaultWriter.WriteKeystoreAsync(_v4KeystoreDataModel, cancellationToken);
- if (_v4ConfigDataModel is not null)
- await _vaultWriter.WriteV4ConfigurationAsync(_v4ConfigDataModel, cancellationToken);
- else
- await _vaultWriter.WriteConfigurationAsync(_configDataModel, cancellationToken);
+ await _vaultWriter.WriteV4ConfigurationAsync(_configDataModel, cancellationToken);
// Key copies need to be created because the original ones are disposed of here
using (_keyPair)
diff --git a/src/Core/SecureFolderFS.Core/Routines/Operational/RecoverRoutine.cs b/src/Core/SecureFolderFS.Core/Routines/Operational/RecoverRoutine.cs
index 54a555b4f..267839826 100644
--- a/src/Core/SecureFolderFS.Core/Routines/Operational/RecoverRoutine.cs
+++ b/src/Core/SecureFolderFS.Core/Routines/Operational/RecoverRoutine.cs
@@ -14,8 +14,7 @@ namespace SecureFolderFS.Core.Routines.Operational
public sealed class RecoverRoutine : ICredentialsRoutine, IFinalizationRoutine
{
private readonly VaultReader _vaultReader;
- private VaultConfigurationDataModel? _configDataModel;
- private V4VaultConfigurationDataModel? _v4ConfigDataModel;
+ private V4VaultConfigurationDataModel? _configDataModel;
private KeyPair? _keyPair;
public RecoverRoutine(VaultReader vaultReader)
@@ -26,19 +25,7 @@ public RecoverRoutine(VaultReader vaultReader)
///
public async Task InitAsync(CancellationToken cancellationToken)
{
- _configDataModel = await _vaultReader.ReadConfigurationAsync(cancellationToken);
-
- if (_configDataModel.AuthenticationMethod.Contains(Constants.Vault.Authentication.AUTH_APP_PLATFORM, StringComparison.Ordinal))
- {
- try
- {
- _v4ConfigDataModel = await _vaultReader.ReadV4ConfigurationAsync(cancellationToken);
- }
- catch (Exception)
- {
- _v4ConfigDataModel = null;
- }
- }
+ _configDataModel = await _vaultReader.ReadV4ConfigurationAsync(cancellationToken);
}
///
@@ -57,10 +44,7 @@ public async Task FinalizeAsync(CancellationToken cancellationToken
{
// Check if the payload has not been tampered with
var validator = new ConfigurationValidator(_keyPair.MacKey);
- if (_v4ConfigDataModel is not null)
- await validator.V4ValidateAsync(_v4ConfigDataModel, cancellationToken);
- else
- await validator.ValidateAsync(_configDataModel, cancellationToken);
+ await validator.ValidateAsync(_configDataModel, cancellationToken);
// In this case, we rely on the consumer to take ownership of the keys, and thus manage their lifetimes
// Key copies need to be created because the original ones are disposed of here
diff --git a/src/Core/SecureFolderFS.Core/Routines/Operational/UnlockRoutine.cs b/src/Core/SecureFolderFS.Core/Routines/Operational/UnlockRoutine.cs
index 0c096a8c6..b8a37bd93 100644
--- a/src/Core/SecureFolderFS.Core/Routines/Operational/UnlockRoutine.cs
+++ b/src/Core/SecureFolderFS.Core/Routines/Operational/UnlockRoutine.cs
@@ -14,10 +14,8 @@ namespace SecureFolderFS.Core.Routines.Operational
internal sealed class UnlockRoutine : ICredentialsRoutine
{
private readonly VaultReader _vaultReader;
- private V3VaultKeystoreDataModel? _keystoreDataModel;
- private V4VaultKeystoreDataModel? _v4KeystoreDataModel;
- private VaultConfigurationDataModel? _configDataModel;
- private V4VaultConfigurationDataModel? _v4ConfigDataModel;
+ private V4VaultKeystoreDataModel? _keystoreDataModel;
+ private V4VaultConfigurationDataModel? _configDataModel;
private SecureKey? _dekKey;
private SecureKey? _macKey;
@@ -29,46 +27,19 @@ public UnlockRoutine(VaultReader vaultReader)
///
public async Task InitAsync(CancellationToken cancellationToken)
{
- _configDataModel = await _vaultReader.ReadConfigurationAsync(cancellationToken);
-
- if (_configDataModel.AuthenticationMethod.Contains(Constants.Vault.Authentication.AUTH_APP_PLATFORM, StringComparison.Ordinal))
- {
- try
- {
- _v4ConfigDataModel = await _vaultReader.ReadV4ConfigurationAsync(cancellationToken);
- }
- catch (Exception)
- {
- _v4ConfigDataModel = null;
- }
- }
-
- if (_configDataModel.Version >= Constants.Vault.Versions.V4)
- _v4KeystoreDataModel = await _vaultReader.ReadKeystoreAsync(cancellationToken);
- else
- _keystoreDataModel = await _vaultReader.ReadKeystoreAsync(cancellationToken);
+ _configDataModel = await _vaultReader.ReadV4ConfigurationAsync(cancellationToken);
+ _keystoreDataModel = await _vaultReader.ReadKeystoreAsync(cancellationToken);
}
///
public void SetCredentials(IKeyUsage passkey)
{
ArgumentNullException.ThrowIfNull(_configDataModel);
+ ArgumentNullException.ThrowIfNull(_keystoreDataModel);
- if (_v4KeystoreDataModel is not null)
- {
- var derived = passkey.UseKey(key => VaultParser.V4DeriveKeystore(key, _v4KeystoreDataModel));
- _dekKey = SecureKey.TakeOwnership(derived.dekKey);
- _macKey = SecureKey.TakeOwnership(derived.macKey);
- }
- else
- {
- ArgumentNullException.ThrowIfNull(_keystoreDataModel);
-
- // V3 path: unchanged
- var derived = passkey.UseKey(key => VaultParser.V3DeriveKeystore(key, _keystoreDataModel));
- _dekKey = SecureKey.TakeOwnership(derived.dekKey);
- _macKey = SecureKey.TakeOwnership(derived.macKey);
- }
+ var derived = passkey.UseKey(key => VaultParser.V4DeriveKeystore(key, _keystoreDataModel));
+ _dekKey = SecureKey.TakeOwnership(derived.dekKey);
+ _macKey = SecureKey.TakeOwnership(derived.macKey);
}
///
@@ -84,10 +55,7 @@ public async Task FinalizeAsync(CancellationToken cancellationToken
{
// Check if the payload has not been tampered with
var validator = new ConfigurationValidator(_macKey);
- if (_v4ConfigDataModel is not null)
- await validator.V4ValidateAsync(_v4ConfigDataModel, cancellationToken);
- else
- await validator.ValidateAsync(_configDataModel, cancellationToken);
+ await validator.ValidateAsync(_configDataModel, cancellationToken);
// In this case, we rely on the consumer to take ownership of the keys, and thus manage their lifetimes
// Key copies need to be created because the original ones are disposed of here
diff --git a/src/Core/SecureFolderFS.Core/Validators/ConfigurationValidator.cs b/src/Core/SecureFolderFS.Core/Validators/ConfigurationValidator.cs
index bd2c71a91..804a9908b 100644
--- a/src/Core/SecureFolderFS.Core/Validators/ConfigurationValidator.cs
+++ b/src/Core/SecureFolderFS.Core/Validators/ConfigurationValidator.cs
@@ -9,7 +9,7 @@
namespace SecureFolderFS.Core.Validators
{
- internal sealed class ConfigurationValidator : IAsyncValidator
+ internal sealed class ConfigurationValidator : IAsyncValidator
{
private readonly IKeyUsage _macKey;
@@ -19,25 +19,19 @@ public ConfigurationValidator(IKeyUsage macKey)
}
///
- public async Task ValidateAsync(VaultConfigurationDataModel value, CancellationToken cancellationToken = default)
+ public async Task ValidateAsync(V4VaultConfigurationDataModel value, CancellationToken cancellationToken = default)
{
Validate(value);
await Task.CompletedTask;
}
- public async Task V4ValidateAsync(V4VaultConfigurationDataModel value, CancellationToken cancellationToken = default)
- {
- V4Validate(value);
- await Task.CompletedTask;
- }
-
[SkipLocalsInit]
- private void Validate(VaultConfigurationDataModel value)
+ private void Validate(V4VaultConfigurationDataModel value)
{
var isEqual = _macKey.UseKey(macKey =>
{
Span payloadMac = stackalloc byte[HMACSHA256.HashSizeInBytes];
- VaultParser.CalculateConfigMac(value, macKey, payloadMac);
+ VaultParser.V4CalculateConfigMac(value, macKey, payloadMac);
// Check if stored hash equals to computed hash using constant-time comparison to prevent timing attacks
return CryptographicOperations.FixedTimeEquals(payloadMac, value.PayloadMac);
@@ -47,19 +41,5 @@ private void Validate(VaultConfigurationDataModel value)
if (!isEqual)
throw new CryptographicException("Vault hash doesn't match the computed hash.");
}
-
- [SkipLocalsInit]
- private void V4Validate(V4VaultConfigurationDataModel value)
- {
- var isEqual = _macKey.UseKey(macKey =>
- {
- Span payloadMac = stackalloc byte[HMACSHA256.HashSizeInBytes];
- VaultParser.V4CalculateConfigMac(value, macKey, payloadMac);
- return CryptographicOperations.FixedTimeEquals(payloadMac, value.PayloadMac);
- });
-
- if (!isEqual)
- throw new CryptographicException("Vault hash doesn't match the computed hash.");
- }
}
}
diff --git a/src/Core/SecureFolderFS.Core/Validators/VersionValidator.cs b/src/Core/SecureFolderFS.Core/Validators/VersionValidator.cs
index c63b4edb9..9788cf2ee 100644
--- a/src/Core/SecureFolderFS.Core/Validators/VersionValidator.cs
+++ b/src/Core/SecureFolderFS.Core/Validators/VersionValidator.cs
@@ -36,7 +36,7 @@ public async Task ValidateAsync(Stream value, CancellationToken cancellationToke
_ = versionDataModel.Version switch
{
// (V1 or Vn) except LATEST_VERSION are not supported
- (V1 or V2) and not LATEST_VERSION =>
+ (V1 or V2 or V3) and not LATEST_VERSION =>
throw new NotSupportedException($"Vault version {versionDataModel.Version} is not supported.") { Data = { { "Version", versionDataModel.Version } } },
// More cases...
diff --git a/src/Core/SecureFolderFS.Core/VaultAccess/VaultParser.cs b/src/Core/SecureFolderFS.Core/VaultAccess/VaultParser.cs
index 5dee36a09..cdbe96281 100644
--- a/src/Core/SecureFolderFS.Core/VaultAccess/VaultParser.cs
+++ b/src/Core/SecureFolderFS.Core/VaultAccess/VaultParser.cs
@@ -211,8 +211,8 @@ public static V4VaultKeystoreDataModel V4EncryptKeystore(
///
/// Re-encrypts cryptographic keys into a new while
- /// preserving the existing . Used during credential
- /// changes so the vault remains accessible after the passkey changes.
+ /// preserving the provided .
+ /// This is an optional credential-rotation path when the previous passkey is available.
///
/// The new passkey credential.
/// The DEK key (unchanged from the existing keystore).
@@ -233,8 +233,9 @@ public static V4VaultKeystoreDataModel V4ReEncryptKeystore(
///
/// Decrypts the from an existing
- /// keystore using the current passkey. Used during credential changes to recover the entropy
- /// value before re-encrypting it under the new passkey.
+ /// keystore using the previous passkey.
+ /// This is only required for preserve-entropy rotation; fresh-entropy rotation uses
+ /// .
///
/// The current (old) passkey.
/// The existing V4 keystore.
diff --git a/src/Platforms/SecureFolderFS.UI/ServiceImplementation/VaultManagerService.cs b/src/Platforms/SecureFolderFS.UI/ServiceImplementation/VaultManagerService.cs
index a231b1c9e..e7c197cfa 100644
--- a/src/Platforms/SecureFolderFS.UI/ServiceImplementation/VaultManagerService.cs
+++ b/src/Platforms/SecureFolderFS.UI/ServiceImplementation/VaultManagerService.cs
@@ -67,5 +67,17 @@ public virtual async Task ModifyAuthenticationAsync(IFolder vaultFolder, IDispos
using var result = await credentialsRoutine.FinalizeAsync(cancellationToken);
}
+
+ ///
+ public virtual async Task ModifyAuthenticationAsync(IFolder vaultFolder, IDisposable unlockContract, IKeyUsage oldPasskey, IKeyUsage newPasskey, VaultOptions vaultOptions, CancellationToken cancellationToken = default)
+ {
+ using var credentialsRoutine = (await VaultRoutines.CreateRoutinesAsync(vaultFolder, StreamSerializer.Instance, cancellationToken)).ModifyCredentials();
+ await credentialsRoutine.InitAsync(cancellationToken);
+ credentialsRoutine.SetUnlockContract(unlockContract);
+ credentialsRoutine.SetOptions(vaultOptions);
+ credentialsRoutine.SetCredentials(oldPasskey, newPasskey, cancellationToken);
+
+ using var result = await credentialsRoutine.FinalizeAsync(cancellationToken);
+ }
}
}
diff --git a/src/Sdk/SecureFolderFS.Sdk/Services/IVaultManagerService.cs b/src/Sdk/SecureFolderFS.Sdk/Services/IVaultManagerService.cs
index b8ac5976b..97e0e6ec8 100644
--- a/src/Sdk/SecureFolderFS.Sdk/Services/IVaultManagerService.cs
+++ b/src/Sdk/SecureFolderFS.Sdk/Services/IVaultManagerService.cs
@@ -58,5 +58,18 @@ public interface IVaultManagerService
/// A that cancels this action.
/// A that represents the asynchronous operation.
Task ModifyAuthenticationAsync(IFolder vaultFolder, IDisposable unlockContract, IKeyUsage newPasskey, VaultOptions vaultOptions, CancellationToken cancellationToken = default);
+
+ ///
+ /// Modifies the configured authentication for the specified
+ /// using both old and new passkeys.
+ ///
+ /// The that represents the vault.
+ /// The recovery key used to decrypt the vault
+ /// The currently configured passkey.
+ /// The new passkey to secure the vault with.
+ /// The required options to set for this vault.
+ /// A that cancels this action.
+ /// A that represents the asynchronous operation.
+ Task ModifyAuthenticationAsync(IFolder vaultFolder, IDisposable unlockContract, IKeyUsage oldPasskey, IKeyUsage newPasskey, VaultOptions vaultOptions, CancellationToken cancellationToken = default);
}
}
diff --git a/src/Sdk/SecureFolderFS.Sdk/ViewModels/Views/Credentials/CredentialsConfirmationViewModel.cs b/src/Sdk/SecureFolderFS.Sdk/ViewModels/Views/Credentials/CredentialsConfirmationViewModel.cs
index 124fab46d..43d69c1ee 100644
--- a/src/Sdk/SecureFolderFS.Sdk/ViewModels/Views/Credentials/CredentialsConfirmationViewModel.cs
+++ b/src/Sdk/SecureFolderFS.Sdk/ViewModels/Views/Credentials/CredentialsConfirmationViewModel.cs
@@ -35,6 +35,8 @@ public sealed partial class CredentialsConfirmationViewModel : ObservableObject,
public required IDisposable UnlockContract { private get; init; }
+ public KeySequence? OldPasskey { private get; init; }
+
public CredentialsConfirmationViewModel(IFolder vaultFolder, RegisterViewModel registerViewModel, AuthenticationStage authenticationStage)
{
ServiceProvider = DI.Default;
@@ -86,23 +88,29 @@ AuthenticationMethod GetAuthenticationMethod()
private async Task RemoveAsync(CancellationToken cancellationToken)
{
+ ArgumentNullException.ThrowIfNull(OldPasskey);
if (_authenticationStage != AuthenticationStage.ProceedingStageOnly)
return;
- var key = RegisterViewModel.Credentials.Keys.First();
+ var firstStageKey = OldPasskey.Keys.First();
var configuredOptions = await VaultService.GetVaultOptionsAsync(_vaultFolder, cancellationToken);
var authenticationMethod = new AuthenticationMethod([configuredOptions.UnlockProcedure.Methods[0]], null);
- await ChangeCredentialsAsync(key, configuredOptions, authenticationMethod, cancellationToken);
+ await ChangeCredentialsAsync(firstStageKey, configuredOptions, authenticationMethod, cancellationToken);
}
private async Task ChangeCredentialsAsync(IKeyUsage key, VaultOptions configuredOptions, AuthenticationMethod unlockProcedure, CancellationToken cancellationToken)
{
// Modify the current unlock procedure
- await VaultManagerService.ModifyAuthenticationAsync(_vaultFolder, UnlockContract, key, configuredOptions with
+ var updatedOptions = configuredOptions with
{
UnlockProcedure = unlockProcedure
- }, cancellationToken);
+ };
+
+ if (OldPasskey is not null)
+ await VaultManagerService.ModifyAuthenticationAsync(_vaultFolder, UnlockContract, OldPasskey, key, updatedOptions, cancellationToken);
+ else
+ await VaultManagerService.ModifyAuthenticationAsync(_vaultFolder, UnlockContract, key, updatedOptions, cancellationToken);
// Revoke (invalidate) old configured credentials if those are different from newly configured ones.
// If both are the same, the authentication method should override the old ones; otherwise we would be deleting
diff --git a/src/Sdk/SecureFolderFS.Sdk/ViewModels/Views/Credentials/CredentialsSelectionViewModel.cs b/src/Sdk/SecureFolderFS.Sdk/ViewModels/Views/Credentials/CredentialsSelectionViewModel.cs
index d60ed142d..9bf0f6986 100644
--- a/src/Sdk/SecureFolderFS.Sdk/ViewModels/Views/Credentials/CredentialsSelectionViewModel.cs
+++ b/src/Sdk/SecureFolderFS.Sdk/ViewModels/Views/Credentials/CredentialsSelectionViewModel.cs
@@ -15,6 +15,7 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using SecureFolderFS.Shared.Models;
namespace SecureFolderFS.Sdk.ViewModels.Views.Credentials
{
@@ -32,6 +33,8 @@ public sealed partial class CredentialsSelectionViewModel : ObservableObject, IA
public IDisposable? UnlockContract { private get; set; }
+ public KeySequence? OldPasskey { private get; set; }
+
public event EventHandler? ConfirmationRequested;
public CredentialsSelectionViewModel(IFolder vaultFolder, AuthenticationStage authenticationStage)
@@ -76,6 +79,7 @@ private void RemoveCredentials()
IsRemoving = true,
IsComplementationAvailable = false,
UnlockContract = UnlockContract,
+ OldPasskey = OldPasskey,
ConfiguredViewModel = ConfiguredViewModel
});
}
@@ -97,6 +101,7 @@ private async Task ItemSelected(AuthenticationViewModel? authenticationViewModel
IsRemoving = false,
IsComplementationAvailable = _authenticationStage != AuthenticationStage.FirstStageOnly,
UnlockContract = UnlockContract,
+ OldPasskey = OldPasskey,
ConfiguredViewModel = ConfiguredViewModel
});
}
diff --git a/src/Sdk/SecureFolderFS.Sdk/ViewModels/Views/Overlays/CredentialsOverlayViewModel.cs b/src/Sdk/SecureFolderFS.Sdk/ViewModels/Views/Overlays/CredentialsOverlayViewModel.cs
index 5e5bed8d4..6a1fb9226 100644
--- a/src/Sdk/SecureFolderFS.Sdk/ViewModels/Views/Overlays/CredentialsOverlayViewModel.cs
+++ b/src/Sdk/SecureFolderFS.Sdk/ViewModels/Views/Overlays/CredentialsOverlayViewModel.cs
@@ -24,7 +24,8 @@ namespace SecureFolderFS.Sdk.ViewModels.Views.Overlays
public sealed partial class CredentialsOverlayViewModel : OverlayViewModel, IAsyncInitialize, IDisposable
{
private readonly IFolder _vaultFolder;
- private readonly KeySequence _keySequence;
+ private readonly KeySequence _loginKeySequence;
+ private readonly KeySequence _registerKeySequence;
private readonly AuthenticationStage _authenticationStage;
[ObservableProperty] private LoginViewModel _LoginViewModel;
@@ -35,12 +36,13 @@ public sealed partial class CredentialsOverlayViewModel : OverlayViewModel, IAsy
public CredentialsOverlayViewModel(IFolder vaultFolder, string? vaultName, AuthenticationStage authenticationStage)
{
ServiceProvider = DI.Default;
- _keySequence = new();
+ _loginKeySequence = new();
+ _registerKeySequence = new();
_vaultFolder = vaultFolder;
_authenticationStage = authenticationStage;
- RegisterViewModel = new(authenticationStage, _keySequence);
- LoginViewModel = new(vaultFolder, LoginViewType.Basic, _keySequence) { Title = vaultName };
+ RegisterViewModel = new(authenticationStage, _registerKeySequence);
+ LoginViewModel = new(vaultFolder, LoginViewType.Basic, _loginKeySequence) { Title = vaultName };
SelectionViewModel = new(vaultFolder, authenticationStage);
SelectedViewModel = LoginViewModel;
Title = "Authenticate".ToLocalized();
@@ -85,7 +87,7 @@ private void LoginViewModel_VaultUnlocked(object? sender, VaultUnlockedEventArgs
// Note: We can omit the fact that a flag other than FirstStage is passed to the ResetViewModel (via RegisterViewModel).
// The flag is manipulating the order at which keys are placed in the key sequence, so it shouldn't matter if it's cleared here
- _keySequence.Dispose();
+ _loginKeySequence.Dispose();
SelectedViewModel = new CredentialsResetViewModel(_vaultFolder, e.UnlockContract, RegisterViewModel).WithInitAsync();
}
else
@@ -93,6 +95,13 @@ private void LoginViewModel_VaultUnlocked(object? sender, VaultUnlockedEventArgs
Title = "SelectAuthentication".ToLocalized();
PrimaryText = null;
SelectionViewModel.UnlockContract = e.UnlockContract;
+ SelectionViewModel.OldPasskey = _loginKeySequence;
+
+ // Seed the register sequence with the already-authenticated first-stage key
+ // so that when a second-stage method is added, the combined passkey is complete
+ foreach (var key in _loginKeySequence.Keys)
+ _registerKeySequence.SetOrAdd(0, key); // First-stage lives at index 0
+
SelectionViewModel.RegisterViewModel = RegisterViewModel;
SelectedViewModel = SelectionViewModel;
}
@@ -121,6 +130,8 @@ public void Dispose()
(SelectedViewModel as IDisposable)?.Dispose();
SelectionViewModel.Dispose();
LoginViewModel.Dispose();
+ _loginKeySequence.Dispose();
+ _registerKeySequence.Dispose();
}
}
}