From 68ec8f983dfdda2c5f7bc5e1b3fd98ef705385a2 Mon Sep 17 00:00:00 2001 From: Alexandru Macocian Date: Thu, 15 Jul 2021 16:11:05 +0300 Subject: [PATCH] Implementation of SecureString and CryptoRngProvider --- .../AesEncrypterTests.cs | 2 +- .../CryptoRngProviderTests.cs | 39 +++++ .../SecureStringTests.cs | 134 ++++++++++++++++++ .../Sha256HashingServiceTests.cs | 2 +- .../Encryption/SecureString.cs | 89 ++++++++++++ .../Rng/CryptoRngProvider.cs | 47 ++++++ .../Rng/ICryptoRngProvider.cs | 8 ++ ...stemExtensions.NetStandard.Security.csproj | 6 +- 8 files changed, 324 insertions(+), 3 deletions(-) create mode 100644 SystemExtensions.NetStandard.Security.Tests/CryptoRngProviderTests.cs create mode 100644 SystemExtensions.NetStandard.Security.Tests/SecureStringTests.cs create mode 100644 SystemExtensions.NetStandard.Security/Encryption/SecureString.cs create mode 100644 SystemExtensions.NetStandard.Security/Rng/CryptoRngProvider.cs create mode 100644 SystemExtensions.NetStandard.Security/Rng/ICryptoRngProvider.cs diff --git a/SystemExtensions.NetStandard.Security.Tests/AesEncrypterTests.cs b/SystemExtensions.NetStandard.Security.Tests/AesEncrypterTests.cs index 943cdab..875dc46 100644 --- a/SystemExtensions.NetStandard.Security.Tests/AesEncrypterTests.cs +++ b/SystemExtensions.NetStandard.Security.Tests/AesEncrypterTests.cs @@ -7,7 +7,7 @@ using System.Text; using System.Threading.Tasks; using SystemExtensions.NetStandard.Security.Encryption; -namespace VaultO.Tests +namespace SystemExtensions.NetStandard.Security.Tests { [TestClass] public class AesEncrypterTests diff --git a/SystemExtensions.NetStandard.Security.Tests/CryptoRngProviderTests.cs b/SystemExtensions.NetStandard.Security.Tests/CryptoRngProviderTests.cs new file mode 100644 index 0000000..5bba5c0 --- /dev/null +++ b/SystemExtensions.NetStandard.Security.Tests/CryptoRngProviderTests.cs @@ -0,0 +1,39 @@ +using FluentAssertions; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Linq; +using System.Rng; + +namespace SystemExtensions.NetStandard.Security.Tests +{ + [TestClass] + public class CryptoRngProviderTests + { + private CryptoRngProvider cryptoRngProvider; + + [TestInitialize] + public void TestInitialize() + { + this.cryptoRngProvider = new CryptoRngProvider(); + } + + [TestMethod] + public void GetBytes_ShouldSetValues() + { + var bytes = new byte[100]; + + this.cryptoRngProvider.GetBytes(bytes); + + bytes.All(b => b == 0).Should().BeFalse(); + } + + [TestMethod] + public void GetNonZeroBytes_ShouldSetNonZeroValues() + { + var bytes = new byte[100]; + + this.cryptoRngProvider.GetNonZeroBytes(bytes); + + bytes.All(b => b != 0).Should().BeTrue(); + } + } +} \ No newline at end of file diff --git a/SystemExtensions.NetStandard.Security.Tests/SecureStringTests.cs b/SystemExtensions.NetStandard.Security.Tests/SecureStringTests.cs new file mode 100644 index 0000000..e6d099c --- /dev/null +++ b/SystemExtensions.NetStandard.Security.Tests/SecureStringTests.cs @@ -0,0 +1,134 @@ +using FluentAssertions; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.Encryption; + +namespace SystemExtensions.NetStandard.Security.Tests +{ + [TestClass] + public class SecureStringTests + { + [TestMethod] + public void SecureString_NewSecureString_ReturnsValue() + { + var str = new SecureString("hello"); + + str.Value.Should().Be("hello"); + } + + [TestMethod] + public void SecureStringEmpty_AreEqual() + { + var str = SecureString.Empty; + str.Should().Be(SecureString.Empty); + } + + [TestMethod] + public void SecureStringEmpty_Equals_StringEmpty() + { + var ss = SecureString.Empty; + var s = string.Empty; + + ss.Should().Be(s); + } + + [TestMethod] + public void SecureString_Equals_OtherSecureString() + { + var ss1 = new SecureString("Hello"); + var ss2 = new SecureString("Hello"); + + ss1.Equals(ss2).Should().BeTrue(); + } + + [TestMethod] + [DataRow("hello", "hello", true)] + [DataRow("hello", "henlo", false)] + public void SecureString_EqualOperator_OtherSecureString(string str1, string str2, bool isEqual) + { + var ss1 = new SecureString(str1); + var ss2 = new SecureString(str2); + + (ss1 == ss2).Should().Be(isEqual); + } + + [TestMethod] + [DataRow("hello", "hello", false)] + [DataRow("hello", "henlo", true)] + public void SecureString_DifferentOperator_OtherSecureString(string str1, string str2, bool isDifferent) + { + var ss1 = new SecureString(str1); + var ss2 = new SecureString(str2); + + (ss1 != ss2).Should().Be(isDifferent); + } + + [TestMethod] + [DataRow("hello", "hello", true)] + [DataRow("hello", "henlo", false)] + public void SecureString_EqualOperator_String(string str1, string str2, bool isEqual) + { + var ss1 = new SecureString(str1); + + (ss1 == str2).Should().Be(isEqual); + } + + [TestMethod] + [DataRow("hello", "hello", false)] + [DataRow("hello", "henlo", true)] + public void SecureString_DifferentOperator_String(string str1, string str2, bool isDifferent) + { + var ss1 = new SecureString(str1); + + (ss1 != str2).Should().Be(isDifferent); + } + + [TestMethod] + public void SecureString_PlusOperator_SecureString() + { + var ss1 = new SecureString("Hello "); + var ss2 = new SecureString("World"); + + (ss1 + ss2).Should().Be("Hello World"); + } + + [TestMethod] + public void SecureString_PlusOperator_String() + { + var ss1 = new SecureString("Hello "); + var ss2 = "World"; + + (ss1 + ss2).Should().Be("Hello World"); + } + + [TestMethod] + public void SecureString_PlusOperator_Char() + { + var ss1 = new SecureString("Hello "); + var c = 'W'; + + (ss1 + c).Should().Be("Hello W"); + } + + [TestMethod] + public void SecureString_WithOptionalEntropy_Matches() + { + SecureString.AddOptionalEntropy(new byte[] { 10, 20, 25, 34, 56, 12, 10, 81, 200, 155, 123, 144, 123, 192, 122, 1 }); + var ss1 = new SecureString("Hello"); + var ss2 = new SecureString("Hello"); + + ss1.Should().Be(ss2); + } + + [TestMethod] + public void SecureString_ChangingEntropy_ThrowsOnPreviousValue() + { + var ss1 = new SecureString("Hello"); + SecureString.AddOptionalEntropy(new byte[] { 10, 20, 25, 34, 56, 12, 10, 81, 200, 155, 123, 144, 123, 192, 122, 1 }); + + var action = new Func(() => ss1.Value); + + action.Should().Throw(); + } + } +} diff --git a/SystemExtensions.NetStandard.Security.Tests/Sha256HashingServiceTests.cs b/SystemExtensions.NetStandard.Security.Tests/Sha256HashingServiceTests.cs index 4d593cf..c0ab393 100644 --- a/SystemExtensions.NetStandard.Security.Tests/Sha256HashingServiceTests.cs +++ b/SystemExtensions.NetStandard.Security.Tests/Sha256HashingServiceTests.cs @@ -6,7 +6,7 @@ using System.Text; using System.Threading.Tasks; using SystemExtensions.NetStandard.Security.Hashing; -namespace VaultO.Tests +namespace SystemExtensions.NetStandard.Security.Tests { [TestClass] public sealed class Sha256HashingServiceTests diff --git a/SystemExtensions.NetStandard.Security/Encryption/SecureString.cs b/SystemExtensions.NetStandard.Security/Encryption/SecureString.cs new file mode 100644 index 0000000..1781edf --- /dev/null +++ b/SystemExtensions.NetStandard.Security/Encryption/SecureString.cs @@ -0,0 +1,89 @@ +using System.Security.Cryptography; +using System.Text; + +namespace System.Encryption +{ + public sealed class SecureString + { + private static byte[] optionalEntropy; + private byte[] encryptedValue; + + public string Value { + get => this.encryptedValue is not null ? Encoding.UTF8.GetString(ProtectedData.Unprotect(this.encryptedValue, optionalEntropy, DataProtectionScope.CurrentUser)) : null; + private set => this.encryptedValue = ProtectedData.Protect(Encoding.UTF8.GetBytes(value), optionalEntropy, DataProtectionScope.CurrentUser); + } + + public SecureString(string value) + { + this.Value = value; + } + + public override bool Equals(object obj) + { + if (obj is string) + { + return this == (obj as string); + } + else if (obj is SecureString) + { + return this == (obj as SecureString); + } + else + { + return base.Equals(obj); + } + } + public override int GetHashCode() + { + return this.Value.GetHashCode(); + } + public override string ToString() + { + return this.Value; + } + + public static readonly SecureString Empty = new(string.Empty); + public static implicit operator string(SecureString ss) => ss is null ? string.Empty : ss.Value; + public static implicit operator SecureString(string s) => new(s); + public static SecureString operator +(SecureString ss1, SecureString ss2) + { + if (ss1 is null) throw new ArgumentNullException(nameof(ss1)); + if (ss2 is null) throw new ArgumentNullException(nameof(ss2)); + + return new SecureString(ss1.Value + ss2.Value); + } + public static SecureString operator +(SecureString ss1, string s2) + { + if (ss1 is null) throw new ArgumentNullException(nameof(ss1)); + + return new SecureString(ss1.Value + s2); + } + public static SecureString operator +(SecureString ss1, char c) + { + if (ss1 is null) throw new ArgumentNullException(nameof(ss1)); + + return new SecureString(ss1.Value + c); + } + public static bool operator ==(SecureString ss1, SecureString ss2) + { + return ss1?.Value == ss2?.Value; + } + public static bool operator !=(SecureString ss1, SecureString ss2) + { + return !(ss1 == ss2); + } + public static bool operator ==(SecureString ss1, string s2) + { + return ss1?.Value == s2; + } + public static bool operator !=(SecureString ss1, string s2) + { + return !(ss1?.Value == s2); + } + + public static void AddOptionalEntropy(byte[] entropy) + { + optionalEntropy = entropy; + } + } +} diff --git a/SystemExtensions.NetStandard.Security/Rng/CryptoRngProvider.cs b/SystemExtensions.NetStandard.Security/Rng/CryptoRngProvider.cs new file mode 100644 index 0000000..452befa --- /dev/null +++ b/SystemExtensions.NetStandard.Security/Rng/CryptoRngProvider.cs @@ -0,0 +1,47 @@ +using System.Security.Cryptography; + +namespace System.Rng +{ + public sealed class CryptoRngProvider : ICryptoRngProvider, IDisposable + { + private readonly RNGCryptoServiceProvider rngProvider; + private bool disposedValue; + + public CryptoRngProvider() + { + this.rngProvider = new RNGCryptoServiceProvider(); + } + public CryptoRngProvider(CspParameters cspParams) + { + this.rngProvider = new RNGCryptoServiceProvider(cspParams); + } + + + public void GetBytes(byte[] data) + { + this.rngProvider.GetBytes(data); + } + public void GetNonZeroBytes(byte[] data) + { + this.rngProvider.GetNonZeroBytes(data); + } + + private void Dispose(bool disposing) + { + if (!this.disposedValue) + { + if (disposing) + { + this.rngProvider.Dispose(); + } + + this.disposedValue = true; + } + } + public void Dispose() + { + this.Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/SystemExtensions.NetStandard.Security/Rng/ICryptoRngProvider.cs b/SystemExtensions.NetStandard.Security/Rng/ICryptoRngProvider.cs new file mode 100644 index 0000000..8eae503 --- /dev/null +++ b/SystemExtensions.NetStandard.Security/Rng/ICryptoRngProvider.cs @@ -0,0 +1,8 @@ +namespace System.Rng +{ + public interface ICryptoRngProvider + { + public void GetBytes(byte[] data); + public void GetNonZeroBytes(byte[] data); + } +} diff --git a/SystemExtensions.NetStandard.Security/SystemExtensions.NetStandard.Security.csproj b/SystemExtensions.NetStandard.Security/SystemExtensions.NetStandard.Security.csproj index d723ff9..bc0819e 100644 --- a/SystemExtensions.NetStandard.Security/SystemExtensions.NetStandard.Security.csproj +++ b/SystemExtensions.NetStandard.Security/SystemExtensions.NetStandard.Security.csproj @@ -7,7 +7,7 @@ LICENSE true System - 1.0.0 + 1.1.0 Alexandru Macocian https://github.com/AlexMacocian/SystemExtensions @@ -19,4 +19,8 @@ + + + + \ No newline at end of file