Skip to content
This repository has been archived by the owner on Apr 1, 2024. It is now read-only.

Commit

Permalink
Update IDeriveBytes to allow specifying hash algorithm
Browse files Browse the repository at this point in the history
This makes a couple tests fail on UWP because UWP is consuming the netstandard2.0 build of the library. Adding a uap10.0 target hits novotnyllc/MSBuildSdkExtras#237.
  • Loading branch information
AArnott committed Aug 17, 2020
1 parent 214606a commit 298cf26
Show file tree
Hide file tree
Showing 12 changed files with 94 additions and 72 deletions.
2 changes: 1 addition & 1 deletion src/PCLCrypto/ECDiffieHellmanCngPublicKeyFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public IECDiffieHellmanPublicKey FromByteArray(byte[] publicKey)

#if NETSTANDARD2_0
throw new NotImplementedByReferenceAssemblyException();
#elif __IOS__ || __ANDROID__
#elif __IOS__ || __ANDROID__ || WINDOWS_UWP
throw new PlatformNotSupportedException();
#else
return new ECDiffieHellmanPublicKey(
Expand Down
2 changes: 2 additions & 0 deletions src/PCLCrypto/ECDiffieHellmanFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ public IECDiffieHellman Create()
{
#if NETSTANDARD2_0
throw new NotImplementedByReferenceAssemblyException();
#elif WINDOWS_UWP
throw new PlatformNotSupportedException();
#else
return new ECDiffieHellman(Platform.ECDiffieHellman.Create());
#endif
Expand Down
19 changes: 5 additions & 14 deletions src/PCLCrypto/IDeriveBytes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,7 @@

namespace PCLCrypto
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Security.Cryptography;

/// <summary>
/// Provides fixed-length key derivation from passwords or byte buffers of arbitrary size.
Expand All @@ -20,17 +17,11 @@ public interface IDeriveBytes
/// <param name="salt">The salt.</param>
/// <param name="iterations">The rounds of computation to use in deriving a stronger key. The larger this is, the longer attacks will take.</param>
/// <param name="countBytes">The desired key size in bytes.</param>
/// <param name="hashAlgorithm">The hash algorithm to use.</param>
/// <returns>The generated key.</returns>byte[] GetBytes(string keyMaterial, byte[] salt, int iterations, int countBytes);
byte[] GetBytes(string keyMaterial, byte[] salt, int iterations, int countBytes);
byte[] GetBytes(string keyMaterial, byte[] salt, int iterations, int countBytes, HashAlgorithmName hashAlgorithm);

/// <summary>
/// Derives a cryptographically strong key from the specified bytes.
/// </summary>
/// <param name="keyMaterial">The user-supplied password.</param>
/// <param name="salt">The salt.</param>
/// <param name="iterations">The rounds of computation to use in deriving a stronger key. The larger this is, the longer attacks will take.</param>
/// <param name="countBytes">The desired key size in bytes.</param>
/// <returns>The generated key.</returns>
byte[] GetBytes(byte[] keyMaterial, byte[] salt, int iterations, int countBytes);
/// <inheritdoc cref="GetBytes(string, byte[], int, int, HashAlgorithmName)"/>
byte[] GetBytes(byte[] keyMaterial, byte[] salt, int iterations, int countBytes, HashAlgorithmName hashAlgorithm);
}
}
33 changes: 24 additions & 9 deletions src/PCLCrypto/NetFx/DeriveBytes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,40 +11,55 @@ namespace PCLCrypto
using System.Threading.Tasks;
using Microsoft;

#pragma warning disable CA5379 // Do Not Use Weak Key Derivation Function Algorithm

