diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/IOL.Helpers.sln | 34 | ||||
| -rw-r--r-- | src/IOL.Helpers/CryptographyHelpers.cs | 173 | ||||
| -rw-r--r-- | src/IOL.Helpers/DateTimeHelpers.cs | 22 | ||||
| -rw-r--r-- | src/IOL.Helpers/DoubleHelpers.cs | 11 | ||||
| -rw-r--r-- | src/IOL.Helpers/EnumHelpers.cs | 13 | ||||
| -rw-r--r-- | src/IOL.Helpers/HttpRequestHelpers.cs | 18 | ||||
| -rw-r--r-- | src/IOL.Helpers/IOL.Helpers.csproj | 12 | ||||
| -rw-r--r-- | src/IOL.Helpers/PasswordHelpers.cs | 70 | ||||
| -rw-r--r-- | src/IOL.Helpers/RandomStringGenerator.cs | 18 | ||||
| -rw-r--r-- | src/IOL.Helpers/SlugGenerator.cs | 108 | ||||
| -rw-r--r-- | src/IOL.Helpers/StringHelpers.cs | 67 | ||||
| -rw-r--r-- | src/IOL.Helpers/Validators.cs | 18 |
12 files changed, 564 insertions, 0 deletions
diff --git a/src/IOL.Helpers.sln b/src/IOL.Helpers.sln new file mode 100644 index 0000000..b166d6e --- /dev/null +++ b/src/IOL.Helpers.sln @@ -0,0 +1,34 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.6.30114.105 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IOL.Helpers", "IOL.Helpers\IOL.Helpers.csproj", "{DE4D43B0-FBE6-40B7-9C8F-FFFA7987F87B}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {DE4D43B0-FBE6-40B7-9C8F-FFFA7987F87B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DE4D43B0-FBE6-40B7-9C8F-FFFA7987F87B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DE4D43B0-FBE6-40B7-9C8F-FFFA7987F87B}.Debug|x64.ActiveCfg = Debug|Any CPU + {DE4D43B0-FBE6-40B7-9C8F-FFFA7987F87B}.Debug|x64.Build.0 = Debug|Any CPU + {DE4D43B0-FBE6-40B7-9C8F-FFFA7987F87B}.Debug|x86.ActiveCfg = Debug|Any CPU + {DE4D43B0-FBE6-40B7-9C8F-FFFA7987F87B}.Debug|x86.Build.0 = Debug|Any CPU + {DE4D43B0-FBE6-40B7-9C8F-FFFA7987F87B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DE4D43B0-FBE6-40B7-9C8F-FFFA7987F87B}.Release|Any CPU.Build.0 = Release|Any CPU + {DE4D43B0-FBE6-40B7-9C8F-FFFA7987F87B}.Release|x64.ActiveCfg = Release|Any CPU + {DE4D43B0-FBE6-40B7-9C8F-FFFA7987F87B}.Release|x64.Build.0 = Release|Any CPU + {DE4D43B0-FBE6-40B7-9C8F-FFFA7987F87B}.Release|x86.ActiveCfg = Release|Any CPU + {DE4D43B0-FBE6-40B7-9C8F-FFFA7987F87B}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/src/IOL.Helpers/CryptographyHelpers.cs b/src/IOL.Helpers/CryptographyHelpers.cs new file mode 100644 index 0000000..6ea18b6 --- /dev/null +++ b/src/IOL.Helpers/CryptographyHelpers.cs @@ -0,0 +1,173 @@ +using System; +using System.Linq; +using System.Security.Cryptography; +using System.Text; + +namespace IOL.Helpers +{ + public static class CryptographyHelpers + { + // https://github.com/DuendeSoftware/IdentityServer/blob/main/src/IdentityServer/Extensions/HashExtensions.cs + + private const int AES_BLOCK_BYTE_SIZE = 128 / 8; + private static readonly RandomNumberGenerator _random = RandomNumberGenerator.Create(); + + /// <summary> + /// Creates a MD5 hash of the specified input. + /// </summary> + /// <returns>A hash</returns> + public static string Md5(this string input, string salt = default) { + if (input.IsNullOrWhiteSpace()) return string.Empty; + + var hmacMd5 = salt.HasValue() ? new HMACMD5(Encoding.UTF8.GetBytes(salt ?? "")) : new HMACMD5(); + var saltedHash = hmacMd5.ComputeHash(Encoding.UTF8.GetBytes(input)); + return Convert.ToBase64String(saltedHash); + } + + + /// <summary> + /// Method to perform a very simple (and classical) encryption for a string. This is NOT at + /// all secure, it is only intended to make the string value non-obvious at a first glance. + /// + /// The shiftOrUnshift argument is an arbitrary "key value", and must be a non-zero integer + /// between -65535 and 65535 (inclusive). To decrypt the encrypted string you use the negative + /// value. For example, if you encrypt with -42, then you decrypt with +42, or vice-versa. + /// + /// This is inspired by, and largely based on, this: + /// https://stackoverflow.com/a/13026595/253938 + /// </summary> + /// <param name="inputString">string to be encrypted or decrypted, must not be null</param> + /// <param name="shiftOrUnshift">see above</param> + /// <returns>encrypted or decrypted string</returns> + public static string CaesarCipher(string inputString, int shiftOrUnshift) { + const int C64_K = ushort.MaxValue + 1; + if (inputString == null) throw new ArgumentException("Must not be null.", nameof(inputString)); + switch (shiftOrUnshift) { + case 0: throw new ArgumentException("Must not be zero.", nameof(shiftOrUnshift)); + case <= -C64_K: + case >= C64_K: throw new ArgumentException("Out of range.", nameof(shiftOrUnshift)); + } + + // Perform the Caesar cipher shifting, using modulo operator to provide wrap-around + var charArray = new char[inputString.Length]; + for (var i = 0; i < inputString.Length; i++) { + charArray[i] = + Convert.ToChar((Convert.ToInt32(inputString[i]) + shiftOrUnshift + C64_K) % C64_K); + } + + return new string(charArray); + } + + //https://tomrucki.com/posts/aes-encryption-in-csharp/ + public static string EncryptWithAes(this string toEncrypt, string password) { + var key = GetKey(password); + + using var aes = CreateAes(); + var iv = GenerateRandomBytes(AES_BLOCK_BYTE_SIZE); + var plainText = Encoding.UTF8.GetBytes(toEncrypt); + + using var encryptor = aes.CreateEncryptor(key, iv); + var cipherText = encryptor + .TransformFinalBlock(plainText, 0, plainText.Length); + + var result = new byte[iv.Length + cipherText.Length]; + iv.CopyTo(result, 0); + cipherText.CopyTo(result, iv.Length); + + return Convert.ToBase64String(result); + } + + private static Aes CreateAes() { + var aes = Aes.Create(); + aes.Mode = CipherMode.CBC; + aes.Padding = PaddingMode.PKCS7; + return aes; + } + + public static string DecryptWithAes(this string input, string password) { + var key = GetKey(password); + var encryptedData = Convert.FromBase64String(input); + + using var aes = CreateAes(); + var iv = encryptedData.Take(AES_BLOCK_BYTE_SIZE).ToArray(); + var cipherText = encryptedData.Skip(AES_BLOCK_BYTE_SIZE).ToArray(); + + using var decryptor = aes.CreateDecryptor(key, iv); + var decryptedBytes = decryptor + .TransformFinalBlock(cipherText, 0, cipherText.Length); + return Encoding.UTF8.GetString(decryptedBytes); + } + + private static byte[] GetKey(string password) { + var keyBytes = Encoding.UTF8.GetBytes(password); + using var md5 = MD5.Create(); + return md5.ComputeHash(keyBytes); + } + + private static byte[] GenerateRandomBytes(int numberOfBytes) { + var randomBytes = new byte[numberOfBytes]; + _random.GetBytes(randomBytes); + return randomBytes; + } + + + /// <summary> + /// Creates a SHA256 hash of the specified input. + /// </summary> + /// <param name="input">The input.</param> + /// <returns>A hash</returns> + public static string Sha256(this string input) { + if (input.IsNullOrWhiteSpace()) return string.Empty; + + using var sha = SHA256.Create(); + var bytes = Encoding.UTF8.GetBytes(input); + var hash = sha.ComputeHash(bytes); + + return Convert.ToBase64String(hash); + } + + /// <summary> + /// Creates a SHA256 hash of the specified input. + /// </summary> + /// <param name="input">The input.</param> + /// <returns>A hash.</returns> + public static byte[] Sha256(this byte[] input) { + if (input == null) { + return null; + } + + using var sha = SHA256.Create(); + return sha.ComputeHash(input); + } + + /// <summary> + /// Creates a SHA512 hash of the specified input. + /// </summary> + /// <param name="input">The input.</param> + /// <returns>A hash</returns> + public static string Sha512(this string input) { + if (input.IsNullOrWhiteSpace()) return string.Empty; + + using var sha = SHA512.Create(); + var bytes = Encoding.UTF8.GetBytes(input); + var hash = sha.ComputeHash(bytes); + + return Convert.ToBase64String(hash); + } + + + /// <summary> + /// Creates a SHA256 hash of the specified input. + /// </summary> + /// <param name="input">The input.</param> + /// <returns>A hash.</returns> + public static byte[] Sha512(this byte[] input) { + if (input == null) { + return null; + } + + using var sha = SHA512.Create(); + return sha.ComputeHash(input); + } + } +}
\ No newline at end of file diff --git a/src/IOL.Helpers/DateTimeHelpers.cs b/src/IOL.Helpers/DateTimeHelpers.cs new file mode 100644 index 0000000..07e951e --- /dev/null +++ b/src/IOL.Helpers/DateTimeHelpers.cs @@ -0,0 +1,22 @@ +using System; + +namespace IOL.Helpers +{ + public static class DateTimeHelpers + { + public static DateTime ToTimeZoneId(this DateTime value, string timeZoneId) { + try { + var cstZone = TimeZoneInfo.FindSystemTimeZoneById(timeZoneId); + return TimeZoneInfo.ConvertTimeFromUtc(value, cstZone); + } catch (TimeZoneNotFoundException) { + Console.WriteLine("The registry does not define the " + timeZoneId + " zone."); + return default; + } catch (InvalidTimeZoneException) { + Console.WriteLine("Registry data on the " + timeZoneId + " zone has been corrupted."); + return default; + } + } + + public static DateTime ToOsloTimeZone(this DateTime value) => ToTimeZoneId(value, "Europe/Oslo"); + } +}
\ No newline at end of file diff --git a/src/IOL.Helpers/DoubleHelpers.cs b/src/IOL.Helpers/DoubleHelpers.cs new file mode 100644 index 0000000..91d88ee --- /dev/null +++ b/src/IOL.Helpers/DoubleHelpers.cs @@ -0,0 +1,11 @@ +using System; + +namespace IOL.Helpers +{ + public static class DoubleHelpers + { + public static string ToStringWithFixedDecimalPoints(this double value) { + return $"{Math.Truncate(value * 10) / 10:0.0}"; + } + } +}
\ No newline at end of file diff --git a/src/IOL.Helpers/EnumHelpers.cs b/src/IOL.Helpers/EnumHelpers.cs new file mode 100644 index 0000000..b63e045 --- /dev/null +++ b/src/IOL.Helpers/EnumHelpers.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace IOL.Helpers +{ + public static class EnumHelpers + { + public static IEnumerable<T> GetValues<T>() { + return Enum.GetValues(typeof(T)).Cast<T>(); + } + } +}
\ No newline at end of file diff --git a/src/IOL.Helpers/HttpRequestHelpers.cs b/src/IOL.Helpers/HttpRequestHelpers.cs new file mode 100644 index 0000000..5a0e145 --- /dev/null +++ b/src/IOL.Helpers/HttpRequestHelpers.cs @@ -0,0 +1,18 @@ +using System; +using Microsoft.AspNetCore.Http; + +namespace IOL.Helpers +{ + public static class HttpRequestHelpers + { + public static string GetAppHost(this HttpRequest request) { + var forwardedHostHeader = request.Headers["X-Forwarded-Host"].ToString(); + var forwardedProtoHeader = request.Headers["X-Forwarded-Proto"].ToString(); + if (forwardedHostHeader.HasValue()) { + return (forwardedProtoHeader ?? "https") + "://" + forwardedHostHeader; + } + + return request.Scheme + "://" + request.Host; + } + } +}
\ No newline at end of file diff --git a/src/IOL.Helpers/IOL.Helpers.csproj b/src/IOL.Helpers/IOL.Helpers.csproj new file mode 100644 index 0000000..2b2842a --- /dev/null +++ b/src/IOL.Helpers/IOL.Helpers.csproj @@ -0,0 +1,12 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>net5.0</TargetFramework> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="Microsoft.AspNetCore.Cryptography.KeyDerivation" Version="5.0.4" /> + <PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" /> + </ItemGroup> + +</Project> diff --git a/src/IOL.Helpers/PasswordHelpers.cs b/src/IOL.Helpers/PasswordHelpers.cs new file mode 100644 index 0000000..5b85219 --- /dev/null +++ b/src/IOL.Helpers/PasswordHelpers.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.Security.Cryptography; +using Microsoft.AspNetCore.Cryptography.KeyDerivation; + +namespace IOL.Helpers +{ + public static class PasswordHelper + { + private const int ITERATION_COUNT = 10000; + private const int SALT_SIZE = 128 / 8; + private const KeyDerivationPrf PRF = KeyDerivationPrf.HMACSHA256; + + public static string HashPassword(string value) { + using var rng = RandomNumberGenerator.Create(); + var salt = new byte[SALT_SIZE]; + rng.GetBytes(salt); + var subkey = KeyDerivation.Pbkdf2(value, salt, PRF, ITERATION_COUNT, 256 / 8); + var outputBytes = new byte[13 + salt.Length + subkey.Length]; + WriteNetworkByteOrder(outputBytes, 1, (uint) PRF); + WriteNetworkByteOrder(outputBytes, 5, (uint) ITERATION_COUNT); + WriteNetworkByteOrder(outputBytes, 9, (uint) SALT_SIZE); + Buffer.BlockCopy(salt, 0, outputBytes, 13, salt.Length); + Buffer.BlockCopy(subkey, 0, outputBytes, 13 + SALT_SIZE, subkey.Length); + return Convert.ToBase64String(outputBytes); + } + + public static bool Verify(string password, string hashedPassword) { + var decodedHashedPassword = Convert.FromBase64String(hashedPassword); + if (decodedHashedPassword.Length == 0) return false; + try { + // Read header information + var networkByteOrder = (KeyDerivationPrf) ReadNetworkByteOrder(decodedHashedPassword, 1); + var saltLength = (int) ReadNetworkByteOrder(decodedHashedPassword, 9); + + // Read the salt: must be >= 128 bits + if (saltLength < SALT_SIZE) return false; + var salt = new byte[saltLength]; + Buffer.BlockCopy(decodedHashedPassword, 13, salt, 0, salt.Length); + + // Read the subkey (the rest of the payload): must be >= 128 bits + var subkeyLength = decodedHashedPassword.Length - 13 - salt.Length; + if (subkeyLength < SALT_SIZE) return false; + var expectedSubkey = new byte[subkeyLength]; + Buffer.BlockCopy(decodedHashedPassword, 13 + salt.Length, expectedSubkey, 0, expectedSubkey.Length); + + // Hash the incoming password and verify it + var actualSubkey = + KeyDerivation.Pbkdf2(password, salt, networkByteOrder, ITERATION_COUNT, subkeyLength); + return CryptographicOperations.FixedTimeEquals(actualSubkey, expectedSubkey); + } catch { + return false; + } + } + + private static uint ReadNetworkByteOrder(IReadOnlyList<byte> buffer, int offset) { + return ((uint) buffer[offset + 0] << 24) + | ((uint) buffer[offset + 1] << 16) + | ((uint) buffer[offset + 2] << 8) + | buffer[offset + 3]; + } + + private static void WriteNetworkByteOrder(IList<byte> buffer, int offset, uint value) { + buffer[offset + 0] = (byte) (value >> 24); + buffer[offset + 1] = (byte) (value >> 16); + buffer[offset + 2] = (byte) (value >> 8); + buffer[offset + 3] = (byte) (value >> 0); + } + } +}
\ No newline at end of file diff --git a/src/IOL.Helpers/RandomStringGenerator.cs b/src/IOL.Helpers/RandomStringGenerator.cs new file mode 100644 index 0000000..0d691db --- /dev/null +++ b/src/IOL.Helpers/RandomStringGenerator.cs @@ -0,0 +1,18 @@ +using System; +using System.Linq; + +namespace IOL.Helpers +{ + public static class RandomString + { + private static readonly Random _random = new Random(); + + public static string Generate(int length, bool numeric = false) { + var chars = numeric switch { + false => "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", + true => "0123456789" + }; + return new string(Enumerable.Repeat(chars, length).Select(s => s[_random.Next(s.Length)]).ToArray()); + } + } +}
\ No newline at end of file diff --git a/src/IOL.Helpers/SlugGenerator.cs b/src/IOL.Helpers/SlugGenerator.cs new file mode 100644 index 0000000..3f53dd6 --- /dev/null +++ b/src/IOL.Helpers/SlugGenerator.cs @@ -0,0 +1,108 @@ +using System.Text; + +namespace IOL.Helpers +{ + public static class Slug + { + public static string Generate(bool toLower, params string[] values) { + return Create(string.Join("-", values), toLower); + } + + /// <summary> + /// Creates a slug. + /// References: + /// http://www.unicode.org/reports/tr15/tr15-34.html + /// https://meta.stackexchange.com/questions/7435/non-us-ascii-characters-dropped-from-full-profile-url/7696#7696 + /// https://stackoverflow.com/questions/25259/how-do-you-include-a-webpage-title-as-part-of-a-webpage-url/25486#25486 + /// https://stackoverflow.com/questions/3769457/how-can-i-remove-accents-on-a-string + /// </summary> + /// <param name="value"></param> + /// <param name="toLower"></param> + /// <returns>Slugified string</returns> + public static string Create(string value, bool toLower) { + if (string.IsNullOrWhiteSpace(value)) + return value; + + var normalised = value.Normalize(NormalizationForm.FormKD); + + const int MAXLEN = 80; + var len = normalised.Length; + var prevDash = false; + var sb = new StringBuilder(len); + + for (var i = 0; i < len; i++) { + var c = normalised[i]; + switch (c) { + case >= 'a' and <= 'z': + case >= '0' and <= '9': { + if (prevDash) { + sb.Append('-'); + prevDash = false; + } + + sb.Append(c); + break; + } + case >= 'A' and <= 'Z': { + if (prevDash) { + sb.Append('-'); + prevDash = false; + } + + // Tricky way to convert to lowercase + if (toLower) + sb.Append((char) (c | 32)); + else + sb.Append(c); + break; + } + case ' ': + case ',': + case '.': + case '/': + case '\\': + case '-': + case '_': + case '=': { + if (!prevDash && sb.Length > 0) { + prevDash = true; + } + + break; + } + default: { + var swap = ConvertEdgeCases(c, toLower); + if (swap != null) { + if (prevDash) { + sb.Append('-'); + prevDash = false; + } + + sb.Append(swap); + } + + break; + } + } + + if (sb.Length == MAXLEN) + break; + } + + return sb.ToString(); + } + + private static string ConvertEdgeCases(char c, bool toLower) => c switch { + 'ı' => "i", + 'ł' => "l", + 'Ł' => toLower ? "l" : "L", + 'đ' => "d", + 'ß' => "ss", + 'ø' => "o", + 'å' => "aa", + 'æ' => "ae", + 'Þ' => "th", + _ => null + }; + } +}
\ No newline at end of file diff --git a/src/IOL.Helpers/StringHelpers.cs b/src/IOL.Helpers/StringHelpers.cs new file mode 100644 index 0000000..1dde161 --- /dev/null +++ b/src/IOL.Helpers/StringHelpers.cs @@ -0,0 +1,67 @@ +using System; +using System.Text; +using System.Text.RegularExpressions; + +namespace IOL.Helpers +{ + public static class StringHelpers + { + public static bool IsNullOrWhiteSpace(this string value) { + return string.IsNullOrWhiteSpace(value); + } + + public static string Slugified(this string input) { + return Slug.Generate(true, input); + } + + public static bool HasValue(this string value) { + return !value.IsNullOrWhiteSpace(); + } + + public static Guid ToGuid(this string value) { + return Guid.Parse(value); + } + + public static string Base64Encode(this string text) { + return Convert.ToBase64String(Encoding.UTF8.GetBytes(text)); + } + + public static string ExtractFileName(this string value) { + if (value.IsNullOrWhiteSpace()) return default; + var lastIndex = value.LastIndexOf('.'); + return lastIndex <= 0 ? default : value.Substring(0, lastIndex); + } + + public static string ExtractExtension(this string value) { + if (value.IsNullOrWhiteSpace()) return default; + var lastIndex = value.LastIndexOf('.'); + return lastIndex <= 0 ? default : value.Substring(lastIndex); + } + + public static string Capitalize(this string input) { + return input.IsNullOrWhiteSpace() + ? input + : Regex.Replace(input, @"\b(\w)", m => m.Value.ToUpper(), RegexOptions.None); + } + + + /// <summary> + /// Check if the given MIME is a JSON MIME. + /// </summary> + /// <param name="mime">MIME</param> + /// <returns>Returns true if MIME type is json.</returns> + public static bool IsJsonMime(this string mime) { + var jsonRegex = new Regex("(?i)^(application/json|[^;/ \t]+/[^;/ \t]+[+]json)[ \t]*(;.*)?$"); + return mime != null && (jsonRegex.IsMatch(mime) || mime.Equals("application/json-patch+json")); + } + + public static string Obfuscate(this string value) { + var last4Chars = "****"; + if (value.HasValue() && value.Length > 4) { + last4Chars = value.Substring(value.Length - 4); + } + + return "****" + last4Chars; + } + } +}
\ No newline at end of file diff --git a/src/IOL.Helpers/Validators.cs b/src/IOL.Helpers/Validators.cs new file mode 100644 index 0000000..d60afcf --- /dev/null +++ b/src/IOL.Helpers/Validators.cs @@ -0,0 +1,18 @@ +using System.Net.Mail; +using System.Text.RegularExpressions; + +namespace IOL.Helpers +{ + public static class Validators + { + private static readonly Regex _norwegianPhoneNumber = new(@"^(0047|\+47|47)?[2-9]\d{7}$"); + + public static bool IsValidEmailAddress(this string value) { + return MailAddress.TryCreate(value, out _); + } + + public static bool IsValidNorwegianPhoneNumber(this string value) { + return _norwegianPhoneNumber.IsMatch(value); + } + } +}
\ No newline at end of file |
