diff --git a/src/Core/SecureFolderFS.Core/Constants.cs b/src/Core/SecureFolderFS.Core/Constants.cs index 6b9b7f4ed..2289294c9 100644 --- a/src/Core/SecureFolderFS.Core/Constants.cs +++ b/src/Core/SecureFolderFS.Core/Constants.cs @@ -12,6 +12,7 @@ public static class Names public const string VAULT_CONTENT_FOLDERNAME = "content"; public const string VAULT_KEYSTORE_FILENAME = $"keystore{CONFIGURATION_EXTENSION}"; public const string VAULT_CONFIGURATION_FILENAME = $"sfconfig{CONFIGURATION_EXTENSION}"; + public const string VAULT_COMPLEMENTATION_FILENAME = $"sfcomplement{CONFIGURATION_EXTENSION}"; } public static class Authentication diff --git a/src/Core/SecureFolderFS.Core/DataModels/VaultSharesDataModel.cs b/src/Core/SecureFolderFS.Core/DataModels/VaultSharesDataModel.cs new file mode 100644 index 000000000..4c13d89c4 --- /dev/null +++ b/src/Core/SecureFolderFS.Core/DataModels/VaultSharesDataModel.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace SecureFolderFS.Core.DataModels +{ + [Serializable] + public sealed record class VaultSharesDataModel + { + [JsonPropertyName("shares")] + public List? Shares { get; init; } + } + + [Serializable] + public sealed record class VaultShareDataModel + { + [JsonPropertyName("authOne")] + public string? AuthenticationMethodId { get; init; } + + [JsonPropertyName("nonce")] + public byte[]? Nonce { get; init; } + + [JsonPropertyName("c_complement")] + public byte[]? WrappedComplementSecret { get; init; } + + [JsonPropertyName("tag")] + public byte[]? Tag { get; init; } + } +} \ No newline at end of file diff --git a/src/Core/SecureFolderFS.Core/Routines/Operational/ModifyComplementationRoutine.cs b/src/Core/SecureFolderFS.Core/Routines/Operational/ModifyComplementationRoutine.cs new file mode 100644 index 000000000..b9039ee7c --- /dev/null +++ b/src/Core/SecureFolderFS.Core/Routines/Operational/ModifyComplementationRoutine.cs @@ -0,0 +1,413 @@ +using System; +using System.Linq; +using System.Security.Cryptography; +using System.Threading; +using System.Threading.Tasks; +using SecureFolderFS.Core.Cryptography; +using SecureFolderFS.Core.DataModels; +using SecureFolderFS.Core.Models; +using SecureFolderFS.Core.Routines; +using SecureFolderFS.Core.VaultAccess; +using SecureFolderFS.Shared.ComponentModel; +using SecureFolderFS.Shared.Models; + +namespace SecureFolderFS.Core.Routines.Operational +{ + public sealed class ModifyComplementationRoutine : IFinalizationRoutine, IContractRoutine, IOptionsRoutine + { + private const int ComplementSecretLength = 32; + + private readonly VaultReader _vaultReader; + private readonly VaultWriter _vaultWriter; + private KeyPair? _keyPair; + private V4VaultKeystoreDataModel? _existingKeystoreDataModel; + private V4VaultKeystoreDataModel? _keystoreDataModel; + private V4VaultConfigurationDataModel? _existingConfigDataModel; + private V4VaultConfigurationDataModel? _configDataModel; + private VaultSharesDataModel? _existingSharesDataModel; + private VaultSharesDataModel? _sharesDataModel; + private bool _writeShares; + + public ModifyComplementationRoutine(VaultReader vaultReader, VaultWriter vaultWriter) + { + _vaultReader = vaultReader; + _vaultWriter = vaultWriter; + } + + /// + public async Task InitAsync(CancellationToken cancellationToken = default) + { + _existingConfigDataModel = await _vaultReader.ReadV4ConfigurationAsync(cancellationToken); + _existingKeystoreDataModel = await _vaultReader.ReadKeystoreAsync(cancellationToken); + _existingSharesDataModel = await _vaultReader.ReadComplementationAsync(cancellationToken); + } + + /// + public void SetUnlockContract(IDisposable unlockContract) + { + if (unlockContract is not IWrapper securityWrapper) + throw new ArgumentException($"The {nameof(unlockContract)} is invalid."); + + _keyPair = securityWrapper.Inner.KeyPair; + } + + /// + public void SetOptions(VaultOptions vaultOptions) + { + _configDataModel = V4VaultConfigurationDataModel.V4FromVaultOptions(vaultOptions); + } + + public void SetCredentials(ComplementationCredentials credentials, CancellationToken cancellationToken = default) + { + _ = cancellationToken; + ArgumentNullException.ThrowIfNull(_keyPair); + ArgumentNullException.ThrowIfNull(_existingConfigDataModel); + ArgumentNullException.ThrowIfNull(_existingKeystoreDataModel); + ArgumentNullException.ThrowIfNull(_configDataModel); + ArgumentNullException.ThrowIfNull(credentials); + + var oldAuthentication = AuthenticationMethod.FromString(_existingConfigDataModel.AuthenticationMethod); + var newAuthentication = AuthenticationMethod.FromString(_configDataModel.AuthenticationMethod); + var primaryChanged = !oldAuthentication.Methods.SequenceEqual(newAuthentication.Methods, StringComparer.Ordinal); + + if (string.IsNullOrWhiteSpace(oldAuthentication.Complementation) && + !string.IsNullOrWhiteSpace(newAuthentication.Complementation)) + { + AddComplementation(credentials, newAuthentication); + return; + } + + if (!string.IsNullOrWhiteSpace(oldAuthentication.Complementation) && + string.IsNullOrWhiteSpace(newAuthentication.Complementation)) + { + RemoveComplementation(credentials, oldAuthentication); + return; + } + + if (!string.IsNullOrWhiteSpace(oldAuthentication.Complementation) && + !string.IsNullOrWhiteSpace(newAuthentication.Complementation)) + { + if (primaryChanged || credentials.NewPrimaryCredential is not null) + ChangePrimaryAndPreserveComplementation(credentials, oldAuthentication, newAuthentication); + else if (!string.Equals(oldAuthentication.Complementation, newAuthentication.Complementation, StringComparison.Ordinal) || + credentials.NewComplementCredential is not null) + ReplaceComplementation(credentials, oldAuthentication, newAuthentication); + else + throw new InvalidOperationException("No complementation change was requested."); + + return; + } + + throw new InvalidOperationException("The requested authentication change does not involve complementation."); + } + + private void AddComplementation( + ComplementationCredentials credentials, + AuthenticationMethod newAuthentication) + { + var newComplementMethod = newAuthentication.Complementation ?? throw new InvalidOperationException("Complementation method is missing."); + var currentKey = ExportKey(credentials.CurrentCredential); + var newComplementKey = ExportKey(credentials.NewComplementCredential ?? throw new InvalidOperationException("New complement credentials are required.")); + byte[]? newPrimaryKey = null; + byte[]? softwareEntropy = null; + byte[]? complementSecret = null; + + try + { + newPrimaryKey = credentials.NewPrimaryCredential is null ? currentKey : ExportKey(credentials.NewPrimaryCredential); + softwareEntropy = DecryptSoftwareEntropy(currentKey); + complementSecret = DeriveComplementSecret(newPrimaryKey, GetPrimaryMethod(newAuthentication)); + + ReEncryptKeystore(complementSecret, softwareEntropy); + _sharesDataModel = CreateShares(VaultParser.V4WrapComplementSecret(complementSecret, newComplementKey, GetVaultId(), newComplementMethod)); + _writeShares = true; + } + finally + { + Zero(newPrimaryKey, currentKey); + Zero(complementSecret); + Zero(softwareEntropy); + Zero(newComplementKey); + Zero(currentKey); + } + + } + + private void ReplaceComplementation( + ComplementationCredentials credentials, + AuthenticationMethod oldAuthentication, + AuthenticationMethod newAuthentication) + { + var newComplementMethod = newAuthentication.Complementation ?? throw new InvalidOperationException("Complementation method is missing."); + var currentKey = ExportKey(credentials.CurrentCredential); + var newComplementKey = ExportKey(credentials.NewComplementCredential ?? throw new InvalidOperationException("New complement credentials are required.")); + byte[]? complementSecret = null; + byte[]? softwareEntropy = null; + + try + { + complementSecret = RecoverComplementSecret(currentKey, oldAuthentication, allowPrimary: true); + softwareEntropy = DecryptSoftwareEntropy(complementSecret); + + ReEncryptKeystore(complementSecret, softwareEntropy); + _sharesDataModel = CreateShares(VaultParser.V4WrapComplementSecret(complementSecret, newComplementKey, GetVaultId(), newComplementMethod)); + _writeShares = true; + } + finally + { + Zero(softwareEntropy); + Zero(complementSecret); + Zero(newComplementKey); + Zero(currentKey); + } + } + + private void RemoveComplementation(ComplementationCredentials credentials, AuthenticationMethod oldAuthentication) + { + var currentPrimaryKey = ExportKey(credentials.CurrentCredential); + byte[]? targetPasskey = null; + byte[]? complementSecret = null; + byte[]? softwareEntropy = null; + + try + { + targetPasskey = credentials.NewPrimaryCredential is null ? currentPrimaryKey : ExportKey(credentials.NewPrimaryCredential); + complementSecret = DeriveComplementSecret(currentPrimaryKey, GetPrimaryMethod(oldAuthentication)); + softwareEntropy = DecryptSoftwareEntropy(complementSecret); + + ReEncryptKeystore(targetPasskey, softwareEntropy); + _sharesDataModel = null; + _writeShares = true; + } + finally + { + Zero(softwareEntropy); + Zero(complementSecret); + Zero(targetPasskey, currentPrimaryKey); + Zero(currentPrimaryKey); + } + } + + private void ChangePrimaryAndPreserveComplementation( + ComplementationCredentials credentials, + AuthenticationMethod oldAuthentication, + AuthenticationMethod newAuthentication) + { + var oldComplementMethod = oldAuthentication.Complementation ?? throw new InvalidOperationException("Complementation method is missing."); + var newComplementMethod = newAuthentication.Complementation ?? throw new InvalidOperationException("Complementation method is missing."); + var currentComplementKey = ExportKey(credentials.CurrentComplementCredential ?? credentials.CurrentCredential); + var newPrimaryKey = ExportKey(credentials.NewPrimaryCredential ?? throw new InvalidOperationException("New primary credentials are required.")); + byte[]? newComplementKey = null; + byte[]? oldComplementSecret = null; + byte[]? newComplementSecret = null; + byte[]? softwareEntropy = null; + + try + { + oldComplementSecret = RecoverComplementSecretFromShare(currentComplementKey, oldComplementMethod); + softwareEntropy = DecryptSoftwareEntropy(oldComplementSecret); + newComplementSecret = DeriveComplementSecret(newPrimaryKey, GetPrimaryMethod(newAuthentication)); + + newComplementKey = string.Equals(oldComplementMethod, newComplementMethod, StringComparison.Ordinal) + ? currentComplementKey + : ExportKey(credentials.NewComplementCredential ?? throw new InvalidOperationException("New complement credentials are required.")); + + ReEncryptKeystore(newComplementSecret, softwareEntropy); + _sharesDataModel = CreateShares(VaultParser.V4WrapComplementSecret(newComplementSecret, newComplementKey, GetVaultId(), newComplementMethod)); + _writeShares = true; + } + finally + { + Zero(softwareEntropy); + Zero(newComplementSecret); + Zero(oldComplementSecret); + Zero(newComplementKey, currentComplementKey); + Zero(newPrimaryKey); + Zero(currentComplementKey); + } + } + + private byte[] RecoverComplementSecret(byte[] currentKey, AuthenticationMethod oldAuthentication, bool allowPrimary) + { + CryptographicException? lastException = null; + + if (allowPrimary) + { + byte[]? complementSecret = null; + byte[]? softwareEntropy = null; + try + { + complementSecret = DeriveComplementSecret(currentKey, GetPrimaryMethod(oldAuthentication)); + softwareEntropy = DecryptSoftwareEntropy(complementSecret); + return complementSecret; + } + catch (CryptographicException ex) + { + lastException = ex; + Zero(complementSecret); + } + finally + { + Zero(softwareEntropy); + } + } + + if (!string.IsNullOrWhiteSpace(oldAuthentication.Complementation)) + return RecoverComplementSecretFromShare(currentKey, oldAuthentication.Complementation, lastException); + + throw lastException ?? new CryptographicException("The complement secret could not be recovered."); + } + + private byte[] RecoverComplementSecretFromShare(byte[] currentKey, string complementMethod, CryptographicException? fallbackException = null) + { + var share = GetShare(complementMethod); + byte[]? complementSecret = null; + byte[]? softwareEntropy = null; + + try + { + complementSecret = VaultParser.V4UnwrapComplementSecret(currentKey, GetVaultId(), share); + softwareEntropy = DecryptSoftwareEntropy(complementSecret); + return complementSecret; + } + catch (CryptographicException) when (fallbackException is not null) + { + Zero(complementSecret); + throw fallbackException; + } + catch + { + Zero(complementSecret); + throw; + } + finally + { + Zero(softwareEntropy); + } + } + + private byte[] DeriveComplementSecret(byte[] passkey, string authenticationMethodId) + { + var complementSecret = new byte[ComplementSecretLength]; + try + { + VaultParser.V4DeriveComplementKey(passkey, GetVaultId(), authenticationMethodId, complementSecret); + return complementSecret; + } + catch + { + Zero(complementSecret); + throw; + } + } + + private byte[] DecryptSoftwareEntropy(byte[] passkey) + { + ArgumentNullException.ThrowIfNull(_existingKeystoreDataModel); + + var softwareEntropy = new byte[ComplementSecretLength]; + try + { + VaultParser.V4DecryptSoftwareEntropy(passkey, _existingKeystoreDataModel, softwareEntropy); + return softwareEntropy; + } + catch + { + Zero(softwareEntropy); + throw; + } + } + + private void ReEncryptKeystore(byte[] passkey, byte[] softwareEntropy) + { + ArgumentNullException.ThrowIfNull(_keyPair); + + var salt = new byte[Cryptography.Constants.KeyTraits.SALT_LENGTH]; + RandomNumberGenerator.Fill(salt); + + _keystoreDataModel = _keyPair.UseKeys((dekKey, macKey) => + VaultParser.V4ReEncryptKeystore(passkey, dekKey, macKey, salt, softwareEntropy)); + } + + private VaultShareDataModel GetShare(string authenticationMethodId) + { + return _existingSharesDataModel?.Shares?.FirstOrDefault(x => + string.Equals(x.AuthenticationMethodId, authenticationMethodId, StringComparison.Ordinal)) + ?? throw new InvalidOperationException($"Complementation share '{authenticationMethodId}' was not found."); + } + + private string GetVaultId() + { + ArgumentNullException.ThrowIfNull(_existingConfigDataModel); + return _existingConfigDataModel.Uid; + } + + private static string GetPrimaryMethod(AuthenticationMethod authenticationMethod) + { + return authenticationMethod.Methods.FirstOrDefault() ?? throw new InvalidOperationException("Primary authentication is missing."); + } + + private static VaultSharesDataModel CreateShares(VaultShareDataModel shareDataModel) + { + return new() + { + Shares = [ shareDataModel ] + }; + } + + private static byte[] ExportKey(IKeyUsage key) + { + var exported = new byte[key.Length]; + try + { + key.UseKey(source => source.CopyTo(exported)); + return exported; + } + catch + { + Zero(exported); + throw; + } + } + + private static void Zero(byte[]? key) + { + if (key is not null) + CryptographicOperations.ZeroMemory(key); + } + + private static void Zero(byte[]? key, byte[] sameAs) + { + if (key is not null && !ReferenceEquals(key, sameAs)) + CryptographicOperations.ZeroMemory(key); + } + + /// + public async Task FinalizeAsync(CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(_keyPair); + ArgumentNullException.ThrowIfNull(_keystoreDataModel); + ArgumentNullException.ThrowIfNull(_configDataModel); + + _keyPair.MacKey.UseKey(macKey => + { + VaultParser.V4CalculateConfigMac(_configDataModel, macKey, _configDataModel.PayloadMac); + }); + + await _vaultWriter.WriteKeystoreAsync(_keystoreDataModel, cancellationToken); + await _vaultWriter.WriteV4ConfigurationAsync(_configDataModel, cancellationToken); + + if (_writeShares) + await _vaultWriter.WriteComplementationAsync(_sharesDataModel, cancellationToken); + + using (_keyPair) + return new SecurityWrapper(_keyPair.CreateCopy(), _configDataModel); + } + + /// + public void Dispose() + { + _keyPair?.Dispose(); + } + } +} diff --git a/src/Core/SecureFolderFS.Core/Routines/Operational/UnlockRoutine.cs b/src/Core/SecureFolderFS.Core/Routines/Operational/UnlockRoutine.cs index 1de4732a1..b3449b2c7 100644 --- a/src/Core/SecureFolderFS.Core/Routines/Operational/UnlockRoutine.cs +++ b/src/Core/SecureFolderFS.Core/Routines/Operational/UnlockRoutine.cs @@ -1,4 +1,6 @@ using System; +using System.Linq; +using System.Security.Cryptography; using System.Threading; using System.Threading.Tasks; using SecureFolderFS.Core.Cryptography; @@ -7,6 +9,7 @@ using SecureFolderFS.Core.Validators; using SecureFolderFS.Core.VaultAccess; using SecureFolderFS.Shared.ComponentModel; +using SecureFolderFS.Shared.Models; using SecureFolderFS.Shared.SecureStore; namespace SecureFolderFS.Core.Routines.Operational @@ -17,6 +20,7 @@ internal sealed class UnlockRoutine : ICredentialsRoutine private readonly VaultReader _vaultReader; private V4VaultKeystoreDataModel? _keystoreDataModel; private V4VaultConfigurationDataModel? _configDataModel; + private VaultSharesDataModel? _sharesDataModel; private SecureKey? _dekKey; private SecureKey? _macKey; @@ -30,6 +34,7 @@ public async Task InitAsync(CancellationToken cancellationToken) { _configDataModel = await _vaultReader.ReadV4ConfigurationAsync(cancellationToken); _keystoreDataModel = await _vaultReader.ReadKeystoreAsync(cancellationToken); + _sharesDataModel = await _vaultReader.ReadComplementationAsync(cancellationToken); } /// @@ -38,11 +43,75 @@ public void SetCredentials(IKeyUsage passkey) ArgumentNullException.ThrowIfNull(_configDataModel); ArgumentNullException.ThrowIfNull(_keystoreDataModel); - var derived = passkey.UseKey(key => VaultParser.V4DeriveKeystore(key, _keystoreDataModel)); + var authenticationMethod = AuthenticationMethod.FromString(_configDataModel.AuthenticationMethod); + var derived = string.IsNullOrWhiteSpace(authenticationMethod.Complementation) + ? passkey.UseKey(key => VaultParser.V4DeriveKeystore(key, _keystoreDataModel)) + : DeriveComplementedKeystore(passkey, authenticationMethod); + _dekKey = SecureKey.TakeOwnership(derived.dekKey); _macKey = SecureKey.TakeOwnership(derived.macKey); } + private (byte[] dekKey, byte[] macKey) DeriveComplementedKeystore(IKeyUsage passkey, AuthenticationMethod authenticationMethod) + { + ArgumentNullException.ThrowIfNull(_configDataModel); + ArgumentNullException.ThrowIfNull(_keystoreDataModel); + + Exception? lastException = null; + var primaryMethodId = authenticationMethod.Methods.FirstOrDefault() ?? throw new InvalidOperationException("Primary authentication is missing."); + + try + { + return passkey.UseKey(key => + { + Span complementSecret = stackalloc byte[32]; + try + { + VaultParser.V4DeriveComplementKey(key, _configDataModel.Uid, primaryMethodId, complementSecret); + return VaultParser.V4DeriveKeystore(complementSecret, _keystoreDataModel); + } + finally + { + CryptographicOperations.ZeroMemory(complementSecret); + } + }); + } + catch (CryptographicException ex) + { + lastException = ex; + } + + foreach (var share in _sharesDataModel?.Shares ?? []) + { + if (share.WrappedComplementSecret is null + || share.Tag is null + || share.Nonce is null + || share.AuthenticationMethodId is null) + continue; + + if (!string.Equals(share.AuthenticationMethodId, authenticationMethod.Complementation, StringComparison.Ordinal)) + continue; + + byte[]? complementSecret = null; + try + { + complementSecret = passkey.UseKey(key => VaultParser.V4UnwrapComplementSecret(key, _configDataModel.Uid, share)); + return VaultParser.V4DeriveKeystore(complementSecret, _keystoreDataModel); + } + catch (CryptographicException ex) + { + lastException = ex; + } + finally + { + if (complementSecret is not null) + CryptographicOperations.ZeroMemory(complementSecret); + } + } + + throw lastException ?? new CryptographicException("The complemented credentials could not unlock this vault."); + } + /// public async Task FinalizeAsync(CancellationToken cancellationToken) { diff --git a/src/Core/SecureFolderFS.Core/Routines/Operational/VaultRoutines.cs b/src/Core/SecureFolderFS.Core/Routines/Operational/VaultRoutines.cs index 07f10296e..448a75927 100644 --- a/src/Core/SecureFolderFS.Core/Routines/Operational/VaultRoutines.cs +++ b/src/Core/SecureFolderFS.Core/Routines/Operational/VaultRoutines.cs @@ -51,6 +51,12 @@ public IModifyCredentialsRoutine ModifyCredentials() return new ModifyCredentialsRoutine(VaultReader, VaultWriter); } + public ModifyComplementationRoutine ModifyComplementation() + { + CheckVaultValidation(); + return new ModifyComplementationRoutine(VaultReader, VaultWriter); + } + private void CheckVaultValidation() { if (!_validationResult.Successful) diff --git a/src/Core/SecureFolderFS.Core/VaultAccess/VaultParser.cs b/src/Core/SecureFolderFS.Core/VaultAccess/VaultParser.cs index 28bfc518d..47fea346b 100644 --- a/src/Core/SecureFolderFS.Core/VaultAccess/VaultParser.cs +++ b/src/Core/SecureFolderFS.Core/VaultAccess/VaultParser.cs @@ -265,6 +265,86 @@ public static void V4DecryptSoftwareEntropy( softwareEntropy); } + public static void V4DeriveComplementKey( + ReadOnlySpan passkey, + string vaultId, + string authenticationMethodId, + Span complementKey) + { + ArgumentException.ThrowIfNullOrWhiteSpace(vaultId); + ArgumentException.ThrowIfNullOrWhiteSpace(authenticationMethodId); + + var salt = Encoding.UTF8.GetBytes(vaultId); + var info = Encoding.UTF8.GetBytes(authenticationMethodId); + + HKDF.DeriveKey( + HashAlgorithmName.SHA256, + passkey, + complementKey, + salt, + info); + } + + public static VaultShareDataModel V4WrapComplementSecret( + ReadOnlySpan complementSecret, + ReadOnlySpan wrappingKeyMaterial, + string vaultId, + string authenticationMethodId) + { + Span complementWrapKey = stackalloc byte[32]; + try + { + V4DeriveComplementKey(wrappingKeyMaterial, vaultId, authenticationMethodId, complementWrapKey); + + var nonce = new byte[12]; + var tag = new byte[16]; + var wrapped = new byte[complementSecret.Length]; + RandomNumberGenerator.Fill(nonce); + + using (var aes = new AesGcm(complementWrapKey, 16)) + aes.Encrypt(nonce, complementSecret, wrapped, tag); + + return new() + { + AuthenticationMethodId = authenticationMethodId, + Nonce = nonce, + WrappedComplementSecret = wrapped, + Tag = tag + }; + } + finally + { + CryptographicOperations.ZeroMemory(complementWrapKey); + } + } + + public static byte[] V4UnwrapComplementSecret( + ReadOnlySpan wrappingKeyMaterial, + string vaultId, + VaultShareDataModel shareDataModel) + { + ArgumentNullException.ThrowIfNull(shareDataModel.AuthenticationMethodId); + ArgumentNullException.ThrowIfNull(shareDataModel.Nonce); + ArgumentNullException.ThrowIfNull(shareDataModel.WrappedComplementSecret); + ArgumentNullException.ThrowIfNull(shareDataModel.Tag); + + Span complementWrapKey = stackalloc byte[32]; + try + { + V4DeriveComplementKey(wrappingKeyMaterial, vaultId, shareDataModel.AuthenticationMethodId, complementWrapKey); + + var complementSecret = new byte[shareDataModel.WrappedComplementSecret.Length]; + using var aes = new AesGcm(complementWrapKey, 16); + aes.Decrypt(shareDataModel.Nonce, shareDataModel.WrappedComplementSecret, shareDataModel.Tag, complementSecret); + + return complementSecret; + } + finally + { + CryptographicOperations.ZeroMemory(complementWrapKey); + } + } + /// /// Shared implementation for both and . /// Encrypts the provided entropy under the passkey and wraps DEK/MAC under the augmented KEK. diff --git a/src/Core/SecureFolderFS.Core/VaultAccess/VaultReader.cs b/src/Core/SecureFolderFS.Core/VaultAccess/VaultReader.cs index 304ed0faa..4615172ee 100644 --- a/src/Core/SecureFolderFS.Core/VaultAccess/VaultReader.cs +++ b/src/Core/SecureFolderFS.Core/VaultAccess/VaultReader.cs @@ -53,6 +53,19 @@ public async Task ReadV4ConfigurationAsync(Cancel return await ReadDataAsync(configFile, _serializer, cancellationToken); } + public async Task ReadComplementationAsync(CancellationToken cancellationToken) + { + try + { + var complementFile = await _vaultFolder.GetFileByNameAsync(Constants.Vault.Names.VAULT_COMPLEMENTATION_FILENAME, cancellationToken); + return await ReadDataAsync(complementFile, _serializer, cancellationToken); + } + catch (Exception) + { + return null; + } + } + public async Task ReadVersionAsync(CancellationToken cancellationToken) { // Get configuration file diff --git a/src/Core/SecureFolderFS.Core/VaultAccess/VaultWriter.cs b/src/Core/SecureFolderFS.Core/VaultAccess/VaultWriter.cs index d30715db7..6c232d541 100644 --- a/src/Core/SecureFolderFS.Core/VaultAccess/VaultWriter.cs +++ b/src/Core/SecureFolderFS.Core/VaultAccess/VaultWriter.cs @@ -2,6 +2,7 @@ using SecureFolderFS.Core.DataModels; using SecureFolderFS.Shared.ComponentModel; using SecureFolderFS.Shared.Extensions; +using SecureFolderFS.Storage.Extensions; using System.IO; using System.Threading; using System.Threading.Tasks; @@ -57,6 +58,29 @@ public async Task WriteV4ConfigurationAsync(V4VaultConfigurationDataModel? confi await WriteDataAsync(configFile, configDataModel, cancellationToken); } + public async Task WriteComplementationAsync(VaultSharesDataModel? sharesDataModel, CancellationToken cancellationToken) + { + if (sharesDataModel is null) + { + if (_vaultFolder is not IModifiableFolder modifiableFolder) + return; + + var existingFile = await modifiableFolder.TryGetFileByNameAsync(Constants.Vault.Names.VAULT_COMPLEMENTATION_FILENAME, cancellationToken); + if (existingFile is not null) + await modifiableFolder.DeleteAsync(existingFile, cancellationToken); + + return; + } + + var complementFile = _vaultFolder switch + { + IModifiableFolder modifiableFolder => await modifiableFolder.CreateFileAsync(Constants.Vault.Names.VAULT_COMPLEMENTATION_FILENAME, true, cancellationToken), + _ => await _vaultFolder.GetFirstByNameAsync(Constants.Vault.Names.VAULT_COMPLEMENTATION_FILENAME, cancellationToken) as IFile + }; + + await WriteDataAsync(complementFile, sharesDataModel, cancellationToken); + } + public async Task WriteAuthenticationAsync(string fileName, TCapability? authDataModel, CancellationToken cancellationToken) where TCapability : VaultCapabilityDataModel { diff --git a/src/Platforms/SecureFolderFS.Maui/Platforms/Android/ServiceImplementation/AndroidVaultCredentialsService.cs b/src/Platforms/SecureFolderFS.Maui/Platforms/Android/ServiceImplementation/AndroidVaultCredentialsService.cs index 002316b66..2915350ac 100644 --- a/src/Platforms/SecureFolderFS.Maui/Platforms/Android/ServiceImplementation/AndroidVaultCredentialsService.cs +++ b/src/Platforms/SecureFolderFS.Maui/Platforms/Android/ServiceImplementation/AndroidVaultCredentialsService.cs @@ -36,7 +36,7 @@ protected override async IAsyncEnumerable GetLoginAsync string vaultId, [EnumeratorCancellation] CancellationToken cancellationToken = default) { - foreach (var item in unlockProcedure.Methods) + foreach (var item in EnumerateLoginMethods(unlockProcedure)) { yield return item switch { diff --git a/src/Platforms/SecureFolderFS.Maui/Platforms/iOS/ServiceImplementation/IOSVaultCredentialsService.cs b/src/Platforms/SecureFolderFS.Maui/Platforms/iOS/ServiceImplementation/IOSVaultCredentialsService.cs index 883f2ce4d..d651c1fc9 100644 --- a/src/Platforms/SecureFolderFS.Maui/Platforms/iOS/ServiceImplementation/IOSVaultCredentialsService.cs +++ b/src/Platforms/SecureFolderFS.Maui/Platforms/iOS/ServiceImplementation/IOSVaultCredentialsService.cs @@ -43,7 +43,7 @@ protected override async IAsyncEnumerable GetLoginAsync [EnumeratorCancellation] CancellationToken cancellationToken = default) { await Task.CompletedTask; - foreach (var item in unlockProcedure.Methods) + foreach (var item in EnumerateLoginMethods(unlockProcedure)) { yield return item switch { diff --git a/src/Platforms/SecureFolderFS.Maui/Popups/CredentialsPopup.xaml b/src/Platforms/SecureFolderFS.Maui/Popups/CredentialsPopup.xaml index 4471df128..08a5c8ec0 100644 --- a/src/Platforms/SecureFolderFS.Maui/Popups/CredentialsPopup.xaml +++ b/src/Platforms/SecureFolderFS.Maui/Popups/CredentialsPopup.xaml @@ -80,6 +80,14 @@ + + + + diff --git a/src/Platforms/SecureFolderFS.Maui/Views/Vault/LoginPage.xaml b/src/Platforms/SecureFolderFS.Maui/Views/Vault/LoginPage.xaml index 6151047f7..3d1e9bc40 100644 --- a/src/Platforms/SecureFolderFS.Maui/Views/Vault/LoginPage.xaml +++ b/src/Platforms/SecureFolderFS.Maui/Views/Vault/LoginPage.xaml @@ -8,14 +8,17 @@ xmlns:mi_cupertino="clr-namespace:MauiIcons.Cupertino;assembly=MauiIcons.Cupertino" xmlns:mi_material="clr-namespace:MauiIcons.Material;assembly=MauiIcons.Material" xmlns:uc="clr-namespace:SecureFolderFS.Maui.UserControls" + xmlns:ucc="clr-namespace:SecureFolderFS.Maui.UserControls.Common" xmlns:uco="clr-namespace:SecureFolderFS.Maui.UserControls.Options" + xmlns:vm="clr-namespace:SecureFolderFS.Sdk.ViewModels.Controls.Authentication;assembly=SecureFolderFS.Sdk" + x:Name="ThisPage" Title="{Binding ViewModel.VaultViewModel.Title, Mode=OneWay}" x:DataType="local:LoginPage"> - + - - + + + - - - - - - -