Skip to content

Commit fc8ce0a

Browse files
committed
Enable Vault V4
1 parent 35edccb commit fc8ce0a

18 files changed

Lines changed: 153 additions & 210 deletions

src/Core/SecureFolderFS.Core/Constants.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ public static class Versions
5757
public const int V2 = 2;
5858
public const int V3 = 3;
5959
public const int V4 = 4;
60-
public const int LATEST_VERSION = V3;
60+
public const int LATEST_VERSION = V4;
6161
}
6262
}
6363

src/Core/SecureFolderFS.Core/DataModels/V4VaultConfigurationDataModel.cs

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,33 +10,62 @@ namespace SecureFolderFS.Core.DataModels
1010
[Serializable]
1111
public sealed record class V4VaultConfigurationDataModel : VersionDataModel
1212
{
13+
/// <summary>
14+
/// Gets the ID for content encryption.
15+
/// </summary>
1316
[JsonPropertyName(Associations.ASSOC_CONTENT_CIPHER_ID)]
1417
[DefaultValue("")]
1518
public required string ContentCipherId { get; init; }
1619

20+
/// <summary>
21+
/// Gets the ID for file name encryption.
22+
/// </summary>
1723
[JsonPropertyName(Associations.ASSOC_FILENAME_CIPHER_ID)]
1824
[DefaultValue("")]
1925
public required string FileNameCipherId { get; init; }
2026

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

34+
/// <summary>
35+
/// Gets the size of the recycle bin.
36+
/// </summary>
37+
/// <remarks>
38+
/// If the size is zero, the recycle bin is disabled.
39+
/// If the size is any value smaller than zero, the recycle bin has unlimited size capacity.
40+
/// Any values above zero indicate the maximum capacity in bytes that is allowed for the recycling operation to proceed.
41+
/// </remarks>
2542
[JsonPropertyName(Associations.ASSOC_RECYCLE_SIZE)]
2643
[DefaultValue(0L)]
27-
public long RecycleBinSize { get; set; } = 0L;
44+
public long RecycleBinSize { get; set; }
2845

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

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

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

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

@@ -55,21 +84,6 @@ public static V4VaultConfigurationDataModel V4FromVaultOptions(VaultOptions vaul
5584
PayloadMac = new byte[HMACSHA256.HashSizeInBytes]
5685
};
5786
}
58-
59-
public VaultConfigurationDataModel ToVaultConfigurationDataModel()
60-
{
61-
return new VaultConfigurationDataModel
62-
{
63-
Version = Version,
64-
ContentCipherId = ContentCipherId,
65-
FileNameCipherId = FileNameCipherId,
66-
FileNameEncodingId = FileNameEncodingId,
67-
AuthenticationMethod = AuthenticationMethod,
68-
RecycleBinSize = RecycleBinSize,
69-
Uid = Uid,
70-
PayloadMac = PayloadMac
71-
};
72-
}
7387
}
7488
}
7589

src/Core/SecureFolderFS.Core/DataModels/V4VaultKeystoreDataModel.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,14 @@ public sealed record class V4VaultKeystoreDataModel
2626

2727
/// <summary>
2828
/// Gets the AES-256-GCM ciphertext of the 256-bit SoftwareEntropy value.
29-
/// SoftwareEntropy is a CSPRNG secret generated at vault creation that is mixed
30-
/// into the Argon2id input via HKDF-Extract, raising the quantum security floor
31-
/// of all authentication methods to 256 bits regardless of auth factor entropy.
29+
/// SoftwareEntropy is a CSPRNG secret mixed into Argon2id input via HKDF-Extract,
30+
/// raising the quantum security floor of all authentication methods to 256 bits
31+
/// regardless of auth factor entropy.
3232
/// It is encrypted under a key derived from the passkey so all active auth
3333
/// factors are required to recover it.
34+
///
35+
/// The value is generated at vault creation and can also be rotated during
36+
/// credential changes when rebuilding the V4 keystore.
3437
/// </summary>
3538
[JsonPropertyName("c_softwareEntropy")]
3639
public byte[]? EncryptedSoftwareEntropy { get; init; }

