Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 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
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 @@ -83,6 +83,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
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);
}
}
}
25 changes: 18 additions & 7 deletions src/Core/SecureFolderFS.Core.Cryptography/Cipher/AesSiv256.cs
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
using System;
using System;
using System.Runtime.CompilerServices;
using Miscreant;

namespace SecureFolderFS.Core.Cryptography.Cipher
{
public sealed class AesSiv256 : IDisposable
{
private readonly Aead _aesCmacSiv;
private readonly Aead? _aesCmacSiv;
private readonly byte[]? _longKey;

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

public static AesSiv256 CreateInstance(ReadOnlySpan<byte> dekKey, ReadOnlySpan<byte> macKey)
Expand All @@ -23,28 +25,37 @@ public static AesSiv256 CreateInstance(ReadOnlySpan<byte> dekKey, ReadOnlySpan<b
dekKey.CopyTo(longKeySpan);
macKey.CopyTo(longKeySpan.Slice(dekKey.Length));

if (Constants.PreferBouncyCastle)
return new AesSiv256(null, longKey);

var aesCmacSiv = Aead.CreateAesCmacSiv(longKey);
return new AesSiv256(aesCmacSiv);
return new AesSiv256(aesCmacSiv, null);
}

[MethodImpl(MethodImplOptions.Synchronized)]
public byte[] Encrypt(ReadOnlySpan<byte> bytes, ReadOnlySpan<byte> associatedData)
{
return _aesCmacSiv.Seal(bytes.ToArray(), data: associatedData.ToArray());
if (_longKey is not null)
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 (_longKey is not null)
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)
{
Expand Down
155 changes: 155 additions & 0 deletions src/Core/SecureFolderFS.Core.Cryptography/Cipher/BouncyCastleAesSiv.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
using System;
using System.Security.Cryptography;
using Org.BouncyCastle.Crypto.Engines;
using Org.BouncyCastle.Crypto.Macs;
using Org.BouncyCastle.Crypto.Parameters;

namespace SecureFolderFS.Core.Cryptography.Cipher
{
/// <summary>
/// A pure-managed AES-CMAC-SIV (RFC 5297) implementation built on BouncyCastle.
/// </summary>
internal static class BouncyCastleAesSiv
{
private const int BlockSize = 16;

/// <summary>
/// Seals <paramref name="plaintext"/> with a single associated-data item, returning
/// SIV(16) || ciphertext.
/// </summary>
public static byte[] Seal(ReadOnlySpan<byte> key, ReadOnlySpan<byte> associatedData, ReadOnlySpan<byte> plaintext)
{
SplitKey(key, out var macKey, out var ctrKey);

var message = plaintext.ToArray();
var v = S2V(macKey, associatedData, message);

var output = new byte[BlockSize + message.Length];
v.CopyTo(output.AsSpan(0, BlockSize));
Ctr(ctrKey, v, message, 0, message.Length, output, BlockSize);
return output;
}

/// <summary>
/// Opens SIV(16) || ciphertext, returning the plaintext or throwing on an integrity failure.
/// </summary>
public static byte[] Open(ReadOnlySpan<byte> key, ReadOnlySpan<byte> associatedData, ReadOnlySpan<byte> input)
{
if (input.Length < BlockSize)
throw new CryptographicException("Malformed or corrupt ciphertext.");

SplitKey(key, out var macKey, out var ctrKey);

var v = input.Slice(0, BlockSize).ToArray();
var ciphertext = input.Slice(BlockSize);

var plaintext = new byte[ciphertext.Length];
Ctr(ctrKey, v, ciphertext.ToArray(), 0, ciphertext.Length, plaintext, 0);

var expected = S2V(macKey, associatedData, plaintext);
if (!CryptographicOperations.FixedTimeEquals(expected, v))
throw new CryptographicException("Malformed or corrupt ciphertext.");

return plaintext;
}

private static void SplitKey(ReadOnlySpan<byte> key, out byte[] macKey, out byte[] ctrKey)
{
if (key.Length != 32 && key.Length != 64)
throw new CryptographicException("Specified key is not a valid size for this algorithm.");

var half = key.Length / 2;
macKey = key.Slice(0, half).ToArray();
ctrKey = key.Slice(half).ToArray();
}

/// <summary>RFC 5297 S2V over a single header string and the message.</summary>
private static byte[] S2V(byte[] macKey, ReadOnlySpan<byte> header, byte[] message)
{
var d = Cmac(macKey, new byte[BlockSize]);

// Single associated-data item
Dbl(d);
Xor(d, Cmac(macKey, header.ToArray()), BlockSize);

if (message.Length >= BlockSize)
{
// T = message with its last block XORed into D
var t = (byte[])message.Clone();
var offset = t.Length - BlockSize;
for (var i = 0; i < BlockSize; i++)
t[offset + i] ^= d[i];

return Cmac(macKey, t);
}

var padded = new byte[BlockSize];
message.CopyTo(padded, 0);
padded[message.Length] = 0x80; // pad

Dbl(d);
Xor(d, padded, BlockSize);
return Cmac(macKey, d);
}

private static byte[] Cmac(byte[] key, byte[] data)
{
var mac = new CMac(new AesEngine());
mac.Init(new KeyParameter(key));
mac.BlockUpdate(data, 0, data.Length);
var result = new byte[mac.GetMacSize()];
mac.DoFinal(result, 0);
return result;
}

private static void Ctr(byte[] key, byte[] siv, byte[] input, int inputOffset, int length, byte[] output, int outputOffset)
{
// Zero out the two bits that RFC 5297 reserves so the counter never wraps into them.
var counter = (byte[])siv.Clone();
counter[counter.Length - 8] &= 0x7F;
counter[counter.Length - 4] &= 0x7F;

// Manual CTR mode in which the full 128-bit counter is incremented (big-endian) and its AES
// encryption is XORed into the data (the partial final block is handled by only consuming as many keystream bytes as remain).
var engine = new AesEngine();
engine.Init(true, new KeyParameter(key));

var keystream = new byte[BlockSize];
for (var position = 0; position < length; position += BlockSize)
{
engine.ProcessBlock(counter, 0, keystream, 0);

var count = Math.Min(BlockSize, length - position);
for (var i = 0; i < count; i++)
output[outputOffset + position + i] = (byte)(input[inputOffset + position + i] ^ keystream[i]);

IncrementBigEndian(counter);
}
}

private static void IncrementBigEndian(byte[] counter)
{
for (var i = counter.Length - 1; i >= 0; i--)
{
if (++counter[i] != 0)
break;
}
}

/// <summary>Doubles a 128-bit value in GF(2^128) (the "dbl" operation).</summary>
private static void Dbl(byte[] block)
{
var carry = block[0] >> 7;
for (var i = 0; i < BlockSize - 1; i++)
block[i] = (byte)((block[i] << 1) | (block[i + 1] >> 7));

block[BlockSize - 1] = (byte)((block[BlockSize - 1] << 1) ^ (carry == 1 ? 0x87 : 0x00));
}

private static void Xor(byte[] destination, byte[] source, int length)
{
for (var i = 0; i < length; i++)
destination[i] ^= source[i];
}
}
}
44 changes: 36 additions & 8 deletions src/Core/SecureFolderFS.Core.Cryptography/Cipher/Rfc3394KeyWrap.cs
Original file line number Diff line number Diff line change
@@ -1,33 +1,61 @@
using RFC3394;
using RFC3394;
using System;
using System.Security.Cryptography;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Engines;
using Org.BouncyCastle.Crypto.Parameters;

namespace SecureFolderFS.Core.Cryptography.Cipher
{
// TODO: Needs docs
public sealed class Rfc3394KeyWrap : IDisposable
{
private readonly RFC3394Algorithm _rfc3394;
private readonly RFC3394Algorithm? _rfc3394;

public Rfc3394KeyWrap()
{
_rfc3394 = new();
_rfc3394 = Constants.PreferBouncyCastle ? null : new();
}

public byte[] WrapKey(ReadOnlySpan<byte> bytes, ReadOnlySpan<byte> kek)
{
return _rfc3394.Wrap(kek: kek.ToArray(), plainKey: bytes.ToArray());
if (_rfc3394 is not null)
return _rfc3394.Wrap(kek: kek.ToArray(), plainKey: bytes.ToArray());

var engine = new AesWrapEngine();
engine.Init(true, new KeyParameter(kek.ToArray()));
var plain = bytes.ToArray();
return engine.Wrap(plain, 0, plain.Length);
}

public void UnwrapKey(ReadOnlySpan<byte> bytes, ReadOnlySpan<byte> kek, Span<byte> result)
{
var result2 = _rfc3394.Unwrap(kek: kek.ToArray(), wrappedKey: bytes.ToArray());
result2.CopyTo(result);
if (_rfc3394 is not null)
{
var unwrapped = _rfc3394.Unwrap(kek: kek.ToArray(), wrappedKey: bytes.ToArray());
unwrapped.CopyTo(result);
return;
}

var engine = new AesWrapEngine();
engine.Init(false, new KeyParameter(kek.ToArray()));
var wrapped = bytes.ToArray();
try
{
var unwrapped = engine.Unwrap(wrapped, 0, wrapped.Length);
unwrapped.CopyTo(result);
}
catch (InvalidCipherTextException ex)
{
// The native RFC3394.net path throws CryptographicException on an integrity failure;
// surface the same type so unlock's wrong-credential handling is unchanged.
throw new CryptographicException("The wrapped key failed its integrity check.", ex);
}
}

/// <inheritdoc/>
public void Dispose()
{
_rfc3394.Dispose();
_rfc3394?.Dispose();
}
}
}
6 changes: 5 additions & 1 deletion src/Core/SecureFolderFS.Core.Cryptography/Constants.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
namespace SecureFolderFS.Core.Cryptography
using System;

namespace SecureFolderFS.Core.Cryptography
{
public static class Constants
{
public static bool PreferBouncyCastle { get; set; } = OperatingSystem.IsBrowser();

public static class KeyTraits
{
public const string KEY_TEXT_SEPARATOR = "@@@";
Expand Down
Loading
Loading