Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Core/SecureFolderFS.Core/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
28 changes: 28 additions & 0 deletions src/Core/SecureFolderFS.Core/DataModels/VaultSharesDataModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;

namespace SecureFolderFS.Core.DataModels
{
[Serializable]
public sealed record class VaultSharesDataModel
{
public List<VaultShareDataModel>? 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; }
}
}

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System;
using System.Linq;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
using SecureFolderFS.Core.Cryptography;
Expand All @@ -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
Expand All @@ -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;

Expand All @@ -30,6 +34,7 @@ public async Task InitAsync(CancellationToken cancellationToken)
{
_configDataModel = await _vaultReader.ReadV4ConfigurationAsync(cancellationToken);
_keystoreDataModel = await _vaultReader.ReadKeystoreAsync<V4VaultKeystoreDataModel>(cancellationToken);
_sharesDataModel = await _vaultReader.ReadComplementationAsync(cancellationToken);
}

/// <inheritdoc/>
Expand All @@ -38,11 +43,62 @@ 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);

CryptographicException? lastException = null;
var primaryMethodId = authenticationMethod.Methods.FirstOrDefault() ?? throw new InvalidOperationException("Primary authentication is missing.");

try
{
return passkey.UseKey(key =>
{
Span<byte> complementSecret = stackalloc byte[32];
VaultParser.V4DeriveComplementKey(key, _configDataModel.Uid, primaryMethodId, complementSecret);
return VaultParser.V4DeriveKeystore(complementSecret, _keystoreDataModel);
Comment thread
d2dyno1 marked this conversation as resolved.
Outdated
});
}
catch (CryptographicException ex)
{
lastException = ex;
}

foreach (var share in _sharesDataModel?.Shares ?? [])
{
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);
}
Comment thread
d2dyno1 marked this conversation as resolved.
}

throw lastException ?? new CryptographicException("The complemented credentials could not unlock this vault.");
}

/// <inheritdoc/>
public async Task<IDisposable> FinalizeAsync(CancellationToken cancellationToken)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
80 changes: 80 additions & 0 deletions src/Core/SecureFolderFS.Core/VaultAccess/VaultParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,86 @@ public static void V4DecryptSoftwareEntropy(
softwareEntropy);
}

public static void V4DeriveComplementKey(
ReadOnlySpan<byte> passkey,
string vaultId,
string authenticationMethodId,
Span<byte> 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<byte> complementSecret,
ReadOnlySpan<byte> wrappingKeyMaterial,
string vaultId,
string authenticationMethodId)
{
Span<byte> 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<byte> wrappingKeyMaterial,
string vaultId,
VaultShareDataModel shareDataModel)
{
ArgumentNullException.ThrowIfNull(shareDataModel.AuthenticationMethodId);
ArgumentNullException.ThrowIfNull(shareDataModel.Nonce);
ArgumentNullException.ThrowIfNull(shareDataModel.WrappedComplementSecret);
ArgumentNullException.ThrowIfNull(shareDataModel.Tag);

Span<byte> 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);
}
}

/// <summary>
/// Shared implementation for both <see cref="V4EncryptKeystore"/> and <see cref="V4ReEncryptKeystore"/>.
/// Encrypts the provided entropy under the passkey and wraps DEK/MAC under the augmented KEK.
Expand Down
13 changes: 13 additions & 0 deletions src/Core/SecureFolderFS.Core/VaultAccess/VaultReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,19 @@ public async Task<V4VaultConfigurationDataModel> ReadV4ConfigurationAsync(Cancel
return await ReadDataAsync<V4VaultConfigurationDataModel>(configFile, _serializer, cancellationToken);
}

public async Task<VaultSharesDataModel?> ReadComplementationAsync(CancellationToken cancellationToken)
{
try
{
var complementFile = await _vaultFolder.GetFileByNameAsync(Constants.Vault.Names.VAULT_COMPLEMENTATION_FILENAME, cancellationToken);
return await ReadDataAsync<VaultSharesDataModel?>(complementFile, _serializer, cancellationToken);
}
catch (Exception)
{
return null;
}
}
Comment thread
d2dyno1 marked this conversation as resolved.

