365 lines
12 KiB
C#
365 lines
12 KiB
C#
namespace StellaOps.Cryptography.Plugin.Sm;
|
|
|
|
using Org.BouncyCastle.Crypto;
|
|
using Org.BouncyCastle.Crypto.Digests;
|
|
using Org.BouncyCastle.Crypto.Engines;
|
|
using Org.BouncyCastle.Crypto.Generators;
|
|
using Org.BouncyCastle.Crypto.Modes;
|
|
using Org.BouncyCastle.Crypto.Parameters;
|
|
using Org.BouncyCastle.Crypto.Signers;
|
|
using Org.BouncyCastle.Math;
|
|
using Org.BouncyCastle.Math.EC;
|
|
using Org.BouncyCastle.Security;
|
|
using StellaOps.Plugin.Abstractions;
|
|
using StellaOps.Plugin.Abstractions.Capabilities;
|
|
using StellaOps.Plugin.Abstractions.Context;
|
|
using StellaOps.Plugin.Abstractions.Health;
|
|
using StellaOps.Plugin.Abstractions.Lifecycle;
|
|
|
|
/// <summary>
|
|
/// Chinese national cryptographic standards plugin.
|
|
/// Implements SM2 (signatures), SM3 (hash), and SM4 (symmetric encryption).
|
|
/// </summary>
|
|
public sealed class SmPlugin : CryptoPluginBase
|
|
{
|
|
private SmOptions? _options;
|
|
private AsymmetricCipherKeyPair? _keyPair;
|
|
private readonly SecureRandom _random = new();
|
|
|
|
/// <inheritdoc />
|
|
public override PluginInfo Info => new(
|
|
Id: "com.stellaops.crypto.sm",
|
|
Name: "Chinese SM Cryptography Provider",
|
|
Version: "1.0.0",
|
|
Vendor: "Stella Ops",
|
|
Description: "Chinese national cryptographic standards SM2/SM3/SM4 (GM/T 0003-0004)",
|
|
LicenseId: "AGPL-3.0-or-later");
|
|
|
|
/// <inheritdoc />
|
|
public override IReadOnlyList<string> SupportedAlgorithms => new[]
|
|
{
|
|
"SM2-SM3",
|
|
"SM2-SHA256",
|
|
"SM3",
|
|
"SM4-CBC",
|
|
"SM4-ECB",
|
|
"SM4-GCM"
|
|
};
|
|
|
|
/// <inheritdoc />
|
|
protected override Task InitializeCryptoServiceAsync(IPluginContext context, CancellationToken ct)
|
|
{
|
|
_options = context.Configuration.Bind<SmOptions>() ?? new SmOptions();
|
|
|
|
if (!string.IsNullOrEmpty(_options.PrivateKeyHex))
|
|
{
|
|
LoadKeyFromHex(_options.PrivateKeyHex);
|
|
Context?.Logger.Info("SM provider initialized with configured key");
|
|
}
|
|
else if (_options.GenerateKeyOnInit)
|
|
{
|
|
_keyPair = GenerateSm2KeyPair();
|
|
Context?.Logger.Info("SM provider initialized with generated key pair");
|
|
}
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override bool CanHandle(CryptoOperation operation, string algorithm)
|
|
{
|
|
return algorithm.StartsWith("SM", StringComparison.OrdinalIgnoreCase) &&
|
|
SupportedAlgorithms.Contains(algorithm, StringComparer.OrdinalIgnoreCase);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override Task<byte[]> SignAsync(ReadOnlyMemory<byte> data, CryptoSignOptions options, CancellationToken ct)
|
|
{
|
|
EnsureActive();
|
|
ct.ThrowIfCancellationRequested();
|
|
|
|
if (_keyPair == null)
|
|
{
|
|
throw new InvalidOperationException("No signing key available.");
|
|
}
|
|
|
|
// SM2 signature with SM3 digest
|
|
var signer = new SM2Signer();
|
|
signer.Init(true, _keyPair.Private);
|
|
signer.BlockUpdate(data.Span.ToArray(), 0, data.Length);
|
|
var signature = signer.GenerateSignature();
|
|
|
|
Context?.Logger.Debug("Signed {DataLength} bytes with SM2", data.Length);
|
|
|
|
return Task.FromResult(signature);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override Task<bool> VerifyAsync(ReadOnlyMemory<byte> data, ReadOnlyMemory<byte> signature, CryptoVerifyOptions options, CancellationToken ct)
|
|
{
|
|
EnsureActive();
|
|
ct.ThrowIfCancellationRequested();
|
|
|
|
if (_keyPair == null)
|
|
{
|
|
throw new InvalidOperationException("No verification key available.");
|
|
}
|
|
|
|
var verifier = new SM2Signer();
|
|
verifier.Init(false, _keyPair.Public);
|
|
verifier.BlockUpdate(data.Span.ToArray(), 0, data.Length);
|
|
var isValid = verifier.VerifySignature(signature.ToArray());
|
|
|
|
Context?.Logger.Debug("Verified SM2 signature: {IsValid}", isValid);
|
|
|
|
return Task.FromResult(isValid);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override Task<byte[]> EncryptAsync(ReadOnlyMemory<byte> data, CryptoEncryptOptions options, CancellationToken ct)
|
|
{
|
|
EnsureActive();
|
|
ct.ThrowIfCancellationRequested();
|
|
|
|
var algorithm = options.Algorithm;
|
|
var keyBytes = GetSymmetricKey(options.KeyId);
|
|
byte[] encrypted;
|
|
|
|
if (algorithm.Contains("GCM", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
encrypted = Sm4GcmEncrypt(data.ToArray(), keyBytes, options.Iv, options.Aad);
|
|
}
|
|
else if (algorithm.Contains("CBC", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
encrypted = Sm4CbcEncrypt(data.ToArray(), keyBytes, options.Iv);
|
|
}
|
|
else
|
|
{
|
|
encrypted = Sm4EcbEncrypt(data.ToArray(), keyBytes);
|
|
}
|
|
|
|
Context?.Logger.Debug("Encrypted {DataLength} bytes with {Algorithm}", data.Length, algorithm);
|
|
|
|
return Task.FromResult(encrypted);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override Task<byte[]> DecryptAsync(ReadOnlyMemory<byte> data, CryptoDecryptOptions options, CancellationToken ct)
|
|
{
|
|
EnsureActive();
|
|
ct.ThrowIfCancellationRequested();
|
|
|
|
var algorithm = options.Algorithm;
|
|
var keyBytes = GetSymmetricKey(options.KeyId);
|
|
byte[] decrypted;
|
|
|
|
if (algorithm.Contains("GCM", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
decrypted = Sm4GcmDecrypt(data.ToArray(), keyBytes, options.Iv, options.Aad);
|
|
}
|
|
else if (algorithm.Contains("CBC", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
decrypted = Sm4CbcDecrypt(data.ToArray(), keyBytes, options.Iv);
|
|
}
|
|
else
|
|
{
|
|
decrypted = Sm4EcbDecrypt(data.ToArray(), keyBytes);
|
|
}
|
|
|
|
Context?.Logger.Debug("Decrypted {DataLength} bytes with {Algorithm}", data.Length, algorithm);
|
|
|
|
return Task.FromResult(decrypted);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override Task<byte[]> HashAsync(ReadOnlyMemory<byte> data, string algorithm, CancellationToken ct)
|
|
{
|
|
EnsureActive();
|
|
ct.ThrowIfCancellationRequested();
|
|
|
|
var digest = new SM3Digest();
|
|
digest.BlockUpdate(data.Span.ToArray(), 0, data.Length);
|
|
var hash = new byte[digest.GetDigestSize()];
|
|
digest.DoFinal(hash, 0);
|
|
|
|
Context?.Logger.Debug("Computed SM3 hash of {DataLength} bytes", data.Length);
|
|
|
|
return Task.FromResult(hash);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override ValueTask DisposeAsync()
|
|
{
|
|
_keyPair = null;
|
|
State = PluginLifecycleState.Stopped;
|
|
return ValueTask.CompletedTask;
|
|
}
|
|
|
|
private AsymmetricCipherKeyPair GenerateSm2KeyPair()
|
|
{
|
|
var domainParams = GetSm2DomainParameters();
|
|
var generator = new ECKeyPairGenerator();
|
|
generator.Init(new ECKeyGenerationParameters(domainParams, _random));
|
|
return generator.GenerateKeyPair();
|
|
}
|
|
|
|
private void LoadKeyFromHex(string privateKeyHex)
|
|
{
|
|
var d = new BigInteger(privateKeyHex, 16);
|
|
var domainParams = GetSm2DomainParameters();
|
|
var privateKey = new ECPrivateKeyParameters(d, domainParams);
|
|
var q = domainParams.G.Multiply(d);
|
|
var publicKey = new ECPublicKeyParameters(q, domainParams);
|
|
_keyPair = new AsymmetricCipherKeyPair(publicKey, privateKey);
|
|
}
|
|
|
|
private static ECDomainParameters GetSm2DomainParameters()
|
|
{
|
|
// SM2 recommended parameters (GM/T 0003.5-2012)
|
|
var p = new BigInteger("FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFF", 16);
|
|
var a = new BigInteger("FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFC", 16);
|
|
var b = new BigInteger("28E9FA9E9D9F5E344D5A9E4BCF6509A7F39789F515AB8F92DDBCBD414D940E93", 16);
|
|
var n = new BigInteger("FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFF7203DF6B21C6052B53BBF40939D54123", 16);
|
|
var h = BigInteger.One;
|
|
var gx = new BigInteger("32C4AE2C1F1981195F9904466A39C9948FE30BBFF2660BE1715A4589334C74C7", 16);
|
|
var gy = new BigInteger("BC3736A2F4F6779C59BDCEE36B692153D0A9877CC62A474002DF32E52139F0A0", 16);
|
|
|
|
var curve = new FpCurve(p, a, b, n, h);
|
|
var g = curve.CreatePoint(gx, gy);
|
|
return new ECDomainParameters(curve, g, n, h);
|
|
}
|
|
|
|
private byte[] GetSymmetricKey(string keyId)
|
|
{
|
|
// Derive 128-bit key from key ID using SM3
|
|
var digest = new SM3Digest();
|
|
var keyIdBytes = System.Text.Encoding.UTF8.GetBytes(keyId);
|
|
digest.BlockUpdate(keyIdBytes, 0, keyIdBytes.Length);
|
|
var hash = new byte[32];
|
|
digest.DoFinal(hash, 0);
|
|
return hash.Take(16).ToArray(); // SM4 uses 128-bit keys
|
|
}
|
|
|
|
private byte[] Sm4EcbEncrypt(byte[] data, byte[] key)
|
|
{
|
|
var engine = new SM4Engine();
|
|
engine.Init(true, new KeyParameter(key));
|
|
return ProcessBlocks(engine, data);
|
|
}
|
|
|
|
private byte[] Sm4EcbDecrypt(byte[] data, byte[] key)
|
|
{
|
|
var engine = new SM4Engine();
|
|
engine.Init(false, new KeyParameter(key));
|
|
return ProcessBlocks(engine, data);
|
|
}
|
|
|
|
private byte[] Sm4CbcEncrypt(byte[] data, byte[] key, byte[]? iv)
|
|
{
|
|
iv ??= GenerateIv(16);
|
|
var cipher = new CbcBlockCipher(new SM4Engine());
|
|
cipher.Init(true, new ParametersWithIV(new KeyParameter(key), iv));
|
|
var encrypted = ProcessBlocks(cipher, data);
|
|
|
|
// Prepend IV to ciphertext
|
|
var result = new byte[iv.Length + encrypted.Length];
|
|
Array.Copy(iv, 0, result, 0, iv.Length);
|
|
Array.Copy(encrypted, 0, result, iv.Length, encrypted.Length);
|
|
return result;
|
|
}
|
|
|
|
private byte[] Sm4CbcDecrypt(byte[] data, byte[] key, byte[]? iv)
|
|
{
|
|
if (iv == null)
|
|
{
|
|
// Extract IV from ciphertext
|
|
iv = data.Take(16).ToArray();
|
|
data = data.Skip(16).ToArray();
|
|
}
|
|
|
|
var cipher = new CbcBlockCipher(new SM4Engine());
|
|
cipher.Init(false, new ParametersWithIV(new KeyParameter(key), iv));
|
|
return ProcessBlocks(cipher, data);
|
|
}
|
|
|
|
private byte[] Sm4GcmEncrypt(byte[] data, byte[] key, byte[]? iv, byte[]? aad)
|
|
{
|
|
iv ??= GenerateIv(12);
|
|
var cipher = new GcmBlockCipher(new SM4Engine());
|
|
var parameters = new AeadParameters(new KeyParameter(key), 128, iv, aad ?? Array.Empty<byte>());
|
|
cipher.Init(true, parameters);
|
|
|
|
var output = new byte[cipher.GetOutputSize(data.Length)];
|
|
var len = cipher.ProcessBytes(data, 0, data.Length, output, 0);
|
|
cipher.DoFinal(output, len);
|
|
|
|
// Prepend IV to ciphertext
|
|
var result = new byte[iv.Length + output.Length];
|
|
Array.Copy(iv, 0, result, 0, iv.Length);
|
|
Array.Copy(output, 0, result, iv.Length, output.Length);
|
|
return result;
|
|
}
|
|
|
|
private byte[] Sm4GcmDecrypt(byte[] data, byte[] key, byte[]? iv, byte[]? aad)
|
|
{
|
|
if (iv == null)
|
|
{
|
|
iv = data.Take(12).ToArray();
|
|
data = data.Skip(12).ToArray();
|
|
}
|
|
|
|
var cipher = new GcmBlockCipher(new SM4Engine());
|
|
var parameters = new AeadParameters(new KeyParameter(key), 128, iv, aad ?? Array.Empty<byte>());
|
|
cipher.Init(false, parameters);
|
|
|
|
var output = new byte[cipher.GetOutputSize(data.Length)];
|
|
var len = cipher.ProcessBytes(data, 0, data.Length, output, 0);
|
|
cipher.DoFinal(output, len);
|
|
return output;
|
|
}
|
|
|
|
private static byte[] ProcessBlocks(IBlockCipher engine, byte[] data)
|
|
{
|
|
var blockSize = engine.GetBlockSize();
|
|
var paddedLength = ((data.Length + blockSize - 1) / blockSize) * blockSize;
|
|
var padded = new byte[paddedLength];
|
|
Array.Copy(data, padded, data.Length);
|
|
|
|
var output = new byte[paddedLength];
|
|
for (var i = 0; i < paddedLength; i += blockSize)
|
|
{
|
|
engine.ProcessBlock(padded, i, output, i);
|
|
}
|
|
|
|
return output;
|
|
}
|
|
|
|
private byte[] GenerateIv(int length)
|
|
{
|
|
var iv = new byte[length];
|
|
_random.NextBytes(iv);
|
|
return iv;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Configuration options for SM cryptography plugin.
|
|
/// </summary>
|
|
public sealed class SmOptions
|
|
{
|
|
/// <summary>
|
|
/// Private key in hexadecimal format.
|
|
/// </summary>
|
|
public string? PrivateKeyHex { get; init; }
|
|
|
|
/// <summary>
|
|
/// Generate a new key pair on initialization if no key configured.
|
|
/// </summary>
|
|
public bool GenerateKeyOnInit { get; init; } = true;
|
|
|
|
/// <summary>
|
|
/// User identifier for SM2 signature (ZA computation).
|
|
/// </summary>
|
|
public string UserId { get; init; } = "1234567812345678";
|
|
}
|