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
9 changes: 6 additions & 3 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
[submodule "lib/Tmds.Fuse"]
path = lib/Tmds.Fuse
url = https://github.com/securefolderfs-community/Tmds.Fuse
[submodule "src/Platforms/SecureFolderFS.Dashboard"]
path = src/Platforms/SecureFolderFS.Dashboard
url = git@github.com:securefolderfs-community/SecureFolderFS.Dashboard.git
[submodule "src/Platforms/SecureFolderFS.AppPlatform"]
path = src/Platforms/SecureFolderFS.AppPlatform
url = git@github.com:securefolderfs-community/SecureFolderFS.AppPlatform.git
[submodule "src/Platforms/SecureFolderFS.AppPlatform.Server"]
path = src/Platforms/SecureFolderFS.AppPlatform.Server
url = git@github.com:securefolderfs-community/SecureFolderFS.AppPlatform.Server.git
21 changes: 20 additions & 1 deletion SecureFolderFS.sln
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{724C2A9B
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SecureFolderFS.Tests", "tests\SecureFolderFS.Tests\SecureFolderFS.Tests.csproj", "{67ED86B1-D287-4F36-A8BE-189F68502B4C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SecureFolderFS.Dashboard", "src\Platforms\SecureFolderFS.Dashboard\SecureFolderFS.Dashboard.csproj", "{9CF66911-1E7E-4A82-B7B4-97B2DE8BA9B0}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SecureFolderFS.AppPlatform", "src\Platforms\SecureFolderFS.AppPlatform\SecureFolderFS.AppPlatform.csproj", "{9CF66911-1E7E-4A82-B7B4-97B2DE8BA9B0}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SecureFolderFS.Sdk.Ftp", "src\Sdk\SecureFolderFS.Sdk.Ftp\SecureFolderFS.Sdk.Ftp.csproj", "{17592A5B-EFB4-478C-87A1-C4A10BDECA50}"
EndProject
Expand All @@ -82,6 +82,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SecureFolderFS.Sdk.Dropbox"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SecureFolderFS.Sdk.WebDavClient", "src\Sdk\SecureFolderFS.Sdk.WebDavClient\SecureFolderFS.Sdk.WebDavClient.csproj", "{E9D21865-C31B-49AD-B9CE-A8A9491789D5}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SecureFolderFS.AppPlatform.Server", "src\Platforms\SecureFolderFS.AppPlatform.Server\SecureFolderFS.AppPlatform.Server.csproj", "{4440EBF8-9707-41DD-A723-F52987F83E1F}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -558,6 +560,22 @@ Global
{E9D21865-C31B-49AD-B9CE-A8A9491789D5}.Release|x64.Build.0 = Release|Any CPU
{E9D21865-C31B-49AD-B9CE-A8A9491789D5}.Release|x86.ActiveCfg = Release|Any CPU
{E9D21865-C31B-49AD-B9CE-A8A9491789D5}.Release|x86.Build.0 = Release|Any CPU
{4440EBF8-9707-41DD-A723-F52987F83E1F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4440EBF8-9707-41DD-A723-F52987F83E1F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4440EBF8-9707-41DD-A723-F52987F83E1F}.Debug|arm64.ActiveCfg = Debug|Any CPU
{4440EBF8-9707-41DD-A723-F52987F83E1F}.Debug|arm64.Build.0 = Debug|Any CPU
{4440EBF8-9707-41DD-A723-F52987F83E1F}.Debug|x64.ActiveCfg = Debug|Any CPU
{4440EBF8-9707-41DD-A723-F52987F83E1F}.Debug|x64.Build.0 = Debug|Any CPU
{4440EBF8-9707-41DD-A723-F52987F83E1F}.Debug|x86.ActiveCfg = Debug|Any CPU
{4440EBF8-9707-41DD-A723-F52987F83E1F}.Debug|x86.Build.0 = Debug|Any CPU
{4440EBF8-9707-41DD-A723-F52987F83E1F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4440EBF8-9707-41DD-A723-F52987F83E1F}.Release|Any CPU.Build.0 = Release|Any CPU
{4440EBF8-9707-41DD-A723-F52987F83E1F}.Release|arm64.ActiveCfg = Release|Any CPU
{4440EBF8-9707-41DD-A723-F52987F83E1F}.Release|arm64.Build.0 = Release|Any CPU
{4440EBF8-9707-41DD-A723-F52987F83E1F}.Release|x64.ActiveCfg = Release|Any CPU
{4440EBF8-9707-41DD-A723-F52987F83E1F}.Release|x64.Build.0 = Release|Any CPU
{4440EBF8-9707-41DD-A723-F52987F83E1F}.Release|x86.ActiveCfg = Release|Any CPU
{4440EBF8-9707-41DD-A723-F52987F83E1F}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -596,6 +614,7 @@ Global
{85FE77EA-9F89-4F42-BD79-26C82F847DDC} = {086CDAC6-2730-4F09-BA28-B41F737E6C4D}
{FD52B782-4E07-41B2-8EA9-DE2347DEB9E2} = {086CDAC6-2730-4F09-BA28-B41F737E6C4D}
{E9D21865-C31B-49AD-B9CE-A8A9491789D5} = {086CDAC6-2730-4F09-BA28-B41F737E6C4D}
{4440EBF8-9707-41DD-A723-F52987F83E1F} = {66BC1E2B-D99A-49E2-8B8F-EF7851493CB0}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {A1906FD8-BB54-4688-BC0F-9ED7532D2CB0}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ public static async Task RestoreAsync(IStorableChild recycleBinItem, IModifiable
// A new item name should be chosen fit for the new folder (so that Directory ID match)
var ciphertextName = await AbstractPathHelpers.EncryptNameAsync(plaintextOriginalName, ciphertextDestinationFolder, specifics, cancellationToken);

// Get an available name if the destination already exists
ciphertextName = await GetAvailableDestinationNameAsync(ciphertextDestinationFolder, ciphertextName, plaintextOriginalName, specifics, cancellationToken);

// Rename and move item to destination
_ = await ciphertextDestinationFolder.MoveStorableFromAsync(recycleBinItem, modifiableRecycleBin, false, ciphertextName, null, cancellationToken);
}
Expand All @@ -89,6 +92,9 @@ public static async Task RestoreAsync(IStorableChild recycleBinItem, IModifiable
// The same name could be used since the Directory IDs match
var ciphertextName = Path.ChangeExtension(await AbstractPathHelpers.EncryptNameAsync(plaintextOriginalName, ciphertextDestinationFolder, specifics, cancellationToken), Constants.Names.ENCRYPTED_FILE_EXTENSION);

// Get an available name if the destination already exists
ciphertextName = await GetAvailableDestinationNameAsync(ciphertextDestinationFolder, ciphertextName, plaintextOriginalName, specifics, cancellationToken);

// Rename and move item to destination
_ = await ciphertextDestinationFolder.MoveStorableFromAsync(recycleBinItem, modifiableRecycleBin, false, ciphertextName, null, cancellationToken);
}
Expand Down Expand Up @@ -216,6 +222,28 @@ public static async Task DeleteOrRecycleAsync(
}
}

private static async Task<string> GetAvailableDestinationNameAsync(IFolder ciphertextDestinationFolder, string ciphertextName, string plaintextOriginalName, FileSystemSpecifics specifics, CancellationToken cancellationToken)
{
// Check if the item already exists
var existing = await ciphertextDestinationFolder.TryGetFirstByNameAsync(ciphertextName, cancellationToken);
if (existing is not null)
{
// If the item already exists, append a suffix to the name
var nameWithoutExtension = Path.GetFileNameWithoutExtension(plaintextOriginalName);
var extension = Path.GetExtension(plaintextOriginalName);
var suffix = 1;
do
{
var newPlaintextName = $"{nameWithoutExtension} ({suffix}){extension}";
ciphertextName = Path.ChangeExtension(await AbstractPathHelpers.EncryptNameAsync(newPlaintextName, ciphertextDestinationFolder, specifics, cancellationToken), Constants.Names.ENCRYPTED_FILE_EXTENSION);
existing = await ciphertextDestinationFolder.TryGetFirstByNameAsync(ciphertextName, cancellationToken);
suffix++;
} while (existing is not null);
}

return ciphertextName;
}

private static async Task<bool> IsRecentlyCreatedAsync(IStorable storable, CancellationToken cancellationToken)
{
try
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ public override async Task<IResult> ValidateResultAsync((IFolder, IProgress<IRes

await foreach (var item in scannedFolder.GetItemsAsync(StorableType.All, cancellationToken).ConfigureAwait(false))
{
cancellationToken.ThrowIfCancellationRequested();
if (PathHelpers.IsCoreName(item.Name))
continue;

Expand Down
2 changes: 2 additions & 0 deletions src/Core/SecureFolderFS.Core/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public static class Authentication
public const string AUTH_APPLE_BIOMETRIC = "apple_secure_enclave";
public const string AUTH_ANDROID_BIOMETRIC = "android_biometrics";
public const string AUTH_DEVICE_LINK = "device_link";
public const string AUTH_APP_PLATFORM = "app_platform";
}

[Obsolete]
Expand All @@ -46,6 +47,7 @@ public static class Associations
public const string ASSOC_SPECIALIZATION = "spec";
public const string ASSOC_AUTHENTICATION = "authMode";
public const string ASSOC_VAULT_ID = "vaultId";
public const string ASSOC_APP_PLATFORM = "appPlatform";
public const string ASSOC_VERSION = "version";
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
using SecureFolderFS.Shared.Models;
using System;
using System.ComponentModel;
using System.Security.Cryptography;
using System.Text.Json.Serialization;
using static SecureFolderFS.Core.Constants.Vault;

namespace SecureFolderFS.Core.DataModels
{
[Serializable]
public sealed record class V4VaultConfigurationDataModel : VersionDataModel
{
[JsonPropertyName(Associations.ASSOC_CONTENT_CIPHER_ID)]
[DefaultValue("")]
public required string ContentCipherId { get; init; }

[JsonPropertyName(Associations.ASSOC_FILENAME_CIPHER_ID)]
[DefaultValue("")]
public required string FileNameCipherId { get; init; }

[JsonPropertyName(Associations.ASSOC_FILENAME_ENCODING_ID)]
[DefaultValue("")]
public string FileNameEncodingId { get; set; } = Cryptography.Constants.CipherId.ENCODING_BASE64URL;

[JsonPropertyName(Associations.ASSOC_RECYCLE_SIZE)]
[DefaultValue(0L)]
public long RecycleBinSize { get; set; } = 0L;

[JsonPropertyName(Associations.ASSOC_AUTHENTICATION)]
[DefaultValue("")]
public required string AuthenticationMethod { get; set; } = string.Empty;

[JsonPropertyName(Associations.ASSOC_VAULT_ID)]
[DefaultValue("")]
public required string Uid { get; init; } = string.Empty;

[JsonPropertyName(Associations.ASSOC_APP_PLATFORM)]
public AppPlatformVaultOptions? AppPlatform { get; init; }

[JsonPropertyName("hmacsha256mac")]
public byte[]? PayloadMac { get; set; }

public static V4VaultConfigurationDataModel V4FromVaultOptions(VaultOptions vaultOptions)
{
return new()
{
Version = vaultOptions.Version < 1 ? Versions.LATEST_VERSION : vaultOptions.Version,
ContentCipherId = vaultOptions.ContentCipherId ?? Cryptography.Constants.CipherId.XCHACHA20_POLY1305,
FileNameCipherId = vaultOptions.FileNameCipherId ?? Cryptography.Constants.CipherId.AES_SIV,
FileNameEncodingId = vaultOptions.NameEncodingId ?? Cryptography.Constants.CipherId.ENCODING_BASE64URL,
AuthenticationMethod = vaultOptions.UnlockProcedure.ToString(),
RecycleBinSize = vaultOptions.RecycleBinSize,
Uid = vaultOptions.VaultId ?? Guid.NewGuid().ToString(),
AppPlatform = vaultOptions.AppPlatform,
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
@@ -1,8 +1,8 @@
using SecureFolderFS.Shared.Models;
using System;
using System;
using System.ComponentModel;
using System.Security.Cryptography;
using System.Text.Json.Serialization;
using SecureFolderFS.Shared.Models;
using static SecureFolderFS.Core.Constants.Vault;

namespace SecureFolderFS.Core.DataModels
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ internal sealed class CreationRoutine : ICreationRoutine
private V3VaultKeystoreDataModel? _keystoreDataModel;
private V4VaultKeystoreDataModel? _v4KeystoreDataModel;
private VaultConfigurationDataModel? _configDataModel;
private V4VaultConfigurationDataModel? _v4ConfigDataModel;
private IKeyUsage? _dekKey;
private IKeyUsage? _macKey;

Expand Down Expand Up @@ -81,7 +82,16 @@ public void V4SetCredentials(IKeyUsage passkey)
/// <inheritdoc/>
public void SetOptions(VaultOptions vaultOptions)
{
_configDataModel = VaultConfigurationDataModel.FromVaultOptions(vaultOptions);
if (vaultOptions.AppPlatform is null)
{
_configDataModel = VaultConfigurationDataModel.FromVaultOptions(vaultOptions);
_v4ConfigDataModel = null;
}
else
{
_v4ConfigDataModel = V4VaultConfigurationDataModel.V4FromVaultOptions(vaultOptions);
_configDataModel = _v4ConfigDataModel.ToVaultConfigurationDataModel();
}
}

/// <inheritdoc/>
Expand All @@ -95,13 +105,19 @@ public async Task<IDisposable> FinalizeAsync(CancellationToken cancellationToken
// First, we need to fill in the PayloadMac of the content
_macKey.UseKey(macKey =>
{
VaultParser.CalculateConfigMac(_configDataModel, macKey, _configDataModel.PayloadMac);
if (_v4ConfigDataModel is not null)
VaultParser.V4CalculateConfigMac(_v4ConfigDataModel, macKey, _v4ConfigDataModel.PayloadMac);
else
VaultParser.CalculateConfigMac(_configDataModel, macKey, _configDataModel.PayloadMac);
});

// Write the whole configuration
await _vaultWriter.WriteKeystoreAsync(_keystoreDataModel, cancellationToken);
//await _vaultWriter.WriteV4KeystoreAsync(_v4KeystoreDataModel, cancellationToken);
await _vaultWriter.WriteConfigurationAsync(_configDataModel, cancellationToken);
if (_v4ConfigDataModel is not null)
await _vaultWriter.WriteV4ConfigurationAsync(_v4ConfigDataModel, cancellationToken);
else
await _vaultWriter.WriteConfigurationAsync(_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 @@ -23,6 +23,7 @@ internal sealed class ModifyCredentialsRoutine : IModifyCredentialsRoutine
private V3VaultKeystoreDataModel? _keystoreDataModel;
private V4VaultKeystoreDataModel? _v4KeystoreDataModel;
private VaultConfigurationDataModel? _configDataModel;
private V4VaultConfigurationDataModel? _v4ConfigDataModel;

public ModifyCredentialsRoutine(VaultReader vaultReader, VaultWriter vaultWriter)
{
Expand All @@ -49,7 +50,16 @@ public void SetUnlockContract(IDisposable unlockContract)
/// <inheritdoc/>
public void SetOptions(VaultOptions vaultOptions)
{
_configDataModel = VaultConfigurationDataModel.FromVaultOptions(vaultOptions);
if (vaultOptions.AppPlatform is null)
{
_configDataModel = VaultConfigurationDataModel.FromVaultOptions(vaultOptions);
_v4ConfigDataModel = null;
}
else
{
_v4ConfigDataModel = V4VaultConfigurationDataModel.V4FromVaultOptions(vaultOptions);
_configDataModel = _v4ConfigDataModel.ToVaultConfigurationDataModel();
}
}

/// <inheritdoc/>
Expand Down Expand Up @@ -137,13 +147,19 @@ public async Task<IDisposable> FinalizeAsync(CancellationToken cancellationToken
// First, we need to fill in the PayloadMac of the content
_keyPair.MacKey.UseKey(macKey =>
{
VaultParser.CalculateConfigMac(_configDataModel, macKey, _configDataModel.PayloadMac);
if (_v4ConfigDataModel is not null)
VaultParser.V4CalculateConfigMac(_v4ConfigDataModel, macKey, _v4ConfigDataModel.PayloadMac);
else
VaultParser.CalculateConfigMac(_configDataModel, macKey, _configDataModel.PayloadMac);
});

// Write the whole configuration
await _vaultWriter.WriteKeystoreAsync(_keystoreDataModel, cancellationToken);
//await _vaultWriter.WriteKeystoreAsync(_v4KeystoreDataModel, cancellationToken);
await _vaultWriter.WriteConfigurationAsync(_configDataModel, cancellationToken);
if (_v4ConfigDataModel is not null)
await _vaultWriter.WriteV4ConfigurationAsync(_v4ConfigDataModel, cancellationToken);
else
await _vaultWriter.WriteConfigurationAsync(_configDataModel, cancellationToken);

// Key copies need to be created because the original ones are disposed of here
using (_keyPair)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public sealed class RecoverRoutine : ICredentialsRoutine, IFinalizationRoutine
{
private readonly VaultReader _vaultReader;
private VaultConfigurationDataModel? _configDataModel;
private V4VaultConfigurationDataModel? _v4ConfigDataModel;
private KeyPair? _keyPair;

public RecoverRoutine(VaultReader vaultReader)
Expand All @@ -26,6 +27,18 @@ public RecoverRoutine(VaultReader vaultReader)
public async Task InitAsync(CancellationToken cancellationToken)
{
_configDataModel = await _vaultReader.ReadConfigurationAsync(cancellationToken);

if (_configDataModel.AuthenticationMethod.Contains(Constants.Vault.Authentication.AUTH_APP_PLATFORM, StringComparison.Ordinal))
{
try
{
_v4ConfigDataModel = await _vaultReader.ReadV4ConfigurationAsync(cancellationToken);
}
catch (Exception)
{
_v4ConfigDataModel = null;
}
}
}

/// <inheritdoc/>
Expand All @@ -44,7 +57,10 @@ public async Task<IDisposable> FinalizeAsync(CancellationToken cancellationToken
{
// Check if the payload has not been tampered with
var validator = new ConfigurationValidator(_keyPair.MacKey);
await validator.ValidateAsync(_configDataModel, cancellationToken);
if (_v4ConfigDataModel is not null)
await validator.V4ValidateAsync(_v4ConfigDataModel, cancellationToken);
else
await validator.ValidateAsync(_configDataModel, cancellationToken);

// In this case, we rely on the consumer to take ownership of the keys, and thus manage their lifetimes
// Key copies need to be created because the original ones are disposed of here
Expand Down
Loading