/// <summary>
/// Exposes the .NET Framework implementation of <see cref="IDeriveBytes"/>.
/// </summary>
internal class DeriveBytes : IDeriveBytes
{
/// <inheritdoc/>
public byte[] GetBytes(string keyMaterial, byte[] salt, int iterations, int countBytes)
public byte[] GetBytes(string keyMaterial, byte[] salt, int iterations, int countBytes, HashAlgorithmName hashAlgorithm)
{
Requires.NotNullOrEmpty(keyMaterial, "keyMaterial");
Requires.NotNull(salt, nameof(salt));
Requires.Range(iterations > 0, "iterations");
Requires.Range(countBytes > 0, "countBytes");

var keyStrengthening = new Rfc2898DeriveBytes(keyMaterial, salt, iterations);
try
#if NETSTANDARD2_0
if (hashAlgorithm == HashAlgorithmName.SHA1)
{
using var keyStrengthening = new Rfc2898DeriveBytes(keyMaterial, salt, iterations);
return keyStrengthening.GetBytes(countBytes);
}
finally
{
(keyStrengthening as IDisposable)?.Dispose();
}

throw new NotImplementedByReferenceAssemblyException();
#else
using var keyStrengthening = new Rfc2898DeriveBytes(keyMaterial, salt, iterations, hashAlgorithm);
return keyStrengthening.GetBytes(countBytes);
#endif
}

/// <inheritdoc/>
public byte[] GetBytes(byte[] keyMaterial, byte[] salt, int iterations, int countBytes)
public byte[] GetBytes(byte[] keyMaterial, byte[] salt, int iterations, int countBytes, HashAlgorithmName hashAlgorithm)
{
Requires.NotNullOrEmpty(keyMaterial, "keyMaterial");
Requires.NotNull(salt, nameof(salt));
Requires.Range(iterations > 0, "iterations");
Requires.Range(countBytes > 0, "countBytes");

using var keyStrengthening = new Rfc2898DeriveBytes(keyMaterial, salt, iterations);
#if NETSTANDARD2_0
if (hashAlgorithm == HashAlgorithmName.SHA1)
{
using var keyStrengthening = new Rfc2898DeriveBytes(keyMaterial, salt, iterations);
return keyStrengthening.GetBytes(countBytes);
}

throw new NotImplementedByReferenceAssemblyException();
#else
using var keyStrengthening = new Rfc2898DeriveBytes(keyMaterial, salt, iterations, hashAlgorithm);
return keyStrengthening.GetBytes(countBytes);
#endif
}
}
}
31 changes: 24 additions & 7 deletions src/PCLCrypto/NetFx/KeyDerivationCryptographicKey.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,7 @@
namespace PCLCrypto
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Security.Cryptography;
using Microsoft;
using Platform = System.Security.Cryptography;

Expand Down Expand Up @@ -84,6 +81,7 @@ protected internal override byte[] DeriveKeyMaterial(IKeyDerivationParameters pa
// more parameter types than just BuildForPbkdf2, we might need to adjust this code
// to handle each type of parameter.
byte[] salt = parameters.KdfGenericBinary;
#pragma warning disable CA5379 // Do Not Use Weak Key Derivation Function Algorithm
switch (this.Algorithm)
{
case KeyDerivationAlgorithm.Pbkdf2Sha1:
Expand All @@ -93,10 +91,29 @@ protected internal override byte[] DeriveKeyMaterial(IKeyDerivationParameters pa
}

default:
// TODO: consider using Platform.PasswordDeriveBytes if it can
// support some more of these algorithms.
throw new NotSupportedException("Only KeyDerivationAlgorithm.Pbkdf2Sha1 is supported for this platform.");
#if NETSTANDARD2_0
throw new NotImplementedByReferenceAssemblyException();
#else
using (var deriveBytes = new Platform.Rfc2898DeriveBytes(this.Key, salt, parameters.IterationCount, GetHashAlgorithm(this.Algorithm)))
{
return deriveBytes.GetBytes(desiredKeySize);
}
#endif
}
#pragma warning restore CA5379 // Do Not Use Weak Key Derivation Function Algorithm
}