src/Core/SecureFolderFS.Core/DataModels/VaultConfigurationDataModel.cs

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,7 @@ public sealed record class VaultConfigurationDataModel : VersionDataModel
4141
/// </remarks>
4242
[JsonPropertyName(Associations.ASSOC_RECYCLE_SIZE)]
4343
[DefaultValue(0L)]
44-
public long RecycleBinSize { get; set; } = 0L;
45-
46-
///// <summary>
47-
///// Gets the specialization of the vault that hints how the user data should be handled.
48-
///// </summary>
49-
//[JsonPropertyName(Associations.ASSOC_SPECIALIZATION)]
50-
//[DefaultValue("")]
51-
//public required string Specialization { get; init; } = string.Empty;
44+
public long RecycleBinSize { get; set; }
5245

5346
/// <summary>
5447
/// Gets the information about the authentication method used for this vault.

src/Core/SecureFolderFS.Core/Models/SecurityWrapper.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ namespace SecureFolderFS.Core.Models
1212
internal sealed class SecurityWrapper : IWrapper<Security>, IEnumerable<KeyValuePair<string, object>>, IDisposable
1313
{
1414
private readonly KeyPair _keyPair;
15-
private readonly VaultConfigurationDataModel _configDataModel;
15+
private readonly V4VaultConfigurationDataModel _configDataModel;
1616
private Security? _security;
1717

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

25-
public SecurityWrapper(KeyPair keyPair, VaultConfigurationDataModel configurationDataModel)
25+
public SecurityWrapper(KeyPair keyPair, V4VaultConfigurationDataModel configDataModel)
2626
{
2727
_keyPair = keyPair;
28-
_configDataModel = configurationDataModel;
28+
_configDataModel = configDataModel;
2929
}
3030

3131
/// <inheritdoc/>
Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1-
namespace SecureFolderFS.Core.Routines
1+
using SecureFolderFS.Shared.ComponentModel;
2+
using System.Threading;
3+
4+
namespace SecureFolderFS.Core.Routines
25
{
36
public interface IModifyCredentialsRoutine : ICredentialsRoutine, IContractRoutine, IOptionsRoutine
47
{
8+
void SetCredentials(IKeyUsage oldPasskey, IKeyUsage newPasskey, CancellationToken cancellationToken = default);
59
}
610
}

src/Core/SecureFolderFS.Core/Routines/Operational/CreationRoutine.cs

Lines changed: 7 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,8 @@ internal sealed class CreationRoutine : ICreationRoutine
1919
{
2020
private readonly IFolder _vaultFolder;
2121
private readonly VaultWriter _vaultWriter;
22-
private V3VaultKeystoreDataModel? _keystoreDataModel;
23-
private V4VaultKeystoreDataModel? _v4KeystoreDataModel;
24-
private VaultConfigurationDataModel? _configDataModel;
25-
private V4VaultConfigurationDataModel? _v4ConfigDataModel;
22+
private V4VaultKeystoreDataModel? _keystoreDataModel;
23+
private V4VaultConfigurationDataModel? _configDataModel;
2624
private IKeyUsage? _dekKey;
2725
private IKeyUsage? _macKey;
2826

@@ -46,33 +44,13 @@ public void SetCredentials(IKeyUsage passkey)
4644
var macKey = new byte[KeyTraits.MAC_KEY_LENGTH];
4745
var salt = new byte[KeyTraits.SALT_LENGTH];
4846

49-
// Fill keys
50-
RandomNumberGenerator.Fill(dekKey);
51-
RandomNumberGenerator.Fill(macKey);
52-
RandomNumberGenerator.Fill(salt);
53-
54-
// Generate keystore
55-
_keystoreDataModel = passkey.UseKey(key => VaultParser.V3EncryptKeystore(key, dekKey, macKey, salt));
56-
57-
// Create key copies for later use
58-
_dekKey = SecureKey.TakeOwnership(dekKey);
59-
_macKey = SecureKey.TakeOwnership(macKey);
60-
}
61-
62-
public void V4SetCredentials(IKeyUsage passkey)
63-
{
64-
// Allocate keys for later use
65-
var dekKey = new byte[KeyTraits.DEK_KEY_LENGTH];
66-
var macKey = new byte[KeyTraits.MAC_KEY_LENGTH];
67-
var salt = new byte[KeyTraits.SALT_LENGTH];
68-
6947
// Fill keys and salt
7048
RandomNumberGenerator.Fill(dekKey);
7149
RandomNumberGenerator.Fill(macKey);
7250
RandomNumberGenerator.Fill(salt);
7351

74-
// Generate V4 keystore — SoftwareEntropy is generated internally by V4EncryptKeystore
75-
_v4KeystoreDataModel = passkey.UseKey(key => VaultParser.V4EncryptKeystore(key, dekKey, macKey, salt));
52+
// Generate V4 keystore
53+
_keystoreDataModel = passkey.UseKey(key => VaultParser.V4EncryptKeystore(key, dekKey, macKey, salt));
7654

7755
// Create key copies for later use
7856
_dekKey = SecureKey.TakeOwnership(dekKey);
@@ -82,16 +60,7 @@ public void V4SetCredentials(IKeyUsage passkey)
8260
/// <inheritdoc/>
8361
public void SetOptions(VaultOptions vaultOptions)
8462
{
85-
if (vaultOptions.AppPlatform is null)
86-
{
87-
_configDataModel = VaultConfigurationDataModel.FromVaultOptions(vaultOptions);
88-
_v4ConfigDataModel = null;
89-
}
90-
else
91-
{
92-
_v4ConfigDataModel = V4VaultConfigurationDataModel.V4FromVaultOptions(vaultOptions);
93-
_configDataModel = _v4ConfigDataModel.ToVaultConfigurationDataModel();
94-
}
63+
_configDataModel = V4VaultConfigurationDataModel.V4FromVaultOptions(vaultOptions);
9564
}
9665

9766
/// <inheritdoc/>
@@ -105,19 +74,12 @@ public async Task<IDisposable> FinalizeAsync(CancellationToken cancellationToken
10574
// First, we need to fill in the PayloadMac of the content
10675
_macKey.UseKey(macKey =>
10776
{
108-
if (_v4ConfigDataModel is not null)
109-
VaultParser.V4CalculateConfigMac(_v4ConfigDataModel, macKey, _v4ConfigDataModel.PayloadMac);
110-
else
111-
VaultParser.CalculateConfigMac(_configDataModel, macKey, _configDataModel.PayloadMac);
77+
VaultParser.V4CalculateConfigMac(_configDataModel, macKey, _configDataModel.PayloadMac);
11278
});
11379

11480
// Write the whole configuration
11581
await _vaultWriter.WriteKeystoreAsync(_keystoreDataModel, cancellationToken);
116-
//await _vaultWriter.WriteV4KeystoreAsync(_v4KeystoreDataModel, cancellationToken);
117-
if (_v4ConfigDataModel is not null)
118-
await _vaultWriter.WriteV4ConfigurationAsync(_v4ConfigDataModel, cancellationToken);
119-
else
120-
await _vaultWriter.WriteConfigurationAsync(_configDataModel, cancellationToken);
82+
await _vaultWriter.WriteV4ConfigurationAsync(_configDataModel, cancellationToken);
12183

12284
// Create the content folder
12385
if (_vaultFolder is IModifiableFolder modifiableFolder)

src/Core/SecureFolderFS.Core/Routines/Operational/ModifyCredentialsRoutine.cs

Lines changed: 20 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using SecureFolderFS.Core.Models;
1010
using SecureFolderFS.Core.VaultAccess;
1111
using SecureFolderFS.Shared.ComponentModel;
12+
using SecureFolderFS.Shared.Extensions;
1213
using SecureFolderFS.Shared.Models;
1314

1415
namespace SecureFolderFS.Core.Routines.Operational
@@ -20,10 +21,8 @@ internal sealed class ModifyCredentialsRoutine : IModifyCredentialsRoutine
2021
private readonly VaultWriter _vaultWriter;
2122
private KeyPair? _keyPair;
2223
private V4VaultKeystoreDataModel? _existingV4KeystoreDataModel;
23-
private V3VaultKeystoreDataModel? _keystoreDataModel;
24-
private V4VaultKeystoreDataModel? _v4KeystoreDataModel;
25-
private VaultConfigurationDataModel? _configDataModel;
26-
private V4VaultConfigurationDataModel? _v4ConfigDataModel;
24+
private V4VaultKeystoreDataModel? _keystoreDataModel;
25+
private V4VaultConfigurationDataModel? _configDataModel;
2726

2827
public ModifyCredentialsRoutine(VaultReader vaultReader, VaultWriter vaultWriter)
2928
{
@@ -34,8 +33,7 @@ public ModifyCredentialsRoutine(VaultReader vaultReader, VaultWriter vaultWriter
3433
/// <inheritdoc/>
3534
public async Task InitAsync(CancellationToken cancellationToken = default)
3635
{
37-
await Task.CompletedTask;
38-
//_existingV4KeystoreDataModel = await _vaultReader.ReadKeystoreAsync<V4VaultKeystoreDataModel>(cancellationToken);
36+
_existingV4KeystoreDataModel = await _vaultReader.ReadKeystoreAsync<V4VaultKeystoreDataModel>(cancellationToken);
3937
}
4038

4139
/// <inheritdoc/>
@@ -50,28 +48,18 @@ public void SetUnlockContract(IDisposable unlockContract)
5048
/// <inheritdoc/>
5149
public void SetOptions(VaultOptions vaultOptions)
5250
{
53-
if (vaultOptions.AppPlatform is null)
54-
{
55-
_configDataModel = VaultConfigurationDataModel.FromVaultOptions(vaultOptions);
56-
_v4ConfigDataModel = null;
57-
}
58-
else
59-
{
60-
_v4ConfigDataModel = V4VaultConfigurationDataModel.V4FromVaultOptions(vaultOptions);
61-
_configDataModel = _v4ConfigDataModel.ToVaultConfigurationDataModel();
62-
}
51+
_configDataModel = V4VaultConfigurationDataModel.V4FromVaultOptions(vaultOptions);
6352
}
6453

6554
/// <inheritdoc/>
6655
public unsafe void SetCredentials(IKeyUsage passkey)
6756
{
6857
ArgumentNullException.ThrowIfNull(_keyPair);
6958

70-
// Generate new salt
59+
// Recovery/unlock-contract flow: rotate to a fresh entropy value under the new passkey.
7160
var salt = new byte[Cryptography.Constants.KeyTraits.SALT_LENGTH];
7261
RandomNumberGenerator.Fill(salt);
7362

74-
// Encrypt a new keystore
7563
passkey.UseKey(key =>
7664
{
7765
fixed (byte* keyPtr = key)
@@ -80,26 +68,26 @@ public unsafe void SetCredentials(IKeyUsage passkey)
8068
_keyPair.UseKeys(state, (dekKey, macKey, s) =>
8169
{
8270
var k = new ReadOnlySpan<byte>((byte*)s.keyPtr, s.keyLen);
83-
_keystoreDataModel = VaultParser.V3EncryptKeystore(k, dekKey, macKey, salt);
71+
_keystoreDataModel = VaultParser.V4EncryptKeystore(k, dekKey, macKey, salt);
8472
});
8573
}
8674
});
8775
}
8876

77+
/// <inheritdoc/>
8978
[SkipLocalsInit]
90-
public unsafe void V4SetCredentials(IKeyUsage oldPasskey, IKeyUsage newPasskey, CancellationToken cancellationToken = default)
79+
public unsafe void SetCredentials(IKeyUsage oldPasskey, IKeyUsage newPasskey, CancellationToken cancellationToken = default)
9180
{
9281
ArgumentNullException.ThrowIfNull(_keyPair);
9382
ArgumentNullException.ThrowIfNull(_existingV4KeystoreDataModel);
9483

95-
// Generate new salt for the re-encrypted keystore
9684
var salt = new byte[Cryptography.Constants.KeyTraits.SALT_LENGTH];
9785
RandomNumberGenerator.Fill(salt);
9886

99-
// Decrypt existing SoftwareEntropy using the old passkey, then re-encrypt
100-
// it under the new passkey alongside the (unchanged) DEK and MAC keys.
101-
// SoftwareEntropy must be preserved - regenerating it would change the KEK
102-
// derivation and make the vault permanently unreadable.
87+
// Optional step-up flow: preserve existing entropy by decrypting it with the old passkey
88+
// and re-encrypting it under the new passkey next to unchanged DEK and MAC keys.
89+
// If old passkey material is unavailable (for example recovery-key driven rotation),
90+
// the single-passkey overload rotates to fresh entropy and still yields a valid keystore.
10391
Span<byte> softwareEntropy = stackalloc byte[32];
10492
try
10593
{
@@ -113,6 +101,9 @@ public unsafe void V4SetCredentials(IKeyUsage oldPasskey, IKeyUsage newPasskey,
113101
});
114102
}
115103

104+
if (softwareEntropy.IsAllZeros())
105+
throw new CryptographicException("The old passkey material is unavailable.");
106+
116107
fixed (byte* softwareEntropyPtr = softwareEntropy)
117108
{
118109
var state = (sePtr: (nint)softwareEntropyPtr, seLen: softwareEntropy.Length);
@@ -126,7 +117,7 @@ public unsafe void V4SetCredentials(IKeyUsage oldPasskey, IKeyUsage newPasskey,
126117
var nk = new ReadOnlySpan<byte>((byte*)s2.nkPtr, s2.nkLen);
127118
var se = new Span<byte>((byte*)s2.outerState.sePtr, s2.outerState.seLen);
128119

129-
_v4KeystoreDataModel = VaultParser.V4ReEncryptKeystore(nk, dekKey, macKey, salt, se);
120+
_keystoreDataModel = VaultParser.V4ReEncryptKeystore(nk, dekKey, macKey, salt, se);
130121
});
131122
}
132123
});
@@ -142,24 +133,18 @@ public unsafe void V4SetCredentials(IKeyUsage oldPasskey, IKeyUsage newPasskey,
142133
public async Task<IDisposable> FinalizeAsync(CancellationToken cancellationToken)
143134
{
144135
ArgumentNullException.ThrowIfNull(_keyPair);
136+
ArgumentNullException.ThrowIfNull(_keystoreDataModel);
145137
ArgumentNullException.ThrowIfNull(_configDataModel);
146138

147139
// First, we need to fill in the PayloadMac of the content
148140
_keyPair.MacKey.UseKey(macKey =>
149141
{
150-
if (_v4ConfigDataModel is not null)
151-
VaultParser.V4CalculateConfigMac(_v4ConfigDataModel, macKey, _v4ConfigDataModel.PayloadMac);
152-
else
153-
VaultParser.CalculateConfigMac(_configDataModel, macKey, _configDataModel.PayloadMac);
142+
VaultParser.V4CalculateConfigMac(_configDataModel, macKey, _configDataModel.PayloadMac);
154143
});
155144

156145
// Write the whole configuration
157146
await _vaultWriter.WriteKeystoreAsync(_keystoreDataModel, cancellationToken);
158-
//await _vaultWriter.WriteKeystoreAsync(_v4KeystoreDataModel, cancellationToken);
159-
if (_v4ConfigDataModel is not null)
160-
await _vaultWriter.WriteV4ConfigurationAsync(_v4ConfigDataModel, cancellationToken);
161-
else
162-
await _vaultWriter.WriteConfigurationAsync(_configDataModel, cancellationToken);
147+
await _vaultWriter.WriteV4ConfigurationAsync(_configDataModel, cancellationToken);
163148

164149
// Key copies need to be created because the original ones are disposed of here
165150
using (_keyPair)

0 commit comments

Comments
 (0)