Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
95 commits
Select commit Hold shift + click to select a range
9d319eb
Begin working on App Platform
d2dyno1 May 31, 2026
2d21278
Added APP_PLATFORM_PRESENT compile constant
d2dyno1 May 31, 2026
2c88472
Added missing components
d2dyno1 May 31, 2026
e4254e8
Fixed build
d2dyno1 Jun 1, 2026
56415ff
Update Directory.Build.props
d2dyno1 Jun 1, 2026
385bc16
Added DeviceSetupDialog
d2dyno1 Jun 5, 2026
80154e9
Use AES-256
d2dyno1 Jun 5, 2026
81d58a3
Removed stale methods
d2dyno1 Jun 5, 2026
f79a156
Added the option to request App Platform account reset
d2dyno1 Jun 6, 2026
e88870e
Added equality and ComputeJwkThumbprint methods
d2dyno1 Jun 8, 2026
12ec907
Fixed build
d2dyno1 Jun 9, 2026
c9888c4
Apply code review
d2dyno1 Jun 9, 2026
728e058
Adjusted PBKDF2 iterations
d2dyno1 Jun 9, 2026
1116a28
Added IUriLauncher
d2dyno1 Jun 9, 2026
c975b4a
Encrypt pairing requests and mitigate MITM in Device Link
d2dyno1 Jun 10, 2026
3ac1f9e
Added complementation generation
d2dyno1 Jun 11, 2026
12fd891
Replaced Device Link's ExpectedHmac with BindingSecret
d2dyno1 Jun 12, 2026
71d93c2
Removed obsolete DeviceLinkVaultDataModel
d2dyno1 Jun 12, 2026
feaab08
Update AccountKeyHelper.cs
d2dyno1 Jun 13, 2026
57c73a9
Added Accounts settings page
d2dyno1 Jun 15, 2026
8c5ed8f
Refactored FileDeviceKeyStore
d2dyno1 Jun 15, 2026
9b21ed5
Removed MacCatalyst target
d2dyno1 Jun 15, 2026
551bd7f
Disable changing credentials for App Platform vaults
d2dyno1 Jun 15, 2026
c1a0ce9
Update FileDeviceKeyStore.cs
d2dyno1 Jun 15, 2026
f95db4a
Added cancellation when logging into App Platform
d2dyno1 Jun 17, 2026
34fb042
Force re-login on App Platform creation
d2dyno1 Jun 18, 2026
2a7dd34
Added localization strings for App Platform
d2dyno1 Jun 21, 2026
9e8b57d
Fixed build
d2dyno1 Jun 21, 2026
b47aac1
Update Resources.resx
d2dyno1 Jun 23, 2026
31b1c1e
Added new strings
d2dyno1 Jun 25, 2026
21906d6
Update Resources.resx
d2dyno1 Jun 27, 2026
30b5add
Update Resources.resx
d2dyno1 Jun 30, 2026
1c0e01f
Merge remote-tracking branch 'upstream/f_vault4' into appPlatform_pro…
d2dyno1 Jul 6, 2026
b69678d
Added fallback BouncyCastle crypto implementation
d2dyno1 Jul 7, 2026
939e02e
Fixed CryptoFolder's GetCiphertextRepresentationAsync
d2dyno1 Jul 12, 2026
5ae37bf
Made Argon2id asynchronous
d2dyno1 Jul 14, 2026
c95e4d3
Update WebDavClientAccountViewModel.cs
d2dyno1 Jul 14, 2026
95c824a
Added probing to DavClientFolder
d2dyno1 Jul 14, 2026
be0e9db
Added async support to PlaintextStream
d2dyno1 Jul 14, 2026
04239f5
Added support for app auto-start
d2dyno1 Jul 14, 2026
7d0cc71
Added the option to automatically unlock a vault on app startup
d2dyno1 Jul 14, 2026
e62ebd3
Fixed tests
d2dyno1 Jul 14, 2026
dd8add3
More secure PdfStreamServer
d2dyno1 Jul 15, 2026
6d39461
Better reliability for archive extraction
d2dyno1 Jul 15, 2026
712361a
Added manual save option to text previewer
d2dyno1 Jul 15, 2026
e25a48f
Added ascending and descending sorting
d2dyno1 Jul 15, 2026
01bee8f
Fixed selection rectangle math
d2dyno1 Jul 15, 2026
5566d57
Always show selection checkboxes in selection mode
d2dyno1 Jul 15, 2026
9a8e6d4
Added fail-safe in TransferControl
d2dyno1 Jul 15, 2026
4230a66
Enumerate folder items in batches
d2dyno1 Jul 15, 2026
5d377a5
Snapshot file icons
d2dyno1 Jul 15, 2026
c83588d
Fixed rectangle selection on Android
d2dyno1 Jul 15, 2026
825f277
Added new selection bar options when selecting items
d2dyno1 Jul 15, 2026
adf4315
Added folder item count in properties
d2dyno1 Jul 15, 2026
e3355bc
Added delete option to file previewer
d2dyno1 Jul 15, 2026
fddf06d
Added selection-to-archive support
d2dyno1 Jul 15, 2026
9c44236
Update SwipeSelectionItemTouchListener.cs
d2dyno1 Jul 15, 2026
94e2bc6
Added support for more archive types
d2dyno1 Jul 15, 2026
f2527a5
Added missing App Platform login/register UI on MAUI
d2dyno1 Jul 16, 2026
a98c212
Added App Platform accounts section to MAUI's SettingsPage
d2dyno1 Jul 16, 2026
99fbbae
Moved FileDeviceKeyStore to the UI project
d2dyno1 Jul 16, 2026
c3c5d77
Added SecurePropertyKeyStore
d2dyno1 Jul 16, 2026
99e4dcb
Added MauiOidcProvider
d2dyno1 Jul 16, 2026
96e455a
Increased PBES2 iterations
d2dyno1 Jul 16, 2026
d2c9d06
Fixed build
d2dyno1 Jul 16, 2026
04514f8
Use platform-protected secure storage
d2dyno1 Jul 16, 2026
611b838
Update UnsafeNative.Imports.cs
d2dyno1 Jul 16, 2026
234225f
Merge remote-tracking branch 'upstream/f_vault4' into appPlatform_pro…
d2dyno1 Jul 21, 2026
b268f66
Update UnsafeNative.Imports.cs
d2dyno1 Jul 21, 2026
638649f
Update UnsafeNative.Imports.cs
d2dyno1 Jul 21, 2026
ef5951a
Fixed SecurePropertyKeyStore
d2dyno1 Jul 21, 2026
24f6fb9
Don't omit checks for zeroed-out nonce in chunks
d2dyno1 Jul 24, 2026
0185c11
Removed software entropy
d2dyno1 Jul 25, 2026
ee3080e
Update App.xaml.cs
d2dyno1 Jul 31, 2026
ec123d1
Fixed build
d2dyno1 Aug 1, 2026
91ca10a
Respect IsReadOnly flag on Dokany SetFileTime
d2dyno1 Aug 1, 2026
ef81d29
Do not use unlink in FUSE
d2dyno1 Aug 1, 2026
b98e207
Added chunk cache eviction when truncating files
d2dyno1 Aug 1, 2026
22faaaf
Added truncation tests
d2dyno1 Aug 1, 2026
fafb680
Avoid surviving DEK+MAC keys in AES-SIV in heap
d2dyno1 Aug 1, 2026
07ec99a
Added IsWriteFlag read-only check for OpenOrCreate flags
d2dyno1 Aug 1, 2026
3e41025
Added ToNativePath that prepends NUL-termination for libc API
d2dyno1 Aug 1, 2026
f97d1d7
Use canonicalized Android SAF document IDs
d2dyno1 Aug 2, 2026
01b6976
Avoid using unverified config files when modifying credentials
d2dyno1 Aug 2, 2026
b7bacb5
Ensure valid display names in Android SAF
d2dyno1 Aug 2, 2026
c28e293
Update OpenCryptFileManager.cs
d2dyno1 Aug 2, 2026
f095374
Added HMAC-SHA verification for recycle bin items
d2dyno1 Aug 3, 2026
2bc457c
Merge remote-tracking branch 'upstream/f_vault4' into appPlatform_pro…
d2dyno1 Aug 3, 2026
65632b5
Fixed localization on MacOS
d2dyno1 Aug 3, 2026
1942b39
Require user confirmation when restoring a vault
d2dyno1 Aug 3, 2026
9c6f769
Added some missing strings
d2dyno1 Aug 4, 2026
30d9919
Minor adjustments
d2dyno1 Aug 4, 2026
04a58cc
Added vault V3 to V4 migration
d2dyno1 Aug 5, 2026
12036a1
Fixed vault AutoUnlock crash on MacOS
d2dyno1 Aug 6, 2026
f3116ec
Added error expansion to TransferControl
d2dyno1 Aug 8, 2026
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
1 change: 1 addition & 0 deletions SecureFolderFS.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@
</Project>
</Folder>
<Folder Name="/src/Sdk/">
<Project Path="src/Sdk/SecureFolderFS.Sdk.AppPlatform/SecureFolderFS.Sdk.AppPlatform.csproj" />
<Project Path="src/Sdk/SecureFolderFS.Sdk.Accounts/SecureFolderFS.Sdk.Accounts.csproj" />
<Project Path="src/Sdk/SecureFolderFS.Sdk.DeviceLink/SecureFolderFS.Sdk.DeviceLink.csproj" />
<Project Path="src/Sdk/SecureFolderFS.Sdk.Dropbox/SecureFolderFS.Sdk.Dropbox.csproj" />
Expand Down
2 changes: 1 addition & 1 deletion global.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"msbuild-sdks": {
"Uno.Sdk": "6.6.29"
"Uno.Sdk": "6.6.33"
}
}
61 changes: 59 additions & 2 deletions src/Core/SecureFolderFS.Core.Cryptography/Cipher/AesGcm256.cs
Original file line number Diff line number Diff line change
@@ -1,19 +1,37 @@
using System;
using System.Security.Cryptography;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Engines;
using Org.BouncyCastle.Crypto.Modes;
using Org.BouncyCastle.Crypto.Parameters;

