Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
using SecureFolderFS.Core.FileSystem.AppModels;
using System;
using System.Collections.Generic;
using SecureFolderFS.Core.FileSystem.AppModels;
using SecureFolderFS.Shared.Extensions;
using SecureFolderFS.Storage.VirtualFileSystem;
using System;
using System.Collections.Generic;

namespace SecureFolderFS.Core.Dokany.AppModels
{
Expand Down Expand Up @@ -36,6 +36,7 @@ public static DokanyOptions ToOptions(IDictionary<string, object> options)
IsCachingFileNames = (bool?)options.Get(nameof(IsCachingFileNames)) ?? true,
IsCachingDirectoryIds = (bool?)options.Get(nameof(IsCachingDirectoryIds)) ?? true,
RecycleBinSize = (long?)options.Get(nameof(RecycleBinSize)) ?? 0L,
ShorteningThreshold = (int?)options.Get(nameof(ShorteningThreshold)) ?? 0,

// Dokany specific
MountPoint = (string?)options.Get(nameof(MountPoint))
Expand Down
29 changes: 28 additions & 1 deletion src/Core/SecureFolderFS.Core.Dokany/Callbacks/OnDeviceDokany.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ public override NtStatus CreateFile(string fileName, FileAccess access, FileShar
{
}

// Materialize sidecar for the new directory name if shortened
ciphertextPath = GetCiphertextPathForUse(fileName) ?? ciphertextPath;

// Create directory
_ = Directory.CreateDirectory(ciphertextPath);

Expand Down Expand Up @@ -172,6 +175,10 @@ public override NtStatus CreateFile(string fileName, FileAccess access, FileShar
if (specifics.Options.IsReadOnly && mode.IsWriteFlag())
throw FileSystemExceptions.FileSystemReadOnly;

// Materialize sidecar for the new file name if shortened
if (mode is FileMode.CreateNew or FileMode.Create or FileMode.OpenOrCreate)
ciphertextPath = GetCiphertextPathForUse(fileName) ?? ciphertextPath;

var openAccess = readAccess ? System.IO.FileAccess.Read : System.IO.FileAccess.ReadWrite;
if (mode == FileMode.CreateNew && readAccess)
openAccess = System.IO.FileAccess.ReadWrite;
Expand Down Expand Up @@ -252,6 +259,11 @@ public override void Cleanup(string fileName, IDokanFileInfo info)
{
NativeRecycleBinHelpers.DeleteOrRecycle(ciphertextPath, specifics, StorableType.File);
}

// Clean up sidecar after successful delete/recycle
NativePathHelpers.DeleteSidecarFile(
Path.GetFileName(ciphertextPath),
Path.GetDirectoryName(ciphertextPath) ?? string.Empty);
}
catch (UnauthorizedAccessException)
{
Expand Down Expand Up @@ -537,7 +549,7 @@ public override NtStatus DeleteDirectory(string fileName, IDokanFileInfo info)
public override NtStatus MoveFile(string oldName, string newName, bool replace, IDokanFileInfo info)
{
var oldCiphertextPath = GetCiphertextPath(oldName);
var newCiphertextPath = GetCiphertextPath(newName);
var newCiphertextPath = GetCiphertextPathForUse(newName);
var fileNameCombined = $"{oldName} -> {newName}";

if (oldCiphertextPath is null || newCiphertextPath is null)
Expand All @@ -564,6 +576,11 @@ public override NtStatus MoveFile(string oldName, string newName, bool replace,
File.Move(oldCiphertextPath, newCiphertextPath);
}

// Clean up old sidecar after successful move
NativePathHelpers.DeleteSidecarFile(
Path.GetFileName(oldCiphertextPath),
Path.GetDirectoryName(oldCiphertextPath) ?? string.Empty);

return Trace(DokanResult.Success, fileNameCombined, info);
}
else if (replace)
Expand All @@ -579,6 +596,11 @@ public override NtStatus MoveFile(string oldName, string newName, bool replace,
File.Delete(newCiphertextPath);
File.Move(oldCiphertextPath, newCiphertextPath);

// Clean up old sidecar after successful move
NativePathHelpers.DeleteSidecarFile(
Path.GetFileName(oldCiphertextPath),
Path.GetDirectoryName(oldCiphertextPath) ?? string.Empty);

return Trace(DokanResult.Success, fileNameCombined, info);
}
else
Expand Down Expand Up @@ -824,5 +846,10 @@ public override NtStatus FindStreams(string fileName, out IList<FileInformation>
{
return NativePathHelpers.GetCiphertextPath(plaintextName, specifics);
}

private string? GetCiphertextPathForUse(string plaintextName)
{
return NativePathHelpers.GetCiphertextPathForUse(plaintextName, specifics);
}
}
}
1 change: 1 addition & 0 deletions src/Core/SecureFolderFS.Core.FUSE/AppModels/FuseOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ public static FuseOptions ToOptions(IDictionary<string, object> options)
IsCachingFileNames = (bool?)options.Get(nameof(IsCachingFileNames)) ?? true,
IsCachingDirectoryIds = (bool?)options.Get(nameof(IsCachingDirectoryIds)) ?? true,
RecycleBinSize = (long?)options.Get(nameof(RecycleBinSize)) ?? 0L,
ShorteningThreshold = (int?)options.Get(nameof(ShorteningThreshold)) ?? 0,

// FUSE specific
MountPoint = (string?)options.Get(nameof(MountPoint)),
Expand Down
104 changes: 83 additions & 21 deletions src/Core/SecureFolderFS.Core.FUSE/Callbacks/OnDeviceFuse.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
using System.Runtime.CompilerServices;
using System.Text;
using OwlCore.Storage;
using SecureFolderFS.Core.FileSystem;
using SecureFolderFS.Core.FileSystem.Helpers;
using SecureFolderFS.Core.FileSystem.Helpers.Paths;
using SecureFolderFS.Core.FileSystem.Helpers.Paths.Abstract;
using SecureFolderFS.Core.FileSystem.Helpers.Paths.Native;
using SecureFolderFS.Core.FileSystem.Helpers.RecycleBin.Native;
using SecureFolderFS.Core.FUSE.OpenHandles;
using SecureFolderFS.Core.FUSE.UnsafeNative;
using System.Runtime.CompilerServices;
using System.Text;
using SecureFolderFS.Core.FileSystem.Helpers.Paths.Abstract;
using Tmds.Fuse;
using Tmds.Linux;
using static SecureFolderFS.Core.FUSE.UnsafeNative.UnsafeNativeApis;
Expand Down Expand Up @@ -65,7 +68,7 @@ public override unsafe int Create(ReadOnlySpan<byte> path, mode_t mode, ref Fuse
if (FuseOptions!.IsReadOnly)
return -EROFS;

var ciphertextPath = GetCiphertextPath(path);
var ciphertextPath = GetCiphertextPathForUse(path);
if (ciphertextPath is null)
return -ENOENT;

Expand Down Expand Up @@ -259,7 +262,7 @@ public override unsafe int MkDir(ReadOnlySpan<byte> path, mode_t mode)
if (FuseOptions!.IsReadOnly)
return -EROFS;

var ciphertextPath = GetCiphertextPath(path);
var ciphertextPath = GetCiphertextPathForUse(path);
if (ciphertextPath is null)
return -ENOENT;

Expand Down Expand Up @@ -410,7 +413,7 @@ public override unsafe int Rename(ReadOnlySpan<byte> path, ReadOnlySpan<byte> ne
return -EROFS;

var ciphertextPath = GetCiphertextPath(path);
var newCiphertextPath = GetCiphertextPath(newPath);
var newCiphertextPath = GetCiphertextPathForUse(newPath);
if (ciphertextPath is null || newCiphertextPath is null)
return -ENOENT;

Expand All @@ -421,10 +424,15 @@ public override unsafe int Rename(ReadOnlySpan<byte> path, ReadOnlySpan<byte> ne
return -errno;
}

// Clean up old sidecar after successful rename
NativePathHelpers.DeleteSidecarFile(
Path.GetFileName(ciphertextPath),
Path.GetDirectoryName(ciphertextPath) ?? string.Empty);

return 0;
}

public override unsafe int RmDir(ReadOnlySpan<byte> path)
public override int RmDir(ReadOnlySpan<byte> path)
{
if (FuseOptions!.IsReadOnly)
return -EROFS;
Expand All @@ -437,17 +445,38 @@ public override unsafe int RmDir(ReadOnlySpan<byte> path)
return -ENOTEMPTY;

var directoryIdPath = Path.Combine(ciphertextPath, FileSystem.Constants.Names.DIRECTORY_ID_FILENAME);

// Remove DirectoryID
File.Delete(directoryIdPath);
specifics.DirectoryIdCache.CacheRemove(directoryIdPath);

fixed (byte *ciphertextPathPtr = Encoding.UTF8.GetBytes(ciphertextPath))
try
{
if (rmdir(ciphertextPathPtr) == -1)
return -errno;
NativeRecycleBinHelpers.DeleteOrRecycle(ciphertextPath, specifics, StorableType.Folder);
}
catch (FileNotFoundException)
{
return -ENOENT;
}
catch (DirectoryNotFoundException)
{
return -ENOENT;
}
catch (UnauthorizedAccessException)
{
return -EACCES;
}
catch (IOException ioEx) when (ErrorHandlingHelpers.IsDiskFullException(ioEx))
{
return -ENOSPC;
}
catch
{
return -EIO;
}
Comment thread
d2dyno1 marked this conversation as resolved.

// Clean up sidecar after successful delete/recycle
NativePathHelpers.DeleteSidecarFile(
Path.GetFileName(ciphertextPath),
Path.GetDirectoryName(ciphertextPath) ?? string.Empty);

return 0;
}

Expand Down Expand Up @@ -529,7 +558,7 @@ public override int Truncate(ReadOnlySpan<byte> path, ulong length, FuseFileInfo
/// <remarks>
/// This method is also responsible for file deletion.
/// </remarks>
public override unsafe int Unlink(ReadOnlySpan<byte> path)
public override int Unlink(ReadOnlySpan<byte> path)
{
if (FuseOptions!.IsReadOnly)
return -EROFS;
Expand All @@ -541,11 +570,35 @@ public override unsafe int Unlink(ReadOnlySpan<byte> path)
if (Directory.Exists(ciphertextPath))
return -EISDIR;

fixed (byte *ciphertextPathPtr = Encoding.UTF8.GetBytes(ciphertextPath))
try
{
if (unlink(ciphertextPathPtr) == -1)
return -errno;
NativeRecycleBinHelpers.DeleteOrRecycle(ciphertextPath, specifics, StorableType.File);
}
catch (FileNotFoundException)
{
return -ENOENT;
}
catch (DirectoryNotFoundException)
{
return -ENOENT;
}
catch (UnauthorizedAccessException)
{
return -EACCES;
}
catch (IOException ioEx) when (ErrorHandlingHelpers.IsDiskFullException(ioEx))
{
return -ENOSPC;
}
catch
{
return -EIO;
}
Comment thread
d2dyno1 marked this conversation as resolved.

// Clean up sidecar after successful delete/recycle
NativePathHelpers.DeleteSidecarFile(
Path.GetFileName(ciphertextPath),
Path.GetDirectoryName(ciphertextPath) ?? string.Empty);

return 0;
}
Expand All @@ -569,7 +622,7 @@ public override unsafe int UpdateTimestamps(ReadOnlySpan<byte> path, ref timespe
return -errno;

var result = futimens(*(int*)fd, times);
UnsafeNativeApis.CloseDir(fd);
CloseDir(fd);

if (result == -1)
return -errno;
Expand Down Expand Up @@ -614,12 +667,21 @@ public override int Write(ReadOnlySpan<byte> path, ulong offset, ReadOnlySpan<by
return buffer.Length;
}

protected override unsafe string? GetCiphertextPath(ReadOnlySpan<byte> plaintextName)
protected override unsafe string? GetCiphertextPath(ReadOnlySpan<byte> nativePlaintextName)
{
fixed (byte *plaintextNamePtr = nativePlaintextName)
{
var directoryId = new byte[FileSystem.Constants.DIRECTORY_ID_SIZE];
return NativePathHelpers.GetCiphertextPath(Encoding.UTF8.GetString(plaintextNamePtr, nativePlaintextName.Length), specifics, directoryId);
}
}

private unsafe string? GetCiphertextPathForUse(ReadOnlySpan<byte> nativePlaintextName)
{
fixed (byte *plaintextNamePtr = plaintextName)
fixed (byte *plaintextNamePtr = nativePlaintextName)
{
var directoryId = new byte[FileSystem.Constants.DIRECTORY_ID_SIZE];
return NativePathHelpers.GetCiphertextPath(Encoding.UTF8.GetString(plaintextNamePtr, plaintextName.Length), specifics, directoryId);
return NativePathHelpers.GetCiphertextPathForUse(Encoding.UTF8.GetString(plaintextNamePtr, nativePlaintextName.Length), specifics, directoryId);
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions src/Core/SecureFolderFS.Core.FileSystem/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ public static class FileSystem
public static class Names
{
public const string ENCRYPTED_FILE_EXTENSION = ".sffs";
public const string SHORTENED_FILE_EXTENSION = ".sffsn";
public const string SIDECAR_FILE_EXTENSION = ".sffsi";
public const string DIRECTORY_ID_FILENAME = "dirid.iv";
public const string RECYCLE_BIN_NAME = "recycle_bin";
public const string RECYCLE_BIN_CONFIGURATION_FILENAME = "recycle_bin.cfg";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System;

namespace SecureFolderFS.Core.FileSystem.Exceptions
{
/// <summary>
/// Exception thrown when a sidecar file (.sffsi) exists without a matching shortened file (.sffsn).
/// </summary>
public sealed class OrphanSidecarException : Exception
{
public OrphanSidecarException(string sidecarName)
: base($"Orphan sidecar file has no matching shortened file: {sidecarName}")
{
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
using System.Threading;
using System.Threading.Tasks;
using OwlCore.Storage;
using SecureFolderFS.Core.Cryptography;
using SecureFolderFS.Core.FileSystem.Helpers.Paths;
using SecureFolderFS.Core.FileSystem.Helpers.Paths.Abstract;
using SecureFolderFS.Shared.ComponentModel;
Expand All @@ -15,10 +14,10 @@ namespace SecureFolderFS.Core.FileSystem.Helpers.Health
{
public static partial class HealthHelpers
{
public static async Task<IResult> RepairDirectoryAsync(IFolder affected, Security security, CancellationToken cancellationToken)
public static async Task<IResult> RepairDirectoryAsync(IFolder affected, FileSystemSpecifics specifics, CancellationToken cancellationToken)
{
// Return success if no encryption is used
if (security.NameCrypt is null)
if (specifics.Security.NameCrypt is null)
return Result.Success;

if (affected is not IRenamableFolder renamableFolder)
Expand All @@ -39,9 +38,15 @@ public static async Task<IResult> RepairDirectoryAsync(IFolder affected, Securit
if (PathHelpers.IsCoreName(item.Name))
continue;

// Encrypt a new name and rename
var encryptedName = AbstractPathHelpers.EncryptNewName(item.Name, directoryId, security);
// Remember old name for sidecar cleanup
var oldName = item.Name;

// Encrypt a new name (writes sidecar if shortening applies) and rename
var encryptedName = await AbstractPathHelpers.EncryptNewNameForUseAsync(item.Name, directoryId, affected, specifics, cancellationToken);
_ = await renamableFolder.RenameAsync(item, encryptedName, cancellationToken);

// Clean up old sidecar if the previous name was shortened
await AbstractPathHelpers.DeleteSidecarFileAsync(oldName, renamableFolder, cancellationToken);
}

return Result.Success;
Expand Down
Loading
Loading