0
\$\begingroup\$

I'm sorting a Dictionary in a lexicographical order and applying MD5 encryption. I would like to wrap the sorting somewhere inside the encryption method or so.

private static string GenerateSign(Dictionary<string, object> query, string apiSecret)
{
    var sb = new StringBuilder();

    var queryParameterString = string.Join("&",
        query.Where(kvp => !string.IsNullOrWhiteSpace(kvp.Value.ToString()))
            .Select(kvp => $"{kvp.Key}={HttpUtility.UrlEncode(kvp.Value.ToString())}"));
    sb.Append(queryParameterString);

    if (sb.Length > 0)
    {
        sb.Append('&');
    }

    sb.Append("api_secret=").Append(apiSecret);

    return sb.ToString();
}

private static string Sign(string source)
{
    using var md5 = MD5.Create();
    var sourceBytes = Encoding.UTF8.GetBytes(source);
    var hash = md5.ComputeHash(sourceBytes);
    return BitConverter.ToString(hash).Replace("-", string.Empty).ToLowerInvariant();
}

public ValueTask SubscribeToPrivateAsync()
{
    var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
    var @params = new Dictionary<string, object>
    {
        { "api_key", _apiKey },
        { "req_time", now },
        { "op", "sub.personal" }
    };

    var javaSorted = @params.OrderBy(item => item.Key, StringComparer.Ordinal)
        .ToDictionary(i => i.Key, i => i.Value);

    var signature = Sign(GenerateSign(javaSorted, _apiSecret));

    var request = new PersonalRequest("sub.personal", _apiKey, signature, now);
    return SendAsync(request);
}

