Skip to content

Commit afe0259

Browse files
authored
Unify file name encryption (#128)
1 parent 7a7adbf commit afe0259

8 files changed

Lines changed: 170 additions & 164 deletions

File tree

src/Core/SecureFolderFS.Core.FileSystem/Helpers/Health/HealthHelpers.Directory.cs

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,23 @@
1-
using OwlCore.Storage;
1+
using System;
2+
using System.IO;
3+
using System.Threading;
4+
using System.Threading.Tasks;
5+
using OwlCore.Storage;
26
using SecureFolderFS.Core.Cryptography;
37
using SecureFolderFS.Core.FileSystem.Helpers.Paths;
8+
using SecureFolderFS.Core.FileSystem.Helpers.Paths.Abstract;
49
using SecureFolderFS.Shared.ComponentModel;
510
using SecureFolderFS.Shared.Models;
611
using SecureFolderFS.Storage.Extensions;
712
using SecureFolderFS.Storage.Renamable;
8-
using System;
9-
using System.IO;
10-
using System.Threading;
11-
using System.Threading.Tasks;
1213

1314
namespace SecureFolderFS.Core.FileSystem.Helpers.Health
1415
{
1516
public static partial class HealthHelpers
1617
{
1718
public static async Task<IResult> RepairDirectoryAsync(IFolder affected, Security security, CancellationToken cancellationToken)
1819
{
19-
// Return success, if no encryption is used
20+
// Return success if no encryption is used
2021
if (security.NameCrypt is null)
2122
return Result.Success;
2223

@@ -38,11 +39,8 @@ public static async Task<IResult> RepairDirectoryAsync(IFolder affected, Securit
3839
if (PathHelpers.IsCoreName(item.Name))
3940
continue;
4041

41-
// Encrypt a new name
42-
var encryptedName = security.NameCrypt.EncryptName(item.Name, directoryId);
43-
encryptedName = $"{encryptedName}{Constants.Names.ENCRYPTED_FILE_EXTENSION}";
44-
45-
// Rename
42+
// Encrypt a new name and rename
43+
var encryptedName = AbstractPathHelpers.EncryptNewName(item.Name, directoryId, security);
4644
_ = await renamableFolder.RenameAsync(item, encryptedName, cancellationToken);
4745
}
4846

src/Core/SecureFolderFS.Core.FileSystem/Helpers/Health/HealthHelpers.Name.cs

Lines changed: 8 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
1-
using OwlCore.Storage;
2-
using SecureFolderFS.Core.Cryptography;
3-
using SecureFolderFS.Shared.ComponentModel;
4-
using SecureFolderFS.Shared.Models;
5-
using SecureFolderFS.Storage.Extensions;
6-
using SecureFolderFS.Storage.Renamable;
7-
using System;
8-
using System.IO;
1+
using System;
92
using System.Threading;
103
using System.Threading.Tasks;
4+
using OwlCore.Storage;
5+
using SecureFolderFS.Core.Cryptography;
116
using SecureFolderFS.Core.FileSystem.Helpers.Paths.Abstract;
7+
using SecureFolderFS.Shared.ComponentModel;
128
using SecureFolderFS.Shared.Helpers;
9+
using SecureFolderFS.Shared.Models;
10+
using SecureFolderFS.Storage.Renamable;
1311

1412
namespace SecureFolderFS.Core.FileSystem.Helpers.Health
1513
{
@@ -49,26 +47,8 @@ private static async Task<IResult> RepairNameAsync(IStorableChild affected, Secu
4947
if (parentFolder is not IRenamableFolder renamableFolder)
5048
return Result.Failure(FolderNotRenamable);
5149

52-
byte[] directoryId;
53-
if (parentFolder.Id != contentFolder.Id) // TODO: Remove code duplication with AbstractPathHelpers
54-
{
55-
var directoryIdFile = await parentFolder.GetFileByNameAsync(Constants.Names.DIRECTORY_ID_FILENAME, cancellationToken);
56-
await using var directoryIdStream = await directoryIdFile.OpenStreamAsync(FileAccess.Read, FileShare.Read, cancellationToken);
57-
58-
directoryId = new byte[Constants.DIRECTORY_ID_SIZE];
59-
var read = await directoryIdStream.ReadAsync(directoryId, cancellationToken);
60-
61-
if (read < Constants.DIRECTORY_ID_SIZE)
62-
throw new IOException($"The data inside Directory ID file is of incorrect size: {read}.");
63-
}
64-
else
65-
directoryId = Array.Empty<byte>();
66-
67-
// Encrypt new name
68-
var encryptedName = security.NameCrypt.EncryptName(newName, directoryId);
69-
encryptedName = $"{encryptedName}{Constants.Names.ENCRYPTED_FILE_EXTENSION}";
70-
71-
// Rename
50+
// Encrypt new name and rename
51+
var encryptedName = await AbstractPathHelpers.EncryptNameAsync(newName, parentFolder, contentFolder, security, cancellationToken);
7252
_ = await renamableFolder.RenameAsync(affected, encryptedName, cancellationToken);
7353
}
7454
catch (Exception ex)

src/Core/SecureFolderFS.Core.FileSystem/Helpers/Paths/Abstract/AbstractPathHelpers.Directory.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,26 @@ namespace SecureFolderFS.Core.FileSystem.Helpers.Paths.Abstract
1010
{
1111
public static partial class AbstractPathHelpers
1212
{
13+
public static async Task<bool> GetDirectoryIdAsync(
14+
IFolder folderOfDirectoryId,
15+
IFolder contentFolder,
16+
Memory<byte> directoryId,
17+
CancellationToken cancellationToken)
18+
{
19+
if (folderOfDirectoryId.Id == contentFolder.Id)
20+
return false;
21+
22+
var directoryIdFile = await folderOfDirectoryId.GetFileByNameAsync(Constants.Names.DIRECTORY_ID_FILENAME, cancellationToken).ConfigureAwait(false);
23+
await using var directoryIdStream = await directoryIdFile.OpenStreamAsync(FileAccess.Read, FileShare.Read, cancellationToken).ConfigureAwait(false);
24+
25+
var read = await directoryIdStream.ReadAsync(directoryId, cancellationToken).ConfigureAwait(false);
26+
if (read < Constants.DIRECTORY_ID_SIZE)
27+
throw new IOException($"The data inside Directory ID file is of incorrect size: {read}.");
28+
29+
// The Directory ID is not empty - return true
30+
return true;
31+
}
32+
1333
public static async Task<bool> GetDirectoryIdAsync(
1434
IFolder folderOfDirectoryId,
1535
FileSystemSpecifics specifics,

src/Core/SecureFolderFS.Core.FileSystem/Helpers/Paths/Abstract/AbstractPathHelpers.Names.cs

Lines changed: 74 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,30 +2,90 @@
22
using System.Threading;
33
using System.Threading.Tasks;
44
using OwlCore.Storage;
5+
using SecureFolderFS.Core.Cryptography;
56
using SecureFolderFS.Core.FileSystem.FileNames;
67

78
namespace SecureFolderFS.Core.FileSystem.Helpers.Paths.Abstract
89
{
910
public static partial class AbstractPathHelpers
1011
{
12+
/// <inheritdoc cref="EncryptNameAsync(string,OwlCore.Storage.IFolder,SecureFolderFS.Core.FileSystem.FileSystemSpecifics,System.Byte[],System.Threading.CancellationToken)"/>
13+
public static async Task<string> EncryptNameAsync(string plaintextName, IFolder ciphertextParentFolder,
14+
FileSystemSpecifics specifics, CancellationToken cancellationToken = default)
15+
{
16+
if (specifics.Security.NameCrypt is null)
17+
return plaintextName;
18+
19+
var directoryId = AllocateDirectoryId(specifics.Security, plaintextName);
20+
return await EncryptNameAsync(plaintextName, ciphertextParentFolder, specifics, directoryId, cancellationToken);
21+
}
22+
1123
/// <summary>
1224
/// Encrypts the provided <paramref name="plaintextName"/>.
1325
/// </summary>
1426
/// <param name="plaintextName">The name to encrypt.</param>
1527
/// <param name="ciphertextParentFolder">The ciphertext parent folder.</param>
1628
/// <param name="specifics">The <see cref="FileSystemSpecifics"/> instance associated with the item.</param>
29+
/// <param name="expendableDirectoryId">A buffer of size <see cref="Constants.DIRECTORY_ID_SIZE"/> which will be used to hold the Directory ID data.</param>
1730
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that cancels this action.</param>
18-
/// <returns>A <see cref="Task"/> that represents the asynchronous operation. Value is an encrypted name.</returns>
31+
/// <returns>A <see cref="Task"/> that represents the asynchronous operation. Value is an encrypted name with the appropriate file extension appended.</returns>
1932
public static async Task<string> EncryptNameAsync(string plaintextName, IFolder ciphertextParentFolder,
20-
FileSystemSpecifics specifics, CancellationToken cancellationToken = default)
33+
FileSystemSpecifics specifics, byte[]? expendableDirectoryId = null, CancellationToken cancellationToken = default)
2134
{
2235
if (specifics.Security.NameCrypt is null)
2336
return plaintextName;
2437

25-
var directoryId = AllocateDirectoryId(specifics.Security, plaintextName);
26-
var result = await GetDirectoryIdAsync(ciphertextParentFolder, specifics, directoryId, cancellationToken);
38+
expendableDirectoryId ??= AllocateDirectoryId(specifics.Security, plaintextName);
39+
var result = await GetDirectoryIdAsync(ciphertextParentFolder, specifics, expendableDirectoryId, cancellationToken);
2740

28-
return specifics.Security.NameCrypt.EncryptName(plaintextName, result ? directoryId : ReadOnlySpan<byte>.Empty) + Constants.Names.ENCRYPTED_FILE_EXTENSION;
41+
return specifics.Security.NameCrypt.EncryptName(plaintextName, result ? expendableDirectoryId : ReadOnlySpan<byte>.Empty) + Constants.Names.ENCRYPTED_FILE_EXTENSION;
42+
}
43+
44+
/// <summary>
45+
/// Encrypts the provided <paramref name="plaintextName"/>.
46+
/// </summary>
47+
/// <param name="plaintextName">The name to encrypt.</param>
48+
/// <param name="ciphertextParentFolder">The ciphertext parent folder.</param>
49+
/// <param name="contentFolder">The content folder.</param>
50+
/// <param name="security">The <see cref="Security"/> instance associated with the item.</param>
51+
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that cancels this action.</param>
52+
/// <returns>A <see cref="Task"/> that represents the asynchronous operation. Value is an encrypted name with the appropriate file extension appended.</returns>
53+
public static async Task<string> EncryptNameAsync(string plaintextName, IFolder ciphertextParentFolder, IFolder contentFolder,
54+
Security security, CancellationToken cancellationToken = default)
55+
{
56+
if (security.NameCrypt is null)
57+
return plaintextName;
58+
59+
var directoryId = AllocateDirectoryId(security, plaintextName);
60+
var result = await GetDirectoryIdAsync(ciphertextParentFolder, contentFolder, directoryId, cancellationToken);
61+
62+
return security.NameCrypt.EncryptName(plaintextName, result ? directoryId : ReadOnlySpan<byte>.Empty) + Constants.Names.ENCRYPTED_FILE_EXTENSION;
63+
}
64+
65+
/// <summary>
66+
/// Encrypts a plaintext name using the specified Directory ID and security parameters.
67+
/// </summary>
68+
/// <param name="plaintextName">The original plaintext name to encrypt.</param>
69+
/// <param name="newDirectoryId">A new Directory ID used for encryption.</param>
70+
/// <param name="security">The <see cref="Security"/> instance associated with the item.</param>
71+
/// <returns>The encrypted name with the appropriate file extension appended.</returns>
72+
public static string EncryptNewName(string plaintextName, byte[] newDirectoryId, Security security)
73+
{
74+
if (security.NameCrypt is null)
75+
return plaintextName;
76+
77+
return security.NameCrypt.EncryptName(plaintextName, newDirectoryId) + Constants.Names.ENCRYPTED_FILE_EXTENSION;
78+
}
79+
80+
/// <inheritdoc cref="DecryptNameAsync(string,OwlCore.Storage.IFolder,SecureFolderFS.Core.FileSystem.FileSystemSpecifics,System.Byte[],System.Threading.CancellationToken)"/>
81+
public static async Task<string?> DecryptNameAsync(string ciphertextName, IFolder ciphertextParentFolder,
82+
FileSystemSpecifics specifics, CancellationToken cancellationToken = default)
83+
{
84+
if (specifics.Security.NameCrypt is null)
85+
return ciphertextName;
86+
87+
var directoryId = AllocateDirectoryId(specifics.Security, ciphertextName);
88+
return await DecryptNameAsync(ciphertextName, ciphertextParentFolder, specifics, directoryId, cancellationToken);
2989
}
3090

3191
/// <summary>
@@ -34,21 +94,22 @@ public static async Task<string> EncryptNameAsync(string plaintextName, IFolder
3494
/// <param name="ciphertextName">The name to decrypt.</param>
3595
/// <param name="ciphertextParentFolder">The ciphertext parent folder.</param>
3696
/// <param name="specifics">The <see cref="FileSystemSpecifics"/> instance associated with the item.</param>
97+
/// <param name="expendableDirectoryId">A buffer of size <see cref="Constants.DIRECTORY_ID_SIZE"/> which will be used to hold the Directory ID data.</param>
3798
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that cancels this action.</param>
3899
/// <returns>A <see cref="Task"/> that represents the asynchronous operation. Value is a decrypted name.</returns>
39100
public static async Task<string?> DecryptNameAsync(string ciphertextName, IFolder ciphertextParentFolder,
40-
FileSystemSpecifics specifics, CancellationToken cancellationToken = default)
101+
FileSystemSpecifics specifics, byte[]? expendableDirectoryId, CancellationToken cancellationToken = default)
41102
{
103+
if (specifics.Security.NameCrypt is null)
104+
return ciphertextName;
105+
42106
try
43107
{
44-
if (specifics.Security.NameCrypt is null)
45-
return ciphertextName;
46-
47-
var directoryId = AllocateDirectoryId(specifics.Security, ciphertextName);
48-
var result = await GetDirectoryIdAsync(ciphertextParentFolder, specifics, directoryId, cancellationToken);
108+
expendableDirectoryId ??= AllocateDirectoryId(specifics.Security, ciphertextName);
109+
var result = await GetDirectoryIdAsync(ciphertextParentFolder, specifics, expendableDirectoryId, cancellationToken);
49110

50111
var normalizedName = RemoveCiphertextExtension(ciphertextName);
51-
return specifics.Security.NameCrypt.DecryptName(normalizedName, result ? directoryId : ReadOnlySpan<byte>.Empty);
112+
return specifics.Security.NameCrypt.DecryptName(normalizedName, result ? expendableDirectoryId : ReadOnlySpan<byte>.Empty);
52113
}
53114
catch (Exception)
54115
{
@@ -96,7 +157,7 @@ public static async Task<string> EncryptNameAsync(string plaintextName, IFolder
96157
/// <param name="ciphertextParentFolder">The ciphertext parent folder.</param>
97158
/// <param name="specifics">The <see cref="FileSystemSpecifics"/> instance associated with the item.</param>
98159
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that cancels this action.</param>
99-
/// <returns>A <see cref="Task"/> that represents the asynchronous operation. The result is an encrypted name, retrieved from the cache if available, or newly encrypted if not.</returns>
160+
/// <returns>A <see cref="Task"/> that represents the asynchronous operation. Value is an encrypted name with the appropriate file extension appended, retrieved from the cache if available, or newly encrypted if not.</returns>
100161
public static async Task<string> CacheEncryptNameAsync(string plaintextName, IFolder ciphertextParentFolder,
101162
FileSystemSpecifics specifics, CancellationToken cancellationToken = default)
102163
{

src/Core/SecureFolderFS.Core.FileSystem/Helpers/Paths/Abstract/AbstractPathHelpers.Paths.cs

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -56,17 +56,13 @@ public static partial class AbstractPathHelpers
5656
foreach (var item in folderChain)
5757
{
5858
// Walk through plaintext folder chain and retrieve ciphertext folders
59-
var subResult = await GetDirectoryIdAsync(finalFolder, specifics, expendableDirectoryId, cancellationToken).ConfigureAwait(false);
60-
var subCiphertextName = specifics.Security.NameCrypt.EncryptName(item.Name, subResult ? expendableDirectoryId : ReadOnlySpan<byte>.Empty);
61-
62-
finalFolder = await finalFolder.GetFolderByNameAsync($"{subCiphertextName}{Constants.Names.ENCRYPTED_FILE_EXTENSION}", cancellationToken);
59+
var subCiphertextName = await EncryptNameAsync(item.Name, finalFolder, specifics, expendableDirectoryId, cancellationToken).ConfigureAwait(false);
60+
finalFolder = await finalFolder.GetFolderByNameAsync(subCiphertextName, cancellationToken);
6361
}
6462

6563
// Encrypt and retrieve the final item
66-
var result = await GetDirectoryIdAsync(finalFolder, specifics, expendableDirectoryId, cancellationToken).ConfigureAwait(false);
67-
var ciphertextName = specifics.Security.NameCrypt.EncryptName(plaintextStorable.Name, result ? expendableDirectoryId : ReadOnlySpan<byte>.Empty);
68-
69-
return await finalFolder.GetFirstByNameAsync($"{ciphertextName}{Constants.Names.ENCRYPTED_FILE_EXTENSION}", cancellationToken);
64+
var ciphertextName = await EncryptNameAsync(plaintextStorable.Name, finalFolder, specifics, expendableDirectoryId, cancellationToken).ConfigureAwait(false);
65+
return await finalFolder.GetFirstByNameAsync(ciphertextName, cancellationToken);
7066
}
7167

7268
public static async Task<string?> GetPlaintextPathAsync(IStorableChild ciphertextStorable, FileSystemSpecifics specifics, CancellationToken cancellationToken)
@@ -83,8 +79,7 @@ public static partial class AbstractPathHelpers
8379
if (!currentParent.Id.Contains(specifics.ContentFolder.Id))
8480
break;
8581

86-
var result = await GetDirectoryIdAsync(currentParent, specifics, expendableDirectoryId, cancellationToken).ConfigureAwait(false);
87-
var plaintextName = specifics.Security.NameCrypt.DecryptName(Path.GetFileNameWithoutExtension(currentStorable.Name), result ? expendableDirectoryId : ReadOnlySpan<byte>.Empty);
82+
var plaintextName = await DecryptNameAsync(currentStorable.Name, currentParent, specifics, expendableDirectoryId, cancellationToken).ConfigureAwait(false);
8883
if (plaintextName is null)
8984
return null;
9085

0 commit comments

Comments
 (0)