-
Notifications
You must be signed in to change notification settings - Fork 732
/
HmacRedactor.cs
170 lines (140 loc) · 6.14 KB
/
HmacRedactor.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Security.Cryptography;
using Microsoft.Extensions.Options;
using Microsoft.Shared.Diagnostics;
using Microsoft.Shared.Text;
#if NET6_0_OR_GREATER
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
#else
using System.Diagnostics.CodeAnalysis;
using System.Text;
#endif
namespace Microsoft.Extensions.Compliance.Redaction;
/// <summary>
/// A redactor using HMACSHA256 to encode data being redacted.
/// </summary>
public sealed class HmacRedactor : Redactor
{
#if NET6_0_OR_GREATER
private const int SHA256HashSizeInBytes = 32;
#endif
private const int BytesOfHashWeUse = 16;
/// <remarks>
/// Magic numbers are formula for calculating base64 length with padding.
/// </remarks>
private const int Base64HashLength = ((BytesOfHashWeUse + 2) / 3) * 4;
private readonly int _redactedLength;
private readonly byte[] _hashKey;
private readonly string _keyId;
/// <summary>
/// Initializes a new instance of the <see cref="HmacRedactor"/> class.
/// </summary>
/// <param name="options">Controls the behavior of the redactor.</param>
public HmacRedactor(IOptions<HmacRedactorOptions> options)
{
var value = Throw.IfMemberNull(options, options?.Value);
_hashKey = Convert.FromBase64String(value.Key);
_keyId = value.KeyId.HasValue ? value.KeyId.Value.ToInvariantString() + ':' : string.Empty;
_redactedLength = Base64HashLength + _keyId.Length;
}
/// <inheritdoc />
public override int GetRedactedLength(ReadOnlySpan<char> input)
{
if (input.IsEmpty)
{
return 0;
}
return _redactedLength;
}
#if NET6_0_OR_GREATER
/// <inheritdoc />
public override int Redact(ReadOnlySpan<char> source, Span<char> destination)
{
var length = GetRedactedLength(source);
if (length == 0)
{
return 0;
}
Throw.IfBufferTooSmall(destination.Length, length, nameof(destination));
_keyId.AsSpan().CopyTo(destination);
return CreateSha256Hash(source, destination[_keyId.Length..], _hashKey) + _keyId.Length;
}
[SkipLocalsInit]
private static int CreateSha256Hash(ReadOnlySpan<char> source, Span<char> destination, byte[] hashKey)
{
Span<byte> hashBuffer = stackalloc byte[SHA256HashSizeInBytes];
_ = HMACSHA256.HashData(hashKey, MemoryMarshal.AsBytes(source), hashBuffer);
// this won't fail, we ensured the destination is big enough previously
_ = Convert.TryToBase64Chars(hashBuffer.Slice(0, BytesOfHashWeUse), destination, out int charsWritten);
return charsWritten;
}
#else
/// <inheritdoc />
public override int Redact(ReadOnlySpan<char> source, Span<char> destination)
{
const int RemainingBytesToPadForBase64Hash = BytesOfHashWeUse % 3;
var length = GetRedactedLength(source);
if (length == 0)
{
return 0;
}
Throw.IfBufferTooSmall(destination.Length, length, nameof(destination));
_keyId.AsSpan().CopyTo(destination);
return ConvertBytesToBase64(CreateSha256Hash(source, _hashKey), destination, RemainingBytesToPadForBase64Hash, _keyId.Length) + _keyId.Length;
}
private static byte[] CreateSha256Hash(ReadOnlySpan<char> value, byte[] hashKey)
{
using var hmac = new HMACSHA256(hashKey);
return hmac.ComputeHash(Encoding.Unicode.GetBytes(value.ToArray()));
}
private static readonly char[] _base64CharactersTable =
{
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O',
'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd',
'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's',
't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7',
'8', '9', '+', '/', '=',
};
[SuppressMessage("Code smell", "S109", Justification = "Bit operation.")]
private static int ConvertBytesToBase64(byte[] hashToConvert, Span<char> destination, int remainingBytesToPad, int startOffset)
{
var iterations = BytesOfHashWeUse - remainingBytesToPad;
var offset = startOffset;
unchecked
{
for (var i = 0; i < iterations; i += 3)
{
destination[offset] = _base64CharactersTable[(hashToConvert[i] & 0xfc) >> 2];
destination[offset + 1] = _base64CharactersTable[((hashToConvert[i] & 0x03) << 4) | ((hashToConvert[i + 1] & 0xf0) >> 4)];
destination[offset + 2] = _base64CharactersTable[((hashToConvert[i + 1] & 0x0f) << 2) | ((hashToConvert[i + 2] & 0xc0) >> 6)];
destination[offset + 3] = _base64CharactersTable[hashToConvert[i + 2] & 0x3f];
offset += 4;
}
#if false
// this code is disabled since it is never visited given the limited use of this function. We leave it here in case the code is needed in the future
if (remainingBytesToPad == 2)
{
destination[offset] = _base64CharactersTable[(hashToConvert[iterations] & 0xfc) >> 2];
destination[offset + 1] = _base64CharactersTable[((hashToConvert[iterations] & 0x03) << 4) | ((hashToConvert[iterations + 1] & 0xf0) >> 4)];
destination[offset + 2] = _base64CharactersTable[(hashToConvert[iterations + 1] & 0x0f) << 2];
destination[offset + 3] = _base64CharactersTable[64];
offset += 4;
}
#endif
if (remainingBytesToPad == 1)
{
destination[offset] = _base64CharactersTable[(hashToConvert[iterations] & 0xfc) >> 2];
destination[offset + 1] = _base64CharactersTable[(hashToConvert[iterations] & 0x03) << 4];
destination[offset + 2] = _base64CharactersTable[64];
destination[offset + 3] = _base64CharactersTable[64];
offset += 4;
}
}
var charsWritten = offset - startOffset;
return charsWritten;
}
#endif
}