private ValueTask SendAsync<TRequest>(TRequest request)
    where TRequest : SocketRequest
{
    var json = JsonSerializer.Serialize(request);
    var message = new Message(Encoding.UTF8.GetBytes(json));
    return _websocketClient.SendAsync(message);
}
```
\$\endgroup\$
2
  • \$\begingroup\$ is there a reason to assign api_secret separately ? why wasn't passed within the dictionary like the others ? \$\endgroup\$
    – iSR5
    Commented Jun 11, 2022 at 19:10
  • \$\begingroup\$ @isR5, that's the way their API works. You can take a look at the Java code. github.com/mxcdevelop/mexc-api-demo/blob/… \$\endgroup\$
    – nop
    Commented Jun 11, 2022 at 21:32

1 Answer 1

1
\$\begingroup\$

For the MD5 hashing, I would suggest making it an extension method, if it's going to be used outside this scope, however, if it's only used in this class, you can keep it as is, and if it's only going to be used only for this payload, then you can put it inside the GenerateSign method. Also, you can use Convert.ToHexString instead of BitConverter.ToString.

this part :

var @params = new Dictionary<string, object>
{
    { "api_key", _apiKey },
    { "req_time", now },
    { "op", "sub.personal" }
};

var javaSorted = @params.OrderBy(item => item.Key, StringComparer.Ordinal)
    .ToDictionary(i => i.Key, i => i.Value);

can be simplified by using SortedDictionary<string, object>, so it would be something like this :

var @params = new SortedDictionary<string, object>(StringComparer.Ordinal)
{
    { "api_key", _apiKey },
    { "req_time", now },
    { "op", "sub.personal" }
};
// the dictionary is sorted, and ready to use

in GenerateSign, Join, LINQ, and StringBuilder are mixed, it's fine, but we can add more readability to it and also add some micro-improvements, by simply use foreach and a StringBuilder. something like this :

private static string GenerateSign(IDictionary<string, object> items, string apiSecret)
{
    if (items?.Count == 0) return null;

    if (string.IsNullOrWhiteSpace(apiSecret)) return null;

    var builder = new StringBuilder();

    foreach(var item in items)
    {
        var key = item.Key;

        var value = item.Value?.ToString();

        if (!string.IsNullOrWhiteSpace(value))
        {
            if (builder.Length > 0)
            {
                builder.Append('&');
            }

            builder.Append($"{key}={HttpUtility.UrlEncode(value)}");
        }
    }

    builder.Append("&api_secret=").Append(apiSecret);

    // hash the payload
    using var md5 = MD5.Create();
    var sourceBytes = Encoding.UTF8.GetBytes(builder.ToString());
    var hash = md5.ComputeHash(sourceBytes);
    return Convert.ToHexString(hash).ToLowerInvariant();
}

There is another option here, which I found it feasable, since the payload (from the documentation) is small, and fixed length.

this is the request payload example:

{
  "op":"sub.personal",
  "api_key": "api_key",
  "sign": "b8d2ff6432798ef858782d7fd109ab41",
  "req_time": "1561433613583"
}

if this is going to have the same object length and order, then how about implementing a class model for it, instead of dictionary ? and use JsonProperty attribute to set the key name and order.

Example with Json.NET :

public class MexcSubPersonalPayload
{
    private readonly string _apiSecret; 

    [JsonProperty("op", Order = 1)]
    public string Operation => "sub.personal";

    [JsonProperty("api_key", Order = 2)]
    public string ApiKey { get; private set; }

    [JsonProperty("sign", Order = 3)]
    public string Signature => Sign(this.ToString());

    [JsonProperty("req_time", Order = 4)]
    public long RequestTime => DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();

    private string Sign(string str)
    {
        if (string.IsNullOrWhiteSpace(str)) return null;
        using var md5 = MD5.Create();
        var sourceBytes = Encoding.UTF8.GetBytes(str);
        var hash = md5.ComputeHash(sourceBytes);
        return Convert.ToHexString(hash).ToLowerInvariant();
    }

    public MexcSubPersonalPayload(string apiKey, string apiSecret)
    {
        ApiKey = apiKey;
        _apiSecret = apiSecret;
    }

    public override string ToString()
    {
        return $"api_key={ApiKey}&req_time={RequestTime}&op={Operation}&api_secret={_apiSecret}";
    }

}

Example with System.Text.Json:

public class MexcSubPersonalPayload
{
    private readonly string _apiSecret; 

    [JsonPropertyName("op")]
    [JsonPropertyOrder(1)]
    public string Operation => "sub.personal";

    [JsonPropertyName("api_key")]
    [JsonPropertyOrder(2)]
    public string ApiKey { get; private set; }

    [JsonPropertyName("sign")]
    [JsonPropertyOrder(3)]
    public string Signature => Sign(this.ToString());

    [JsonPropertyName("req_time")]
    [JsonPropertyOrder(4)]
    public long RequestTime => DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();

    private string Sign(string str)
    {
        if (string.IsNullOrWhiteSpace(str)) return null;
        using var md5 = MD5.Create();
        var sourceBytes = Encoding.UTF8.GetBytes(str);
        var hash = md5.ComputeHash(sourceBytes);
        return Convert.ToHexString(hash).ToLowerInvariant();
    }

    public MexcSubPersonalPayload(string apiKey, string apiSecret)
    {
        ApiKey = apiKey;
        _apiSecret = apiSecret;
    }

    public override string ToString()
    {
        return $"api_key={ApiKey}&req_time={RequestTime}&op={Operation}&api_secret={_apiSecret}";
    }

}

I don't see a need for using UrlEncode in this model, as the values will be always valid, except for api_key and _apiSecret, if they have invalid characters, then you might use UrlEncode in them, but I don't know if is it going to invalidate the keys (public and private keys) or the service provider would handle the escaped characters (in this case it won't invalidate them).

anyhow, using the above model, you would only need to pass the public and private keys, and then convert it to JSON.

usage with Json.NET

var json = JsonConvert.SerializeObject(new MexcSubPersonalPayload("api_key", "api_secret"));

usage with System.Text.Json

var json = JsonSerializer.Serialize(new MexcSubPersonalPayload("api_key", "api_secret"));

UPDATE FROM THE COMMENTS

Combine javaSorted with GenerateSign

private static string GenerateSign(IDictionary<string, object> items, string apiSecret)
{
    if (items?.Count == 0) return null;

    if (string.IsNullOrWhiteSpace(apiSecret)) return null;

    var builder = new StringBuilder();

    foreach (var item in items.Where(x => !string.IsNullOrWhiteSpace(x.Value?.ToString())).OrderBy(x => x.Key, StringComparer.Ordinal))
    {
        var key = item.Key;

        var value = item.Value.ToString();

        if (builder.Length > 0)
        {
            builder.Append('&');
        }

        builder.Append($"{key}={HttpUtility.UrlEncode(value)}");
    }

    builder.Append("&api_secret=").Append(apiSecret);

    //hash the payload
    using var md5 = MD5.Create();
    var sourceBytes = Encoding.UTF8.GetBytes(builder.ToString());
    var hash = md5.ComputeHash(sourceBytes);
    return Convert.ToHexString(hash).ToLowerInvariant();
}
\$\endgroup\$
14
  • \$\begingroup\$ Thank you for the answer! I didn't know there was Convert.ToHexString. Actually the lexicographical order javaSorted would be nice if it's inside the GenerateSign if you know a way \$\endgroup\$
    – nop
    Commented Jun 12, 2022 at 0:13
  • \$\begingroup\$ Something is also wrong with your GenerateSign, it returns {"channel":"sub.personal","msg":"signature validation failed"}. It previously worked fine {"channel":"sub.personal","msg":"OK"} \$\endgroup\$
    – nop
    Commented Jun 12, 2022 at 1:02
  • \$\begingroup\$ One more thing I'm using System.Text.Json not Json.Net \$\endgroup\$
    – nop
    Commented Jun 12, 2022 at 1:11
  • \$\begingroup\$ @nop the GenerateSign was missing & from the api_secret=. It's fixed now, it should give you the same results as the original. I'll update my answer with System.Text.Json and also put the javaSorted inside the GenerateSign \$\endgroup\$
    – iSR5
    Commented Jun 12, 2022 at 1:46
  • 1
    \$\begingroup\$ @nop glad you've figured it out, and I've mentioned before that you can use UrlEncode with the api_key and _apiSecret if they're required to be escaped. perhaps, they are required to be escaped after all. \$\endgroup\$
    – iSR5
    Commented Jun 12, 2022 at 18:02

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.