Skip to content
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="Use as substitute" VerticalOptions="Center" />
</HorizontalStackLayout>

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

Expand Down
Loading
Loading