private static HashAlgorithmName GetHashAlgorithm(KeyDerivationAlgorithm keyDerivationAlgorithm)
{
return keyDerivationAlgorithm switch
{
KeyDerivationAlgorithm.Pbkdf2Md5 => HashAlgorithmName.MD5,
KeyDerivationAlgorithm.Pbkdf2Sha1 => HashAlgorithmName.SHA1,
KeyDerivationAlgorithm.Pbkdf2Sha256 => HashAlgorithmName.SHA256,
KeyDerivationAlgorithm.Pbkdf2Sha384 => HashAlgorithmName.SHA384,
KeyDerivationAlgorithm.Pbkdf2Sha512 => HashAlgorithmName.SHA512,
_ => throw new NotSupportedException($"{keyDerivationAlgorithm} is not supported on this platform."),
};
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright (c) Andrew Arnott. All rights reserved.
// Licensed under the Microsoft Public License (Ms-PL) license. See LICENSE file in the project root for full license information.

#if NETSTANDARD
#if NETSTANDARD2_0

namespace PCLCrypto
{
Expand Down
4 changes: 2 additions & 2 deletions src/PCLCrypto/PCLCrypto.csproj
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project Sdk="MSBuild.Sdk.Extras">
<Project Sdk="MSBuild.Sdk.Extras">
<PropertyGroup>
<TargetFrameworks>net472;netstandard2.0;MonoAndroid9;XamariniOS;netcoreapp2.1</TargetFrameworks>
<TargetFrameworks>net472;netstandard2.0;netstandard2.1;MonoAndroid9;XamariniOS;netcoreapp2.1</TargetFrameworks>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>

<Title>PCL Crypto - Portable Crypto APIs</Title>
Expand Down
2 changes: 1 addition & 1 deletion src/PCLCrypto/desktop/ECDiffieHellman.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright (c) Andrew Arnott. All rights reserved.
// Licensed under the Microsoft Public License (Ms-PL) license. See LICENSE file in the project root for full license information.

#if !NETSTANDARD2_0
#if !(NETSTANDARD2_0 || WINDOWS_UWP)

namespace PCLCrypto
{
Expand Down
15 changes: 8 additions & 7 deletions test/PCLCrypto.Tests.Shared/DeriveBytesTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using PCLCrypto;
using Xunit;
Expand All @@ -20,36 +21,36 @@ public class DeriveBytesTests
[Fact]
public void GetBytes()
{
byte[] keyFromPassword = NetFxCrypto.DeriveBytes.GetBytes(Password1, Salt1, 5, 10);
byte[] keyFromBytes = NetFxCrypto.DeriveBytes.GetBytes(Encoding.UTF8.GetBytes(Password1), Salt1, 5, 10);
byte[] keyFromPassword = NetFxCrypto.DeriveBytes.GetBytes(Password1, Salt1, 5, 10, HashAlgorithmName.SHA1);
byte[] keyFromBytes = NetFxCrypto.DeriveBytes.GetBytes(Encoding.UTF8.GetBytes(Password1), Salt1, 5, 10, HashAlgorithmName.SHA1);
CollectionAssertEx.AreEqual(keyFromPassword, keyFromBytes);
Assert.Equal(DerivedKey, Convert.ToBase64String(keyFromPassword));

byte[] keyWithOtherSalt = NetFxCrypto.DeriveBytes.GetBytes(Password1, Salt2, 5, 10);
byte[] keyWithOtherSalt = NetFxCrypto.DeriveBytes.GetBytes(Password1, Salt2, 5, 10, HashAlgorithmName.SHA1);
CollectionAssertEx.AreNotEqual(keyFromPassword, keyWithOtherSalt);
}

[Fact]
public void GetBytes_NullBytes()
{
Assert.Throws<ArgumentNullException>(() => NetFxCrypto.DeriveBytes.GetBytes((byte[])null!, Salt1, 5, 10));
Assert.Throws<ArgumentNullException>(() => NetFxCrypto.DeriveBytes.GetBytes((byte[])null!, Salt1, 5, 10, HashAlgorithmName.SHA1));
}

[Fact]
public void GetBytes_NullPassword()
{
Assert.Throws<ArgumentNullException>(() => NetFxCrypto.DeriveBytes.GetBytes((string)null!, Salt1, 5, 10));
Assert.Throws<ArgumentNullException>(() => NetFxCrypto.DeriveBytes.GetBytes((string)null!, Salt1, 5, 10, HashAlgorithmName.SHA1));
}

[Fact]
public void GetBytes_Password_NullSalt()
{
Assert.Throws<ArgumentNullException>(() => NetFxCrypto.DeriveBytes.GetBytes(Password1, null!, 5, 10));
Assert.Throws<ArgumentNullException>(() => NetFxCrypto.DeriveBytes.GetBytes(Password1, null!, 5, 10, HashAlgorithmName.SHA1));
}

[Fact]
public void GetBytes_Bytes_NullSalt()
{
Assert.Throws<ArgumentNullException>(() => NetFxCrypto.DeriveBytes.GetBytes(Encoding.UTF8.GetBytes(Password1), null!, 5, 10));
Assert.Throws<ArgumentNullException>(() => NetFxCrypto.DeriveBytes.GetBytes(Encoding.UTF8.GetBytes(Password1), null!, 5, 10, HashAlgorithmName.SHA1));
}
}
46 changes: 20 additions & 26 deletions test/PCLCrypto.Tests.Shared/KeyDerivationAlgorithmProviderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,6 @@ public class KeyDerivationAlgorithmProviderTests
private readonly byte[] originalKey = new byte[] { 0x1, 0x2, 0x3, 0x5 };
private readonly byte[] salt = new byte[8];
private readonly int iterations = 100;
private readonly Dictionary<KeyDerivationAlgorithm, string> stretchedKeyBase64 = new Dictionary<KeyDerivationAlgorithm, string>
{
{ KeyDerivationAlgorithm.Pbkdf2Sha1, "3HWzwI225INl7y6+G9Jv7Af8UGE=" },
{ KeyDerivationAlgorithm.Pbkdf2Sha256, "t420R6yC8H2CDK/0sSGmwKHLooM=" },
};

private readonly ITestOutputHelper logger;

Expand Down Expand Up @@ -55,30 +50,29 @@ public void CreateKey_InvalidInputs()
() => algorithm.CreateKey(null!));
}

