Skip to content
Open
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
57 changes: 56 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ This is a `(t, n)` threshold scheme:
- **Byte-Oriented:** Primarily designed to split `byte[]` secrets. Convenience methods for `string` secrets (using UTF-8 encoding by default) are also provided.
- **Finite Field Arithmetic:** Performs calculations in GF(257) by default, suitable for byte-wise secret sharing (each byte 0-255 becomes a field element). The prime can be configured.
- **Share Serialization:** Includes methods to serialize shares to and from strings for easier storage or transmission.
- **Authenticated Shares:** Support for cryptographically authenticated shares with HMAC-SHA256 signatures to detect tampering and temporal validity tracking.
- **Unit Tested:** Comes with a set of MSTest unit tests to verify correctness.

## How to Use
Expand Down Expand Up @@ -121,6 +122,55 @@ The `Share` record provides methods for serialization:
- `share.ToString()`: Converts a `Share` object to a string like `"X:Y0,Y1,Y2,..."`.
- `Share.Parse(string s)`: Converts a serialized string back into a `Share` object.

### Authenticated Shares

For enhanced security, the library provides authenticated shares that include cryptographic signatures to detect tampering:

```csharp
using ShamirSecretSharing;

// Create an authenticator with a shared key
var key = Encoding.UTF8.GetBytes("YourSecureKeyAtLeast16BytesLong!");
var authenticator = new HmacShareAuthenticator(key);

// Or create with a random key
var (authenticator, generatedKey) = HmacShareAuthenticator.CreateWithRandomKey();

// Create the authenticated service
var authService = new AuthenticatedShamirService(authenticator);

// Split a secret with authentication
string secret = "Sensitive data requiring tamper protection";
int n = 5; // Total shares
int t = 3; // Threshold
var expiresIn = TimeSpan.FromDays(7); // Optional expiration

AuthenticatedShare[] authShares = authService.SplitAuthenticatedSecret(
secret, n, t, expiresIn);

// Serialize authenticated shares for storage/transmission
foreach (var authShare in authShares)
{
string serialized = authShare.ToString();
Console.WriteLine($"Authenticated Share {authShare.Share.X}: {serialized}");
}

// Reconstruct from authenticated shares
var collectedShares = authShares.Take(t).ToList(); // Use any t shares
string reconstructed = authService.ReconstructAuthenticatedSecretString(
collectedShares, t);

// The service automatically validates signatures and expiration
// If any share is tampered with or expired, reconstruction will fail with an exception
```

#### Key Features of Authenticated Shares:

- **Tamper Detection:** Each share includes an HMAC-SHA256 signature that is verified during reconstruction
- **Temporal Validity:** Shares can have optional expiration times
- **Validation:** The `ValidateShares` method allows checking share integrity before reconstruction
- **Flexible Authentication:** The `IShareAuthenticator` interface allows custom authentication implementations

### Prime Number (`_field.Prime`)

- The default prime used is 257. This is suitable for splitting `byte[]` secrets, as each byte (0-255) can be a field element.
Expand All @@ -131,7 +181,8 @@ The `Share` record provides methods for serialization:

- **Randomness:** The security of SSS relies on the cryptographic randomness of the coefficients chosen for the polynomial. This implementation uses `System.Security.Cryptography.RandomNumberGenerator` for this purpose.
- **Share Security:** Each individual share must be kept secret. If an attacker obtains `t` or more shares, they can reconstruct the secret. SSS protects against the loss/compromise of *up to* `t-1` shares.
- **Integrity/Authenticity:** This basic SSS implementation does not inherently protect against malicious shares (a participant providing a fake or altered share during reconstruction). For such scenarios, Verifiable Secret Sharing (VSS) schemes are needed.
- **Integrity/Authenticity:** The library now provides authenticated shares using HMAC-SHA256 signatures to protect against malicious or corrupted shares. The `AuthenticatedShamirService` automatically validates share integrity during reconstruction, rejecting tampered shares.
- **Key Management:** When using authenticated shares, the HMAC key must be securely shared among all authorized parties. Consider using asymmetric authentication (RSA/ECDSA) for scenarios where key distribution is challenging.
- **Side Channels:** As with any cryptographic implementation, consider potential side-channel attacks depending on the environment where share generation or reconstruction occurs.

## Limitations
Expand All @@ -144,6 +195,10 @@ The `Share` record provides methods for serialization:
- `FiniteField.cs`: Implements arithmetic operations in a Galois Field GF(p).
- `Share.cs`: Defines the `Share` record and its serialization/deserialization logic.
- `ShamirSecretSharingService.cs`: Contains the core logic for splitting and reconstructing secrets.
- `AuthenticatedShare.cs`: Extends shares with cryptographic signatures and timestamps.
- `IShareAuthenticator.cs`: Interface for implementing different authentication strategies.
- `HmacShareAuthenticator.cs`: HMAC-SHA256 based share authentication implementation.
- `AuthenticatedShamirService.cs`: Service for creating and verifying authenticated shares.
- `ShamirSecretSharingTests/` (Separate Project): Contains MSTest unit tests.
- `ShamirSecretSharing.Console/` (Separate Project): Contains a console application for testing the library interactively.

