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; /// /// Chinese national cryptographic standards plugin. /// Implements SM2 (signatures), SM3 (hash), and SM4 (symmetric encryption). /// public sealed class SmPlugin : CryptoPluginBase { private SmOptions? _options; private AsymmetricCipherKeyPair? _keyPair; private readonly SecureRandom _random = new(); /// 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"); /// public override IReadOnlyList SupportedAlgorithms => new[] { "SM2-SM3", "SM2-SHA256", "SM3", "SM4-CBC", "SM4-ECB", "SM4-GCM" }; /// protected override Task InitializeCryptoServiceAsync(IPluginContext context, CancellationToken ct) { _options = context.Configuration.Bind() ?? 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; } /// public override bool CanHandle(CryptoOperation operation, string algorithm) { return algorithm.StartsWith("SM", StringComparison.OrdinalIgnoreCase) && SupportedAlgorithms.Contains(algorithm, StringComparer.OrdinalIgnoreCase); } /// public override Task SignAsync(ReadOnlyMemory 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); } /// public override Task VerifyAsync(ReadOnlyMemory data, ReadOnlyMemory 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); } /// public override Task EncryptAsync(ReadOnlyMemory 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); } /// public override Task DecryptAsync(ReadOnlyMemory 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); } /// public override Task HashAsync(ReadOnlyMemory 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); } /// 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()); 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()); 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; } } /// /// Configuration options for SM cryptography plugin. /// public sealed class SmOptions { /// /// Private key in hexadecimal format. /// public string? PrivateKeyHex { get; init; } /// /// Generate a new key pair on initialization if no key configured. /// public bool GenerateKeyOnInit { get; init; } = true; /// /// User identifier for SM2 signature (ZA computation). /// public string UserId { get; init; } = "1234567812345678"; }