namespace SecureFolderFS.Core.Cryptography.Cipher
{
public static class AesGcm256
{
private const int TAG_SIZE = 16;

public static void Encrypt(ReadOnlySpan<byte> bytes, ReadOnlySpan<byte> key, ReadOnlySpan<byte> nonce, Span<byte> tag, Span<byte> result, ReadOnlySpan<byte> associatedData)
{
using var aesGcm = new AesGcm(key, Constants.Crypto.Chunks.AesGcm.CHUNK_TAG_SIZE);
if (Constants.PreferBouncyCastle)
{
BcEncrypt(bytes, key, nonce, tag, result, associatedData);
return;
}

using var aesGcm = new AesGcm(key, TAG_SIZE);
aesGcm.Encrypt(nonce, bytes, result, tag, associatedData);
}

public static void Decrypt(ReadOnlySpan<byte> bytes, ReadOnlySpan<byte> key, ReadOnlySpan<byte> nonce, ReadOnlySpan<byte> tag, Span<byte> result, ReadOnlySpan<byte> associatedData)
{
using var aesGcm = new AesGcm(key, Constants.Crypto.Chunks.AesGcm.CHUNK_TAG_SIZE);
if (Constants.PreferBouncyCastle)
{
BcDecrypt(bytes, key, nonce, tag, result, associatedData);
return;
}

using var aesGcm = new AesGcm(key, TAG_SIZE);
aesGcm.Decrypt(nonce, bytes, tag, result, associatedData);
}

Expand All @@ -29,5 +47,44 @@ public static bool TryDecrypt(ReadOnlySpan<byte> bytes, ReadOnlySpan<byte> key,
return false;
}
}

private static void BcEncrypt(ReadOnlySpan<byte> bytes, ReadOnlySpan<byte> key, ReadOnlySpan<byte> nonce, Span<byte> tag, Span<byte> result, ReadOnlySpan<byte> associatedData)
{
var gcm = new GcmBlockCipher(new AesEngine());
gcm.Init(true, new AeadParameters(new KeyParameter(key.ToArray()), TAG_SIZE * 8, nonce.ToArray(), associatedData.ToArray()));

// BC concatenates ciphertext || tag into a single output buffer.
var output = new byte[gcm.GetOutputSize(bytes.Length)];
var written = gcm.ProcessBytes(bytes.ToArray(), 0, bytes.Length, output, 0);
gcm.DoFinal(output, written);

output.AsSpan(0, bytes.Length).CopyTo(result);
output.AsSpan(bytes.Length, TAG_SIZE).CopyTo(tag);
}

private static void BcDecrypt(ReadOnlySpan<byte> bytes, ReadOnlySpan<byte> key, ReadOnlySpan<byte> nonce, ReadOnlySpan<byte> tag, Span<byte> result, ReadOnlySpan<byte> associatedData)
{
var gcm = new GcmBlockCipher(new AesEngine());
gcm.Init(false, new AeadParameters(new KeyParameter(key.ToArray()), TAG_SIZE * 8, nonce.ToArray(), associatedData.ToArray()));

// BC expects ciphertext || tag as one input buffer.
var input = new byte[bytes.Length + tag.Length];
bytes.CopyTo(input);
tag.CopyTo(input.AsSpan(bytes.Length));

var output = new byte[gcm.GetOutputSize(input.Length)];
try
{
var written = gcm.ProcessBytes(input, 0, input.Length, output, 0);
gcm.DoFinal(output, written);
}
catch (InvalidCipherTextException ex)
{
// Match the native AesGcm contract so TryDecrypt and callers behave identically.
throw new CryptographicException("The authentication tag did not match.", ex);
}

output.AsSpan(0, result.Length).CopyTo(result);
}
}
}
62 changes: 49 additions & 13 deletions src/Core/SecureFolderFS.Core.Cryptography/Cipher/AesSiv256.cs
Original file line number Diff line number Diff line change
@@ -1,56 +1,92 @@
using System;
using System;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using Miscreant;

