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
2 changes: 1 addition & 1 deletion src/Core/SecureFolderFS.Core/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,33 +10,62 @@ namespace SecureFolderFS.Core.DataModels
[Serializable]
public sealed record class V4VaultConfigurationDataModel : VersionDataModel
{
/// <summary>
/// Gets the ID for content encryption.
/// </summary>
[JsonPropertyName(Associations.ASSOC_CONTENT_CIPHER_ID)]
[DefaultValue("")]
public required string ContentCipherId { get; init; }

/// <summary>
/// Gets the ID for file name encryption.
/// </summary>
[JsonPropertyName(Associations.ASSOC_FILENAME_CIPHER_ID)]
[DefaultValue("")]
public required string FileNameCipherId { get; init; }

/// <summary>
/// Gets the ID for file name encoding.
/// </summary>
[JsonPropertyName(Associations.ASSOC_FILENAME_ENCODING_ID)]
[DefaultValue("")]
public string FileNameEncodingId { get; set; } = Cryptography.Constants.CipherId.ENCODING_BASE64URL;

/// <summary>
/// Gets the size of the recycle bin.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
[JsonPropertyName(Associations.ASSOC_RECYCLE_SIZE)]
[DefaultValue(0L)]
public long RecycleBinSize { get; set; } = 0L;
public long RecycleBinSize { get; set; }

/// <summary>
/// Gets the information about the authentication method used for this vault.
/// </summary>
[JsonPropertyName(Associations.ASSOC_AUTHENTICATION)]
[DefaultValue("")]
public required string AuthenticationMethod { get; set; } = string.Empty;

/// <summary>
/// Gets the unique identifier of the vault represented by a GUID.
/// </summary>
[JsonPropertyName(Associations.ASSOC_VAULT_ID)]
[DefaultValue("")]
public required string Uid { get; init; } = string.Empty;

/// <summary>
/// Gets the App Platform used by this vault.
/// </summary>
[JsonPropertyName(Associations.ASSOC_APP_PLATFORM)]
public AppPlatformVaultOptions? AppPlatform { get; init; }

/// <summary>
/// Gets the HMAC-SHA256 hash of the payload.
/// </summary>
[JsonPropertyName("hmacsha256mac")]
public byte[]? PayloadMac { get; set; }

Expand All @@ -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
};
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,14 @@ public sealed record class V4VaultKeystoreDataModel

/// <summary>
/// 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.
/// </summary>
[JsonPropertyName("c_softwareEntropy")]
public byte[]? EncryptedSoftwareEntropy { get; init; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,7 @@ public sealed record class VaultConfigurationDataModel : VersionDataModel
/// </remarks>
[JsonPropertyName(Associations.ASSOC_RECYCLE_SIZE)]
[DefaultValue(0L)]
public long RecycleBinSize { get; set; } = 0L;

///// <summary>
///// Gets the specialization of the vault that hints how the user data should be handled.
///// </summary>
//[JsonPropertyName(Associations.ASSOC_SPECIALIZATION)]
//[DefaultValue("")]
//public required string Specialization { get; init; } = string.Empty;
public long RecycleBinSize { get; set; }

/// <summary>
/// Gets the information about the authentication method used for this vault.
Expand Down
6 changes: 3 additions & 3 deletions src/Core/SecureFolderFS.Core/Models/SecurityWrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ namespace SecureFolderFS.Core.Models
internal sealed class SecurityWrapper : IWrapper<Security>, IEnumerable<KeyValuePair<string, object>>, IDisposable
{
private readonly KeyPair _keyPair;
private readonly VaultConfigurationDataModel _configDataModel;
private readonly V4VaultConfigurationDataModel _configDataModel;
private Security? _security;

/// <inheritdoc/>
Expand All @@ -22,10 +22,10 @@ internal sealed class SecurityWrapper : IWrapper<Security>, IEnumerable<KeyValue
fileNameCipherId: _configDataModel.FileNameCipherId,
fileNameEncodingId: _configDataModel.FileNameEncodingId);

public SecurityWrapper(KeyPair keyPair, VaultConfigurationDataModel configurationDataModel)
public SecurityWrapper(KeyPair keyPair, V4VaultConfigurationDataModel configDataModel)
{
_keyPair = keyPair;
_configDataModel = configurationDataModel;
_configDataModel = configDataModel;
}

/// <inheritdoc/>
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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);
Expand All @@ -82,16 +60,7 @@ public void V4SetCredentials(IKeyUsage passkey)
/// <inheritdoc/>
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);
}

/// <inheritdoc/>
Expand All @@ -105,19 +74,12 @@ public async Task<IDisposable> 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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
{
Expand All @@ -34,8 +33,7 @@ public ModifyCredentialsRoutine(VaultReader vaultReader, VaultWriter vaultWriter
/// <inheritdoc/>
public async Task InitAsync(CancellationToken cancellationToken = default)
{
await Task.CompletedTask;
//_existingV4KeystoreDataModel = await _vaultReader.ReadKeystoreAsync<V4VaultKeystoreDataModel>(cancellationToken);
_existingV4KeystoreDataModel = await _vaultReader.ReadKeystoreAsync<V4VaultKeystoreDataModel>(cancellationToken);
}

/// <inheritdoc/>
Expand All @@ -50,28 +48,18 @@ public void SetUnlockContract(IDisposable unlockContract)
/// <inheritdoc/>
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);
}

/// <inheritdoc/>
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)
Expand All @@ -80,26 +68,26 @@ public unsafe void SetCredentials(IKeyUsage passkey)
_keyPair.UseKeys(state, (dekKey, macKey, s) =>
{
var k = new ReadOnlySpan<byte>((byte*)s.keyPtr, s.keyLen);
_keystoreDataModel = VaultParser.V3EncryptKeystore(k, dekKey, macKey, salt);
_keystoreDataModel = VaultParser.V4EncryptKeystore(k, dekKey, macKey, salt);
});
}
});
}

/// <inheritdoc/>
[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<byte> softwareEntropy = stackalloc byte[32];
try
{
Expand All @@ -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);
Expand All @@ -126,7 +117,7 @@ public unsafe void V4SetCredentials(IKeyUsage oldPasskey, IKeyUsage newPasskey,
var nk = new ReadOnlySpan<byte>((byte*)s2.nkPtr, s2.nkLen);
var se = new Span<byte>((byte*)s2.outerState.sePtr, s2.outerState.seLen);

_v4KeystoreDataModel = VaultParser.V4ReEncryptKeystore(nk, dekKey, macKey, salt, se);
_keystoreDataModel = VaultParser.V4ReEncryptKeystore(nk, dekKey, macKey, salt, se);
});
}
});
Expand All @@ -142,24 +133,18 @@ public unsafe void V4SetCredentials(IKeyUsage oldPasskey, IKeyUsage newPasskey,
public async Task<IDisposable> 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)
Expand Down
Loading