11 Feb 2023

Secure Secret Management with Windows DPAPI in C#

Learn how to securely store and retrieve sensitive data using Windows Data Protection API (DPAPI) in C#

Windows Data Protection API (DPAPI) provides a secure way to encrypt and decrypt sensitive data using user or machine-specific keys. Let’s explore how to implement secret management using DPAPI in C#.

Understanding DPAPI

DPAPI is a cryptographic API built into Windows that handles key management automatically. It offers two main protection scopes:

  1. CurrentUser: Data can only be decrypted by the same Windows user account
  2. LocalMachine: Data can be decrypted by any process on the same machine

Implementation

Let’s create a robust secret manager class that handles both storing and retrieving secrets:

using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;

public class DpapiSecretManager
{
    private readonly string _storageDirectory;
    private readonly DataProtectionScope _scope;

    public DpapiSecretManager(string storageDirectory, DataProtectionScope scope = DataProtectionScope.CurrentUser)
    {
        _storageDirectory = storageDirectory;
        _scope = scope;
        
        // Ensure storage directory exists
        Directory.CreateDirectory(_storageDirectory);
    }

    /// <summary>
    /// Stores a secret using DPAPI encryption
    /// </summary>
    /// <param name="secretName">Unique identifier for the secret</param>
    /// <param name="secretValue">The secret value to encrypt</param>
    public void StoreSecret(string secretName, string secretValue)
    {
        if (string.IsNullOrEmpty(secretName))
            throw new ArgumentNullException(nameof(secretName));
            
        if (string.IsNullOrEmpty(secretValue))
            throw new ArgumentNullException(nameof(secretValue));

        try
        {
            // Convert secret to bytes
            byte[] secretBytes = Encoding.UTF8.GetBytes(secretValue);
            
            // Encrypt the secret
            byte[] encryptedSecret = ProtectedData.Protect(
                secretBytes,
                optionalEntropy: null,
                scope: _scope);
            
            // Save to file
            string filePath = Path.Combine(_storageDirectory, $"{secretName}.encrypted");
            File.WriteAllBytes(filePath, encryptedSecret);
        }
        catch (CryptographicException ex)
        {
            throw new Exception($"Failed to encrypt secret: {secretName}", ex);
        }
        catch (IOException ex)
        {
            throw new Exception($"Failed to save secret: {secretName}", ex);
        }
    }

    /// <summary>
    /// Retrieves a secret using DPAPI decryption
    /// </summary>
    /// <param name="secretName">Name of the secret to retrieve</param>
    /// <returns>The decrypted secret value</returns>
    public string RetrieveSecret(string secretName)
    {
        if (string.IsNullOrEmpty(secretName))
            throw new ArgumentNullException(nameof(secretName));

        string filePath = Path.Combine(_storageDirectory, $"{secretName}.encrypted");
        
        if (!File.Exists(filePath))
            throw new FileNotFoundException($"Secret not found: {secretName}");

        try
        {
            // Read encrypted bytes
            byte[] encryptedSecret = File.ReadAllBytes(filePath);
            
            // Decrypt the secret
            byte[] decryptedSecret = ProtectedData.Unprotect(
                encryptedSecret,
                optionalEntropy: null,
                scope: _scope);
            
            // Convert back to string
            return Encoding.UTF8.GetString(decryptedSecret);
        }
        catch (CryptographicException ex)
        {
            throw new Exception($"Failed to decrypt secret: {secretName}", ex);
        }
        catch (IOException ex)
        {
            throw new Exception($"Failed to read secret: {secretName}", ex);
        }
    }

    /// <summary>
    /// Deletes a stored secret
    /// </summary>
    /// <param name="secretName">Name of the secret to delete</param>
    public void DeleteSecret(string secretName)
    {
        if (string.IsNullOrEmpty(secretName))
            throw new ArgumentNullException(nameof(secretName));

        string filePath = Path.Combine(_storageDirectory, $"{secretName}.encrypted");
        
        if (File.Exists(filePath))
        {
            try
            {
                File.Delete(filePath);
            }
            catch (IOException ex)
            {
                throw new Exception($"Failed to delete secret: {secretName}", ex);
            }
        }
    }

    /// <summary>
    /// Checks if a secret exists
    /// </summary>
    /// <param name="secretName">Name of the secret to check</param>
    /// <returns>True if the secret exists, false otherwise</returns>
    public bool SecretExists(string secretName)
    {
        if (string.IsNullOrEmpty(secretName))
            throw new ArgumentNullException(nameof(secretName));

        string filePath = Path.Combine(_storageDirectory, $"{secretName}.encrypted");
        return File.Exists(filePath);
    }
}

Usage Examples

Here’s how to use the DpapiSecretManager class:

// Initialize the secret manager
var secretManager = new DpapiSecretManager(
    @"C:\Secrets",
    DataProtectionScope.CurrentUser
);

// Store a secret
secretManager.StoreSecret("apiKey", "my-super-secret-api-key");

// Retrieve a secret
string apiKey = secretManager.RetrieveSecret("apiKey");

// Check if secret exists
bool exists = secretManager.SecretExists("apiKey");

// Delete a secret
secretManager.DeleteSecret("apiKey");

Best Practices

  1. Error Handling
    • Always wrap DPAPI operations in try-catch blocks
    • Provide meaningful error messages
    • Clean up resources in case of failures
  2. Security Considerations
    • Use CurrentUser scope when possible for better security
    • Don’t store the encryption key (DPAPI handles this)
    • Secure the storage directory with appropriate file system permissions
  3. File Management
    • Use unique filenames for each secret
    • Clean up old secrets when they’re no longer needed
    • Implement proper file locking mechanisms for concurrent access

Additional Security Layers

You can enhance the security further by:

  1. Adding Entropy ```csharp // Generate random entropy byte[] entropy = new byte[16]; using (var rng = new RNGCryptoServiceProvider()) { rng.GetBytes(entropy); }

// Use entropy in Protect/Unprotect calls byte[] encryptedData = ProtectedData.Protect(data, entropy, scope);


2. **Implementing Access Controls**
```csharp
DirectorySecurity securityRules = new DirectorySecurity();
securityRules.AddAccessRule(new FileSystemAccessRule(
    userName,
    FileSystemRights.ReadAndExecute,
    AccessControlType.Allow));

Common Issues and Solutions

  1. Cross-Machine Access
    • Use LocalMachine scope if secrets need to be accessed by multiple users
    • Be aware that this reduces security
  2. Backup and Recovery
    • DPAPI keys are tied to user accounts
    • Back up secrets before user profile changes
    • Consider implementing a recovery mechanism

Performance Considerations

  1. Caching
    • Consider caching frequently accessed secrets
    • Implement proper cache invalidation
    • Use thread-safe caching mechanisms
  2. File I/O
    • Minimize disk operations
    • Use async operations for better scalability
    • Implement proper disposal patterns

What’s Next?

  • Implementing secret rotation
  • Adding audit logging
  • Integrating with key management systems
  • Building a secret recovery system

References

  1. Microsoft Documentation: Windows Data Protection
  2. OWASP: Secret Management