Skip to content
Draft
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
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
77 changes: 77 additions & 0 deletions src/Core/SecureFolderFS.Core.Cryptography/Jwe/AccountKeyHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
using System;
using System.Security.Cryptography;
using Jose;

namespace SecureFolderFS.Core.Cryptography.Jwe
{
/// <summary>
/// Provides PBES2-based JWE operations for Account Key (passphrase) wrapping of EC private keys.
/// Used to bootstrap new devices when no device-specific JWE exists yet.
/// </summary>
public static class AccountKeyHelper
{
/// <summary>
/// Wraps an EC private key (in DER format) under a user-provided passphrase using PBES2-HS256+A128KW / A256GCM.
/// </summary>
/// <param name="privateKeyBytes">The EC private key bytes (DER-encoded) to wrap.</param>
/// <param name="passphrase">The user-provided Account Key passphrase.</param>
/// <returns>A JWE compact serialization string containing the encrypted private key.</returns>
public static string Wrap(byte[] privateKeyBytes, string passphrase)
{
return JWT.EncodeBytes(privateKeyBytes, passphrase, JweAlgorithm.PBES2_HS256_A128KW, JweEncryption.A256GCM);
}

/// <summary>
/// Unwraps an EC private key from a PBES2-protected JWE using the Account Key passphrase.
/// </summary>
/// <param name="jweCompact">The JWE compact serialization containing the wrapped private key.</param>
/// <param name="passphrase">The user-provided Account Key passphrase.</param>
/// <returns>The EC private key bytes (DER-encoded).</returns>
public static byte[] Unwrap(string jweCompact, string passphrase)
{
return JWT.DecodeBytes(jweCompact, passphrase, JweAlgorithm.PBES2_HS256_A128KW, JweEncryption.A256GCM);
}

/// <summary>
/// Wraps a user's EC private key for Account Key bootstrap.
/// The private key is stored in JWK format inside the JWE for cross-platform compatibility.
/// </summary>
/// <param name="userPrivateKey">The user's EC private key to wrap.</param>
/// <param name="passphrase">The user-provided Account Key passphrase.</param>
/// <returns>A JWE compact serialization containing the encrypted user private key (as JWK).</returns>
public static string WrapUserKey(ECDiffieHellman userPrivateKey, string passphrase)
{
var privateKeyJwk = EcKeyHelper.ExportPrivateKeyJwk(userPrivateKey);
var privateKeyBytes = System.Text.Encoding.UTF8.GetBytes(privateKeyJwk);
try
{
return Wrap(privateKeyBytes, passphrase);
}
finally
{
CryptographicOperations.ZeroMemory(privateKeyBytes);
}
}

/// <summary>
/// Unwraps a user's EC private key from an Account Key-protected JWE.
/// Expects the JWE to contain the private key in JWK format.
/// </summary>
/// <param name="jweCompact">The JWE compact serialization containing the wrapped user private key.</param>
/// <param name="passphrase">The user-provided Account Key passphrase.</param>
/// <returns>An <see cref="ECDiffieHellman"/> instance with the decrypted user private key.</returns>
public static ECDiffieHellman UnwrapUserKey(string jweCompact, string passphrase)
{
var privateKeyBytes = Unwrap(jweCompact, passphrase);
try
{
var jwk = System.Text.Encoding.UTF8.GetString(privateKeyBytes);
return EcKeyHelper.ImportPrivateKeyJwk(jwk);
}
finally
{
CryptographicOperations.ZeroMemory(privateKeyBytes);
}
}
}
}
159 changes: 159 additions & 0 deletions src/Core/SecureFolderFS.Core.Cryptography/Jwe/EcKeyHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
using System;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;