public async Task<VersionDataModel> ReadVersionAsync(CancellationToken cancellationToken)
{
// Get configuration file
Expand Down
11 changes: 11 additions & 0 deletions src/Core/SecureFolderFS.Core/VaultAccess/VaultWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,17 @@ public async Task WriteV4ConfigurationAsync(V4VaultConfigurationDataModel? confi
await WriteDataAsync(configFile, configDataModel, cancellationToken);
}

public async Task WriteComplementationAsync(VaultSharesDataModel? sharesDataModel, CancellationToken cancellationToken)
{
var complementFile = _vaultFolder switch
{
IModifiableFolder modifiableFolder when sharesDataModel is not null => 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);
}
Comment thread
d2dyno1 marked this conversation as resolved.

public async Task WriteAuthenticationAsync<TCapability>(string fileName, TCapability? authDataModel, CancellationToken cancellationToken)
where TCapability : VaultCapabilityDataModel
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ protected override async IAsyncEnumerable<AuthenticationViewModel> GetLoginAsync
string vaultId,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
foreach (var item in unlockProcedure.Methods)
foreach (var item in EnumerateLoginMethods(unlockProcedure))
{
yield return item switch
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ protected override async IAsyncEnumerable<AuthenticationViewModel> GetLoginAsync
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
await Task.CompletedTask;
foreach (var item in unlockProcedure.Methods)
foreach (var item in EnumerateLoginMethods(unlockProcedure))
{
yield return item switch
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,5 +85,14 @@ public virtual async IAsyncEnumerable<AuthenticationViewModel> GetLoginAsync(IFo

/// <inheritdoc/>
public abstract IAsyncEnumerable<AuthenticationViewModel> GetCreationAsync(IFolder vaultFolder, string vaultId, CancellationToken cancellationToken = default);

protected static IEnumerable<string> EnumerateLoginMethods(AuthenticationMethod unlockProcedure)
{
foreach (var item in unlockProcedure.Methods)
yield return item;

if (!string.IsNullOrWhiteSpace(unlockProcedure.Complementation))
yield return unlockProcedure.Complementation;
Comment thread
d2dyno1 marked this conversation as resolved.
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,18 @@ public virtual async Task<IDisposable> RecoverAsync(IFolder vaultFolder, string

return await recoveryRoutine.FinalizeAsync(cancellationToken);
}

/// <inheritdoc/>
public virtual async Task ModifyComplementationAsync(IFolder vaultFolder, IDisposable unlockContract, ComplementationCredentials credentials, VaultOptions vaultOptions, CancellationToken cancellationToken = default)
{
using var complementationRoutine = (await VaultRoutines.CreateRoutinesAsync(vaultFolder, StreamSerializer.Instance, cancellationToken)).ModifyComplementation();
await complementationRoutine.InitAsync(cancellationToken);
complementationRoutine.SetUnlockContract(unlockContract);
complementationRoutine.SetOptions(vaultOptions);
complementationRoutine.SetCredentials(credentials, cancellationToken);

using var result = await complementationRoutine.FinalizeAsync(cancellationToken);
}

/// <inheritdoc/>
public virtual async Task ModifyAuthenticationAsync(IFolder vaultFolder, IDisposable unlockContract, IKeyUsage newPasskey, VaultOptions vaultOptions, CancellationToken cancellationToken = default)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public abstract class KeyFileViewModel : AuthenticationViewModel
public override event EventHandler<EventArgs>? StateChanged;

/// <inheritdoc/>
public sealed override bool CanComplement { get; } = false;
public sealed override bool CanComplement { get; } = true;

/// <inheritdoc/>
public sealed override AuthenticationStage Availability { get; } = AuthenticationStage.Any;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,12 +166,7 @@
x:Load="{x:Bind IsRemoving, Mode=OneWay, Converter={StaticResource BoolInvertConverter}}"
Spacing="24">
<uc:RegisterControl CurrentViewModel="{x:Bind RegisterViewModel.CurrentViewModel, Mode=OneWay}" />
<StackPanel
x:Name="SubstitutePanel"
x:Load="{x:Bind IsComplementationAvailable, Mode=OneWay}"
Orientation="Horizontal"
Spacing="8"
Visibility="Collapsed">
<StackPanel Orientation="Horizontal" Spacing="8">
<CheckBox
Content="Use as substitute"
IsChecked="{x:Bind IsComplementing, Mode=TwoWay}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ protected override async IAsyncEnumerable<AuthenticationViewModel> GetLoginAsync
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
await Task.CompletedTask;
foreach (var item in unlockProcedure.Methods)
foreach (var item in EnumerateLoginMethods(unlockProcedure))
{
yield return item switch
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public override async IAsyncEnumerable<AuthenticationViewModel> GetLoginAsync(IF
var config = await vaultReader.ReadConfigurationAsync(cancellationToken);
var authenticationMethod = AuthenticationMethod.FromString(config.AuthenticationMethod);

foreach (var item in authenticationMethod.Methods)
foreach (var item in EnumerateLoginMethods(authenticationMethod))
{
yield return item switch
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ protected override async IAsyncEnumerable<AuthenticationViewModel> GetLoginAsync
AuthenticationMethod unlockProcedure, string vaultId,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
foreach (var item in unlockProcedure.Methods)
foreach (var item in EnumerateLoginMethods(unlockProcedure))
{
yield return item switch
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
<UserControl
x:Class="SecureFolderFS.Uno.UserControls.InterfaceRoot.VaultPreviewRootControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
Expand Down Expand Up @@ -51,10 +51,15 @@
<uc:LoginControl CurrentViewModel="{x:Bind ViewModel.LoginViewModel.CurrentViewModel, Mode=OneWay}" ProvideContinuationButton="True" />
<uc:LoginOptions
AreCredentialsSaved="{x:Bind ViewModel.LoginViewModel.AreCredentialsSaved, Mode=OneWay}"
AuthenticationOptions="{x:Bind ViewModel.LoginViewModel.AuthenticationOptions, Mode=OneWay}"
DiscardSavedCredentialsCommand="{x:Bind ViewModel.LoginViewModel.DiscardSavedCredentialsCommand, Mode=OneWay}"
IsAlternativeLogin="{x:Bind ViewModel.LoginViewModel.IsAlternativeLogin, Mode=OneWay}"
IsLoginSequence="{x:Bind ViewModel.LoginViewModel.IsLoginSequence, Mode=OneWay}"
IsReadOnly="{x:Bind ViewModel.IsReadOnly, Mode=TwoWay}"
RecoverAccessCommand="{x:Bind ViewModel.RecoverAccessCommand, Mode=OneWay}"
RestartLoginCommand="{x:Bind ViewModel.LoginViewModel.RestartLoginProcessCommand, Mode=OneWay}"
SelectedAuthenticationOption="{x:Bind ViewModel.LoginViewModel.SelectedAuthenticationOption, Mode=TwoWay}"
SelectAuthenticationOptionCommand="{x:Bind ViewModel.LoginViewModel.SelectAuthenticationOptionCommand, Mode=OneWay}"
ShouldSaveCredentials="{x:Bind ViewModel.LoginViewModel.ShouldSaveCredentials, Mode=TwoWay}"
Visibility="{x:Bind ViewModel.LoginViewModel.CurrentViewModel, Mode=OneWay, Converter={StaticResource TypeNameVisibilityConverter}, ConverterParameter='MigrationViewModel,ErrorViewModel|invert'}" />
</StackPanel>
Expand Down
Loading
Loading