namespace SecureFolderFS.Core.Cryptography.Cipher
{
public sealed class AesSiv256 : IDisposable
{
private readonly Aead _aesCmacSiv;
private readonly Aead? _aesCmacSiv;
private readonly bool _preferBouncyCastle;

private AesSiv256(Aead aesCmacSiv)
/// <summary>
/// Holds the concatenated DEK and MAC key.
/// </summary>
/// <remarks>
/// Allocated pinned so the garbage collector cannot relocate it and leave copies of the
/// master keys scattered across the heap, and zeroed in <see cref="Dispose"/> so it does
/// not survive in a memory image after the vault is locked.
/// </remarks>
private readonly byte[] _longKey;

private AesSiv256(Aead? aesCmacSiv, byte[] longKey, bool preferBouncyCastle)
{
_aesCmacSiv = aesCmacSiv;
_longKey = longKey;
_preferBouncyCastle = preferBouncyCastle;
}

public static AesSiv256 CreateInstance(ReadOnlySpan<byte> dekKey, ReadOnlySpan<byte> macKey)
{
// The longKey will be split into two keys - one for S2V and the other one for CTR
var longKey = new byte[dekKey.Length + macKey.Length];
var longKeySpan = longKey.AsSpan();
var longKey = GC.AllocateArray<byte>(dekKey.Length + macKey.Length, pinned: true);
try
{
var longKeySpan = longKey.AsSpan();

// Copy keys
dekKey.CopyTo(longKeySpan);
macKey.CopyTo(longKeySpan.Slice(dekKey.Length));
// Copy keys
dekKey.CopyTo(longKeySpan);
macKey.CopyTo(longKeySpan.Slice(dekKey.Length));

var aesCmacSiv = Aead.CreateAesCmacSiv(longKey);
return new AesSiv256(aesCmacSiv);
if (Constants.PreferBouncyCastle)
return new AesSiv256(null, longKey, true);

var aesCmacSiv = Aead.CreateAesCmacSiv(longKey);
return new AesSiv256(aesCmacSiv, longKey, false);
}
catch (Exception)
{
CryptographicOperations.ZeroMemory(longKey);
throw;
}
}

[MethodImpl(MethodImplOptions.Synchronized)]
public byte[] Encrypt(ReadOnlySpan<byte> bytes, ReadOnlySpan<byte> associatedData)
{
return _aesCmacSiv.Seal(bytes.ToArray(), data: associatedData.ToArray());
if (_preferBouncyCastle)
return BouncyCastleAesSiv.Seal(_longKey, associatedData, bytes);

return _aesCmacSiv!.Seal(bytes.ToArray(), data: associatedData.ToArray());
}

[MethodImpl(MethodImplOptions.Synchronized)]
public byte[] Decrypt(ReadOnlySpan<byte> bytes, ReadOnlySpan<byte> associatedData)
{
return _aesCmacSiv.Open(bytes.ToArray(), data: associatedData.ToArray());
if (_preferBouncyCastle)
return BouncyCastleAesSiv.Open(_longKey, associatedData, bytes);

return _aesCmacSiv!.Open(bytes.ToArray(), data: associatedData.ToArray());
}

/// <inheritdoc/>
public void Dispose()
{
try
{
_aesCmacSiv.Dispose();
_aesCmacSiv?.Dispose();
}
catch (Exception ex)
{
// TODO: Investigate. Sometimes an exception is thrown when disposing the Aead instance
_ = ex;
}
finally
{
// Zero the master key material last, so the AEAD is torn down before its key disappears
CryptographicOperations.ZeroMemory(_longKey);
}
}
}
}
92 changes: 79 additions & 13 deletions src/Core/SecureFolderFS.Core.Cryptography/Cipher/Argon2id.cs
Original file line number Diff line number Diff line change
@@ -1,29 +1,95 @@
using System;
using System.Security.Cryptography;
using System.Threading.Tasks;

