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

Minor improvements in EventUtility #1500

Merged
merged 1 commit into from
Feb 3, 2019
Merged
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
40 changes: 40 additions & 0 deletions src/Stripe.net/Infrastructure/StringUtils.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
namespace Stripe.Infrastructure
{
using System;
using System.Runtime.CompilerServices;
using System.Text.RegularExpressions;

internal static class StringUtils
Expand All @@ -12,5 +14,43 @@ public static string ToSnakeCase(string str)
var tmp = Regex.Replace(str, "(.)([A-Z][a-z]+)", "$1_$2");
return Regex.Replace(tmp, "([a-z0-9])([A-Z])", "$1_$2").ToLower();
}

/// <summary>
/// Determines whether the two specified <see cref="string"/> objects have the same value.
/// The comparison is done in constant time to prevent timing attacks.
/// </summary>
/// <param name="a">The first string to compare.</param>
/// <param name="b">The second string to compare.</param>
/// <returns>
/// <c>true</c> if the value of the <c>a</c> parameter is equal to the value of the <c>b</c>
/// parameter; otherwise, <c>false</c>.
/// </returns>
[MethodImpl(MethodImplOptions.NoOptimization)]
public static bool SecureEquals(string a, string b)
{
if (a == null)
{
throw new ArgumentNullException(nameof(a));
}

if (b == null)
{
throw new ArgumentNullException(nameof(b));
}

if (a.Length != b.Length)
{
return false;
}

var result = 0;

for (var i = 0; i < a.Length; i++)
{
result |= a[i] ^ b[i];
}

return result == 0;
}
}
}
130 changes: 96 additions & 34 deletions src/Stripe.net/Services/Events/EventUtility.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,38 @@ namespace Stripe
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using System.Text;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Stripe.Infrastructure;

/// <summary>
/// This class contains utility methods to process event objects in Stripe's webhooks.
/// </summary>
public static class EventUtility
{
internal static readonly UTF8Encoding SafeUTF8 = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true);

internal static readonly UTF8Encoding SafeUTF8
= new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true);

