463 lines
13 KiB
C#
Executable File
463 lines
13 KiB
C#
Executable File
using System.Security.Cryptography;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using System.Text.RegularExpressions;
|
|
using Microsoft.Extensions.Logging;
|
|
using MarketAlly.AIPlugin.Context.Configuration;
|
|
|
|
namespace MarketAlly.AIPlugin.Context.Security
|
|
{
|
|
/// <summary>
|
|
/// Provides encryption and security features for context storage
|
|
/// </summary>
|
|
public class EncryptedContextStorage
|
|
{
|
|
private readonly ContextConfiguration _configuration;
|
|
private readonly ILogger<EncryptedContextStorage> _logger;
|
|
private readonly SensitiveDataDetector _sensitiveDataDetector;
|
|
private readonly byte[] _encryptionKey;
|
|
private readonly AesCryptoServiceProvider _aes;
|
|
|
|
public EncryptedContextStorage(ContextConfiguration configuration, ILogger<EncryptedContextStorage> logger)
|
|
{
|
|
_configuration = configuration;
|
|
_logger = logger;
|
|
_sensitiveDataDetector = new SensitiveDataDetector(configuration.Security.SensitiveDataPatterns);
|
|
|
|
// Initialize encryption key
|
|
_encryptionKey = DeriveEncryptionKey(configuration.Security.EncryptionKey);
|
|
_aes = new AesCryptoServiceProvider
|
|
{
|
|
Key = _encryptionKey,
|
|
Mode = CipherMode.CBC,
|
|
Padding = PaddingMode.PKCS7
|
|
};
|
|
|
|
_logger.LogInformation("Encrypted context storage initialized with {SecurityLevel} security level",
|
|
configuration.Security.EnableEncryption ? "High" : "Standard");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Processes context content for security, encrypting sensitive data if configured
|
|
/// </summary>
|
|
public async Task<SecureContextEntry> SecureContextAsync(StoredContextEntry entry)
|
|
{
|
|
var secureEntry = new SecureContextEntry
|
|
{
|
|
Id = entry.Id,
|
|
Type = entry.Type,
|
|
Summary = entry.Summary,
|
|
Tags = entry.Tags,
|
|
ProjectPath = entry.ProjectPath,
|
|
Priority = entry.Priority,
|
|
Timestamp = entry.Timestamp,
|
|
Metadata = entry.Metadata,
|
|
IsEncrypted = false,
|
|
SensitiveDataDetected = false
|
|
};
|
|
|
|
try
|
|
{
|
|
// Detect sensitive data
|
|
var sensitiveItems = await _sensitiveDataDetector.DetectSensitiveDataAsync(entry.Content);
|
|
secureEntry.SensitiveDataDetected = sensitiveItems.Any();
|
|
|
|
if (sensitiveItems.Any())
|
|
{
|
|
_logger.LogWarning("Detected {Count} sensitive data patterns in context entry {EntryId}",
|
|
sensitiveItems.Count, entry.Id);
|
|
|
|
secureEntry.SensitiveDataTypes = sensitiveItems.Select(i => i.Type).Distinct().ToList();
|
|
}
|
|
|
|
// Encrypt content if encryption is enabled and sensitive data is detected
|
|
if (_configuration.Security.EnableEncryption &&
|
|
(secureEntry.SensitiveDataDetected || _configuration.Security.AutoEncryptSensitiveData))
|
|
{
|
|
secureEntry.Content = await EncryptContentAsync(entry.Content);
|
|
secureEntry.IsEncrypted = true;
|
|
|
|
_logger.LogDebug("Encrypted content for context entry {EntryId}", entry.Id);
|
|
}
|
|
else
|
|
{
|
|
// If not encrypting, optionally redact sensitive data
|
|
if (secureEntry.SensitiveDataDetected && _configuration.Security.EnableSensitiveDataDetection)
|
|
{
|
|
secureEntry.Content = RedactSensitiveData(entry.Content, sensitiveItems);
|
|
secureEntry.ContentRedacted = true;
|
|
}
|
|
else
|
|
{
|
|
secureEntry.Content = entry.Content;
|
|
}
|
|
}
|
|
|
|
return secureEntry;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Failed to secure context entry {EntryId}", entry.Id);
|
|
|
|
// Fallback: return entry with content redacted if there was an error
|
|
secureEntry.Content = "[CONTENT PROCESSING ERROR]";
|
|
secureEntry.ProcessingError = ex.Message;
|
|
return secureEntry;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Decrypts and returns the original context entry
|
|
/// </summary>
|
|
public async Task<StoredContextEntry> UnsecureContextAsync(SecureContextEntry secureEntry)
|
|
{
|
|
var entry = new StoredContextEntry
|
|
{
|
|
Id = secureEntry.Id,
|
|
Type = secureEntry.Type,
|
|
Summary = secureEntry.Summary,
|
|
Tags = secureEntry.Tags,
|
|
ProjectPath = secureEntry.ProjectPath,
|
|
Priority = secureEntry.Priority,
|
|
Timestamp = secureEntry.Timestamp,
|
|
Metadata = secureEntry.Metadata
|
|
};
|
|
|
|
try
|
|
{
|
|
if (secureEntry.IsEncrypted)
|
|
{
|
|
entry.Content = await DecryptContentAsync(secureEntry.Content);
|
|
_logger.LogDebug("Decrypted content for context entry {EntryId}", entry.Id);
|
|
}
|
|
else
|
|
{
|
|
entry.Content = secureEntry.Content;
|
|
}
|
|
|
|
return entry;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Failed to decrypt context entry {EntryId}", entry.Id);
|
|
|
|
// Return entry with error message if decryption fails
|
|
entry.Content = $"[DECRYPTION ERROR: {ex.Message}]";
|
|
return entry;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Encrypts content using AES encryption
|
|
/// </summary>
|
|
private async Task<string> EncryptContentAsync(string content)
|
|
{
|
|
if (string.IsNullOrEmpty(content))
|
|
return content;
|
|
|
|
try
|
|
{
|
|
var contentBytes = Encoding.UTF8.GetBytes(content);
|
|
|
|
using var encryptor = _aes.CreateEncryptor();
|
|
using var msEncrypt = new MemoryStream();
|
|
|
|
// Write IV to the beginning of the stream
|
|
await msEncrypt.WriteAsync(_aes.IV, 0, _aes.IV.Length);
|
|
|
|
using (var csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write))
|
|
{
|
|
await csEncrypt.WriteAsync(contentBytes, 0, contentBytes.Length);
|
|
}
|
|
|
|
var encryptedBytes = msEncrypt.ToArray();
|
|
return Convert.ToBase64String(encryptedBytes);
|
|
}
|
|
finally
|
|
{
|
|
// Generate new IV for next encryption
|
|
_aes.GenerateIV();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Decrypts content using AES decryption
|
|
/// </summary>
|
|
private async Task<string> DecryptContentAsync(string encryptedContent)
|
|
{
|
|
if (string.IsNullOrEmpty(encryptedContent))
|
|
return encryptedContent;
|
|
|
|
var encryptedBytes = Convert.FromBase64String(encryptedContent);
|
|
|
|
using var msDecrypt = new MemoryStream(encryptedBytes);
|
|
|
|
// Read IV from the beginning of the stream
|
|
var iv = new byte[_aes.IV.Length];
|
|
await msDecrypt.ReadAsync(iv, 0, iv.Length);
|
|
|
|
using var decryptor = _aes.CreateDecryptor(_aes.Key, iv);
|
|
using var csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read);
|
|
using var srDecrypt = new StreamReader(csDecrypt);
|
|
|
|
return await srDecrypt.ReadToEndAsync();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Redacts sensitive data from content
|
|
/// </summary>
|
|
private string RedactSensitiveData(string content, IEnumerable<SensitiveDataItem> sensitiveItems)
|
|
{
|
|
var redactedContent = content;
|
|
|
|
foreach (var item in sensitiveItems.OrderByDescending(i => i.StartIndex))
|
|
{
|
|
var redactionText = $"[REDACTED:{item.Type}]";
|
|
redactedContent = redactedContent.Remove(item.StartIndex, item.Length)
|
|
.Insert(item.StartIndex, redactionText);
|
|
}
|
|
|
|
return redactedContent;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Derives an encryption key from the configuration
|
|
/// </summary>
|
|
private byte[] DeriveEncryptionKey(string? configuredKey)
|
|
{
|
|
if (!string.IsNullOrEmpty(configuredKey))
|
|
{
|
|
// Use PBKDF2 to derive a proper key from the configured key
|
|
using var pbkdf2 = new Rfc2898DeriveBytes(
|
|
configuredKey,
|
|
Encoding.UTF8.GetBytes("MarketAlly.Context.Salt"),
|
|
10000,
|
|
HashAlgorithmName.SHA256);
|
|
|
|
return pbkdf2.GetBytes(32); // 256-bit key
|
|
}
|
|
else
|
|
{
|
|
// Generate a random key (this should be stored securely in production)
|
|
_logger.LogWarning("No encryption key configured, using randomly generated key. " +
|
|
"This key will not persist across application restarts.");
|
|
|
|
var key = new byte[32];
|
|
using var rng = RandomNumberGenerator.Create();
|
|
rng.GetBytes(key);
|
|
return key;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validates the integrity of encrypted data
|
|
/// </summary>
|
|
public async Task<SecurityValidationResult> ValidateSecurityAsync(SecureContextEntry entry)
|
|
{
|
|
var result = new SecurityValidationResult
|
|
{
|
|
EntryId = entry.Id,
|
|
IsValid = true,
|
|
ValidationTime = DateTime.UtcNow
|
|
};
|
|
|
|
try
|
|
{
|
|
// Test decryption if entry is encrypted
|
|
if (entry.IsEncrypted)
|
|
{
|
|
var decryptedContent = await DecryptContentAsync(entry.Content);
|
|
result.CanDecrypt = !string.IsNullOrEmpty(decryptedContent) &&
|
|
!decryptedContent.StartsWith("[DECRYPTION ERROR");
|
|
|
|
if (!result.CanDecrypt)
|
|
{
|
|
result.IsValid = false;
|
|
result.ValidationErrors.Add("Content cannot be decrypted");
|
|
}
|
|
}
|
|
|
|
// Validate sensitive data detection consistency
|
|
if (entry.SensitiveDataDetected && !entry.SensitiveDataTypes.Any())
|
|
{
|
|
result.ValidationWarnings.Add("Sensitive data detected but no types specified");
|
|
}
|
|
|
|
// Check for processing errors
|
|
if (!string.IsNullOrEmpty(entry.ProcessingError))
|
|
{
|
|
result.IsValid = false;
|
|
result.ValidationErrors.Add($"Processing error: {entry.ProcessingError}");
|
|
}
|
|
|
|
return result;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Security validation failed for entry {EntryId}", entry.Id);
|
|
|
|
result.IsValid = false;
|
|
result.ValidationErrors.Add($"Validation exception: {ex.Message}");
|
|
return result;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets security statistics for monitoring
|
|
/// </summary>
|
|
public SecurityStatistics GetSecurityStatistics()
|
|
{
|
|
return new SecurityStatistics
|
|
{
|
|
EncryptionEnabled = _configuration.Security.EnableEncryption,
|
|
SensitiveDataDetectionEnabled = _configuration.Security.EnableSensitiveDataDetection,
|
|
AutoEncryptionEnabled = _configuration.Security.AutoEncryptSensitiveData,
|
|
DetectionPatterns = _configuration.Security.SensitiveDataPatterns.Count,
|
|
EncryptionAlgorithm = "AES-256-CBC"
|
|
};
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
_aes?.Dispose();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Detects sensitive data in content using configurable patterns
|
|
/// </summary>
|
|
public class SensitiveDataDetector
|
|
{
|
|
private readonly List<SensitiveDataPattern> _patterns;
|
|
|
|
public SensitiveDataDetector(IEnumerable<string> patternStrings)
|
|
{
|
|
_patterns = patternStrings.Select((pattern, index) => new SensitiveDataPattern
|
|
{
|
|
Type = GetPatternType(pattern),
|
|
Regex = new Regex(pattern, RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
|
Pattern = pattern
|
|
}).ToList();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Detects sensitive data in the given content
|
|
/// </summary>
|
|
public async Task<List<SensitiveDataItem>> DetectSensitiveDataAsync(string content)
|
|
{
|
|
var items = new List<SensitiveDataItem>();
|
|
|
|
if (string.IsNullOrEmpty(content))
|
|
return items;
|
|
|
|
await Task.Run(() =>
|
|
{
|
|
foreach (var pattern in _patterns)
|
|
{
|
|
var matches = pattern.Regex.Matches(content);
|
|
|
|
foreach (Match match in matches)
|
|
{
|
|
items.Add(new SensitiveDataItem
|
|
{
|
|
Type = pattern.Type,
|
|
StartIndex = match.Index,
|
|
Length = match.Length,
|
|
Value = match.Value,
|
|
Pattern = pattern.Pattern
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
return items.OrderBy(i => i.StartIndex).ToList();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determines the pattern type from the regex pattern
|
|
/// </summary>
|
|
private string GetPatternType(string pattern)
|
|
{
|
|
return pattern switch
|
|
{
|
|
var p when p.Contains("@") => "Email",
|
|
var p when p.Contains("\\d{3}-\\d{2}-\\d{4}") => "SSN",
|
|
var p when p.Contains("\\d{4}") && p.Contains("[-\\s]") => "CreditCard",
|
|
var p when p.Contains("[A-Za-z0-9+/]{40,}") => "APIKey",
|
|
var p when p.Contains("bearer") => "BearerToken",
|
|
var p when p.Contains("password") => "Password",
|
|
_ => "Unknown"
|
|
};
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Secure context entry with encryption and security metadata
|
|
/// </summary>
|
|
public class SecureContextEntry
|
|
{
|
|
public string Id { get; set; } = "";
|
|
public string Type { get; set; } = "";
|
|
public string Content { get; set; } = "";
|
|
public string Summary { get; set; } = "";
|
|
public List<string> Tags { get; set; } = new();
|
|
public string ProjectPath { get; set; } = "";
|
|
public string Priority { get; set; } = "";
|
|
public DateTime Timestamp { get; set; }
|
|
public Dictionary<string, object> Metadata { get; set; } = new();
|
|
|
|
// Security properties
|
|
public bool IsEncrypted { get; set; }
|
|
public bool SensitiveDataDetected { get; set; }
|
|
public bool ContentRedacted { get; set; }
|
|
public List<string> SensitiveDataTypes { get; set; } = new();
|
|
public string? ProcessingError { get; set; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Pattern for detecting sensitive data
|
|
/// </summary>
|
|
public class SensitiveDataPattern
|
|
{
|
|
public string Type { get; set; } = "";
|
|
public Regex Regex { get; set; } = null!;
|
|
public string Pattern { get; set; } = "";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Detected sensitive data item
|
|
/// </summary>
|
|
public class SensitiveDataItem
|
|
{
|
|
public string Type { get; set; } = "";
|
|
public int StartIndex { get; set; }
|
|
public int Length { get; set; }
|
|
public string Value { get; set; } = "";
|
|
public string Pattern { get; set; } = "";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Result of security validation
|
|
/// </summary>
|
|
public class SecurityValidationResult
|
|
{
|
|
public string EntryId { get; set; } = "";
|
|
public bool IsValid { get; set; }
|
|
public bool CanDecrypt { get; set; }
|
|
public DateTime ValidationTime { get; set; }
|
|
public List<string> ValidationErrors { get; set; } = new();
|
|
public List<string> ValidationWarnings { get; set; } = new();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Security configuration and statistics
|
|
/// </summary>
|
|
public class SecurityStatistics
|
|
{
|
|
public bool EncryptionEnabled { get; set; }
|
|
public bool SensitiveDataDetectionEnabled { get; set; }
|
|
public bool AutoEncryptionEnabled { get; set; }
|
|
public int DetectionPatterns { get; set; }
|
|
public string EncryptionAlgorithm { get; set; } = "";
|
|
}
|
|
} |