namespace SecureFolderFS.Core.Cryptography.Cipher
{
public static class Argon2id
{
public static void V2_DeriveKey(ReadOnlySpan<byte> password, ReadOnlySpan<byte> salt, Span<byte> result)
/// <summary>
/// Derives a KEK without blocking the calling thread.
/// </summary>
/// <param name="password">The password.</param>
/// <param name="salt">The salt.</param>
/// <param name="result">The result.</param>
/// <remarks>
/// Parallelism lanes: defined by <see cref="Constants.Crypto.Argon2.DEGREE_OF_PARALLELISM"/>.<br/>
/// Iterations: defined by <see cref="Constants.Crypto.Argon2.ITERATIONS"/>.<br/>
/// Memory: defined by <see cref="Constants.Crypto.Argon2.MEMORY_SIZE_KIBIBYTES"/>.<br/>
/// </remarks>
public static async Task DeriveKeyAsync(byte[] password, byte[] salt, byte[] result)
{
using var argon2id = new Konscious.Security.Cryptography.Argon2id(password.ToArray());
argon2id.Salt = salt.ToArray();
argon2id.DegreeOfParallelism = 8;
argon2id.Iterations = 8;
argon2id.MemorySize = 102400;
using var argon2id = new Konscious.Security.Cryptography.Argon2id(password);
argon2id.Salt = salt;
argon2id.DegreeOfParallelism = Constants.Crypto.Argon2.DEGREE_OF_PARALLELISM;
argon2id.Iterations = Constants.Crypto.Argon2.ITERATIONS;
argon2id.MemorySize = Constants.Crypto.Argon2.MEMORY_SIZE_KIBIBYTES;

argon2id.GetBytes(Constants.KeyTraits.ARGON2_KEK_LENGTH).CopyTo(result);
var kek = await argon2id.GetBytesAsync(Constants.KeyTraits.ARGON2_KEK_LENGTH).ConfigureAwait(false);
kek.CopyTo(result, 0);
CryptographicOperations.ZeroMemory(kek);
}

/// <summary>
/// Derives a KEK synchronously.
/// </summary>
/// <param name="password">The password.</param>
/// <param name="salt">The salt.</param>
/// <param name="result">The result.</param>
/// <remarks>
/// Parallelism lanes: defined by <see cref="Constants.Crypto.Argon2.DEGREE_OF_PARALLELISM"/>.<br/>
/// Iterations: defined by <see cref="Constants.Crypto.Argon2.ITERATIONS"/>.<br/>
/// Memory: defined by <see cref="Constants.Crypto.Argon2.MEMORY_SIZE_KIBIBYTES"/>.<br/>
/// </remarks>
public static void DeriveKey(ReadOnlySpan<byte> password, ReadOnlySpan<byte> salt, Span<byte> result)
{
using var argon2id = new Konscious.Security.Cryptography.Argon2id(password.ToArray());
argon2id.Salt = salt.ToArray();
argon2id.DegreeOfParallelism = Constants.Crypto.Argon2.DEGREE_OF_PARALLELISM;
argon2id.Iterations = Constants.Crypto.Argon2.ITERATIONS;
argon2id.MemorySize = Constants.Crypto.Argon2.MEMORY_SIZE_KIBIBYTES;
DeriveKeyCore(
password,
salt,
result,
Constants.Crypto.Argon2.DEGREE_OF_PARALLELISM,
Constants.Crypto.Argon2.ITERATIONS,
Constants.Crypto.Argon2.MEMORY_SIZE_KIBIBYTES);
}

public static void V2_DeriveKey(ReadOnlySpan<byte> password, ReadOnlySpan<byte> salt, Span<byte> result)
{
DeriveKeyCore(
password,
salt,
result,
degreeOfParallelism: 8,
iterations: 8,
memorySize: 102400);
}

private static void DeriveKeyCore(
ReadOnlySpan<byte> password,
ReadOnlySpan<byte> salt,
Span<byte> result,
int degreeOfParallelism,
int iterations,
int memorySize)
{
var passwordCopy = password.ToArray();
byte[]? kek = null;
try
{
using var argon2id = new Konscious.Security.Cryptography.Argon2id(passwordCopy);
argon2id.Salt = salt.ToArray();
argon2id.DegreeOfParallelism = degreeOfParallelism;
argon2id.Iterations = iterations;
argon2id.MemorySize = memorySize;

argon2id.GetBytes(Constants.KeyTraits.ARGON2_KEK_LENGTH).CopyTo(result);
kek = argon2id.GetBytes(Constants.KeyTraits.ARGON2_KEK_LENGTH);
kek.CopyTo(result);
}
finally
{
CryptographicOperations.ZeroMemory(passwordCopy);
if (kek is not null)
CryptographicOperations.ZeroMemory(kek);
}
}
}
}
Loading
Loading