Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
29 changes: 29 additions & 0 deletions src/Core/SecureFolderFS.Core/DataModels/VaultSharesDataModel.cs
Original file line number Diff line number Diff line change
@@ -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<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,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<byte> 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);
}
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;
}
}

public async Task<VersionDataModel> ReadVersionAsync(CancellationToken cancellationToken)
{
// Get configuration file
Expand Down
24 changes: 24 additions & 0 deletions src/Core/SecureFolderFS.Core/VaultAccess/VaultWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
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 @@ -80,6 +80,14 @@
<!-- Confirmation Panel -->
<VerticalStackLayout IsVisible="{Binding IsRemoving, Mode=OneWay, Converter={StaticResource BoolInvertConverter}}" Spacing="16">
<uc:RegisterControl CurrentViewModel="{Binding RegisterViewModel.CurrentViewModel, Mode=OneWay}" />
<HorizontalStackLayout Spacing="8">
<CheckBox
IsChecked="{Binding IsComplementing, Mode=TwoWay}"
IsEnabled="{Binding IsComplementationAvailable, Mode=OneWay}"
VerticalOptions="Center" />
<Label Text="{l:ResourceString Rid=UseAsAlternativeLogin}" VerticalOptions="Center" />
</HorizontalStackLayout>

<Label Text="{l:ResourceString Rid=RecoveryKeyTip}" />
</VerticalStackLayout>

Expand Down
Loading
Loading