Skip to content

Commit 5ab64ed

Browse files
authored
Merge pull request #8812 from JanKrivanek/proto/hash-task-sha256
Moving from SHA1 to SHA256 for Hash task
2 parents 2f71a7b + f6ccea7 commit 5ab64ed

File tree

3 files changed

+80
-71
lines changed

3 files changed

+80
-71
lines changed

documentation/wiki/ChangeWaves.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ A wave of features is set to "rotate out" (i.e. become standard functionality) t
2727
- [[RAR] Don't do I/O on SDK-provided references](https://github.com/dotnet/msbuild/pull/8688)
2828
- [Delete destination file before copy](https://github.com/dotnet/msbuild/pull/8685)
2929
- [New serialization approach for transferring build exceptions between processes](https://github.com/dotnet/msbuild/pull/8779)
30+
- [Moving from SHA1 to SHA256 for Hash task](https://github.com/dotnet/msbuild/pull/8812)
3031

3132
### 17.6
3233
- [Parse invalid property under target](https://github.com/dotnet/msbuild/pull/8190)

src/Tasks.UnitTests/Hash_Tests.cs

Lines changed: 24 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Linq;
45
using Microsoft.Build.Framework;
56
using Microsoft.Build.UnitTests;
67
using Microsoft.Build.Utilities;
@@ -16,7 +17,7 @@ public class Hash_Tests
1617
public void HashTaskTest()
1718
{
1819
// This hash was pre-computed. If the implementation changes it may need to be adjusted.
19-
var expectedHash = "5593e2db83ac26117cd95ed8917f09b02a02e2a0";
20+
var expectedHash = "3a9e94b896536fdab1343db5038239847e2db371f27e6ac9b5e3e6ea4aa2f2bf";
2021

2122
var actualHash = ExecuteHashTask(new ITaskItem[]
2223
{
@@ -52,7 +53,7 @@ public void HashTaskEmptyInputTest()
5253
public void HashTaskLargeInputCountTest()
5354
{
5455
// This hash was pre-computed. If the implementation changes it may need to be adjusted.
55-
var expectedHash = "8a996bbcb5e481981c2fba7ac408e20d0b4360a5";
56+
var expectedHash = "ae8799dfc1f81c50b08d28ac138e25958947895c8563c8fce080ceb5cb44db6f";
5657

5758
ITaskItem[] itemsToHash = new ITaskItem[1000];
5859
for (int i = 0; i < itemsToHash.Length; i++)
@@ -68,7 +69,7 @@ public void HashTaskLargeInputCountTest()
6869
public void HashTaskLargeInputSizeTest()
6970
{
7071
// This hash was pre-computed. If the implementation changes it may need to be adjusted.
71-
var expectedHash = "0509142dd3d3a733f30a52a0eec37cd727d46122";
72+
var expectedHash = "48a3fdf5cb1afc679497a418015edc85e571282bb70691d7a64f2ab2e32d5dbf";
7273

7374
string[] array = new string[1000];
7475
for (int i = 0; i < array.Length; i++)
@@ -81,44 +82,36 @@ public void HashTaskLargeInputSizeTest()
8182
Assert.Equal(expectedHash, actualHash);
8283
}
8384

84-
#pragma warning disable CA5350
8585
// This test verifies that hash computes correctly for various numbers of characters.
8686
// We would like to process edge of the buffer use cases regardless on the size of the buffer.
8787
[Fact]
8888
public void HashTaskDifferentInputSizesTest()
8989
{
9090
int maxInputSize = 2000;
91-
string input = "";
92-
using (var sha1 = System.Security.Cryptography.SHA1.Create())
91+
MockEngine mockEngine = new();
92+
93+
var hashGroups =
94+
Enumerable.Range(0, maxInputSize)
95+
.Select(cnt => new string('a', cnt))
96+
.Select(GetHash)
97+
.GroupBy(h => h)
98+
.Where(g => g.Count() > 1)
99+
.Select(g => g.Key);
100+
// none of the hashes should repeat
101+
Assert.Empty(hashGroups);
102+
103+
string GetHash(string input)
93104
{
94-
var stringBuilder = new System.Text.StringBuilder(sha1.HashSize);
95-
MockEngine mockEngine = new();
96-
for (int i = 0; i < maxInputSize; i++)
105+
Hash hashTask = new()
97106
{
98-
input += "a";
99-
100-
Hash hashTask = new()
101-
{
102-
BuildEngine = mockEngine,
103-
ItemsToHash = new ITaskItem[] { new TaskItem(input) },
104-
IgnoreCase = false
105-
};
106-
Assert.True(hashTask.Execute());
107-
string actualHash = hashTask.HashResult;
108-
109-
byte[] hash = sha1.ComputeHash(System.Text.Encoding.UTF8.GetBytes(input + '\u2028'));
110-
stringBuilder.Clear();
111-
foreach (var b in hash)
112-
{
113-
stringBuilder.Append(b.ToString("x2"));
114-
}
115-
string expectedHash = stringBuilder.ToString();
116-
117-
Assert.Equal(expectedHash, actualHash);
118-
}
107+
BuildEngine = mockEngine,
108+
ItemsToHash = new ITaskItem[] { new TaskItem(input) },
109+
IgnoreCase = false
110+
};
111+
Assert.True(hashTask.Execute());
112+
return hashTask.HashResult;
119113
}
120114
}
121-
#pragma warning restore CA5350
122115

123116
[Fact]
124117
public void HashTaskIgnoreCaseTest()

src/Tasks/Hash.cs

Lines changed: 55 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,23 @@ namespace Microsoft.Build.Tasks
1414
/// Generates a hash of a given ItemGroup items. Metadata is not considered in the hash.
1515
/// </summary>
1616
/// <remarks>
17-
/// Currently uses SHA1. Implementation subject to change between MSBuild versions.
18-
/// This class is not intended as a cryptographic security measure, only uniqueness between build executions.
17+
/// Currently uses SHA256. Implementation subject to change between MSBuild versions.
18+
/// This class is not intended as a cryptographic security measure, only uniqueness between build executions
19+
/// - collisions can theoretically be possible in the future (should we move to noncrypto hash) and should be handled gracefully by the caller.
20+
///
21+
/// Usage of cryptographic secure hash brings slight performance penalty, but it is considered acceptable.
22+
/// Would this need to be revised - XxHash64 from System.Io.Hashing could be used instead for better performance.
23+
/// (That however currently requires load of additional binary into VS process which has it's own costs)
1924
/// </remarks>
2025
public class Hash : TaskExtension
2126
{
2227
private const char ItemSeparatorCharacter = '\u2028';
2328
private static readonly Encoding s_encoding = Encoding.UTF8;
2429
private static readonly byte[] s_itemSeparatorCharacterBytes = s_encoding.GetBytes(new char[] { ItemSeparatorCharacter });
2530

26-
// Size of buffer where bytes of the strings are stored until sha1.TransformBlock is to be run on them.
27-
// It is needed to get a balance between amount of costly sha1.TransformBlock calls and amount of allocated memory.
28-
private const int Sha1BufferSize = 512;
31+
// Size of buffer where bytes of the strings are stored until sha.TransformBlock is to be run on them.
32+
// It is needed to get a balance between amount of costly sha.TransformBlock calls and amount of allocated memory.
33+
private const int ShaBufferSize = 512;
2934

3035
// Size of chunks in which ItemSpecs would be cut.
3136
// We have chosen this length so itemSpecChunkByteBuffer rented from ArrayPool will be close but not bigger than 512.
@@ -56,42 +61,42 @@ public override bool Execute()
5661
{
5762
if (ItemsToHash?.Length > 0)
5863
{
59-
using (var sha1 = SHA1.Create())
64+
using (var sha = CreateHashAlgorithm())
6065
{
6166
// Buffer in which bytes of the strings are to be stored until their number reaches the limit size.
62-
// Once the limit is reached, the sha1.TransformBlock is to be run on all the bytes of this buffer.
63-
byte[] sha1Buffer = null;
67+
// Once the limit is reached, the sha.TransformBlock is to be run on all the bytes of this buffer.
68+
byte[] shaBuffer = null;
6469

6570
// Buffer in which bytes of items' ItemSpec are to be stored.
6671
byte[] itemSpecChunkByteBuffer = null;
6772

6873
try
6974
{
70-
sha1Buffer = System.Buffers.ArrayPool<byte>.Shared.Rent(Sha1BufferSize);
75+
shaBuffer = System.Buffers.ArrayPool<byte>.Shared.Rent(ShaBufferSize);
7176
itemSpecChunkByteBuffer = System.Buffers.ArrayPool<byte>.Shared.Rent(s_encoding.GetMaxByteCount(MaxInputChunkLength));
7277

73-
int sha1BufferPosition = 0;
78+
int shaBufferPosition = 0;
7479
for (int i = 0; i < ItemsToHash.Length; i++)
7580
{
7681
string itemSpec = IgnoreCase ? ItemsToHash[i].ItemSpec.ToUpperInvariant() : ItemsToHash[i].ItemSpec;
7782

78-
// Slice the itemSpec string into chunks of reasonable size and add them to sha1 buffer.
83+
// Slice the itemSpec string into chunks of reasonable size and add them to sha buffer.
7984
for (int itemSpecPosition = 0; itemSpecPosition < itemSpec.Length; itemSpecPosition += MaxInputChunkLength)
8085
{
8186
int charsToProcess = Math.Min(itemSpec.Length - itemSpecPosition, MaxInputChunkLength);
8287
int byteCount = s_encoding.GetBytes(itemSpec, itemSpecPosition, charsToProcess, itemSpecChunkByteBuffer, 0);
8388

84-
sha1BufferPosition = AddBytesToSha1Buffer(sha1, sha1Buffer, sha1BufferPosition, Sha1BufferSize, itemSpecChunkByteBuffer, byteCount);
89+
shaBufferPosition = AddBytesToShaBuffer(sha, shaBuffer, shaBufferPosition, ShaBufferSize, itemSpecChunkByteBuffer, byteCount);
8590
}
8691

87-
sha1BufferPosition = AddBytesToSha1Buffer(sha1, sha1Buffer, sha1BufferPosition, Sha1BufferSize, s_itemSeparatorCharacterBytes, s_itemSeparatorCharacterBytes.Length);
92+
shaBufferPosition = AddBytesToShaBuffer(sha, shaBuffer, shaBufferPosition, ShaBufferSize, s_itemSeparatorCharacterBytes, s_itemSeparatorCharacterBytes.Length);
8893
}
8994

90-
sha1.TransformFinalBlock(sha1Buffer, 0, sha1BufferPosition);
95+
sha.TransformFinalBlock(shaBuffer, 0, shaBufferPosition);
9196

92-
using (var stringBuilder = new ReuseableStringBuilder(sha1.HashSize))
97+
using (var stringBuilder = new ReuseableStringBuilder(sha.HashSize))
9398
{
94-
foreach (var b in sha1.Hash)
99+
foreach (var b in sha.Hash)
95100
{
96101
stringBuilder.Append(b.ToString("x2"));
97102
}
@@ -100,9 +105,9 @@ public override bool Execute()
100105
}
101106
finally
102107
{
103-
if (sha1Buffer != null)
108+
if (shaBuffer != null)
104109
{
105-
System.Buffers.ArrayPool<byte>.Shared.Return(sha1Buffer);
110+
System.Buffers.ArrayPool<byte>.Shared.Return(shaBuffer);
106111
}
107112
if (itemSpecChunkByteBuffer != null)
108113
{
@@ -114,44 +119,54 @@ public override bool Execute()
114119
return true;
115120
}
116121

122+
private HashAlgorithm CreateHashAlgorithm()
123+
{
124+
return ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave17_8) ?
125+
SHA256.Create() :
126+
#pragma warning disable CA5350
127+
// Kept for back compatibility reasons when chnange wave is opted-out
128+
SHA1.Create();
129+
#pragma warning restore CA5350
130+
}
131+
117132
/// <summary>
118-
/// Add bytes to the sha1 buffer. Once the limit size is reached, sha1.TransformBlock is called and the buffer is flushed.
133+
/// Add bytes to the sha buffer. Once the limit size is reached, sha.TransformBlock is called and the buffer is flushed.
119134
/// </summary>
120-
/// <param name="sha1">Hashing algorithm sha1.</param>
121-
/// <param name="sha1Buffer">The sha1 buffer which stores bytes of the strings. Bytes should be added to this buffer.</param>
122-
/// <param name="sha1BufferPosition">Number of used bytes of the sha1 buffer.</param>
123-
/// <param name="sha1BufferSize">The size of sha1 buffer.</param>
124-
/// <param name="byteBuffer">Bytes buffer which contains bytes to be written to sha1 buffer.</param>
125-
/// <param name="byteCount">Amount of bytes that are to be added to sha1 buffer.</param>
126-
/// <returns>Updated sha1BufferPosition.</returns>
127-
private int AddBytesToSha1Buffer(SHA1 sha1, byte[] sha1Buffer, int sha1BufferPosition, int sha1BufferSize, byte[] byteBuffer, int byteCount)
135+
/// <param name="sha">Hashing algorithm sha.</param>
136+
/// <param name="shaBuffer">The sha buffer which stores bytes of the strings. Bytes should be added to this buffer.</param>
137+
/// <param name="shaBufferPosition">Number of used bytes of the sha buffer.</param>
138+
/// <param name="shaBufferSize">The size of sha buffer.</param>
139+
/// <param name="byteBuffer">Bytes buffer which contains bytes to be written to sha buffer.</param>
140+
/// <param name="byteCount">Amount of bytes that are to be added to sha buffer.</param>
141+
/// <returns>Updated shaBufferPosition.</returns>
142+
private int AddBytesToShaBuffer(HashAlgorithm sha, byte[] shaBuffer, int shaBufferPosition, int shaBufferSize, byte[] byteBuffer, int byteCount)
128143
{
129144
int bytesProcessed = 0;
130-
while (sha1BufferPosition + byteCount >= sha1BufferSize)
145+
while (shaBufferPosition + byteCount >= shaBufferSize)
131146
{
132-
int sha1BufferFreeSpace = sha1BufferSize - sha1BufferPosition;
147+
int shaBufferFreeSpace = shaBufferSize - shaBufferPosition;
133148

134-
if (sha1BufferPosition == 0)
149+
if (shaBufferPosition == 0)
135150
{
136-
// If sha1 buffer is empty and bytes number is big enough there is no need to copy bytes to sha1 buffer.
151+
// If sha buffer is empty and bytes number is big enough there is no need to copy bytes to sha buffer.
137152
// Pass the bytes to TransformBlock right away.
138-
sha1.TransformBlock(byteBuffer, bytesProcessed, sha1BufferSize, null, 0);
153+
sha.TransformBlock(byteBuffer, bytesProcessed, shaBufferSize, null, 0);
139154
}
140155
else
141156
{
142-
Array.Copy(byteBuffer, bytesProcessed, sha1Buffer, sha1BufferPosition, sha1BufferFreeSpace);
143-
sha1.TransformBlock(sha1Buffer, 0, sha1BufferSize, null, 0);
144-
sha1BufferPosition = 0;
157+
Array.Copy(byteBuffer, bytesProcessed, shaBuffer, shaBufferPosition, shaBufferFreeSpace);
158+
sha.TransformBlock(shaBuffer, 0, shaBufferSize, null, 0);
159+
shaBufferPosition = 0;
145160
}
146161

147-
bytesProcessed += sha1BufferFreeSpace;
148-
byteCount -= sha1BufferFreeSpace;
162+
bytesProcessed += shaBufferFreeSpace;
163+
byteCount -= shaBufferFreeSpace;
149164
}
150165

151-
Array.Copy(byteBuffer, bytesProcessed, sha1Buffer, sha1BufferPosition, byteCount);
152-
sha1BufferPosition += byteCount;
166+
Array.Copy(byteBuffer, bytesProcessed, shaBuffer, shaBufferPosition, byteCount);
167+
shaBufferPosition += byteCount;
153168

154-
return sha1BufferPosition;
169+
return shaBufferPosition;
155170
}
156171
}
157172
}

0 commit comments

Comments
 (0)