diff --git a/src/Core/SecureFolderFS.Core.Cryptography/Cipher/AesCtr256.cs b/src/Core/SecureFolderFS.Core.Cryptography/Cipher/AesCtr256.cs index 1a757260c..b88bbb977 100644 --- a/src/Core/SecureFolderFS.Core.Cryptography/Cipher/AesCtr256.cs +++ b/src/Core/SecureFolderFS.Core.Cryptography/Cipher/AesCtr256.cs @@ -11,7 +11,7 @@ public static class AesCtr256 public static void Encrypt(ReadOnlySpan bytes, ReadOnlySpan key, ReadOnlySpan iv, Span result) { - ulong ulIv = BitConverter.ToUInt64(iv); // TODO: ulIv good here? + ulong ulIv = BitConverter.ToUInt64(iv); using var aesCtr = new AesCounterMode(ulIv, CTR_START); var transformEnc = aesCtr.CreateEncryptor(key.ToArray(), null); @@ -25,7 +25,7 @@ public static bool Decrypt(ReadOnlySpan bytes, ReadOnlySpan key, Rea { try { - ulong ulIv = BitConverter.ToUInt64(iv); // TODO: ulIv good here? + ulong ulIv = BitConverter.ToUInt64(iv); using var aesCtr = new AesCounterMode(ulIv, CTR_START); var transformDec = aesCtr.CreateDecryptor(key.ToArray(), null); diff --git a/src/Core/SecureFolderFS.Core.Dokany/Callbacks/BaseDokanyCallbacks.cs b/src/Core/SecureFolderFS.Core.Dokany/Callbacks/BaseDokanyCallbacks.cs index 9a3283132..6f1474161 100644 --- a/src/Core/SecureFolderFS.Core.Dokany/Callbacks/BaseDokanyCallbacks.cs +++ b/src/Core/SecureFolderFS.Core.Dokany/Callbacks/BaseDokanyCallbacks.cs @@ -124,7 +124,6 @@ public virtual NtStatus GetVolumeInformation(out string volumeLabel, out FileSys /// public virtual NtStatus Mounted(string mountPoint, IDokanFileInfo info) { - _ = mountPoint; // TODO: Check if mountPoint is different and update the RootFolder (?) return Trace(DokanResult.Success, null, info); } @@ -358,7 +357,6 @@ public virtual unsafe NtStatus WriteFile(string fileName, IntPtr buffer, uint bu /// public abstract NtStatus SetFileSecurity(string fileName, FileSystemSecurity security, AccessControlSections sections, IDokanFileInfo info); - // TODO: Add checks for nullable in places where this function is called protected abstract string? GetCiphertextPath(string plaintextName); protected void CloseHandle(IDokanFileInfo info) diff --git a/src/Core/SecureFolderFS.Core.FileSystem/DataModels/RecycleBinItemDataModel.cs b/src/Core/SecureFolderFS.Core.FileSystem/DataModels/RecycleBinItemDataModel.cs index e7751300c..9d43ae210 100644 --- a/src/Core/SecureFolderFS.Core.FileSystem/DataModels/RecycleBinItemDataModel.cs +++ b/src/Core/SecureFolderFS.Core.FileSystem/DataModels/RecycleBinItemDataModel.cs @@ -1,5 +1,9 @@ using System; +using System.Buffers.Binary; using System.IO; +using System.Runtime.CompilerServices; +using System.Security.Cryptography; +using System.Text; using System.Text.Json.Serialization; using SecureFolderFS.Core.Cryptography; @@ -38,14 +42,122 @@ public sealed record class RecycleBinItemDataModel [JsonPropertyName("size")] public required long? Size { get; init; } - // TODO: Add MAC key signing for tamper proofing + /// + /// Gets the HMAC-SHA256 tag binding this model's fields to the payload it describes. + /// + [JsonPropertyName("hmacsha256mac")] + public byte[]? PayloadMac { get; set; } public string? DecryptName(Security security) { - if (security.NameCrypt is null) - return Name; + var plaintextName = security.NameCrypt is null + ? Name + : security.NameCrypt.DecryptName(Path.GetFileNameWithoutExtension(Name), DirectoryId); + + // The name is later joined onto a folder path to reattach or restore the payload, and a name + // that is not a single path component would land that payload wherever the attacker chose (e.g., autostart dir). + // AES-SIV already yields separator-free tokens, but CipherId.NONE returns the name verbatim. + // Checked independently of the MAC, because the vault's own code writes these names and a correctly signed one must not escape either + if (plaintextName is null || !IsSingleNameComponent(plaintextName)) + return null; + + return plaintextName; + } + + /// + /// Returns a copy of this model carrying a over its fields and . + /// + /// The name of the payload in the recycle bin that this model describes. + /// The instance holding the vault's MAC key. + public RecycleBinItemDataModel WithMac(string itemName, Security security) + { + return this with { PayloadMac = ComputeMac(this, itemName, security) }; + } + + /// + /// Determines whether authenticates this model against . + /// + /// The name of the payload in the recycle bin that this model describes. + /// The instance holding the vault's MAC key. + /// + /// Every gate on the reattachment and restore paths - the Directory ID lineage match, the deletion + /// recency window, and the original parent path - reads its evidence from this model. Without the + /// tag those gates are authored by whoever can write into the vault's ciphertext directory. + /// + public bool VerifyMac(string itemName, Security security) + { + if (PayloadMac is not { Length: HMACSHA256.HashSizeInBytes }) + return false; + + return CryptographicOperations.FixedTimeEquals(ComputeMac(this, itemName, security), PayloadMac); + } + + // [SkipLocalsInit] - deliberately not used here. + private static byte[] ComputeMac(RecycleBinItemDataModel dataModel, string itemName, Security security) + { + var mac = new byte[HMACSHA256.HashSizeInBytes]; + security.KeyPair.MacKey.UseKey(macKey => + { + using var hmac = IncrementalHash.CreateHMAC(HashAlgorithmName.SHA256, macKey); + + // Every field is length-prefixed. Concatenating them raw would let bytes be shifted + // across the boundary between two adjacent attacker-influenced fields (Name + ParentId) to yield a different model with an identical MAC input. + // The payload's own name is bound too, so a valid configuration cannot be paired with a different payload file + AppendField(hmac, Encoding.UTF8.GetBytes(itemName), true); + AppendField(hmac, dataModel.Name is null ? default : Encoding.UTF8.GetBytes(dataModel.Name), dataModel.Name is not null); + AppendField(hmac, dataModel.ParentId is null ? default : Encoding.UTF8.GetBytes(dataModel.ParentId), dataModel.ParentId is not null); + AppendField(hmac, dataModel.DirectoryId, dataModel.DirectoryId is not null); + + // Ticks and Kind are bound exactly as serialized, so no timezone conversion sits + // between signing and verification + Span timestamp = stackalloc byte[sizeof(long) + 1]; + if (dataModel.DeletionTimestamp is { } deletionTimestamp) + { + BinaryPrimitives.WriteInt64LittleEndian(timestamp, deletionTimestamp.Ticks); + timestamp[sizeof(long)] = (byte)deletionTimestamp.Kind; + } + + AppendField(hmac, timestamp, dataModel.DeletionTimestamp is not null); + + Span size = stackalloc byte[sizeof(long)]; + if (dataModel.Size is { } sizeValue) + BinaryPrimitives.WriteInt64LittleEndian(size, sizeValue); + + AppendField(hmac, size, dataModel.Size is not null); - return security.NameCrypt.DecryptName(Path.GetFileNameWithoutExtension(Name), DirectoryId); + _ = hmac.GetHashAndReset(mac); + }); + + return mac; + + [SkipLocalsInit] + static void AppendField(IncrementalHash hmac, ReadOnlySpan value, bool isPresent) + { + // The presence flag keeps an absent field distinct from a present-but-empty one + Span header = stackalloc byte[sizeof(int) + 1]; + header[0] = isPresent ? (byte)1 : (byte)0; + BinaryPrimitives.WriteInt32LittleEndian(header[1..], isPresent ? value.Length : 0); + + hmac.AppendData(header); + if (isPresent) + hmac.AppendData(value); + } + } + + /// + /// Determines whether is a single path component that cannot escape its parent. + /// + /// The name to check. + /// + /// Both separators are checked regardless of the running platform. + /// + private static bool IsSingleNameComponent(string name) + { + return !string.IsNullOrEmpty(name) + && name is not ("." or "..") + && name.IndexOf('/') < 0 + && name.IndexOf('\\') < 0 + && !Path.IsPathRooted(name); } public string? DecryptParentId(Security security) diff --git a/src/Core/SecureFolderFS.Core.FileSystem/Helpers/RecycleBin/Abstract/AbstractRecycleBinHelpers.Operational.cs b/src/Core/SecureFolderFS.Core.FileSystem/Helpers/RecycleBin/Abstract/AbstractRecycleBinHelpers.Operational.cs index 7d2314936..fcd04354a 100644 --- a/src/Core/SecureFolderFS.Core.FileSystem/Helpers/RecycleBin/Abstract/AbstractRecycleBinHelpers.Operational.cs +++ b/src/Core/SecureFolderFS.Core.FileSystem/Helpers/RecycleBin/Abstract/AbstractRecycleBinHelpers.Operational.cs @@ -30,7 +30,7 @@ public static partial class AbstractRecycleBinHelpers throw new DirectoryNotFoundException("Could not find recycle bin folder."); // Deserialize configuration - var deserialized = await GetItemDataModelAsync(recycleBinItem, recycleBin, streamSerializer, cancellationToken); + var deserialized = await GetItemDataModelAsync(recycleBinItem, recycleBin, specifics.Security, streamSerializer, cancellationToken); if (deserialized is not { ParentId: not null, Name: not null }) throw new FormatException("Could not deserialize recycle bin configuration file."); @@ -63,7 +63,7 @@ public static async Task RestoreAsync(IStorableChild recycleBinItem, IModifiable throw new UnauthorizedAccessException("The recycle bin is not modifiable."); // Deserialize configuration - var deserialized = await GetItemDataModelAsync(recycleBinItem, recycleBin, streamSerializer, cancellationToken); + var deserialized = await GetItemDataModelAsync(recycleBinItem, recycleBin, specifics.Security, streamSerializer, cancellationToken); if (deserialized is not { ParentId: not null, Name: not null }) throw new FormatException("Could not deserialize recycle bin configuration file."); @@ -193,7 +193,7 @@ public static async Task DeleteOrRecycleAsync( // whereas a payload without a configuration file would be unrestorable. var guid = Guid.NewGuid().ToString(); var configurationFile = await modifiableRecycleBin.CreateFileAsync($"{guid}.json", false, cancellationToken); - await WriteItemDataModelAsync(configurationFile, dataModel, streamSerializer, cancellationToken); + await WriteItemDataModelAsync(configurationFile, dataModel, specifics.Security, streamSerializer, cancellationToken); IStorableChild movedItem; try @@ -218,7 +218,7 @@ public static async Task DeleteOrRecycleAsync( // The folded sizes were already part of the occupied total; only the // folder's own entry needs to account for its regained contents if (foldedSize > 0L) - await WriteItemDataModelAsync(configurationFile, dataModel with { Size = sizeHint + foldedSize }, streamSerializer, cancellationToken); + await WriteItemDataModelAsync(configurationFile, dataModel with { Size = sizeHint + foldedSize }, specifics.Security, streamSerializer, cancellationToken); } // Update occupied size @@ -287,7 +287,7 @@ internal static async Task FoldDescendantEntriesAsync( // A single unreadable entry must not abandon the remaining ones await SafetyHelpers.NoFailureAsync(async () => { - var dataModel = await GetItemDataModelAsync(configurationFile, recycleBin, streamSerializer, cancellationToken); + var dataModel = await GetItemDataModelAsync(configurationFile, recycleBin, specifics.Security, streamSerializer, cancellationToken); if (dataModel is not { Name: not null, ParentId: not null, DirectoryId: { Length: Constants.DIRECTORY_ID_SIZE } childDirectoryId }) return; diff --git a/src/Core/SecureFolderFS.Core.FileSystem/Helpers/RecycleBin/Abstract/AbstractRecycleBinHelpers.Shared.cs b/src/Core/SecureFolderFS.Core.FileSystem/Helpers/RecycleBin/Abstract/AbstractRecycleBinHelpers.Shared.cs index e83f5f2ed..32de42e00 100644 --- a/src/Core/SecureFolderFS.Core.FileSystem/Helpers/RecycleBin/Abstract/AbstractRecycleBinHelpers.Shared.cs +++ b/src/Core/SecureFolderFS.Core.FileSystem/Helpers/RecycleBin/Abstract/AbstractRecycleBinHelpers.Shared.cs @@ -4,6 +4,7 @@ using System.Threading; using System.Threading.Tasks; using OwlCore.Storage; +using SecureFolderFS.Core.Cryptography; using SecureFolderFS.Core.FileSystem.DataModels; using SecureFolderFS.Core.FileSystem.Helpers.Paths; using SecureFolderFS.Shared.ComponentModel; @@ -182,22 +183,25 @@ private static async Task GetFolderPlaintextSizeAsync(IFolder ciphertextFo /// /// Serializes into , truncating any previous content. /// - internal static Task WriteItemDataModelAsync(IFile configurationFile, RecycleBinItemDataModel dataModel, IAsyncSerializer streamSerializer, CancellationToken cancellationToken) + internal static Task WriteItemDataModelAsync(IFile configurationFile, RecycleBinItemDataModel dataModel, Security security, IAsyncSerializer streamSerializer, CancellationToken cancellationToken) { + // Sign on the way out so every write (including the size rewrite after folding) is covered + var signedDataModel = dataModel.WithMac(Path.GetFileNameWithoutExtension(configurationFile.Name), security); + return WithTransientIoRetryAsync(async () => { await using var configurationStream = await configurationFile.OpenWriteAsync(cancellationToken); if (configurationStream.CanSeek) configurationStream.SetLength(0L); - await using var serializedStream = await streamSerializer.SerializeAsync(dataModel, cancellationToken); + await using var serializedStream = await streamSerializer.SerializeAsync(signedDataModel, cancellationToken); await serializedStream.CopyToAsync(configurationStream, cancellationToken); await configurationStream.FlushAsync(cancellationToken); return null; }, cancellationToken); } - public static async Task GetItemDataModelAsync(IStorableChild item, IFolder recycleBin, IAsyncSerializer streamSerializer, CancellationToken cancellationToken = default) + public static async Task GetItemDataModelAsync(IStorableChild item, IFolder recycleBin, Security security, IAsyncSerializer streamSerializer, CancellationToken cancellationToken = default) { // Get the configuration file var configurationFile = !item.Name.EndsWith(".json", StringComparison.OrdinalIgnoreCase) @@ -216,6 +220,11 @@ public static async Task GetItemDataModelAsync(IStorabl if (deserialized is not { ParentId: not null }) throw new FormatException("Could not deserialize recycle bin configuration file."); + // Reject anything this vault did not write. Callers treat a throw as a corrupt entry, which + // leaves the payload in place and permanently deletable rather than acting on forged metadata + if (!deserialized.VerifyMac(Path.GetFileNameWithoutExtension(configurationFile.Name), security)) + throw new UnauthorizedAccessException("The recycle bin configuration file is not authentic."); + return deserialized; } diff --git a/src/Core/SecureFolderFS.Core.FileSystem/Helpers/RecycleBin/Native/NativeRecycleBinHelpers.Operational.cs b/src/Core/SecureFolderFS.Core.FileSystem/Helpers/RecycleBin/Native/NativeRecycleBinHelpers.Operational.cs index 5beb09080..85ca93a53 100644 --- a/src/Core/SecureFolderFS.Core.FileSystem/Helpers/RecycleBin/Native/NativeRecycleBinHelpers.Operational.cs +++ b/src/Core/SecureFolderFS.Core.FileSystem/Helpers/RecycleBin/Native/NativeRecycleBinHelpers.Operational.cs @@ -1,6 +1,7 @@ using System; using System.IO; using OwlCore.Storage; +using SecureFolderFS.Core.Cryptography; using SecureFolderFS.Core.FileSystem.DataModels; using SecureFolderFS.Core.FileSystem.Helpers.Paths.Abstract; using SecureFolderFS.Core.FileSystem.Helpers.Paths.Native; @@ -117,7 +118,7 @@ public static void DeleteOrRecycle(string ciphertextPath, FileSystemSpecifics sp var guid = Guid.NewGuid().ToString(); var destinationPath = Path.Combine(recycleBinPath, guid); var configurationPath = $"{destinationPath}.json"; - WriteItemDataModel(configurationPath, dataModel); + WriteItemDataModel(configurationPath, dataModel, specifics.Security); // The folder's original plaintext path must be captured before the move - it is // the key that previously recycled children are folded back in by @@ -149,7 +150,7 @@ public static void DeleteOrRecycle(string ciphertextPath, FileSystemSpecifics sp { // The folded sizes were already part of the occupied total; only the // folder's own entry needs to account for its regained contents - SafetyHelpers.NoFailure(() => WriteItemDataModel(configurationPath, dataModel with { Size = sizeHint + foldedSize })); + SafetyHelpers.NoFailure(() => WriteItemDataModel(configurationPath, dataModel with { Size = sizeHint + foldedSize }, specifics.Security)); } } @@ -212,10 +213,13 @@ static bool IsRecentlyCreated(string path) /// /// Serializes into the file at , truncating any previous content. /// - private static void WriteItemDataModel(string configurationPath, RecycleBinItemDataModel dataModel) + private static void WriteItemDataModel(string configurationPath, RecycleBinItemDataModel dataModel, Security security) { + // Sign on the way out so every write (including the size rewrite after folding) is covered + var signedDataModel = dataModel.WithMac(Path.GetFileNameWithoutExtension(configurationPath), security); + using var configurationStream = File.Create(configurationPath); - using var serializedStream = StreamSerializer.Instance.SerializeAsync(dataModel).ConfigureAwait(false).GetAwaiter().GetResult(); + using var serializedStream = StreamSerializer.Instance.SerializeAsync(signedDataModel).ConfigureAwait(false).GetAwaiter().GetResult(); serializedStream.CopyTo(configurationStream); configurationStream.Flush(); @@ -268,6 +272,10 @@ private static long FoldDescendantEntries(string recycleBinPath, string recycled if (!childDirectoryId.AsSpan().SequenceEqual(folderDirectoryId)) return; + // Check if the data model is authentic + if (!dataModel.VerifyMac(Path.GetFileNameWithoutExtension(configurationPath), specifics.Security)) + return; + // Recency check: don't silently pull in unrelated deletions from long ago if (dataModel.DeletionTimestamp is not { } deletionTimestamp || Math.Abs((DateTime.Now - deletionTimestamp).TotalMilliseconds) > Constants.RECYCLE_BIN_FOLD_WINDOW_MS) diff --git a/src/Core/SecureFolderFS.Core.FileSystem/Validators/FolderValidator.cs b/src/Core/SecureFolderFS.Core.FileSystem/Validators/FolderValidator.cs index 7455a0446..26934c1a4 100644 --- a/src/Core/SecureFolderFS.Core.FileSystem/Validators/FolderValidator.cs +++ b/src/Core/SecureFolderFS.Core.FileSystem/Validators/FolderValidator.cs @@ -32,12 +32,12 @@ public override async Task ValidateAsync(IFolder value, CancellationToken cancel return; // Check if Directory ID exists - var directoryIdFile = await value.GetFileByNameAsync(Core.FileSystem.Constants.Names.DIRECTORY_ID_FILENAME, cancellationToken).ConfigureAwait(false); + var directoryIdFile = await value.GetFileByNameAsync(Constants.Names.DIRECTORY_ID_FILENAME, cancellationToken).ConfigureAwait(false); // Check the size await using var stream = await directoryIdFile.OpenReadAsync(cancellationToken).ConfigureAwait(false); - if (stream.Length != Core.FileSystem.Constants.DIRECTORY_ID_SIZE) - throw new EndOfStreamException($"The Directory ID size is invalid. Expected: {Core.FileSystem.Constants.DIRECTORY_ID_SIZE}; Got: {stream.Length}."); + if (stream.Length != Constants.DIRECTORY_ID_SIZE) + throw new EndOfStreamException($"The Directory ID size is invalid. Expected: {Constants.DIRECTORY_ID_SIZE}; Got: {stream.Length}."); } catch (Exception ex) { diff --git a/src/Platforms/SecureFolderFS.UI/ServiceImplementation/RecycleBinService.cs b/src/Platforms/SecureFolderFS.UI/ServiceImplementation/RecycleBinService.cs index 7ac5f1b68..5b4395916 100644 --- a/src/Platforms/SecureFolderFS.UI/ServiceImplementation/RecycleBinService.cs +++ b/src/Platforms/SecureFolderFS.UI/ServiceImplementation/RecycleBinService.cs @@ -122,7 +122,7 @@ public async Task RecalculateSizesAsync(IVfsRoot vfsRoot, CancellationToken canc // A single corrupt entry must not abandon the recalculation of the remaining ones try { - var dataModel = await AbstractRecycleBinHelpers.GetItemDataModelAsync(configurationFile, recycleBin, StreamSerializer.Instance, cancellationToken); + var dataModel = await AbstractRecycleBinHelpers.GetItemDataModelAsync(configurationFile, recycleBin, specifics.Security, StreamSerializer.Instance, cancellationToken); if (dataModel.Size is { } size and >= 0L) { totalSize += size; diff --git a/src/Platforms/SecureFolderFS.UI/Storage/RecycleBinFolder.cs b/src/Platforms/SecureFolderFS.UI/Storage/RecycleBinFolder.cs index 9b2d71223..8b8537d41 100644 --- a/src/Platforms/SecureFolderFS.UI/Storage/RecycleBinFolder.cs +++ b/src/Platforms/SecureFolderFS.UI/Storage/RecycleBinFolder.cs @@ -75,7 +75,7 @@ public async Task RestoreItemsAsync(IEnumerable items, IFolderPi try { - var dataModel = await AbstractRecycleBinHelpers.GetItemDataModelAsync(item, _recycleBin, _serializer, cancellationToken); + var dataModel = await AbstractRecycleBinHelpers.GetItemDataModelAsync(item, _recycleBin, _specifics.Security, _serializer, cancellationToken); var plaintextParentPath = SafetyHelpers.NoFailureResult(() => dataModel.DecryptParentId(_specifics.Security)); restoreQueue.Add((item, originalItem, plaintextParentPath)); } @@ -231,11 +231,10 @@ public async Task DeleteAsync(IStorableChild item, CancellationToken cancellatio RecycleBinItemDataModel? itemDataModel = null; if (configurationFile is not null) { + // Read through the verifying helper. Forging configuration must not be able to + // dictate the size subtracted from the occupied total. On failure the size below is measured from the payload instead itemDataModel = await SafetyHelpers.NoFailureAsync(async () => - { - await using var configurationStream = await configurationFile.OpenReadAsync(cancellationToken); - return await _serializer.DeserializeAsync(configurationStream, cancellationToken); - }); + await AbstractRecycleBinHelpers.GetItemDataModelAsync(item, _recycleBin, _specifics.Security, _serializer, cancellationToken)); } // Determine the size to subtract before the payload is gone @@ -278,7 +277,7 @@ private async Task MaterializeItemAsync(IStorableChild item, Ca { var recycleBinItem = await SafetyHelpers.NoFailureAsync(async () => { - var dataModel = await AbstractRecycleBinHelpers.GetItemDataModelAsync(item, _recycleBin, StreamSerializer.Instance, cancellationToken); + var dataModel = await AbstractRecycleBinHelpers.GetItemDataModelAsync(item, _recycleBin, _specifics.Security, StreamSerializer.Instance, cancellationToken); if (dataModel.ParentId is null || dataModel.Name is null) return null; diff --git a/src/Platforms/SecureFolderFS.UI/Strings/en-US/Resources.resx b/src/Platforms/SecureFolderFS.UI/Strings/en-US/Resources.resx index 11824f0dd..859f98ebd 100644 --- a/src/Platforms/SecureFolderFS.UI/Strings/en-US/Resources.resx +++ b/src/Platforms/SecureFolderFS.UI/Strings/en-US/Resources.resx @@ -924,7 +924,7 @@ You're in control - Quickly manage your vault from a top-down dashboard view. Modify credentials, browse files, and check the integrity of your files. + Everything you need in one place. Quickly browse your files, modify credentials, and monitor your vault's integrity from a centralized dashboard. Your data — secure, anywhere @@ -933,22 +933,22 @@ In addition to storing vaults on your device, you can also connect to other cloud storage providers. - Authenticate the way you want + Unlock your vaults your way - With our robust security, you choose how you access your files. Whether it's password, Windows Hello, or a key file — your data remains protected and readily available, tailored to your preferences. + With our robust security, you choose how you access your files. Whether it's a password, hardware security key, or biometrics — your data remains protected and readily available. - Your files, always protected + Strong encryption by design - Your files are encrypted using industry-standard AES-256 encryption. Even file and folder names are protected, ensuring that your sensitive documents remain completely private. + Every file is protected with AES-256-GCM encryption. Even file and folder names are encrypted to help keep your data truly private. Seamless virtual file system - Thanks to our seamless virtual file system engine, you can access your data worry-free without compromising speed or security. Your encrypted vault appears as a regular drive. + Your vault appears as a regular drive, allowing you to browse, edit, and use your favorite apps without compromising on performance or security. You're ready to go! @@ -1047,10 +1047,10 @@ Yesterday, {0} - {0:plural:Last week|{} weeks ago|{} weeks ago} + {0:plural:Last week|{} weeks ago} - {0:plural:{} day ago|{} days ago|{} days ago} + {0:plural:{} day ago|{} days ago} Vault is unlocked @@ -1191,13 +1191,13 @@ Clear selection - Copying {Total:choose(0):{Achieved} {Achieved:plural:item|items|items}|{Total:choose(1):{State}|{Achieved}/{Total} {Total:plural:item|items|items}}} + Copying {Total:choose(0):{Achieved} {Achieved:plural:item|items}|{Total:choose(1):{State}|{Achieved}/{Total} {Total:plural:item|items}}} - Moving {Total:choose(0):{Achieved} {Achieved:plural:item|items|items}|{Total:choose(1):{State}|{Achieved}/{Total} {Total:plural:item|items|items}}} + Moving {Total:choose(0):{Achieved} {Achieved:plural:item|items}|{Total:choose(1):{State}|{Achieved}/{Total} {Total:plural:item|items}}} - Deleting {Total:choose(0):{Achieved} {Achieved:plural:item|items|items}|{Total:choose(1):{State}|{Achieved}/{Total} {Total:plural:item|items|items}}} + Deleting {Total:choose(0):{Achieved} {Achieved:plural:item|items}|{Total:choose(1):{State}|{Achieved}/{Total} {Total:plural:item|items}}} Not enough space @@ -1206,7 +1206,7 @@ Deleting item(s) - Are you sure you want to permanently delete {0:plural:one item|{} items|{} items}? + Are you sure you want to permanently delete {0:plural:one item|{} items}? The deleted {0:plural:item exceeds|{} items exceed|{} items exceed} the available space in the recycle bin. Do you want to permanently delete {0:plural:this item|{} items|{} items} instead? @@ -1242,13 +1242,13 @@ Enable Device Link - {0:plural:One item|{} items|{} items} selected + {0:plural:One item|{} items} selected - {0:plural:{} element|{} elements|{} elements} + {0:plural:{} element|{} elements} - Found {0:plural:one issue|{} issues|{} issues} + Found {0:plural:one issue|{} issues} Available widgets @@ -1269,7 +1269,7 @@ This archive format is not supported for extraction - Extracting {Total:choose(0):{Achieved} {Achieved:plural:item|items|items}|{Total:choose(1):{State}|{Achieved}/{Total} {Total:plural:item|items|items}}} + Extracting {Total:choose(0):{Achieved} {Achieved:plural:item|items}|{Total:choose(1):{State}|{Achieved}/{Total} {Total:plural:item|items}}} Extracting... @@ -1359,7 +1359,7 @@ Collecting items ({0}) - Collected {0:plural:{} item|{} items|{} items} + Collected {0:plural:{} item|{} items} Scan completed @@ -1437,7 +1437,7 @@ Lock all - {0:plural:One vault is|{} vaults are|{} vaults are} unlocked + {0:plural:One vault is|{} vaults are} unlocked View in app @@ -1503,10 +1503,10 @@ Couldn't update the vault credentials - Couldn't delete {0:plural:one item|{} items|{} items} + Couldn't delete {0:plural:one item|{} items} - Couldn't restore {0:plural:one item|{} items|{} items} + Couldn't restore {0:plural:one item|{} items} Taken {0} out of {1} diff --git a/src/Platforms/SecureFolderFS.Uno/UserControls/GraphControl.xaml.cs b/src/Platforms/SecureFolderFS.Uno/UserControls/GraphControl.xaml.cs index aff8b93a6..01604bb70 100644 --- a/src/Platforms/SecureFolderFS.Uno/UserControls/GraphControl.xaml.cs +++ b/src/Platforms/SecureFolderFS.Uno/UserControls/GraphControl.xaml.cs @@ -42,7 +42,6 @@ private async void RootButton_Loaded(object sender, RoutedEventArgs e) private async void Chart_Loaded(object sender, RoutedEventArgs e) { // Workaround for the application freezing after unlocking at least 2 vaults - // TODO: Find the cause of the issue and fix it await Task.Delay(500); if (sender is not CartesianChart chart) @@ -63,7 +62,7 @@ private async void Chart_Loaded(object sender, RoutedEventArgs e) DataPadding = new(0.5f, 0), AnimationsSpeed = TimeSpan.FromMilliseconds(0), IsHoverable = false, - GeometrySize = 0d // TODO: Setting this to any value other than 0 causes a bug with jumping line series + GeometrySize = 0d // Setting this to any value other than 0 causes a bug with jumping line series } ]; chart.XAxes = diff --git a/src/Platforms/SecureFolderFS.Uno/UserControls/InterfaceHost/MainAppHostControl.xaml b/src/Platforms/SecureFolderFS.Uno/UserControls/InterfaceHost/MainAppHostControl.xaml index 65e5fca93..b3f967e2a 100644 --- a/src/Platforms/SecureFolderFS.Uno/UserControls/InterfaceHost/MainAppHostControl.xaml +++ b/src/Platforms/SecureFolderFS.Uno/UserControls/InterfaceHost/MainAppHostControl.xaml @@ -104,7 +104,6 @@ -