namespace SecureFolderFS.Core.Cryptography.Jwe
{
/// <summary>
/// Provides EC P-256 key pair generation, JWK serialization, and import/export operations.
/// </summary>
public static class EcKeyHelper
{
/// <summary>
/// Generates a new EC P-256 key pair for ECDH key agreement.
/// </summary>
public static ECDiffieHellman GenerateKeyPair()
{
return ECDiffieHellman.Create(ECCurve.NamedCurves.nistP256);
}

/// <summary>
/// Exports the public key of an <see cref="ECDiffieHellman"/> instance as a JWK JSON string.
/// </summary>
/// <param name="key">The key pair to export the public component from.</param>
/// <returns>A JSON string in JWK format containing the public key.</returns>
public static string ExportPublicKeyJwk(ECDiffieHellman key)
{
var parameters = key.ExportParameters(includePrivateParameters: false);
return SerializeJwk(parameters, includePrivate: false);
}

/// <summary>
/// Exports the full key pair (public + private) as a JWK JSON string.
/// </summary>
/// <param name="key">The key pair to export.</param>
/// <returns>A JSON string in JWK format containing both public and private key components.</returns>
public static string ExportPrivateKeyJwk(ECDiffieHellman key)
{
var parameters = key.ExportParameters(includePrivateParameters: true);
return SerializeJwk(parameters, includePrivate: true);
}

/// <summary>
/// Exports the private key as a DER-encoded byte array suitable for secure storage.
/// </summary>
/// <param name="key">The key pair to export the private key from.</param>
/// <returns>A byte array containing the private key in SEC1/ECPrivateKey format.</returns>
public static byte[] ExportPrivateKeyBytes(ECDiffieHellman key)
{
return key.ExportECPrivateKey();
}

/// <summary>
/// Imports an EC P-256 public key from a JWK JSON string.
/// </summary>
/// <param name="jwk">The JWK JSON string containing the public key.</param>
/// <returns>An <see cref="ECDiffieHellman"/> instance with only the public key component.</returns>
public static ECDiffieHellman ImportPublicKeyJwk(string jwk)
{
var parameters = DeserializeJwk(jwk);
parameters.D = null;
var ecdh = ECDiffieHellman.Create();
ecdh.ImportParameters(parameters);
return ecdh;
}

/// <summary>
/// Imports an EC P-256 key pair from a JWK JSON string that includes the private key.
/// </summary>
/// <param name="jwk">The JWK JSON string containing both public and private key components.</param>
/// <returns>An <see cref="ECDiffieHellman"/> instance with both public and private key components.</returns>
public static ECDiffieHellman ImportPrivateKeyJwk(string jwk)
{
var parameters = DeserializeJwk(jwk);
var ecdh = ECDiffieHellman.Create();
ecdh.ImportParameters(parameters);
return ecdh;
}

/// <summary>
/// Imports a private key from a DER-encoded byte array (SEC1/ECPrivateKey format).
/// </summary>
/// <param name="privateKeyBytes">The DER-encoded private key bytes.</param>
/// <returns>An <see cref="ECDiffieHellman"/> instance with the imported private key.</returns>
public static ECDiffieHellman ImportPrivateKeyBytes(byte[] privateKeyBytes)
{
var ecdh = ECDiffieHellman.Create();
ecdh.ImportECPrivateKey(privateKeyBytes, out _);
return ecdh;
}

private static string SerializeJwk(ECParameters parameters, bool includePrivate)
{
using var stream = new System.IO.MemoryStream();
using (var writer = new Utf8JsonWriter(stream))
{
writer.WriteStartObject();
writer.WriteString("kty", "EC");
writer.WriteString("crv", "P-256");
writer.WriteString("x", Base64UrlEncode(parameters.Q.X!));
writer.WriteString("y", Base64UrlEncode(parameters.Q.Y!));

if (includePrivate && parameters.D is not null)
writer.WriteString("d", Base64UrlEncode(parameters.D));

writer.WriteEndObject();
}

return Encoding.UTF8.GetString(stream.ToArray());
}

private static ECParameters DeserializeJwk(string jwk)
{
using var doc = JsonDocument.Parse(jwk);
var root = doc.RootElement;

var kty = root.GetProperty("kty").GetString();
var crv = root.GetProperty("crv").GetString();

if (kty != "EC" || crv != "P-256")
throw new CryptographicException($"Unsupported JWK key type or curve: kty={kty}, crv={crv}");

var parameters = new ECParameters
{
Curve = ECCurve.NamedCurves.nistP256,
Q = new ECPoint
{
X = Base64UrlDecode(root.GetProperty("x").GetString()!),
Y = Base64UrlDecode(root.GetProperty("y").GetString()!)
}
};

if (root.TryGetProperty("d", out var dElement) && dElement.GetString() is { } dValue)
parameters.D = Base64UrlDecode(dValue);

return parameters;
}

private static string Base64UrlEncode(byte[] data)
{
return Convert.ToBase64String(data)
.TrimEnd('=')
.Replace('+', '-')
.Replace('/', '_');
}

private static byte[] Base64UrlDecode(string base64Url)
{
var s = base64Url.Replace('-', '+').Replace('_', '/');
switch (s.Length % 4)
{
case 2: s += "=="; break;
case 3: s += "="; break;
}

return Convert.FromBase64String(s);
}
}
}
108 changes: 108 additions & 0 deletions src/Core/SecureFolderFS.Core.Cryptography/Jwe/JweHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
using System;
using System.Security.Cryptography;
using Jose;

