Skip to content

Commit 30e873b

Browse files
Add HMAC functionality
1 parent 98b24da commit 30e873b

16 files changed

Lines changed: 302 additions & 91 deletions

src/ImageSharp.Web/Caching/SHA256CacheHash.cs

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -33,18 +33,16 @@ public SHA256CacheHash(IOptions<ImageSharpMiddlewareOptions> options)
3333
public string Create(string value, uint length)
3434
{
3535
int byteCount = Encoding.ASCII.GetByteCount(value);
36-
37-
// Allocating a buffer from the pool is ~27% slower than stackalloc so use that for short strings
38-
if (byteCount < 257)
39-
{
40-
return HashValue(value, length, stackalloc byte[byteCount]);
41-
}
42-
4336
byte[] buffer = null;
37+
4438
try
4539
{
46-
buffer = ArrayPool<byte>.Shared.Rent(byteCount);
47-
return HashValue(value, length, buffer.AsSpan(0, byteCount));
40+
// Allocating a buffer from the pool is ~27% slower than stackalloc so use that for short strings
41+
Span<byte> bytes = byteCount <= 128
42+
? stackalloc byte[byteCount]
43+
: (buffer = ArrayPool<byte>.Shared.Rent(byteCount)).AsSpan(0, byteCount);
44+
45+
return HashValue(value, length, bytes);
4846
}
4947
finally
5048
{

src/ImageSharp.Web/Caching/UriAbsoluteCacheKey.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,6 @@ public class UriAbsoluteCacheKey : ICacheKey
1313
{
1414
/// <inheritdoc/>
1515
public string Create(HttpContext context, CommandCollection commands)
16-
=> CacheKeyHelper.BuildAbsoluteKey(CacheKeyHelper.CaseHandling.None, context.Request.Host, context.Request.PathBase, context.Request.Path, QueryString.Create(commands));
16+
=> CaseHandlingUriBuilder.BuildAbsolute(CaseHandlingUriBuilder.CaseHandling.None, context.Request.Host, context.Request.PathBase, context.Request.Path, QueryString.Create(commands));
1717
}
1818
}

src/ImageSharp.Web/Caching/UriAbsoluteLowerInvariantCacheKey.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,6 @@ public class UriAbsoluteLowerInvariantCacheKey : ICacheKey
1313
{
1414
/// <inheritdoc/>
1515
public string Create(HttpContext context, CommandCollection commands)
16-
=> CacheKeyHelper.BuildAbsoluteKey(CacheKeyHelper.CaseHandling.LowerInvariant, context.Request.Host, context.Request.PathBase, context.Request.Path, QueryString.Create(commands));
16+
=> CaseHandlingUriBuilder.BuildAbsolute(CaseHandlingUriBuilder.CaseHandling.LowerInvariant, context.Request.Host, context.Request.PathBase, context.Request.Path, QueryString.Create(commands));
1717
}
1818
}

src/ImageSharp.Web/Caching/UriRelativeCacheKey.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,6 @@ public class UriRelativeCacheKey : ICacheKey
1313
{
1414
/// <inheritdoc/>
1515
public string Create(HttpContext context, CommandCollection commands)
16-
=> CacheKeyHelper.BuildRelativeKey(CacheKeyHelper.CaseHandling.None, context.Request.PathBase, context.Request.Path, QueryString.Create(commands));
16+
=> CaseHandlingUriBuilder.BuildRelative(CaseHandlingUriBuilder.CaseHandling.None, context.Request.PathBase, context.Request.Path, QueryString.Create(commands));
1717
}
1818
}

src/ImageSharp.Web/Caching/UriRelativeLowerInvariantCacheKey.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,6 @@ public class UriRelativeLowerInvariantCacheKey : ICacheKey
1313
{
1414
/// <inheritdoc/>
1515
public string Create(HttpContext context, CommandCollection commands)
16-
=> CacheKeyHelper.BuildRelativeKey(CacheKeyHelper.CaseHandling.LowerInvariant, context.Request.PathBase, context.Request.Path, QueryString.Create(commands));
16+
=> CaseHandlingUriBuilder.BuildRelative(CaseHandlingUriBuilder.CaseHandling.LowerInvariant, context.Request.PathBase, context.Request.Path, QueryString.Create(commands));
1717
}
1818
}

