Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Solve issue #1 + Cleanup + Readme #2

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 97 additions & 58 deletions AesUtil.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Provides encryption and decryption utilities for Tuya's message format
/// Supporting both AES-GCM and AES-ECB modes
/// </summary>
public static class AesUtil
{
/// <summary>
/// Decrypts a message based on its encryption model
/// </summary>
/// <param name="message">The Pulsar message to decrypt</param>
/// <param name="accessKey">The access key for decryption</param>
/// <returns>Decrypted message content</returns>
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);
/// <summary>
/// Decrypts data using AES-GCM mode
/// </summary>
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))
/// <summary>
/// Decrypts data using AES-ECB mode
/// </summary>
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;
/// <summary>
/// Removes control characters from the decrypted string
/// </summary>
private static string CleanControlCharacters(string input)
{
return input
.Replace("\f", "")
.Replace("\r", "")
.Replace("\n", "")
.Replace("\t", "")
.Replace("\v", "")
.Replace("\b", "");
}
}
}
84 changes: 44 additions & 40 deletions MyAuthentication.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Token-based authentication implementation.
/// </summary>
public class MyAuthentication : IAuthentication
namespace TuyaPulsar
{
private readonly string _authData;

public MyAuthentication(string accessId, string accessKey)
{
_authData = "{\"username\":\"" + accessId +"\", \"password\":\"" + GenPwd(accessId, accessKey) + "\"}";
}

/// <summary>
/// The authentication method name
/// Implements token-based authentication for Tuya's Pulsar service.
/// Handles secure credential generation and authentication data formatting.
/// </summary>
public string AuthenticationMethodName => "auth1";

/// <summary>
/// Get the authentication data
/// </summary>
public async ValueTask<byte[]> 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);
}

/// <summary>
/// Gets the authentication method identifier
/// </summary>
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);
}
/// <summary>
/// Provides the authentication data for the Pulsar connection
/// </summary>
public async ValueTask<byte[]> 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();
}
}
}
Loading