Skip to content

Commit

Permalink
sig verify refactoring
Browse files Browse the repository at this point in the history
  • Loading branch information
smithrobs committed Jan 3, 2017
1 parent b6f7634 commit f7c0f44
Show file tree
Hide file tree
Showing 4 changed files with 126 additions and 130 deletions.
41 changes: 5 additions & 36 deletions Nexmo.Api/Request/ApiRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,37 +14,6 @@ namespace Nexmo.Api.Request
{
internal static class ApiRequest
{
///// There is no built-in byte[] => hex string, so here's an implementation
/// http://stackoverflow.com/questions/311165/how-do-you-convert-byte-array-to-hexadecimal-string-and-vice-versa/24343727#24343727
/// We're not going to going with the unchecked version. Seems overkill for now.
private static readonly uint[] _lookup32 = CreateLookup32();

private static uint[] CreateLookup32()
{
var result = new uint[256];
for (var i = 0; i < 256; i++)
{
var s = i.ToString("X2");
result[i] = s[0] + ((uint)s[1] << 16);
}
return result;
}

private static string ByteArrayToHexViaLookup32(byte[] bytes)
{
var lookup32 = _lookup32;
var result = new char[bytes.Length * 2];
for (var i = 0; i < bytes.Length; i++)
{
var val = lookup32[bytes[i]];
result[2 * i] = (char)val;
result[2 * i + 1] = (char)(val >> 16);
}
return new string(result);
}

//////

private static StringBuilder BuildQueryString(IDictionary<string, string> parameters)
{
var sb = new StringBuilder();
Expand All @@ -58,20 +27,20 @@ private static StringBuilder BuildQueryString(IDictionary<string, string> parame
parameters.Add("api_key", Configuration.Instance.Settings["appSettings:Nexmo.api_key"].ToUpper());
if (string.IsNullOrEmpty(Configuration.Instance.Settings["appSettings:Nexmo.security_secret"]))
{
// do not sign
// security secret not provided, do not sign
parameters.Add("api_secret", Configuration.Instance.Settings["appSettings:Nexmo.api_secret"]);
buildStringFromParams(parameters, sb);
return sb;
}
// sort and sign request
// security secret provided, sort and sign request
parameters.Add("timestamp", ((int)(DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalSeconds).ToString(CultureInfo.InvariantCulture));
var sorted = new SortedDictionary<string, string>(parameters);
buildStringFromParams(sorted, sb);
var sortedParams = new SortedDictionary<string, string>(parameters);
buildStringFromParams(sortedParams, sb);
var queryToSign = "&" + sb;
queryToSign = queryToSign.Remove(queryToSign.Length - 1) + Configuration.Instance.Settings["appSettings:Nexmo.security_secret"].ToUpper();
var hashgen = MD5.Create();
var hash = hashgen.ComputeHash(Encoding.UTF8.GetBytes(queryToSign));
sb.AppendFormat("sig={0}", ByteArrayToHexViaLookup32(hash).ToLower());
sb.AppendFormat("sig={0}", ByteArrayToHexHelper.ByteArrayToHex(hash).ToLower());
return sb;
}

Expand Down
34 changes: 34 additions & 0 deletions Nexmo.Api/Request/ByteArrayToHexHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
namespace Nexmo.Api.Request
{
internal static class ByteArrayToHexHelper
{
///// There is no built-in byte[] => hex string, so here's an implementation
/// http://stackoverflow.com/questions/311165/how-do-you-convert-byte-array-to-hexadecimal-string-and-vice-versa/24343727#24343727
/// We're not going to going with the unchecked version. Seems overkill for now.
internal static readonly uint[] _lookup32 = CreateLookup32();

internal static uint[] CreateLookup32()
{
var result = new uint[256];
for (var i = 0; i < 256; i++)
{
var s = i.ToString("X2");
result[i] = s[0] + ((uint)s[1] << 16);
}
return result;
}

internal static string ByteArrayToHex(byte[] bytes)
{
var lookup32 = _lookup32;
var result = new char[bytes.Length * 2];
for (var i = 0; i < bytes.Length; i++)
{
var val = lookup32[bytes[i]];
result[2 * i] = (char)val;
result[2 * i + 1] = (char)(val >> 16);
}
return new string(result);
}
}
}
63 changes: 63 additions & 0 deletions Nexmo.Api/Request/SignatureHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Net;
using Microsoft.Extensions.Primitives;

namespace Nexmo.Api.Request
{
public static class SignatureHelper
{
private static bool SlowEquals(byte[] a, byte[] b)
{
var diff = (uint)a.Length ^ (uint)b.Length;
for (var i = 0; i < a.Length && i < b.Length; i++)
diff |= (uint)(a[i] ^ b[i]);
return diff == 0;
}

public static bool IsSignatureValid(IEnumerable<KeyValuePair<string, StringValues>> querystring)
{
Action<IDictionary<string, string>, StringBuilder> buildStringFromParams = (param, strings) =>
{
foreach (var kvp in param)
{
strings.AppendFormat("{0}={1}&", WebUtility.UrlEncode(kvp.Key), WebUtility.UrlEncode(kvp.Value));
}
};

// Compare the local time with the timestamp
var querystringList = querystring as IList<KeyValuePair<string, StringValues>> ?? querystring.ToList();
var localTime = (int)(DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalSeconds;
var messageTime = int.Parse(querystringList.Single(kvp => kvp.Key == "timestamp").Value);
// Message cannot be more than 5 minutes old
const int maxDelta = 5 * 60;
var difference = Math.Abs(localTime - messageTime);
if (difference > maxDelta)
{
return false;
}

var sorted = new SortedDictionary<string, string>();
// Sort the query parameters, removing sig as we go
foreach (var kvp in querystringList.Where(kv => kv.Key != "sig"))
{
sorted.Add(kvp.Key, kvp.Value);
}
// Create the signing url using the sorted parameters and your SECURITY_SECRET
var sb = new StringBuilder();
buildStringFromParams(sorted, sb);
var queryToSign = "&" + sb;
queryToSign = queryToSign.Remove(queryToSign.Length - 1) + Configuration.Instance.Settings["appSettings:Nexmo.security_secret"].ToUpper();
// Generate MD5
var hashgen = MD5.Create();
var hash = hashgen.ComputeHash(Encoding.UTF8.GetBytes(queryToSign));
var generatedSig = ByteArrayToHexHelper.ByteArrayToHex(hash).ToLower();
// A timing attack safe string comparison to validate hash
return SlowEquals(Encoding.UTF8.GetBytes(generatedSig),
Encoding.UTF8.GetBytes(querystringList.Single(kvp => kvp.Key == "sig").Value));
}
}
}
118 changes: 24 additions & 94 deletions Nexmo.Samples.Web/Controllers/Api/SmsDeliveryController.cs
Original file line number Diff line number Diff line change
@@ -1,56 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text;
using System.Security.Cryptography;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Memory;
using Nexmo.Api;
using Nexmo.Api.Request;

namespace Nexmo.Samples.Web.Controllers.Api
{
// TODO: REFACTOR!

[Route("/api/SmsDelivery")]
public class SmsDeliveryController : Controller
{
private static readonly uint[] _lookup32 = CreateLookup32();

private static uint[] CreateLookup32()
{
var result = new uint[256];
for (var i = 0; i < 256; i++)
{
var s = i.ToString("X2");
result[i] = s[0] + ((uint)s[1] << 16);
}
return result;
}

private static string ByteArrayToHexViaLookup32(byte[] bytes)
{
var lookup32 = _lookup32;
var result = new char[bytes.Length * 2];
for (var i = 0; i < bytes.Length; i++)
{
var val = lookup32[bytes[i]];
result[2 * i] = (char)val;
result[2 * i + 1] = (char)(val >> 16);
}
return new string(result);
}

private static bool SlowEquals(byte[] a, byte[] b)
{
var diff = (uint)a.Length ^ (uint)b.Length;
for (var i = 0; i < a.Length && i < b.Length; i++)
diff |= (uint)(a[i] ^ b[i]);
return diff == 0;
}

//////////

static IMemoryCache _cache;

public SmsDeliveryController(IMemoryCache cache) {
Expand All @@ -59,73 +18,44 @@ public SmsDeliveryController(IMemoryCache cache) {

readonly object _cacheLock = new object();

[HttpGet]
public ActionResult Get([FromQuery]SMS.SMSDeliveryReceipt response)
private void AddReceipt(SMS.SMSDeliveryReceipt response)
{
Action<IDictionary<string, string>, StringBuilder> buildStringFromParams = (param, strings) =>
lock (_cacheLock)
{
foreach (var kvp in param)
List<SMS.SMSDeliveryReceipt> receipts;
const string cachekey = "sms_receipts";
_cache.TryGetValue(cachekey, out receipts);
if (null == receipts)
{
strings.AppendFormat("{0}={1}&", WebUtility.UrlEncode(kvp.Key), WebUtility.UrlEncode(kvp.Value));
receipts = new List<SMS.SMSDeliveryReceipt>();
}
};
receipts.Add(response);
_cache.Set(cachekey, receipts, DateTimeOffset.MaxValue);
}
}

[HttpGet]
public ActionResult Get([FromQuery]SMS.SMSDeliveryReceipt response)
{
// Upon initial setup with Nexmo, this action will be tested up to 5 times. No response data will be included. Just accept the empty request with a 200.
if (null == response.to && null == response.msisdn)
return new OkResult();

// check for signed receipt
if (Request.Query.ContainsKey("sig"))
if (!Request.Query.ContainsKey("sig"))
{
var querystring = Request.Query;
// Compare the local time with the timestamp
var localTime =
(int) (DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalSeconds;
var messageTime = int.Parse(querystring["timestamp"]);
// Message cannot be more than 5 minutes old
var maxDelta = 5*60;
var difference = Math.Abs(localTime - messageTime);
if (difference > maxDelta)
{
Console.WriteLine("Timestamp difference greater than 5 minutes");
}
else
{
var sorted = new SortedDictionary<string, string>();
// Sort the query parameters, removing sig as we go
foreach (var key in querystring.Keys.Where(k => k != "sig"))
{
sorted.Add(key, querystring[key]);
}
// Create the signing url using the sorted parameters and your SECURITY_SECRET
var sb = new StringBuilder();
buildStringFromParams(sorted, sb);
var queryToSign = "&" + sb;
queryToSign = queryToSign.Remove(queryToSign.Length - 1) + Configuration.Instance.Settings["appSettings:Nexmo.security_secret"].ToUpper();
// Generate MD5
var hashgen = MD5.Create();
var hash = hashgen.ComputeHash(Encoding.UTF8.GetBytes(queryToSign));
var generatedSig = ByteArrayToHexViaLookup32(hash).ToLower();
// A timing attack safe string comparison to validate hash
Console.WriteLine(SlowEquals(Encoding.UTF8.GetBytes(generatedSig),
Encoding.UTF8.GetBytes(querystring["sig"]))
? "Message was sent by Nexmo"
: "Alert: message not sent by Nexmo!");
}
AddReceipt(response);
}
else
{
lock (_cacheLock)
if (SignatureHelper.IsSignatureValid(Request.Query))
{
Console.WriteLine("Message was sent by Nexmo");
AddReceipt(response);
}
else
{
List<SMS.SMSDeliveryReceipt> receipts;
const string cachekey = "sms_receipts";
_cache.TryGetValue(cachekey, out receipts);
if (null == receipts)
{
receipts = new List<SMS.SMSDeliveryReceipt>();
}
receipts.Add(response);
_cache.Set(cachekey, receipts, DateTimeOffset.MaxValue);
Console.WriteLine("Alert: message not sent by Nexmo!");
}
}

Expand Down

0 comments on commit f7c0f44

Please sign in to comment.