namespace SecureFolderFS.Core.Cryptography.Jwe
{
/// <summary>
/// Provides JWE encryption/decryption using ECDH-ES+A256KW key agreement with A256GCM content encryption.
/// </summary>
public static class JweHelper
{
/// <summary>
/// Encrypts a byte payload for a recipient's EC P-256 public key, producing a JWE compact serialization.
/// </summary>
/// <param name="plaintext">The plaintext bytes to encrypt.</param>
/// <param name="recipientPublicKey">The recipient's EC P-256 public key (only the public component is used).</param>
/// <returns>A JWE compact serialization string.</returns>
public static string Encrypt(byte[] plaintext, ECDiffieHellman recipientPublicKey)
{
return JWT.EncodeBytes(plaintext, recipientPublicKey, JweAlgorithm.ECDH_ES_A256KW, JweEncryption.A256GCM);
}

/// <summary>
/// Encrypts a byte payload for a recipient identified by their public key JWK string.
/// </summary>
/// <param name="plaintext">The plaintext bytes to encrypt.</param>
/// <param name="recipientPublicKeyJwk">The recipient's public key as a JWK JSON string.</param>
/// <returns>A JWE compact serialization string.</returns>
public static string Encrypt(byte[] plaintext, string recipientPublicKeyJwk)
{
using var publicKey = EcKeyHelper.ImportPublicKeyJwk(recipientPublicKeyJwk);
return Encrypt(plaintext, publicKey);
}

/// <summary>
/// Decrypts a JWE compact serialization using the recipient's EC P-256 private key.
/// </summary>
/// <param name="jweCompact">The JWE compact serialization string to decrypt.</param>
/// <param name="recipientPrivateKey">The recipient's EC P-256 private key.</param>
/// <returns>The decrypted plaintext bytes.</returns>
public static byte[] Decrypt(string jweCompact, ECDiffieHellman recipientPrivateKey)
{
return JWT.DecodeBytes(jweCompact, recipientPrivateKey, JweAlgorithm.ECDH_ES_A256KW, JweEncryption.A256GCM);
}

/// <summary>
/// Decrypts a JWE compact serialization using a private key loaded from raw bytes.
/// </summary>
/// <param name="jweCompact">The JWE compact serialization string to decrypt.</param>
/// <param name="recipientPrivateKeyBytes">The recipient's private key as DER-encoded bytes.</param>
/// <returns>The decrypted plaintext bytes.</returns>
public static byte[] Decrypt(string jweCompact, byte[] recipientPrivateKeyBytes)
{
using var privateKey = EcKeyHelper.ImportPrivateKeyBytes(recipientPrivateKeyBytes);
return Decrypt(jweCompact, privateKey);
}

/// <summary>
/// Encrypts a vault key (DEK + MAC concatenated) for a recipient, producing a JWE.
/// </summary>
/// <param name="dekKey">The 32-byte Data Encryption Key.</param>
/// <param name="macKey">The 32-byte Message Authentication Code key.</param>
/// <param name="recipientPublicKeyJwk">The recipient's public key as a JWK JSON string.</param>
/// <returns>A JWE compact serialization containing the encrypted vault key material.</returns>
public static string EncryptVaultKey(ReadOnlySpan<byte> dekKey, ReadOnlySpan<byte> macKey, string recipientPublicKeyJwk)
{
var combined = new byte[dekKey.Length + macKey.Length];
try
{
dekKey.CopyTo(combined);
macKey.CopyTo(combined.AsSpan(dekKey.Length));
return Encrypt(combined, recipientPublicKeyJwk);
}
finally
{
CryptographicOperations.ZeroMemory(combined);
}
}

/// <summary>
/// Decrypts a JWE containing a vault key and splits it into DEK and MAC components.
/// </summary>
/// <param name="jweCompact">The JWE compact serialization containing the encrypted vault key.</param>
/// <param name="recipientPrivateKey">The recipient's EC P-256 private key.</param>
/// <returns>A tuple of (dekKey, macKey) byte arrays. Caller is responsible for zeroing these when done.</returns>
public static (byte[] dekKey, byte[] macKey) DecryptVaultKey(string jweCompact, ECDiffieHellman recipientPrivateKey)
{
var combined = Decrypt(jweCompact, recipientPrivateKey);
try
{
if (combined.Length != Constants.KeyTraits.DEK_KEY_LENGTH + Constants.KeyTraits.MAC_KEY_LENGTH)
throw new CryptographicException($"Decrypted vault key has unexpected length: {combined.Length}");

var dekKey = new byte[Constants.KeyTraits.DEK_KEY_LENGTH];
var macKey = new byte[Constants.KeyTraits.MAC_KEY_LENGTH];

combined.AsSpan(0, Constants.KeyTraits.DEK_KEY_LENGTH).CopyTo(dekKey);
combined.AsSpan(Constants.KeyTraits.DEK_KEY_LENGTH, Constants.KeyTraits.MAC_KEY_LENGTH).CopyTo(macKey);

return (dekKey, macKey);
}
finally
{
CryptographicOperations.ZeroMemory(combined);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

<ItemGroup>
<PackageReference Include="Base4K" Version="1.0.4" />
<PackageReference Include="jose-jwt" Version="5.3.0" />
<PackageReference Include="Konscious.Security.Cryptography.Argon2" Version="1.3.1" />
<PackageReference Include="Miscreant" Version="0.3.3" />
<PackageReference Include="NSec.Cryptography" Version="26.4.0" />
Expand Down
5 changes: 4 additions & 1 deletion src/Core/SecureFolderFS.Core/Models/SecurityWrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

namespace SecureFolderFS.Core.Models
{
internal sealed class SecurityWrapper : IWrapper<Security>, IEnumerable<KeyValuePair<string, object>>, IDisposable
internal sealed class SecurityWrapper : IWrapper<Security>, IWrapper<KeyPair>, IEnumerable<KeyValuePair<string, object>>, IDisposable
{
private readonly KeyPair _keyPair;
private readonly VaultConfigurationDataModel _configDataModel;
Expand All @@ -21,6 +21,9 @@ internal sealed class SecurityWrapper : IWrapper<Security>, IEnumerable<KeyValue
fileNameCipherId: _configDataModel.FileNameCipherId,
fileNameEncodingId: _configDataModel.FileNameEncodingId);

/// <inheritdoc/>
KeyPair IWrapper<KeyPair>.Inner => _keyPair;

public SecurityWrapper(KeyPair keyPair, VaultConfigurationDataModel configDataModel)
{
_keyPair = keyPair;
Expand Down
Loading
Loading