/// <summary>
/// Parses a JSON string from a Stripe webhook into a <see cref="Event"/> object.
/// </summary>
/// <param name="json">The JSON string to parse.</param>
/// <param name="throwOnApiVersionMismatch">
/// If <c>true</c> (default), the method will throw a <see cref="StripeException"/> if the
/// API version of the event doesn't match Stripe.net's default API version (see
/// <see cref="StripeConfiguration.ApiVersion"/>).
/// </param>
/// <returns>The deserialized <see cref="Event"/>.</returns>
/// <exception cref="StripeException">
/// Thrown if the API version of the event doesn't match Stripe.net's default API version.
/// </exception>
/// <remarks>
/// This method doesn't verify <a href="https://stripe.com/docs/webhooks/signatures">webhook
/// signatures</a>. It's recommended that you use
/// <see cref="ConstructEvent(string, string, string, long, bool)"/> instead.
/// </remarks>
public static Event ParseEvent(string json, bool throwOnApiVersionMismatch = true)
{
var stripeEvent = JsonConvert.DeserializeObject<Event>(
Expand All @@ -35,17 +56,72 @@ public static Event ParseEvent(string json, bool throwOnApiVersionMismatch = tru
return stripeEvent;
}

public static T ParseEventDataItem<T>(dynamic dataItem)
/// <summary>
/// Parses a JSON string from a Stripe webhook into a <see cref="Event"/> object, while
/// verifying the <a href="https://stripe.com/docs/webhooks/signatures">webhook's
/// signature</a>.
/// </summary>
/// <param name="json">The JSON string to parse.</param>
/// <param name="stripeSignatureHeader">
/// The value of the <c>Stripe-Signature</c> header from the webhook request.
/// </param>
/// <param name="secret">The webhook endpoint's signing secret.</param>
/// <param name="tolerance">The time tolerance, in seconds (default 300).</param>
/// <param name="throwOnApiVersionMismatch">
/// If <c>true</c> (default), the method will throw a <see cref="StripeException"/> if the
/// API version of the event doesn't match Stripe.net's default API version (see
/// <see cref="StripeConfiguration.ApiVersion"/>).
/// </param>
/// <returns>The deserialized <see cref="Event"/>.</returns>
/// <exception cref="StripeException">
/// Thrown if the signature verification fails for any reason, of if the API version of the
/// event doesn't match Stripe.net's default API version.
/// </exception>
public static Event ConstructEvent(
string json,
string stripeSignatureHeader,
string secret,
long tolerance = 300,
bool throwOnApiVersionMismatch = true)
{
return JsonConvert.DeserializeObject<T>((dataItem as JObject).ToString());
}

public static Event ConstructEvent(string json, string stripeSignatureHeader, string secret, long tolerance = 300, bool throwOnApiVersionMismatch = true)
{
return ConstructEvent(json, stripeSignatureHeader, secret, tolerance, DateTime.UtcNow.ConvertDateTimeToEpoch(), throwOnApiVersionMismatch);
return ConstructEvent(
json,
stripeSignatureHeader,
secret,
tolerance,
DateTime.UtcNow.ConvertDateTimeToEpoch(),
throwOnApiVersionMismatch);
}

public static Event ConstructEvent(string json, string stripeSignatureHeader, string secret, long tolerance, long utcNow, bool throwOnApiVersionMismatch = true)
/// <summary>
/// Parses a JSON string from a Stripe webhook into a <see cref="Event"/> object, while
/// verifying the <a href="https://stripe.com/docs/webhooks/signatures">webhook's
/// signature</a>.
/// </summary>
/// <param name="json">The JSON string to parse.</param>
/// <param name="stripeSignatureHeader">
/// The value of the <c>Stripe-Signature</c> header from the webhook request.
/// </param>
/// <param name="secret">The webhook endpoint's signing secret.</param>
/// <param name="tolerance">The time tolerance, in seconds.</param>
/// <param name="utcNow">The timestamp to use for the current time.</param>
/// <param name="throwOnApiVersionMismatch">
/// If <c>true</c> (default), the method will throw a <see cref="StripeException"/> if the
/// API version of the event doesn't match Stripe.net's default API version (see
/// <see cref="StripeConfiguration.ApiVersion"/>).
/// </param>
/// <returns>The deserialized <see cref="Event"/>.</returns>
/// <exception cref="StripeException">
/// Thrown if the signature verification fails for any reason, of if the API version of the
/// event doesn't match Stripe.net's default API version.
/// </exception>
public static Event ConstructEvent(
string json,
string stripeSignatureHeader,
string secret,
long tolerance,
long utcNow,
bool throwOnApiVersionMismatch = true)
{
var signatureItems = ParseStripeSignature(stripeSignatureHeader);
var signature = string.Empty;
Expand All @@ -56,19 +132,23 @@ public static Event ConstructEvent(string json, string stripeSignatureHeader, st
}
catch (EncoderFallbackException ex)
{
throw new StripeException("The webhook cannot be processed because the signature cannot be safely calculated.", ex);
throw new StripeException(
"The webhook cannot be processed because the signature cannot be safely calculated.",
ex);
}

if (!IsSignaturePresent(signature, signatureItems["v1"]))
{
throw new StripeException("The signature for the webhook is not present in the Stripe-Signature header.");
throw new StripeException(
"The signature for the webhook is not present in the Stripe-Signature header.");
}

var webhookUtc = Convert.ToInt32(signatureItems["t"].FirstOrDefault());

if (Math.Abs(utcNow - webhookUtc) > tolerance)
{
throw new StripeException("The webhook cannot be processed because the current timestamp is outside of the allowed tolerance.");
throw new StripeException(
"The webhook cannot be processed because the current timestamp is outside of the allowed tolerance.");
}

return ParseEvent(json, throwOnApiVersionMismatch);
Expand All @@ -78,13 +158,13 @@ private static ILookup<string, string> ParseStripeSignature(string stripeSignatu
{
return stripeSignatureHeader.Trim()
.Split(',')
.Select(item => item.Trim().Split('='))
.Select(item => item.Trim().Split(new char[] { '=' }, 2))
.ToLookup(item => item[0], item => item[1]);
}

private static bool IsSignaturePresent(string signature, IEnumerable<string> signatures)
{
return signatures.Any(key => SecureCompare(key, signature));
return signatures.Any(key => StringUtils.SecureEquals(key, signature));
}

private static string ComputeSignature(string secret, string timestamp, string payload)
Expand All @@ -98,23 +178,5 @@ private static string ComputeSignature(string secret, string timestamp, string p
return BitConverter.ToString(hash).Replace("-", string.Empty).ToLowerInvariant();
}
}

[MethodImpl(MethodImplOptions.NoOptimization)]
private static bool SecureCompare(string a, string b)
{
if (a.Length != b.Length)
{
return false;
}

var result = 0;

for (var i = 0; i < a.Length; i++)
{
result |= a[i] ^ b[i];
}

return result == 0;
}
}
}
23 changes: 23 additions & 0 deletions src/StripeTests/Infrastructure/StringUtilsTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,28 @@ public void ToSnakeCase()
Assert.Equal(testCase.want, StringUtils.ToSnakeCase(testCase.data));
}
}

[Fact]
public void SecureEquals()
{
var testCases = new[]
{
new { data = new { a = "Hello", b = "Hello" }, want = true },
new { data = new { a = "Hello", b = "hello" }, want = false },
new { data = new { a = "Hello", b = "Helloo" }, want = false },
new { data = new { a = "Hello", b = "Hell" }, want = false },
new { data = new { a = "Hello", b = string.Empty }, want = false },
new { data = new { a = string.Empty, b = "Hello" }, want = false },
new { data = new { a = string.Empty, b = string.Empty }, want = true },
new { data = new { a = "\0AAAAAAAAA", b = "\0BBBBBBBBBBBB" }, want = false },
};

foreach (var testCase in testCases)
{
Assert.Equal(
testCase.want,
StringUtils.SecureEquals(testCase.data.a, testCase.data.b));
}
}
}
}