[Fact]
public void CreateKey()
[Theory]
[InlineData(KeyDerivationAlgorithm.Pbkdf2Sha1, "3HWzwI225INl7y6+G9Jv7Af8UGE=")]
[InlineData(KeyDerivationAlgorithm.Pbkdf2Sha256, "t420R6yC8H2CDK/0sSGmwKHLooM=")]
public void CreateKey(KeyDerivationAlgorithm algorithmName, string result)
{
foreach (KeyValuePair<KeyDerivationAlgorithm, string> algorithmAndExpectedResult in this.stretchedKeyBase64)
{
this.logger.WriteLine("Testing algorithm: {0}", algorithmAndExpectedResult.Key);
IKeyDerivationAlgorithmProvider? algorithm = WinRTCrypto.KeyDerivationAlgorithmProvider.OpenAlgorithm(algorithmAndExpectedResult.Key);
ICryptographicKey key = algorithm.CreateKey(this.originalKey);
Assert.NotNull(key);
Assert.Equal(this.originalKey.Length * 8, key.KeySize);
this.logger.WriteLine("Testing algorithm: {0}", algorithmName);
IKeyDerivationAlgorithmProvider? algorithm = WinRTCrypto.KeyDerivationAlgorithmProvider.OpenAlgorithm(algorithmName);
ICryptographicKey key = algorithm.CreateKey(this.originalKey);
Assert.NotNull(key);
Assert.Equal(this.originalKey.Length * 8, key.KeySize);

IKeyDerivationParameters parameters = WinRTCrypto.KeyDerivationParameters.BuildForPbkdf2(this.salt, this.iterations);
Assert.Equal(this.iterations, parameters.IterationCount);
CollectionAssertEx.AreEqual(this.salt, parameters.KdfGenericBinary);
IKeyDerivationParameters parameters = WinRTCrypto.KeyDerivationParameters.BuildForPbkdf2(this.salt, this.iterations);
Assert.Equal(this.iterations, parameters.IterationCount);
CollectionAssertEx.AreEqual(this.salt, parameters.KdfGenericBinary);

try
{
byte[] keyMaterial = WinRTCrypto.CryptographicEngine.DeriveKeyMaterial(key, parameters, 20);
Assert.Equal(algorithmAndExpectedResult.Value, Convert.ToBase64String(keyMaterial));
}
catch (NotSupportedException)
{
this.logger.WriteLine(" - Not supported on this platform");
}
try
{
byte[] keyMaterial = WinRTCrypto.CryptographicEngine.DeriveKeyMaterial(key, parameters, 20);
Assert.Equal(result, Convert.ToBase64String(keyMaterial));
}
catch (NotSupportedException)
{
this.logger.WriteLine(" - Not supported on this platform");
}
}
}
8 changes: 5 additions & 3 deletions test/PCLCrypto.Tests.Shared/PlatformSupport.cs
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,12 @@ public void PublicKeyFormat(CryptographicPublicKeyBlobType format)
Assert.NotEmpty(serialized);
}

[SkippableFact(typeof(NotSupportedException), typeof(PlatformNotSupportedException))]
public void KeyDerivation_Pbkdf2()
[SkippableTheory(typeof(NotSupportedException), typeof(PlatformNotSupportedException))]
[InlineData(KeyDerivationAlgorithm.Pbkdf2Sha1)]
[InlineData(KeyDerivationAlgorithm.Pbkdf2Sha256)]
public void KeyDerivation_Pbkdf2(KeyDerivationAlgorithm algorithmName)
{
IKeyDerivationAlgorithmProvider? provider = WinRTCrypto.KeyDerivationAlgorithmProvider.OpenAlgorithm(KeyDerivationAlgorithm.Pbkdf2Sha256);
IKeyDerivationAlgorithmProvider? provider = WinRTCrypto.KeyDerivationAlgorithmProvider.OpenAlgorithm(algorithmName);
ICryptographicKey? originalKey = provider.CreateKey(Encoding.UTF8.GetBytes("my secret"));
var salt = WinRTCrypto.CryptographicBuffer.GenerateRandom(32);
const int iterationCount = 2;
Expand Down
2 changes: 1 addition & 1 deletion test/PCLCrypto.Tests/PCLCrypto.Tests.csproj
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net472;netcoreapp2.1</TargetFrameworks>
<TargetFrameworks>net472;netcoreapp2.1;netcoreapp3.1</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<Reference Include="System.Numerics" />
Expand Down

0 comments on commit 298cf26

Please sign in to comment.