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(); } } }