src/ImageSharp.Web/Caching/CacheKeyHelper.cs renamed to src/ImageSharp.Web/CaseHandlingUriBuilder.cs

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,29 @@
66
using System.Runtime.CompilerServices;
77
using Microsoft.AspNetCore.Http;
88

9-
namespace SixLabors.ImageSharp.Web.Caching
9+
namespace SixLabors.ImageSharp.Web
1010
{
1111
/// <summary>
12-
/// Optimized helper methods for generating cache keys from URI components. Much of this code has been adapted from the MIT licensed .NET runtime.
12+
/// Optimized helper methods for generating encoded Uris from URI components.
13+
/// Much of this code has been adapted from the MIT licensed .NET runtime.
1314
/// </summary>
14-
internal static class CacheKeyHelper
15+
public static class CaseHandlingUriBuilder
1516
{
1617
private static readonly SpanAction<char, (bool LowerInvariant, string Host, string PathBase, string Path, string Query)> InitializeAbsoluteUriStringSpanAction = new(InitializeAbsoluteUriString);
1718

19+
/// <summary>
20+
/// Provides Uri case handling options.
21+
/// </summary>
1822
public enum CaseHandling
1923
{
24+
/// <summary>
25+
/// No adjustments to casing are made.
26+
/// </summary>
2027
None,
28+
29+
/// <summary>
30+
/// All URI components are converted to lower case using the invariant culture before combining.
31+
/// </summary>
2132
LowerInvariant
2233
}
2334

@@ -29,14 +40,14 @@ public enum CaseHandling
2940
/// <param name="path">The portion of the request path that identifies the requested resource.</param>
3041
/// <param name="query">The query, if any.</param>
3142
/// <returns>The combined URI components, properly encoded for use in HTTP headers.</returns>
32-
public static string BuildRelativeKey(
43+
public static string BuildRelative(
3344
CaseHandling handling,
3445
PathString pathBase = default,
3546
PathString path = default,
3647
QueryString query = default)
3748

3849
// Take any potential performance hit vs concatination for code reading sanity.
39-
=> BuildAbsoluteKey(handling, default, pathBase, path, query);
50+
=> BuildAbsolute(handling, default, pathBase, path, query);
4051

4152
/// <summary>
4253
/// Combines the given URI components into a string that is properly encoded for use in HTTP headers.
@@ -48,7 +59,7 @@ public static string BuildRelativeKey(
4859
/// <param name="path">The portion of the request path that identifies the requested resource.</param>
4960
/// <param name="query">The query, if any.</param>
5061
/// <returns>The combined URI components, properly encoded for use in HTTP headers.</returns>
51-
public static string BuildAbsoluteKey(
62+
public static string BuildAbsolute(
5263
CaseHandling handling,
5364
HostString host,
5465
PathString pathBase = default,
@@ -113,7 +124,7 @@ private static int CopyTextToBufferLowerInvariant(Span<char> buffer, int index,
113124
=> index + text.ToLowerInvariant(buffer.Slice(index, text.Length));
114125

115126
/// <summary>
116-
/// Initializes the URI <see cref="string"/> for <see cref="BuildAbsoluteKey(CaseHandling, HostString, PathString, PathString, QueryString)"/>.
127+
/// Initializes the URI <see cref="string"/> for <see cref="BuildAbsolute(CaseHandling, HostString, PathString, PathString, QueryString)"/>.
117128
/// </summary>
118129
/// <param name="buffer">The URI <see cref="string"/>'s <see cref="char"/> buffer.</param>
119130
/// <param name="uriParts">The URI parts.</param>

src/ImageSharp.Web/HMACUtilities.cs

Lines changed: 56 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
using System;
55
using System.Buffers;
66
using System.Runtime.CompilerServices;
7-
using System.Runtime.InteropServices;
87
using System.Security.Cryptography;
98
using System.Text;
9+
using SixLabors.ImageSharp.Web.Caching;
1010

1111
namespace SixLabors.ImageSharp.Web
1212
{
@@ -15,55 +15,82 @@ namespace SixLabors.ImageSharp.Web
1515
/// </summary>
1616
public static class HMACUtilities
1717
{
18+
/// <summary>
19+
/// The command used by image requests for transporting Hash-based Message Authentication Code (HMAC) tokens.
20+
/// </summary>
21+
public const string TokenCommand = "hmac";
22+
1823
/// <summary>
1924
/// Computes a Hash-based Message Authentication Code (HMAC) by using the SHA256 hash function.
2025
/// </summary>
2126
/// <param name="value">The value to hash</param>
2227
/// <param name="secret">
23-
/// The secret key for <see cref="HMACSHA256"/>encryption.
28+
/// The secret key for <see cref="HMACSHA256"/> encryption.
2429
/// The key can be any length. However, the recommended size is 64 bytes.
2530
/// </param>
2631
/// <returns>The hashed <see cref="string"/>.</returns>
27-
public static unsafe string CreateSHA256HashCode(string value, byte[] secret)
32+
public static unsafe string ComputeHMACSHA256(string value, byte[] secret)
2833
{
29-
static void Action(Span<char> chars, (IntPtr Ptr, int Length, string Value, byte[] Secret) args)
30-
{
31-
var bytes = new Span<byte>((byte*)args.Ptr, args.Length);
32-
Encoding.ASCII.GetBytes(args.Value, bytes);
33-
using var hashAlgorithm = new HMACSHA256(args.Secret);
34-
hashAlgorithm.TryComputeHash(bytes, MemoryMarshal.Cast<char, byte>(chars), out int _);
35-
}
34+
// TODO: In .NET 6 we can use single instance versions
35+
using var hmac = new HMACSHA256(secret);
36+
return CreateHMAC(value, hmac);
37+
}
3638

37-
// Bits to chars - 256/8/2
38-
return CreateHMAC(value, secret, 16, Action);
39+
/// <summary>
40+
/// Computes a Hash-based Message Authentication Code (HMAC) by using the SHA384 hash function.
41+
/// </summary>
42+
/// <param name="value">The value to hash</param>
43+
/// <param name="secret">
44+
/// The secret key for <see cref="HMACSHA256"/> encryption.
45+
/// The key can be any length. However, the recommended size is 128 bytes.
46+
/// </param>
47+
/// <returns>The hashed <see cref="string"/>.</returns>
48+
public static unsafe string ComputeHMACSHA384(string value, byte[] secret)
49+
{
50+
using var hmac = new HMACSHA384(secret);
51+
return CreateHMAC(value, hmac);
52+
}
53+
54+
/// <summary>
55+
/// Computes a Hash-based Message Authentication Code (HMAC) by using the SHA512 hash function.
56+
/// </summary>
57+
/// <param name="value">The value to hash</param>
58+
/// <param name="secret">
59+
/// The secret key for <see cref="HMACSHA256"/> encryption.
60+
/// The key can be any length. However, the recommended size is 128 bytes.
61+
/// </param>
62+
/// <returns>The hashed <see cref="string"/>.</returns>
63+
public static unsafe string ComputeHMACSHA512(string value, byte[] secret)
64+
{
65+
using var hmac = new HMACSHA512(secret);
66+
return CreateHMAC(value, hmac);
3967
}
4068

4169
[MethodImpl(MethodImplOptions.AggressiveInlining)]
42-
private static unsafe string CreateHMAC(string value, byte[] secret, int length, SpanAction<char, (IntPtr Ptr, int Length, string Value, byte[] Secret)> action)
70+
private static unsafe string CreateHMAC(string value, HMAC hmac)
4371
{
4472
int byteCount = Encoding.ASCII.GetByteCount(value);
45-
46-
// Allocating a buffer from the pool is ~27% slower than stackalloc so use that for short strings
47-
if (byteCount < 257)
48-
{
49-
fixed (byte* bytesPtr = stackalloc byte[byteCount])
50-
{
51-
return string.Create(length, ((IntPtr)bytesPtr, byteCount, value, secret), action);
52-
}
53-
}
54-
5573
byte[] buffer = null;
74+
5675
try
5776
{
58-
buffer = ArrayPool<byte>.Shared.Rent(byteCount);
59-
fixed (byte* bytesPtr = buffer.AsSpan(0, byteCount))
60-
{
61-
return string.Create(length, ((IntPtr)bytesPtr, byteCount, value, secret), action);
62-
}
77+
// Allocating a buffer from the pool is ~27% slower than stackalloc so use that for short strings
78+
Span<byte> bytes = byteCount <= 128
79+
? stackalloc byte[byteCount]
80+
: (buffer = ArrayPool<byte>.Shared.Rent(byteCount)).AsSpan(0, byteCount);
81+
82+
Encoding.ASCII.GetBytes(value, bytes);
83+
84+
// Safe to always stackalloc here. We max out at 64 bytes.
85+
Span<byte> hash = stackalloc byte[hmac.HashSize / 8];
86+
hmac.TryComputeHash(bytes, hash, out int _);
87+
88+
// Finally encode the hash to make it web safe.
89+
return HexEncoder.Encode(hash);
6390
}
6491
finally
6592
{
66-
if (buffer != null)
93+
if (buffer is not null)
6794
{
6895
ArrayPool<byte>.Shared.Return(buffer);
6996
}

src/ImageSharp.Web/Middleware/ImageSharpMiddleware.cs

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -191,9 +191,18 @@ public ImageSharpMiddleware(
191191

192192
private async Task Invoke(HttpContext httpContext, bool retry)
193193
{
194-
// We expect to get concrete collection type which removes virtual dispatch concerns and enumerator allocations
195194
CommandCollection commands = this.requestParser.ParseRequestCommands(httpContext);
196195

196+
// First check for a HMAC token and capture before the command is stripped out.
197+
byte[] secret = this.options.HMACSecretKey;
198+
bool doHMAC = false;
199+
string token = null;
200+
if (secret?.Length > 0)
201+
{
202+
doHMAC = true;
203+
token = commands.GetValueOrDefault(HMACUtilities.TokenCommand);
204+
}
205+
197206
if (commands.Count > 0)
198207
{
199208
// Strip out any unknown commands, if needed.
@@ -211,8 +220,22 @@ private async Task Invoke(HttpContext httpContext, bool retry)
211220
}
212221
}
213222

214-
await this.options.OnParseCommandsAsync.Invoke(
215-
new ImageCommandContext(httpContext, commands, this.commandParser, this.parserCulture));
223+
ImageCommandContext imageCommandContext = new(httpContext, commands, this.commandParser, this.parserCulture);
224+
225+
if (doHMAC)
226+
{
227+
// Compare the passed token to our generated mac.
228+
string mac = await this.options.OnComputeHMACAsync(imageCommandContext, secret);
229+
if (mac != token)
230+
{
231+
// Throw a 401. We don't log the error to avoid attempts at log poisoning.
232+
httpContext.Response.Clear();
233+
httpContext.Response.StatusCode = StatusCodes.Status401Unauthorized;
234+
return;
235+
}
236+
}
237+
238+
await this.options.OnParseCommandsAsync.Invoke(imageCommandContext);
216239

217240
// Get the correct service for the request
218241
IImageProvider provider = null;

src/ImageSharp.Web/Middleware/ImageSharpMiddlewareOptions.cs

Lines changed: 32 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
using Microsoft.AspNetCore.Http;
88
using Microsoft.IO;
99
using SixLabors.ImageSharp.Web.Commands;
10-
using SixLabors.ImageSharp.Web.Processors;
1110
using SixLabors.ImageSharp.Web.Providers;
1211

1312
namespace SixLabors.ImageSharp.Web.Middleware
@@ -17,40 +16,18 @@ namespace SixLabors.ImageSharp.Web.Middleware
1716
/// </summary>
1817
public class ImageSharpMiddlewareOptions
1918
{
20-
private Func<ImageCommandContext, Task> onParseCommandsAsync = c =>
19+
private Func<ImageCommandContext, byte[], Task<string>> onComputeHMACAsync = (context, secret) =>
2120
{
22-
if (c.Commands.Count == 0)
23-
{
24-
return Task.CompletedTask;
25-
}
26-
27-
// It's a good idea to have this to provide very basic security.
28-
uint width = c.Parser.ParseValue<uint>(
29-
c.Commands.GetValueOrDefault(ResizeWebProcessor.Width),
30-
c.Culture);
31-
32-
uint height = c.Parser.ParseValue<uint>(
33-
c.Commands.GetValueOrDefault(ResizeWebProcessor.Height),
34-
c.Culture);
35-
36-
if (width > 4000 && height > 4000)
37-
{
38-
c.Commands.Remove(ResizeWebProcessor.Width);
39-
c.Commands.Remove(ResizeWebProcessor.Height);
40-
}
41-
42-
float[] coordinates = c.Parser.ParseValue<float[]>(c.Commands.GetValueOrDefault(ResizeWebProcessor.Xy), c.Culture);
43-
44-
if (coordinates.Length != 2
45-
|| coordinates[1] < 0 || coordinates[1] > 1
46-
|| coordinates[0] < 0 || coordinates[0] > 1)
47-
{
48-
c.Commands.Remove(ResizeWebProcessor.Xy);
49-
}
21+
string uri = CaseHandlingUriBuilder.BuildRelative(
22+
CaseHandlingUriBuilder.CaseHandling.LowerInvariant,
23+
context.Context.Request.PathBase,
24+
context.Context.Request.Path,
25+
QueryString.Create(context.Commands));
5026

51-
return Task.CompletedTask;
27+
return Task.FromResult(HMACUtilities.ComputeHMACSHA256(uri, secret));
5228
};
5329

30+
private Func<ImageCommandContext, Task> onParseCommandsAsync = _ => Task.CompletedTask;
5431
private Func<FormattedImage, Task> onBeforeSaveAsync = _ => Task.CompletedTask;
5532
private Func<ImageProcessingContext, Task> onProcessedAsync = _ => Task.CompletedTask;
5633
private Func<HttpContext, Task> onPrepareResponseAsync = _ => Task.CompletedTask;
@@ -98,6 +75,30 @@ public class ImageSharpMiddlewareOptions
9875
/// </summary>
9976
public uint CacheHashLength { get; set; } = 12;
10077

78+
/// <summary>
79+
/// Gets or sets the secret key for Hash-based Message Authentication Code (HMAC) encryption.
80+
/// </summary>
81+
/// <remarks>
82+
/// The key can be any length. However, the recommended size is at least 64 bytes. If the length is zero then no authentication is performed.
83+
/// </remarks>
84+
public byte[] HMACSecretKey { get; set; } = Array.Empty<byte>();
85+
86+
/// <summary>
87+
/// Gets or sets the method used to compute a Hash-based Message Authentication Code (HMAC) for request authentication.
88+
/// Defaults to <see cref="HMACUtilities.ComputeHMACSHA256(string, byte[])"/> using an invariant lowercase relative Uri
89+
/// generated using <see cref="CaseHandlingUriBuilder.BuildRelative(CaseHandlingUriBuilder.CaseHandling, PathString, PathString, QueryString)"/>.
90+
/// </summary>
91+
public Func<ImageCommandContext, byte[], Task<string>> OnComputeHMACAsync
92+
{
93+
get => this.onComputeHMACAsync;
94+
95+
set
96+
{
97+
Guard.NotNull(value, nameof(this.onComputeHMACAsync));
98+
this.onComputeHMACAsync = value;
99+
}
100+
}
101+
101102
/// <summary>
102103
/// Gets or sets the additional command parsing method that can be used to used to augment commands.
103104
/// This is called once the commands have been gathered and before an <see cref="IImageProvider"/> has been assigned.

0 commit comments

Comments
 (0)