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