diff --git a/AesUtil.cs b/AesUtil.cs index 41927ce..422dfc7 100644 --- a/AesUtil.cs +++ b/AesUtil.cs @@ -1,76 +1,115 @@ -using System.Text; using System.Security.Cryptography; -using DotPulsar.Internal; -using Newtonsoft.Json.Linq; -using Newtonsoft.Json; +using System.Text; using DotPulsar.Abstractions; -using DotPulsar; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +namespace TuyaPulsar +{ + /// + /// Provides encryption and decryption utilities for Tuya's message format + /// Supporting both AES-GCM and AES-ECB modes + /// + public static class AesUtil + { + /// + /// Decrypts a message based on its encryption model + /// + /// The Pulsar message to decrypt + /// The access key for decryption + /// Decrypted message content + public static string DecryptMessage(IMessage message, string accessKey) + { + // Extract encryption model from message properties + message.Properties.TryGetValue("em", out var encryptionModel); + + // Parse message data + var data = Encoding.UTF8.GetString(message.Data); + ArgumentNullException.ThrowIfNull(data, nameof(data)); + + var payloadJson = (JObject?)JsonConvert.DeserializeObject(data); -class AesUtil { + var encryptedData = payloadJson?["data"]?.ToString(); + if (encryptedData == null) + return string.Empty; + var decryptionKey = accessKey.Substring(8, 16); - public static string DecryptMessage(IMessage message, string accessKey) { - message.Properties.TryGetValue("em", out var decrypt_model); - Console.WriteLine($"Received: {decrypt_model}"); - string data = Encoding.UTF8.GetString(message.Data); - ArgumentNullException.ThrowIfNull(data, nameof(data)); - JObject payloadJson = (JObject)JsonConvert.DeserializeObject(data); + // Decrypt based on encryption model + var decryptedData = encryptionModel == "aes_gcm" + ? DecryptUsingGcm(encryptedData, decryptionKey) + : DecryptUsingEcb(encryptedData, decryptionKey); - if (decrypt_model == "aes_gcm") { - return DecryptByGcm(payloadJson["data"].ToString(),accessKey.Substring(8,16)).Replace("\f","").Replace("\r","").Replace("\n","").Replace("\t","").Replace("\v","").Replace("\b",""); - } else { - return DecryptByEcb(payloadJson["data"].ToString(),accessKey.Substring(8,16)).Replace("\f","").Replace("\r","").Replace("\n","").Replace("\t","").Replace("\v","").Replace("\b",""); + // Clean up control characters + return CleanControlCharacters(decryptedData); } - } - //decrypt_by_gcm - private static string DecryptByGcm(string decryptStr, string Key) { - byte[] cadenaBytes = Convert.FromBase64String(decryptStr); - byte[] claveBytes = Encoding.UTF8.GetBytes(Key); - // The first 12 bytes are the nonce - byte[] nonce = new byte[12]; - Array.Copy(cadenaBytes, 0, nonce, 0, nonce.Length); + /// + /// Decrypts data using AES-GCM mode + /// + private static string DecryptUsingGcm(string encryptedData, string key) + { + var encryptedBytes = Convert.FromBase64String(encryptedData); + var keyBytes = Encoding.UTF8.GetBytes(key); + + // Extract components + var nonce = new byte[12]; + Array.Copy(encryptedBytes, 0, nonce, 0, nonce.Length); + + var ciphertext = new byte[encryptedBytes.Length - nonce.Length - 16]; + Array.Copy(encryptedBytes, nonce.Length, ciphertext, 0, ciphertext.Length); + + var tag = new byte[16]; + Array.Copy(encryptedBytes, encryptedBytes.Length - 16, tag, 0, tag.Length); - // The data to decrypt (excluding nonce and tag) - byte[] ciphertext = new byte[cadenaBytes.Length - nonce.Length - 16]; - Array.Copy(cadenaBytes, nonce.Length, ciphertext, 0, ciphertext.Length); + // Decrypt + var plaintext = new byte[ciphertext.Length]; + using var aesGcm = new AesGcm(keyBytes, 16); + aesGcm.Decrypt(nonce, ciphertext, tag, plaintext); - // The last 16 bytes are the authentication tag - byte[] tag = new byte[16]; - Array.Copy(cadenaBytes, cadenaBytes.Length - 16, tag, 0, tag.Length); + return Encoding.UTF8.GetString(plaintext); + } - using (AesGcm aesGcm = new AesGcm(claveBytes)) + /// + /// Decrypts data using AES-ECB mode + /// + private static string DecryptUsingEcb(string encryptedData, string key) { - byte[] plaintext = new byte[ciphertext.Length]; - aesGcm.Decrypt(nonce, ciphertext, tag, plaintext); - return System.Text.Encoding.UTF8.GetString(plaintext); - } - } + try + { + var encryptedBytes = Convert.FromBase64String(encryptedData); + var keyBytes = Encoding.UTF8.GetBytes(key); - //decrypt_by_aes - private static string DecryptByEcb(string decryptStr, string Key) { - try{ - byte[] cadenaBytes = Convert.FromBase64String(decryptStr); - byte[] claveBytes = Encoding.UTF8.GetBytes(Key); + using var aes = Aes.Create(); + aes.Mode = CipherMode.ECB; + aes.BlockSize = 128; + aes.Padding = PaddingMode.Zeros; - RijndaelManaged rijndaelManaged = new RijndaelManaged(); - rijndaelManaged.Mode = CipherMode.ECB; - rijndaelManaged.BlockSize = 128; - rijndaelManaged.Padding = PaddingMode.Zeros; - ICryptoTransform desencriptador; - desencriptador = rijndaelManaged.CreateDecryptor(claveBytes, rijndaelManaged.IV); - MemoryStream memStream = new MemoryStream(cadenaBytes); - CryptoStream cryptoStream; - cryptoStream = new CryptoStream(memStream, desencriptador, CryptoStreamMode.Read); - StreamReader streamReader = new StreamReader(cryptoStream); - string resultStr = streamReader.ReadToEnd(); + using var decryptor = aes.CreateDecryptor(keyBytes, null); + using var memStream = new MemoryStream(encryptedBytes); + using var cryptoStream = new CryptoStream(memStream, decryptor, CryptoStreamMode.Read); + using var reader = new StreamReader(cryptoStream); + + return reader.ReadToEnd(); + } + catch (Exception ex) + { + Console.WriteLine($"Decryption error: {ex.Message}"); + return string.Empty; + } + } - memStream.Close(); - cryptoStream.Close(); - return resultStr; - }catch (Exception ex){ - Console.WriteLine(ex); - return null; + /// + /// Removes control characters from the decrypted string + /// + private static string CleanControlCharacters(string input) + { + return input + .Replace("\f", "") + .Replace("\r", "") + .Replace("\n", "") + .Replace("\t", "") + .Replace("\v", "") + .Replace("\b", ""); } } } \ No newline at end of file diff --git a/MyAuthentication.cs b/MyAuthentication.cs index ab21b35..814743d 100644 --- a/MyAuthentication.cs +++ b/MyAuthentication.cs @@ -1,57 +1,61 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using DotPulsar.Abstractions; using System.Security.Cryptography; using System.Text; +using DotPulsar.Abstractions; -/// -/// Token-based authentication implementation. -/// -public class MyAuthentication : IAuthentication +namespace TuyaPulsar { - private readonly string _authData; - - public MyAuthentication(string accessId, string accessKey) - { - _authData = "{\"username\":\"" + accessId +"\", \"password\":\"" + GenPwd(accessId, accessKey) + "\"}"; - } - /// - /// The authentication method name + /// Implements token-based authentication for Tuya's Pulsar service. + /// Handles secure credential generation and authentication data formatting. /// - public string AuthenticationMethodName => "auth1"; - - /// - /// Get the authentication data - /// - public async ValueTask GetAuthenticationData(CancellationToken cancellationToken) + public class MyAuthentication : IAuthentication { - await Task.Delay(1, cancellationToken); + private readonly string _authData; - return System.Text.Encoding.UTF8.GetBytes(_authData); - } + public MyAuthentication(string accessId, string accessKey) + { + _authData = BuildAuthData(accessId, accessKey); + } + /// + /// Gets the authentication method identifier + /// + public string AuthenticationMethodName => "auth1"; - // pwd - private static string GenPwd(string accessId, string accessKey){ - string md5HexKey = Md5(accessKey); - string mixStr = accessId + md5HexKey; - String md5MixStr = Md5(mixStr); - return md5MixStr.Substring(8,16); - } + /// + /// Provides the authentication data for the Pulsar connection + /// + public async ValueTask GetAuthenticationData(CancellationToken cancellationToken) + { + await Task.Delay(1, cancellationToken); + return Encoding.UTF8.GetBytes(_authData); + } + + private static string BuildAuthData(string accessId, string accessKey) + { + string password = GeneratePassword(accessId, accessKey); + return $"{{\"username\":\"{accessId}\", \"password\":\"{password}\"}}"; + } + + private static string GeneratePassword(string accessId, string accessKey) + { + string md5Key = ComputeMd5Hash(accessKey); + string combinedString = accessId + md5Key; + string finalHash = ComputeMd5Hash(combinedString); + return finalHash.Substring(8, 16); + } - // md5 - private static string Md5(string md5Str) { - using (MD5 md5 = MD5.Create()) + private static string ComputeMd5Hash(string input) { - byte[] dataHash = md5.ComputeHash(Encoding.UTF8.GetBytes(md5Str)); - StringBuilder sb = new StringBuilder(); - foreach (byte b in dataHash) + using var md5 = MD5.Create(); + byte[] hashBytes = md5.ComputeHash(Encoding.UTF8.GetBytes(input)); + + StringBuilder builder = new(); + foreach (byte b in hashBytes) { - sb.Append(b.ToString("x2").ToLower()); + builder.Append(b.ToString("x2").ToLower()); } - return sb.ToString(); + return builder.ToString(); } } } \ No newline at end of file diff --git a/Program.cs b/Program.cs index f3cafb2..d190d0b 100644 --- a/Program.cs +++ b/Program.cs @@ -1,61 +1,82 @@ using DotPulsar; using DotPulsar.Abstractions; using DotPulsar.Extensions; -using DotPulsar.Internal; -using System.Security.Cryptography; -using System.Text; - - -// pulsar server url -const string CN_SERVER_URL = "pulsar+ssl://mqe.tuyacn.com:7285/"; -const string US_SERVER_URL = "pulsar+ssl://mqe.tuyaus.com:7285/"; -const string EU_SERVER_URL = "pulsar+ssl://mqe.tuyaeu.com:7285/"; -const string IND_SERVER_URL = "pulsar+ssl://mqe.tuyain.com:7285/"; -// env -const string MQ_ENV_PROD = "event"; -const string MQ_ENV_TEST = "event-test"; - -// accessId, accessKey,serverUrl,MQ_ENV -const string ACCESS_ID = ""; -const string ACCESS_KEY = ""; -const string PULSAR_SERVER_URL = CN_SERVER_URL; -const string MQ_ENV= MQ_ENV_PROD; - -const string topic = "persistent://" + ACCESS_ID + "/out/" + MQ_ENV; -const string subscrition = ACCESS_ID + "-sub"; - -IAuthentication auth = new MyAuthentication(ACCESS_ID, ACCESS_KEY); - -// connecting to pulsar://localhost:6650 -await using var client = PulsarClient.Builder() - .ServiceUrl(new System.Uri(PULSAR_SERVER_URL)) - .Authentication(auth) - .Build(); - - -// consume messages -await using var consumer = client.NewConsumer() - .SubscriptionName(subscrition) - .Topic(topic) - .SubscriptionType(SubscriptionType.Failover) - .Create(); - -await foreach (var message in consumer.Messages()) + +namespace TuyaPulsar { - string messageId = getMessageId(message.MessageId); - Console.WriteLine($"Received: {messageId}"); - string decryptData = AesUtil.DecryptMessage(message, ACCESS_KEY); - Console.WriteLine($"Received: {messageId} DecryptMessage: {decryptData}"); - handleMessage(message, messageId, decryptData); - await consumer.Acknowledge(message); -} - -string getMessageId(MessageId messageId) { - return $"{messageId.LedgerId}:{messageId.EntryId}:{messageId.Partition}:{messageId.BatchIndex}"; -} - -void handleMessage(IMessage message, string messageId, string decryptData) { - Console.WriteLine($"handle start : {messageId}"); - // TODO handle message - Console.WriteLine($"handle finish : {messageId}"); -} + public class Program + { + // Server URLs for different regions + private const string CN_SERVER_URL = "pulsar+ssl://mqe.tuyacn.com:7285/"; + private const string US_SERVER_URL = "pulsar+ssl://mqe.tuyaus.com:7285/"; + private const string EU_SERVER_URL = "pulsar+ssl://mqe.tuyaeu.com:7285/"; + private const string IND_SERVER_URL = "pulsar+ssl://mqe.tuyain.com:7285/"; + + // Environment settings + private const string MQ_ENV_PROD = "event"; + private const string MQ_ENV_TEST = "event-test"; + + // Configuration parameters + private const string ACCESS_ID = ""; // Your Tuya Access ID + private const string ACCESS_KEY = ""; // Your Tuya Access Key + private const string PULSAR_SERVER_URL = CN_SERVER_URL; + private const string MQ_ENV = MQ_ENV_PROD; + + public static async Task Main() + { + // Configure topic and subscription + string topic = $"persistent://{ACCESS_ID}/out/{MQ_ENV}"; + string subscription = $"{ACCESS_ID}-sub"; + + // Create authentication instance + IAuthentication auth = new MyAuthentication(ACCESS_ID, ACCESS_KEY); + + // Initialize Pulsar client + await using var client = PulsarClient.Builder() + .ServiceUrl(new Uri(PULSAR_SERVER_URL)) + .Authentication(auth) + .Build(); + + // Create consumer + await using var consumer = client.NewConsumer() + .SubscriptionName(subscription) + .Topic(topic) + .SubscriptionType(SubscriptionType.Failover) + .Create(); + + // Process messages + await foreach (var message in consumer.Messages()) + { + try + { + string messageId = GetMessageId(message.MessageId); + Console.WriteLine($"Received message: {messageId}"); + + string decryptedData = AesUtil.DecryptMessage(message, ACCESS_KEY); + Console.WriteLine($"Decrypted message {messageId}: {decryptedData}"); + + await HandleMessage(message, messageId, decryptedData); + await consumer.Acknowledge(message); + } + catch (Exception ex) + { + Console.WriteLine($"Error processing message: {ex.Message}"); + // Implement your error handling strategy here + } + } + } + + private static string GetMessageId(MessageId messageId) + { + return $"{messageId.LedgerId}:{messageId.EntryId}:{messageId.Partition}:{messageId.BatchIndex}"; + } + + private static async Task HandleMessage(IMessage message, string messageId, string decryptedData) + { + Console.WriteLine($"Processing message: {messageId}"); + // TODO: Implement your message handling logic here + await Task.Delay(1); // Placeholder for async operations + Console.WriteLine($"Finished processing message: {messageId}"); + } + } +} \ No newline at end of file diff --git a/README.md b/README.md index 1bf89d1..bea7dfa 100644 --- a/README.md +++ b/README.md @@ -1,47 +1,85 @@ -# tuya-pulsar-sdk-cs -# c# pulsar consumer client sdk example -# Support net framework versions -``` - net9.0 - net9.0 -``` +# Tuya Pulsar SDK for C# -# Install requirements +This project provides a C# implementation example of a Pulsar consumer client for the Tuya ecosystem. It demonstrates how to connect to Tuya's message queue service and process messages securely. -## pulsar client -``` - # current pulsar version ==3.4.0 - # more info: https://github.com/apache/pulsar-dotpulsar or https://pulsar.apache.org/docs/4.0.x/client-libraries-dotnet/ - -``` -## Json -``` - # Json - -``` +## Prerequisites +- .NET 9.0 or higher +- Tuya account with access credentials (ACCESS_ID and ACCESS_KEY) -# Required parameters -``` - ACCESS_ID = "" - ACCESS_KEY = "" - PULSAR_SERVER_URL = "" - MQ_ENV="" +## Installation + +### Required NuGet Packages + +```xml + + ``` -# PULSAR_SERVER_URL +## Configuration + +### Required Parameters + +To use this SDK, you need to configure the following parameters: + +```csharp +ACCESS_ID = "your_access_id" +ACCESS_KEY = "your_access_key" +PULSAR_SERVER_URL = "your_server_url" +MQ_ENV = "your_environment" // "event" for production, "event-test" for testing ``` - //CN - "pulsar+ssl://mqe.tuyacn.com:7285/"; - //US - "pulsar+ssl://mqe.tuyaus.com:7285/"; - //EU - "pulsar+ssl://mqe.tuyaeu.com:7285/"; - //IND - "pulsar+ssl://mqe.tuyain.com:7285/"; + +### Available Server URLs + +Choose the appropriate server URL based on your region: + +```csharp +// China +"pulsar+ssl://mqe.tuyacn.com:7285/" + +// United States +"pulsar+ssl://mqe.tuyaus.com:7285/" + +// Europe +"pulsar+ssl://mqe.tuyaeu.com:7285/" + +// India +"pulsar+ssl://mqe.tuyain.com:7285/" ``` -# Process the messages you receive,business logic is implemented in this method +## Usage + +The SDK provides a simple consumer implementation that handles message decryption and processing. To use it: + +1. Configure your credentials and server URL +2. Initialize the Pulsar client +3. Create a consumer +4. Process incoming messages through the `handleMessage` method + +### Basic Example + +```csharp +// Initialize client +await using var client = PulsarClient.Builder() + .ServiceUrl(new Uri(PULSAR_SERVER_URL)) + .Authentication(new MyAuthentication(ACCESS_ID, ACCESS_KEY)) + .Build(); + +// Create consumer +await using var consumer = client.NewConsumer() + .SubscriptionName(subscription) + .Topic(topic) + .SubscriptionType(SubscriptionType.Failover) + .Create(); ``` - handleMessage -``` \ No newline at end of file + +## Message Processing + +Messages are automatically decrypted using either AES-GCM or AES-ECB depending on the encryption mode specified in the message properties. You can implement your business logic in the `handleMessage` method. + +## Security + +This SDK implements secure authentication and encryption: +- Token-based authentication using MD5 hashing +- Message encryption using AES-GCM and AES-ECB +- SSL connection to Tuya's servers diff --git a/tuya-pulsar-sdk-cs.csproj b/tuya-pulsar-sdk-cs.csproj deleted file mode 100644 index 83e4399..0000000 --- a/tuya-pulsar-sdk-cs.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - - Exe - net9.0 - tuya_pulsar_sdk_cs - enable - enable - - - - - - - diff --git a/tuya-pulsar-sdk-cs.sln b/tuya-pulsar-sdk-cs.sln index 797d104..02900f5 100644 --- a/tuya-pulsar-sdk-cs.sln +++ b/tuya-pulsar-sdk-cs.sln @@ -3,7 +3,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.5.002.0 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "tuya-pulsar-sdk-cs", "tuya-pulsar-sdk-cs.csproj", "{367F536F-EB86-4B0D-8B44-184DCCBBB5EE}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TuyaPulsarSdk.CSharp", "TuyaPulsarSdk.CSharp.csproj", "{3A4E2174-FF2A-4F1A-97C6-E0EB278FF04F}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -11,10 +11,10 @@ Global Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {367F536F-EB86-4B0D-8B44-184DCCBBB5EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {367F536F-EB86-4B0D-8B44-184DCCBBB5EE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {367F536F-EB86-4B0D-8B44-184DCCBBB5EE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {367F536F-EB86-4B0D-8B44-184DCCBBB5EE}.Release|Any CPU.Build.0 = Release|Any CPU + {3A4E2174-FF2A-4F1A-97C6-E0EB278FF04F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3A4E2174-FF2A-4F1A-97C6-E0EB278FF04F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3A4E2174-FF2A-4F1A-97C6-E0EB278FF04F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3A4E2174-FF2A-4F1A-97C6-E0EB278FF04F}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE