You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
164 lines
4.2 KiB
164 lines
4.2 KiB
3 months ago
|
using System;
|
||
|
using System.Security.Cryptography;
|
||
|
using System.Text;
|
||
|
using System.Threading.Tasks;
|
||
|
using CellularManagement.Application.Services;
|
||
|
using CellularManagement.Infrastructure.Options;
|
||
|
using Microsoft.Extensions.Options;
|
||
|
using Microsoft.Extensions.Logging;
|
||
|
|
||
|
namespace CellularManagement.Infrastructure.Services;
|
||
|
|
||
|
/// <summary>
|
||
|
/// 密钥轮换服务实现
|
||
|
/// </summary>
|
||
|
public sealed class KeyRotationService : IKeyRotationService
|
||
|
{
|
||
|
private readonly JwtOptions _jwtOptions;
|
||
|
private readonly ILogger<KeyRotationService> _logger;
|
||
|
private string _currentKey;
|
||
|
private string _nextKey;
|
||
|
private DateTime _lastRotationTime;
|
||
|
|
||
|
/// <summary>
|
||
|
/// 构造函数
|
||
|
/// </summary>
|
||
|
public KeyRotationService(IOptions<JwtOptions> jwtOptions, ILogger<KeyRotationService> logger)
|
||
|
{
|
||
|
_jwtOptions = jwtOptions.Value;
|
||
|
_logger = logger;
|
||
|
_currentKey = _jwtOptions.SecretKey;
|
||
|
_nextKey = GenerateNewKey();
|
||
|
_lastRotationTime = DateTime.UtcNow;
|
||
|
}
|
||
|
|
||
|
/// <inheritdoc />
|
||
|
public async Task InitializeAsync()
|
||
|
{
|
||
|
try
|
||
|
{
|
||
|
// 验证当前密钥强度
|
||
|
ValidateKeyStrength(_currentKey);
|
||
|
|
||
|
// 如果当前密钥即将过期,立即轮换
|
||
|
if (ShouldRotateKey())
|
||
|
{
|
||
|
await RotateKeyAsync();
|
||
|
}
|
||
|
|
||
|
_logger.LogInformation("密钥轮换服务初始化完成");
|
||
|
}
|
||
|
catch (Exception ex)
|
||
|
{
|
||
|
_logger.LogError(ex, "密钥轮换服务初始化失败");
|
||
|
throw;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// <inheritdoc />
|
||
|
public string GetCurrentKey()
|
||
|
{
|
||
|
return _currentKey;
|
||
|
}
|
||
|
|
||
|
/// <inheritdoc />
|
||
|
public string GetNextKey()
|
||
|
{
|
||
|
return _nextKey;
|
||
|
}
|
||
|
|
||
|
/// <inheritdoc />
|
||
|
public async Task RotateKeyAsync()
|
||
|
{
|
||
|
try
|
||
|
{
|
||
|
// 验证新密钥强度
|
||
|
ValidateKeyStrength(_nextKey);
|
||
|
|
||
|
// 更新密钥
|
||
|
_currentKey = _nextKey;
|
||
|
_nextKey = GenerateNewKey();
|
||
|
_lastRotationTime = DateTime.UtcNow;
|
||
|
|
||
|
// 更新配置
|
||
|
_jwtOptions.SecretKey = _currentKey;
|
||
|
await Task.CompletedTask;
|
||
|
_logger.LogInformation("密钥轮换完成");
|
||
|
}
|
||
|
catch (Exception ex)
|
||
|
{
|
||
|
_logger.LogError(ex, "密钥轮换失败");
|
||
|
throw;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// <inheritdoc />
|
||
|
public bool ShouldRotateKey()
|
||
|
{
|
||
|
var timeSinceLastRotation = DateTime.UtcNow - _lastRotationTime;
|
||
|
return timeSinceLastRotation.TotalDays >= _jwtOptions.KeyRotationDays;
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// 生成新的密钥
|
||
|
/// </summary>
|
||
|
private string GenerateNewKey()
|
||
|
{
|
||
|
using var rng = RandomNumberGenerator.Create();
|
||
|
var keyBytes = new byte[_jwtOptions.MinKeyLength];
|
||
|
rng.GetBytes(keyBytes);
|
||
|
return Convert.ToBase64String(keyBytes);
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// 验证密钥强度
|
||
|
/// </summary>
|
||
|
private void ValidateKeyStrength(string key)
|
||
|
{
|
||
|
if (string.IsNullOrEmpty(key))
|
||
|
{
|
||
|
throw new ArgumentException("密钥不能为空");
|
||
|
}
|
||
|
|
||
|
if (key.Length < _jwtOptions.MinKeyLength)
|
||
|
{
|
||
|
throw new ArgumentException($"密钥长度必须至少为 {_jwtOptions.MinKeyLength} 字节");
|
||
|
}
|
||
|
|
||
|
// 检查密钥是否包含足够的随机性
|
||
|
var entropy = CalculateEntropy(key);
|
||
|
if (entropy < 3.5) // 3.5 bits per character is considered good
|
||
|
{
|
||
|
throw new ArgumentException("密钥随机性不足");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// 计算字符串的熵
|
||
|
/// </summary>
|
||
|
private static double CalculateEntropy(string input)
|
||
|
{
|
||
|
var frequencies = new Dictionary<char, int>();
|
||
|
foreach (var c in input)
|
||
|
{
|
||
|
if (frequencies.ContainsKey(c))
|
||
|
{
|
||
|
frequencies[c]++;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
frequencies[c] = 1;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
var length = input.Length;
|
||
|
var entropy = 0.0;
|
||
|
foreach (var frequency in frequencies.Values)
|
||
|
{
|
||
|
var probability = (double)frequency / length;
|
||
|
entropy -= probability * Math.Log2(probability);
|
||
|
}
|
||
|
|
||
|
return entropy;
|
||
|
}
|
||
|
}
|