Expand Down
225 changes: 225 additions & 0 deletions ShamirSecretSharing/AuthenticatedShamirService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
using System.Text;

namespace ShamirSecretSharing;

/// <summary>
/// Provides authenticated Shamir's Secret Sharing functionality with tamper detection and temporal validity.
/// </summary>
/// <remarks>
/// This service extends the basic Shamir's Secret Sharing scheme by adding cryptographic signatures
/// to shares, enabling detection of tampered or corrupted shares before reconstruction.
/// </remarks>
public class AuthenticatedShamirService
{
private readonly ShamirSecretSharingService _shamirService;
private readonly IShareAuthenticator _authenticator;

/// <summary>
/// Initializes a new instance of the <see cref="AuthenticatedShamirService"/> class.
/// </summary>
/// <param name="authenticator">The authenticator to use for signing and verifying shares.</param>
/// <param name="prime">The prime modulus for the finite field (default: 257).</param>
/// <exception cref="ArgumentNullException">Thrown if authenticator is null.</exception>
public AuthenticatedShamirService(IShareAuthenticator authenticator, int prime = 257)
{
_authenticator = authenticator ?? throw new ArgumentNullException(nameof(authenticator));
_shamirService = new ShamirSecretSharingService(prime);
}

/// <summary>
/// Splits a secret byte array into authenticated shares with integrity protection.
/// </summary>
/// <param name="secret">The secret data to split.</param>
/// <param name="n">The total number of shares to create.</param>
/// <param name="t">The threshold of shares required to reconstruct the secret.</param>
/// <param name="expiresIn">Optional duration after which shares expire.</param>
/// <returns>An array of authenticated shares.</returns>
/// <exception cref="ArgumentException">Thrown if the secret is invalid.</exception>
/// <exception cref="ArgumentOutOfRangeException">Thrown if n or t are invalid.</exception>
public AuthenticatedShare[] SplitAuthenticatedSecret(byte[] secret, int n, int t, TimeSpan? expiresIn = null)
{
// Use the underlying service to split the secret
var shares = _shamirService.SplitSecret(secret, n, t);

var createdAt = DateTimeOffset.UtcNow;
var expiresAt = expiresIn.HasValue ? createdAt.Add(expiresIn.Value) : (DateTimeOffset?)null;

var authenticatedShares = new AuthenticatedShare[shares.Length];
for (var i = 0; i < shares.Length; i++)
{
var signature = _authenticator.SignShare(shares[i], createdAt, expiresAt);
authenticatedShares[i] = new AuthenticatedShare(shares[i], signature, createdAt, expiresAt);
}
Comment on lines +47 to +52
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The for loop used to create authenticatedShares can be expressed more concisely and functionally using LINQ's Select method. This improves readability by making the transformation from a Share to an AuthenticatedShare more declarative.

        var authenticatedShares = shares
            .Select(share =>
            {
                var signature = _authenticator.SignShare(share, createdAt, expiresAt);
                return new AuthenticatedShare(share, signature, createdAt, expiresAt);
            })
            .ToArray();


return authenticatedShares;
}

/// <summary>
/// Splits a secret string into authenticated shares with integrity protection.
/// </summary>
/// <param name="secret">The secret string to split.</param>
/// <param name="n">The total number of shares to create.</param>
/// <param name="t">The threshold of shares required to reconstruct the secret.</param>
/// <param name="expiresIn">Optional duration after which shares expire.</param>
/// <param name="encoding">The encoding to use (default: UTF-8).</param>
/// <returns>An array of authenticated shares.</returns>
public AuthenticatedShare[] SplitAuthenticatedSecret(string secret, int n, int t, TimeSpan? expiresIn = null, Encoding? encoding = null)
{
encoding ??= Encoding.UTF8;
return SplitAuthenticatedSecret(encoding.GetBytes(secret), n, t, expiresIn);
}

/// <summary>
/// Reconstructs the secret from authenticated shares after validating their integrity.
/// </summary>
/// <param name="authenticatedShares">The authenticated shares to use for reconstruction.</param>
/// <param name="t">The threshold number of shares required.</param>
/// <param name="validateExpiry">Whether to check if shares have expired (default: true).</param>
/// <returns>The reconstructed secret as a byte array.</returns>
/// <exception cref="ArgumentException">Thrown if shares are invalid, tampered, or expired.</exception>
/// <exception cref="InvalidOperationException">Thrown if share validation fails.</exception>
public byte[] ReconstructAuthenticatedSecret(IReadOnlyList<AuthenticatedShare> authenticatedShares, int t, bool validateExpiry = true)
{
if (authenticatedShares == null || authenticatedShares.Count == 0)
throw new ArgumentException("Authenticated shares list cannot be null or empty.", nameof(authenticatedShares));

if (authenticatedShares.Count < t)
throw new ArgumentException($"Not enough shares provided. Need {t}, got {authenticatedShares.Count}.", nameof(authenticatedShares));

// Validate all shares before reconstruction
var validationResults = ValidateShares(authenticatedShares, validateExpiry);

// Check if we have enough valid shares
var validShares = validationResults
.Where(r => r.IsValid && r.Share != null)
.Select(r => r.Share!)
.ToList();

if (validShares.Count < t)
{
var invalidCount = validationResults.Count(r => !r.IsValid);
var reasons = validationResults
.Where(r => !r.IsValid)
.Select(r => r.FailureReason)
.Distinct()
.ToList();

throw new InvalidOperationException(
$"Not enough valid shares for reconstruction. {invalidCount} shares failed validation. " +
$"Reasons: {string.Join(", ", reasons)}");
}

// Extract the underlying shares for reconstruction
var shares = validShares
.Select(s => s.Share)
.Take(t) // Only use the required threshold number
.ToList();

return _shamirService.ReconstructSecret(shares, t);
}

/// <summary>
/// Reconstructs a secret string from authenticated shares.
/// </summary>
/// <param name="authenticatedShares">The authenticated shares to use for reconstruction.</param>
/// <param name="t">The threshold number of shares required.</param>
/// <param name="validateExpiry">Whether to check if shares have expired (default: true).</param>
/// <param name="encoding">The encoding to use (default: UTF-8).</param>
/// <returns>The reconstructed secret string.</returns>
public string ReconstructAuthenticatedSecretString(
IReadOnlyList<AuthenticatedShare> authenticatedShares,
int t,
bool validateExpiry = true,
Encoding? encoding = null)
{
encoding ??= Encoding.UTF8;
var reconstructedBytes = ReconstructAuthenticatedSecret(authenticatedShares, t, validateExpiry);
return encoding.GetString(reconstructedBytes);
}

/// <summary>
/// Validates a collection of authenticated shares.
/// </summary>
/// <param name="authenticatedShares">The shares to validate.</param>
/// <param name="checkExpiry">Whether to check for expired shares (default: true).</param>
/// <returns>A list of validation results for each share.</returns>
public IReadOnlyList<ShareValidationResult> ValidateShares(
IReadOnlyList<AuthenticatedShare> authenticatedShares,
bool checkExpiry = true)
{
if (authenticatedShares == null)
throw new ArgumentNullException(nameof(authenticatedShares));

var results = new List<ShareValidationResult>();
var currentTime = DateTimeOffset.UtcNow;

foreach (var share in authenticatedShares)
{
if (share == null)
{
results.Add(new ShareValidationResult(null, false, "Share is null"));
continue;
}

// Check expiry
if (checkExpiry && share.IsExpiredAt(currentTime))
{
results.Add(new ShareValidationResult(share, false, "Share has expired"));
continue;
}

// Verify signature
bool signatureValid;
try
{
signatureValid = _authenticator.VerifyShare(share);
}
catch (Exception ex)
{
results.Add(new ShareValidationResult(share, false, $"Signature verification failed: {ex.Message}"));
continue;
}
Comment on lines +173 to +181
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The try-catch block for signature verification catches a generic Exception. This is too broad as it can mask unrelated runtime issues (e.g., OutOfMemoryException) or bugs within the VerifyShare implementation. It's a better practice to catch more specific exceptions that are expected from a cryptographic operation, such as System.Security.Cryptography.CryptographicException.

            catch (System.Security.Cryptography.CryptographicException ex)
            {
                results.Add(new ShareValidationResult(share, false, $"Signature verification failed: {ex.Message}"));
                continue;
            }


if (!signatureValid)
{
results.Add(new ShareValidationResult(share, false, "Invalid signature - share may be tampered"));
continue;
}

results.Add(new ShareValidationResult(share, true, null));
}

return results;
}

/// <summary>
/// Represents the result of validating an authenticated share.
/// </summary>
public record ShareValidationResult
{
/// <summary>
/// Gets the share that was validated.
/// </summary>
public AuthenticatedShare? Share { get; }

/// <summary>
/// Gets whether the share passed validation.
/// </summary>
public bool IsValid { get; }

/// <summary>
/// Gets the reason for validation failure, if any.
/// </summary>
public string? FailureReason { get; }

/// <summary>
/// Initializes a new instance of the <see cref="ShareValidationResult"/> record.
/// </summary>
public ShareValidationResult(AuthenticatedShare? share, bool isValid, string? failureReason)
{
Share = share;
IsValid = isValid;
FailureReason = failureReason;
}
}
}
Loading