diff --git a/src/CSharp/.editorconfig b/src/CSharp/.editorconfig index f237782..fd8e091 100644 --- a/src/CSharp/.editorconfig +++ b/src/CSharp/.editorconfig @@ -112,7 +112,7 @@ csharp_style_prefer_readonly_struct_member = true # Code-block preferences csharp_prefer_braces = true csharp_prefer_simple_using_statement = true -csharp_style_namespace_declarations = block_scoped +csharp_style_namespace_declarations = file_scoped:warning csharp_style_prefer_method_group_conversion = true csharp_style_prefer_primary_constructors = true csharp_style_prefer_top_level_statements = true diff --git a/src/CSharp/Cask.Benchmarks/CompareHashedSignatureBenchmarks.cs b/src/CSharp/Cask.Benchmarks/CompareHashedSignatureBenchmarks.cs index 77336c6..0f6c266 100644 --- a/src/CSharp/Cask.Benchmarks/CompareHashedSignatureBenchmarks.cs +++ b/src/CSharp/Cask.Benchmarks/CompareHashedSignatureBenchmarks.cs @@ -5,63 +5,62 @@ using System.Security.Cryptography; -namespace CommonAnnotatedSecurityKeys.Benchmarks +namespace CommonAnnotatedSecurityKeys.Benchmarks; + +public class CompareHashedSignatureBenchmarks { - public class CompareHashedSignatureBenchmarks + private const int iterations = 10000; + private const string TestProviderSignature = "TEST"; + private static readonly byte[] TestProviderSignatureBytes = Convert.FromBase64String(TestProviderSignature); + + [Benchmark] + public void UseCompareHash() { - private const int iterations = 10000; - private const string TestProviderSignature = "TEST"; - private static readonly byte [] TestProviderSignatureBytes = Convert.FromBase64String(TestProviderSignature); + string key = Cask.Instance.GenerateKey(TestProviderSignature, "99"); + byte[] keyBytes = Convert.FromBase64String(key.FromUrlSafe()); + string hash = Cask.Instance.GenerateHash(keyBytes, keyBytes, 32); + byte[] hashBytes = Convert.FromBase64String(hash.FromUrlSafe()); - [Benchmark] - public void UseCompareHash() + for (int i = 0; i < iterations; i++) { - string key = Cask.Instance.GenerateKey(TestProviderSignature, "99"); - byte[] keyBytes = Convert.FromBase64String(key.FromUrlSafe()); - string hash = Cask.Instance.GenerateHash(keyBytes, keyBytes, 32); - byte[] hashBytes = Convert.FromBase64String(hash.FromUrlSafe()); - - for (int i = 0; i < iterations; i++) + if (!Cask.Instance.CompareHash(hashBytes, keyBytes, keyBytes)) { - if (!Cask.Instance.CompareHash(hashBytes, keyBytes, keyBytes)) - { - throw new InvalidOperationException(); - } + throw new InvalidOperationException(); } } + } - [Benchmark] - public void UseHmacSha256() + [Benchmark] + public void UseHmacSha256() + { + string key = Cask.Instance.GenerateKey(TestProviderSignature, "99"); + byte[] keyBytes = Convert.FromBase64String(key.FromUrlSafe()); + string hash = Cask.Instance.GenerateHash(TestProviderSignatureBytes, keyBytes, 32); + var hmac = new HMACSHA256(keyBytes); + byte[] hashBytes = hmac.ComputeHash(TestProviderSignatureBytes); + + for (int i = 0; i < iterations; i++) { - string key = Cask.Instance.GenerateKey(TestProviderSignature, "99"); - byte[] keyBytes = Convert.FromBase64String(key.FromUrlSafe()); - string hash = Cask.Instance.GenerateHash(TestProviderSignatureBytes, keyBytes, 32); - var hmac = new HMACSHA256(keyBytes); - byte[] hashBytes = hmac.ComputeHash(TestProviderSignatureBytes); + hmac = new HMACSHA256(keyBytes); + byte[] computedHashBytes = hmac.ComputeHash(TestProviderSignatureBytes); - for (int i = 0; i < iterations; i++) + for (int j = 0; j < hashBytes.Length; j++) { - hmac = new HMACSHA256(keyBytes); - byte[] computedHashBytes = hmac.ComputeHash(TestProviderSignatureBytes); - - for (int j = 0; j < hashBytes.Length; j++) + if (hashBytes[j] != computedHashBytes[j]) { - if (hashBytes[j] != computedHashBytes[j]) - { - throw new InvalidOperationException(); - } + throw new InvalidOperationException(); } + } - computedHashBytes = hmac.ComputeHash(TestProviderSignatureBytes); + computedHashBytes = hmac.ComputeHash(TestProviderSignatureBytes); - for (int j = 0; j < hashBytes.Length; j++) + for (int j = 0; j < hashBytes.Length; j++) + { + if (hashBytes[j] != computedHashBytes[j]) { - if (hashBytes[j] != computedHashBytes[j]) - { - throw new InvalidOperationException(); - } + throw new InvalidOperationException(); } } } } -} +} \ No newline at end of file diff --git a/src/CSharp/Cask.Benchmarks/IsCaskBenchmarks.cs b/src/CSharp/Cask.Benchmarks/IsCaskBenchmarks.cs index 447977a..19f5087 100644 --- a/src/CSharp/Cask.Benchmarks/IsCaskBenchmarks.cs +++ b/src/CSharp/Cask.Benchmarks/IsCaskBenchmarks.cs @@ -3,31 +3,30 @@ using BenchmarkDotNet.Attributes; -namespace CommonAnnotatedSecurityKeys.Benchmarks +namespace CommonAnnotatedSecurityKeys.Benchmarks; + +public class IsCaskBenchmarks { - public class IsCaskBenchmarks - { - private const int iterations = 100000; - private const string TestProviderSignature = "TEST"; - private static readonly string key = Cask.Instance.GenerateKey(TestProviderSignature, "99"); - private static readonly byte[] keyBytes = Convert.FromBase64String(key); + private const int iterations = 100000; + private const string TestProviderSignature = "TEST"; + private static readonly string key = Cask.Instance.GenerateKey(TestProviderSignature, "99"); + private static readonly byte[] keyBytes = Convert.FromBase64String(key); - [Benchmark] - public void UseIsCaskString() + [Benchmark] + public void UseIsCaskString() + { + for (int i = 0; i < iterations; i++) { - for (int i = 0; i < iterations; i++) - { - Cask.Instance.IsCask(key); - } + Cask.Instance.IsCask(key); } + } - [Benchmark] - public void UseIsCaskBytes() + [Benchmark] + public void UseIsCaskBytes() + { + for (int i = 0; i < iterations; i++) { - for (int i = 0; i < iterations; i++) - { - Cask.Instance.IsCask(keyBytes); - } + Cask.Instance.IsCask(keyBytes); } } -} +} \ No newline at end of file diff --git a/src/CSharp/Cask.Benchmarks/Program.cs b/src/CSharp/Cask.Benchmarks/Program.cs index 61bf835..eb97896 100644 --- a/src/CSharp/Cask.Benchmarks/Program.cs +++ b/src/CSharp/Cask.Benchmarks/Program.cs @@ -12,4 +12,4 @@ * dotnet run -c Release --framework net8.0 Cask.Benchmarks.csproj */ //Summary summary = BenchmarkRunner.Run(); -Summary summary = BenchmarkRunner.Run(); +Summary summary = BenchmarkRunner.Run(); \ No newline at end of file diff --git a/src/CSharp/Cask.Cli/CloudInstance.cs b/src/CSharp/Cask.Cli/CloudInstance.cs index 586e197..016bcde 100644 --- a/src/CSharp/Cask.Cli/CloudInstance.cs +++ b/src/CSharp/Cask.Cli/CloudInstance.cs @@ -1,11 +1,10 @@ -namespace Cask.Cli +namespace Cask.Cli; + +public enum CloudInstance { - public enum CloudInstance - { - None, - NoCloudSpecified = None, - Other, - Public, - Preproduction, - } -} + None, + NoCloudSpecified = None, + Other, + Public, + Preproduction, +} \ No newline at end of file diff --git a/src/CSharp/Cask.Cli/GenerateCommand.cs b/src/CSharp/Cask.Cli/GenerateCommand.cs index 0de38d3..d6d300d 100644 --- a/src/CSharp/Cask.Cli/GenerateCommand.cs +++ b/src/CSharp/Cask.Cli/GenerateCommand.cs @@ -2,59 +2,58 @@ using System.Text; -namespace CommonAnnotatedSecurityKeys.Cli -{ - public class GenerateCommand - { - private readonly string MicrosoftTenantId = new Guid("782ef2bb-3056-4438-946d-395022a4a19f").ToString(); +namespace CommonAnnotatedSecurityKeys.Cli; - internal int Run(GenerateOptions options) - { - var cask = new Cask(); - byte[] derivationInput = Encoding.UTF8.GetBytes(nameof(derivationInput)); +public class GenerateCommand +{ + private readonly string MicrosoftTenantId = new Guid("782ef2bb-3056-4438-946d-395022a4a19f").ToString(); - string cloudText = "AC"; - string region = EncodeForIdentifiableKey("westus"); - string tenant = EncodeForIdentifiableKey(MicrosoftTenantId); - string providerReserved = "AAAA"; - byte[] reserved = Convert.FromBase64String($"{cloudText}{region}{tenant}{providerReserved}"); + internal int Run(GenerateOptions options) + { + var cask = new Cask(); + byte[] derivationInput = Encoding.UTF8.GetBytes(nameof(derivationInput)); - byte[] providerSignature = Convert.FromBase64String(options.FixedSignature.ToUrlSafe()); + string cloudText = "AC"; + string region = EncodeForIdentifiableKey("westus"); + string tenant = EncodeForIdentifiableKey(MicrosoftTenantId); + string providerReserved = "AAAA"; + byte[] reserved = Convert.FromBase64String($"{cloudText}{region}{tenant}{providerReserved}"); - for (int i = 0; i < options.Count; i++) - { + byte[] providerSignature = Convert.FromBase64String(options.FixedSignature.ToUrlSafe()); - byte[] keyBytes = cask.GenerateKeyBytes(providerSignature, - "99", - reserved, - secretEntropyInBytes: options.SecretEntropyInBytes, - testChar: default); + for (int i = 0; i < options.Count; i++) + { + byte[] keyBytes = cask.GenerateKeyBytes(providerSignature, + "99", + reserved, + secretEntropyInBytes: options.SecretEntropyInBytes, + testChar: default); - string key = Convert.ToBase64String(keyBytes).ToUrlSafe(); - string validity = cask.IsCask(key) ? "Valid Key " : "INVALID KEY "; - Console.WriteLine($"{validity}: {key}"); + string key = Convert.ToBase64String(keyBytes).ToUrlSafe(); - keyBytes = Convert.FromBase64String(key.FromUrlSafe()); - string hash = cask.GenerateHash(derivationInput, keyBytes, options.SecretEntropyInBytes); + string validity = cask.IsCask(key) ? "Valid Key " : "INVALID KEY "; + Console.WriteLine($"{validity}: {key}"); - validity = cask.IsCask(hash) ? "Valid Hash " : "INVALID HASH"; - Console.WriteLine($"{validity}: {hash}"); - } + keyBytes = Convert.FromBase64String(key.FromUrlSafe()); + string hash = cask.GenerateHash(derivationInput, keyBytes, options.SecretEntropyInBytes); - return 0; + validity = cask.IsCask(hash) ? "Valid Hash " : "INVALID HASH"; + Console.WriteLine($"{validity}: {hash}"); } - public static string EncodeForIdentifiableKey(string text) - { - if (string.IsNullOrWhiteSpace(text)) - { - return "AAAAA"; - } + return 0; + } - byte[] hashed = CaskUtilityApi.Sha256.ComputeHash(Encoding.UTF8.GetBytes(text)); - return Convert.ToBase64String(hashed).ToUrlSafe().Substring(0, 5); + public static string EncodeForIdentifiableKey(string text) + { + if (string.IsNullOrWhiteSpace(text)) + { + return "AAAAA"; } + + byte[] hashed = CaskUtilityApi.Sha256.ComputeHash(Encoding.UTF8.GetBytes(text)); + return Convert.ToBase64String(hashed).ToUrlSafe().Substring(0, 5); } -} +} \ No newline at end of file diff --git a/src/CSharp/Cask.Cli/GenerateOptions.cs b/src/CSharp/Cask.Cli/GenerateOptions.cs index 18998d0..5d7631a 100644 --- a/src/CSharp/Cask.Cli/GenerateOptions.cs +++ b/src/CSharp/Cask.Cli/GenerateOptions.cs @@ -2,36 +2,35 @@ #nullable disable using CommandLine; -namespace CommonAnnotatedSecurityKeys.Cli +namespace CommonAnnotatedSecurityKeys.Cli; + +[Verb("generate", HelpText = "Generate one or more common annotated security keys.")] +public class GenerateOptions { - [Verb("generate", HelpText = "Generate one or more common annotated security keys.")] - public class GenerateOptions - { - [Option( - "test", - Required = false, - HelpText = "Generate all test keys.")] - public bool Test { get; set; } + [Option( + "test", + Required = false, + HelpText = "Generate all test keys.")] + public bool Test { get; set; } - [Option( - "signature", - Required = false, - Default = "TEST", - HelpText = "A fixed signature to inject into the generated key).")] - public string FixedSignature { get; set; } + [Option( + "signature", + Required = false, + Default = "TEST", + HelpText = "A fixed signature to inject into the generated key).")] + public string FixedSignature { get; set; } - [Option( - "length", - Required = false, - Default = 32, - HelpText = "The length of the randomized component in bytes. Must be at least 16.")] - public int SecretEntropyInBytes { get; set; } + [Option( + "length", + Required = false, + Default = 32, + HelpText = "The length of the randomized component in bytes. Must be at least 16.")] + public int SecretEntropyInBytes { get; set; } - [Option( - "count", - Required = false, - Default = (uint)1, - HelpText = "The count of keys to generate.")] - public uint Count { get; set; } - } -} + [Option( + "count", + Required = false, + Default = (uint)1, + HelpText = "The count of keys to generate.")] + public uint Count { get; set; } +} \ No newline at end of file diff --git a/src/CSharp/Cask.Cli/Program.cs b/src/CSharp/Cask.Cli/Program.cs index d61f26c..5f67640 100644 --- a/src/CSharp/Cask.Cli/Program.cs +++ b/src/CSharp/Cask.Cli/Program.cs @@ -2,29 +2,28 @@ using CommandLine; -namespace CommonAnnotatedSecurityKeys.Cli +namespace CommonAnnotatedSecurityKeys.Cli; + +internal class Program { - internal class Program + [STAThread] + public static int Main(string[] args) { - [STAThread] - public static int Main(string[] args) + try + { + return Parser.Default.ParseArguments< + GenerateOptions, + ValidateOptions + >(args) + .MapResult( + (GenerateOptions options) => new GenerateCommand().Run(options), + (ValidateOptions options) => new ValidateCommand().Run(options), + _ => 1); + } + catch (Exception e) { - try - { - return Parser.Default.ParseArguments< - GenerateOptions, - ValidateOptions - >(args) - .MapResult( - (GenerateOptions options) => new GenerateCommand().Run(options), - (ValidateOptions options) => new ValidateCommand().Run(options), - _ => 1); - } - catch (Exception e) - { - Console.WriteLine(e); - return 1; - } + Console.WriteLine(e); + return 1; } } -} +} \ No newline at end of file diff --git a/src/CSharp/Cask.Cli/ValidateCommand.cs b/src/CSharp/Cask.Cli/ValidateCommand.cs index f75fb66..3b84ca8 100644 --- a/src/CSharp/Cask.Cli/ValidateCommand.cs +++ b/src/CSharp/Cask.Cli/ValidateCommand.cs @@ -1,12 +1,11 @@ // Copyright (c) Microsoft. All rights reserved. -namespace CommonAnnotatedSecurityKeys.Cli +namespace CommonAnnotatedSecurityKeys.Cli; + +public class ValidateCommand { - public class ValidateCommand + internal int Run(ValidateOptions _) { - internal int Run(ValidateOptions _) - { - return 0; - } + return 0; } -} +} \ No newline at end of file diff --git a/src/CSharp/Cask.Cli/ValidateOptions.cs b/src/CSharp/Cask.Cli/ValidateOptions.cs index 83d96b0..1aeb521 100644 --- a/src/CSharp/Cask.Cli/ValidateOptions.cs +++ b/src/CSharp/Cask.Cli/ValidateOptions.cs @@ -1,10 +1,9 @@ // Copyright (c) Microsoft. All rights reserved. using CommandLine; -namespace CommonAnnotatedSecurityKeys.Cli +namespace CommonAnnotatedSecurityKeys.Cli; + +[Verb("validate", HelpText = "Validate one or more common annotated security keys.")] +public class ValidateOptions { - [Verb("validate", HelpText = "Validate one or more common annotated security keys.")] - public class ValidateOptions - { - } -} +} \ No newline at end of file diff --git a/src/CSharp/CommonAnnotatedSecurityKeys/Cask.cs b/src/CSharp/CommonAnnotatedSecurityKeys/Cask.cs index 15d08f5..898bc7c 100644 --- a/src/CSharp/CommonAnnotatedSecurityKeys/Cask.cs +++ b/src/CSharp/CommonAnnotatedSecurityKeys/Cask.cs @@ -3,246 +3,245 @@ using System.Security.Cryptography; -namespace CommonAnnotatedSecurityKeys +namespace CommonAnnotatedSecurityKeys; + +public class Cask : ICask { - public class Cask : ICask - { - [ThreadStatic] - private static Lazy cask = - new(() => new Cask()); + [ThreadStatic] + private static Lazy cask = + new(() => new Cask()); - public static ICask Instance + public static ICask Instance + { + get { - get - { - return cask.Value; - } + return cask.Value; } + } - public Cask() - { - Utilities = new CaskUtilityApi(); - } + public Cask() + { + Utilities = new CaskUtilityApi(); + } - public ICaskUtilityApi Utilities { get; set; } + public ICaskUtilityApi Utilities { get; set; } - public bool IsCask(string key) + public bool IsCask(string key) + { + if (CaskUtilityApi.CaskSignature[0] != key[key.Length - 16] || + CaskUtilityApi.CaskSignature[1] != key[key.Length - 15] || + CaskUtilityApi.CaskSignature[2] != key[key.Length - 14] || + CaskUtilityApi.CaskSignature[3] != key[key.Length - 13]) { - if (CaskUtilityApi.CaskSignature[0] != key[key.Length - 16] || - CaskUtilityApi.CaskSignature[1] != key[key.Length - 15] || - CaskUtilityApi.CaskSignature[2] != key[key.Length - 14] || - CaskUtilityApi.CaskSignature[3] != key[key.Length - 13]) - { - return false; - } - byte[] keyBytes = Convert.FromBase64String(key.FromUrlSafe()); - return IsCask(keyBytes); + return false; } + byte[] keyBytes = Convert.FromBase64String(key.FromUrlSafe()); + return IsCask(keyBytes); + } - public bool IsCask(byte[] keyBytes) + public bool IsCask(byte[] keyBytes) + { + // This check ensures that 3 bytes of fixed signature are present + // where they belong. We next hash the key and ensure the first + // three bytes of the hash are present where they belong. This + // leads to a chance of collision of 1 in 2 ^ 48, or + // 1 in 281,474,976,710,656, or ~1 million times less likely than + // winning the Powerball lottery. + if (CaskUtilityApi.CaskSignatureBytes[0] != keyBytes[keyBytes.Length - 12] || + CaskUtilityApi.CaskSignatureBytes[1] != keyBytes[keyBytes.Length - 11] || + CaskUtilityApi.CaskSignatureBytes[2] != keyBytes[keyBytes.Length - 10]) { - // This check ensures that 3 bytes of fixed signature are present - // where they belong. We next hash the key and ensure the first - // three bytes of the hash are present where they belong. This - // leads to a chance of collision of 1 in 2 ^ 48, or - // 1 in 281,474,976,710,656, or ~1 million times less likely than - // winning the Powerball lottery. - if (CaskUtilityApi.CaskSignatureBytes[0] != keyBytes[keyBytes.Length - 12] || - CaskUtilityApi.CaskSignatureBytes[1] != keyBytes[keyBytes.Length - 11] || - CaskUtilityApi.CaskSignatureBytes[2] != keyBytes[keyBytes.Length - 10]) - { - return false; - } + return false; + } - Span toChecksum = new Span(keyBytes, 0, keyBytes.Length - 3); - byte[] crc32Bytes = new byte[4]; - Utilities.ComputeCrc32Hash(toChecksum, crc32Bytes); + Span toChecksum = new Span(keyBytes, 0, keyBytes.Length - 3); + byte[] crc32Bytes = new byte[4]; + Utilities.ComputeCrc32Hash(toChecksum, crc32Bytes); - return - crc32Bytes[0] == keyBytes[keyBytes.Length - 3] && - crc32Bytes[1] == keyBytes[keyBytes.Length - 2] && - crc32Bytes[2] == keyBytes[keyBytes.Length - 1]; - } + return + crc32Bytes[0] == keyBytes[keyBytes.Length - 3] && + crc32Bytes[1] == keyBytes[keyBytes.Length - 2] && + crc32Bytes[2] == keyBytes[keyBytes.Length - 1]; + } - public string GenerateKey(string providerSignature, - string allocatorCode, - string? reserved = null, - int secretEntropyInBytes = 32) - { - byte[] reservedBytes = reserved == null - ? Array.Empty() - : Convert.FromBase64String(reserved.FromUrlSafe()); + public string GenerateKey(string providerSignature, + string allocatorCode, + string? reserved = null, + int secretEntropyInBytes = 32) + { + byte[] reservedBytes = reserved == null + ? Array.Empty() + : Convert.FromBase64String(reserved.FromUrlSafe()); - byte[] providerSignatureBytes = Convert.FromBase64String(providerSignature); + byte[] providerSignatureBytes = Convert.FromBase64String(providerSignature); - byte[] keyBytes = GenerateKeyBytes(providerSignatureBytes, - allocatorCode, - reservedBytes, - secretEntropyInBytes); + byte[] keyBytes = GenerateKeyBytes(providerSignatureBytes, + allocatorCode, + reservedBytes, + secretEntropyInBytes); - return Convert.ToBase64String(keyBytes).ToUrlSafe(); - } + return Convert.ToBase64String(keyBytes).ToUrlSafe(); + } - public byte[] GenerateKeyBytes(byte[] providerSignature, - string allocatorCode, - byte[]? reserved = null, - int secretEntropyInBytes = 32, - char testChar = default) - { - // Ensure that the randomBytesCount is a multiple of 3. We keep all data - // aligned along a 3-byte boundary to ensure consistent base64 encoding - // in the key for fixed components. - secretEntropyInBytes = secretEntropyInBytes.RoundUpToMultipleOf(3); + public byte[] GenerateKeyBytes(byte[] providerSignature, + string allocatorCode, + byte[]? reserved = null, + int secretEntropyInBytes = 32, + char testChar = default) + { + // Ensure that the randomBytesCount is a multiple of 3. We keep all data + // aligned along a 3-byte boundary to ensure consistent base64 encoding + // in the key for fixed components. + secretEntropyInBytes = secretEntropyInBytes.RoundUpToMultipleOf(3); - byte[] allocatorAndTimestampBytes = GenerateAllocatorAndTimestampBytes(allocatorCode); + byte[] allocatorAndTimestampBytes = GenerateAllocatorAndTimestampBytes(allocatorCode); - int reservedLength = (reserved?.Length ?? 0); + int reservedLength = (reserved?.Length ?? 0); - int keyLength = secretEntropyInBytes + - reservedLength + - 3 + /* always 3 */ - allocatorAndTimestampBytes.Length + /* always 3 */ - providerSignature.Length + - 3; // Partial HMAC256 is 3 bytes. + int keyLength = secretEntropyInBytes + + reservedLength + + 3 + /* always 3 */ + allocatorAndTimestampBytes.Length + /* always 3 */ + providerSignature.Length + + 3; // Partial HMAC256 is 3 bytes. - // Start by filling the entire key with random bytes. - byte[] keyBytes = new byte[keyLength]; + // Start by filling the entire key with random bytes. + byte[] keyBytes = new byte[keyLength]; - if (testChar == default) - { - CaskUtilityApi.Rng.GetBytes(keyBytes); - } - else - { - string randomComponent = new string(testChar, secretEntropyInBytes.RoundUpToMultipleOf(3)); - Array.Copy(Convert.FromBase64String(randomComponent), 0, keyBytes, 0, secretEntropyInBytes); - } + if (testChar == default) + { + CaskUtilityApi.Rng.GetBytes(keyBytes); + } + else + { + string randomComponent = new string(testChar, secretEntropyInBytes.RoundUpToMultipleOf(3)); + Array.Copy(Convert.FromBase64String(randomComponent), 0, keyBytes, 0, secretEntropyInBytes); + } - int reservedOffset = secretEntropyInBytes; - int caskSignatureOffset = reservedOffset + reservedLength; - int allocatorAndTimestampOffset = caskSignatureOffset + 3; - int providerSignatureOffset = allocatorAndTimestampOffset + allocatorAndTimestampBytes.Length; - int partialHashOffset = providerSignatureOffset + providerSignature.Length; + int reservedOffset = secretEntropyInBytes; + int caskSignatureOffset = reservedOffset + reservedLength; + int allocatorAndTimestampOffset = caskSignatureOffset + 3; + int providerSignatureOffset = allocatorAndTimestampOffset + allocatorAndTimestampBytes.Length; + int partialHashOffset = providerSignatureOffset + providerSignature.Length; - // Copy optional reserved bytes, if provided. - Array.Copy(reserved ?? new byte[] { }, 0, keyBytes, reservedOffset, reserved?.Length ?? 0); + // Copy optional reserved bytes, if provided. + Array.Copy(reserved ?? new byte[] { }, 0, keyBytes, reservedOffset, reserved?.Length ?? 0); - // Copy 'JQQJ', the CASK standard fixed signature, into the key. - Array.Copy(CaskUtilityApi.CaskSignatureBytes, 0, keyBytes, caskSignatureOffset, 3); + // Copy 'JQQJ', the CASK standard fixed signature, into the key. + Array.Copy(CaskUtilityApi.CaskSignatureBytes, 0, keyBytes, caskSignatureOffset, 3); - // Copy the allocator and timestamp into the key. - Array.Copy(allocatorAndTimestampBytes, 0, keyBytes, allocatorAndTimestampOffset, allocatorAndTimestampBytes.Length); + // Copy the allocator and timestamp into the key. + Array.Copy(allocatorAndTimestampBytes, 0, keyBytes, allocatorAndTimestampOffset, allocatorAndTimestampBytes.Length); - // Copy the key provider's signature into the key. - Array.Copy(providerSignature, 0, keyBytes, providerSignatureOffset, providerSignature.Length); + // Copy the key provider's signature into the key. + Array.Copy(providerSignature, 0, keyBytes, providerSignatureOffset, providerSignature.Length); - Span toChecksum = new Span(keyBytes, 0, partialHashOffset); + Span toChecksum = new Span(keyBytes, 0, partialHashOffset); - byte[] crc32Bytes = new byte[4]; - Utilities.ComputeCrc32Hash(toChecksum, crc32Bytes); + byte[] crc32Bytes = new byte[4]; + Utilities.ComputeCrc32Hash(toChecksum, crc32Bytes); - Array.Copy(crc32Bytes, 0, keyBytes, partialHashOffset, 3); + Array.Copy(crc32Bytes, 0, keyBytes, partialHashOffset, 3); - // Done. - return keyBytes; - } + // Done. + return keyBytes; + } - internal byte[] GenerateAllocatorAndTimestampBytes(string allocatorCode) + internal byte[] GenerateAllocatorAndTimestampBytes(string allocatorCode) + { + if (!ContainsOnlyUrlSafeBase64Characters(allocatorCode)) { - if (!ContainsOnlyUrlSafeBase64Characters(allocatorCode)) - { - throw new ArgumentException($"Allocator code includes characters that are not legal URL-safe base64: '{allocatorCode}"); - } - - DateTimeOffset utcNow = Utilities.GetCurrentDateTimeUtc(); + throw new ArgumentException($"Allocator code includes characters that are not legal URL-safe base64: '{allocatorCode}"); + } - if (utcNow.Year < 2024 || utcNow.Year > 2087) - { - throw new ArgumentOutOfRangeException("CASK requires the current year to be between 2024 and 2087."); - } + DateTimeOffset utcNow = Utilities.GetCurrentDateTimeUtc(); - char yearsSince2024 = CaskUtilityApi.OrderedUrlSafeBase64Characters[utcNow.Year - 2024]; - char zeroIndexedMonth = CaskUtilityApi.OrderedUrlSafeBase64Characters[utcNow.Month - 1]; - string allocatorAndTimestamp = $"{allocatorCode}{yearsSince2024}{zeroIndexedMonth}"; - return Convert.FromBase64String(allocatorAndTimestamp.FromUrlSafe()); + if (utcNow.Year < 2024 || utcNow.Year > 2087) + { + throw new ArgumentOutOfRangeException("CASK requires the current year to be between 2024 and 2087."); } - private static bool ContainsOnlyUrlSafeBase64Characters(string allocatorCode) + char yearsSince2024 = CaskUtilityApi.OrderedUrlSafeBase64Characters[utcNow.Year - 2024]; + char zeroIndexedMonth = CaskUtilityApi.OrderedUrlSafeBase64Characters[utcNow.Month - 1]; + string allocatorAndTimestamp = $"{allocatorCode}{yearsSince2024}{zeroIndexedMonth}"; + return Convert.FromBase64String(allocatorAndTimestamp.FromUrlSafe()); + } + + private static bool ContainsOnlyUrlSafeBase64Characters(string allocatorCode) + { + foreach (char c in allocatorCode) { - foreach (char c in allocatorCode) + if (!CaskUtilityApi.UrlSafeBase64Characters.Contains(c)) { - if (!CaskUtilityApi.UrlSafeBase64Characters.Contains(c)) - { - return false; - } + return false; } - - return true; } - public string GenerateHash(byte[] derivationInput, byte[] secret, int secretEntropyInBytes) - { - byte[] hash = GenerateHashedSignatureBytes(derivationInput, secret, secretEntropyInBytes); - return Convert.ToBase64String(hash).ToUrlSafe(); - } + return true; + } + + public string GenerateHash(byte[] derivationInput, byte[] secret, int secretEntropyInBytes) + { + byte[] hash = GenerateHashedSignatureBytes(derivationInput, secret, secretEntropyInBytes); + return Convert.ToBase64String(hash).ToUrlSafe(); + } - internal byte[] GenerateHashedSignatureBytes(byte[] derivationInput, byte[] secret, int secretEntropyInBytes) - { - byte[] allocatorAndTimeStampBytes = new byte[3]; + internal byte[] GenerateHashedSignatureBytes(byte[] derivationInput, byte[] secret, int secretEntropyInBytes) + { + byte[] allocatorAndTimeStampBytes = new byte[3]; - secretEntropyInBytes = secretEntropyInBytes.RoundUpToMultipleOf(3); - int reservedBytesLength = secret.Length - 12 - secretEntropyInBytes; + secretEntropyInBytes = secretEntropyInBytes.RoundUpToMultipleOf(3); + int reservedBytesLength = secret.Length - 12 - secretEntropyInBytes; - using var hmac = new HMACSHA256(secret); - byte[] hash = hmac.ComputeHash(derivationInput); + using var hmac = new HMACSHA256(secret); + byte[] hash = hmac.ComputeHash(derivationInput); - byte[] hashedSignature = new byte[33 + 12 + reservedBytesLength]; + byte[] hashedSignature = new byte[33 + 12 + reservedBytesLength]; - // Move the literal hash over. - Array.Copy(hash, 0, hashedSignature, 0, 32); + // Move the literal hash over. + Array.Copy(hash, 0, hashedSignature, 0, 32); - // Recapitulate other data. - int reservedOffset = 33; - Array.Copy(secret, secretEntropyInBytes, hashedSignature, reservedOffset, reservedBytesLength); + // Recapitulate other data. + int reservedOffset = 33; + Array.Copy(secret, secretEntropyInBytes, hashedSignature, reservedOffset, reservedBytesLength); - int standardOffset = reservedOffset + reservedBytesLength; - Array.Copy(CaskUtilityApi.CaskSignatureBytes, 0, hashedSignature, standardOffset, 3); + int standardOffset = reservedOffset + reservedBytesLength; + Array.Copy(CaskUtilityApi.CaskSignatureBytes, 0, hashedSignature, standardOffset, 3); - byte[] secretAllocatorAndTimeStampBytes = new byte[3]; - int secretAllocatorAndTimeStampBytesOffset = secretEntropyInBytes + reservedBytesLength + 3; - Array.Copy(secret, secretAllocatorAndTimeStampBytesOffset, secretAllocatorAndTimeStampBytes, 0, 3); + byte[] secretAllocatorAndTimeStampBytes = new byte[3]; + int secretAllocatorAndTimeStampBytesOffset = secretEntropyInBytes + reservedBytesLength + 3; + Array.Copy(secret, secretAllocatorAndTimeStampBytesOffset, secretAllocatorAndTimeStampBytes, 0, 3); - byte yearsSince2024 = (byte)(DateTime.UtcNow.Year - 2024); - byte zeroIndexedMonth = (byte)(DateTime.UtcNow.Month - 1); + byte yearsSince2024 = (byte)(DateTime.UtcNow.Year - 2024); + byte zeroIndexedMonth = (byte)(DateTime.UtcNow.Month - 1); - int? metadata = (61 << 18) | (61 << 12) | (yearsSince2024 << 6) | zeroIndexedMonth; - byte[] metadataBytes = BitConverter.GetBytes(metadata.Value); + int? metadata = (61 << 18) | (61 << 12) | (yearsSince2024 << 6) | zeroIndexedMonth; + byte[] metadataBytes = BitConverter.GetBytes(metadata.Value); - int allocatorAndTimestampOffset = standardOffset + 3; + int allocatorAndTimestampOffset = standardOffset + 3; - hashedSignature[allocatorAndTimestampOffset] = secret[secretAllocatorAndTimeStampBytesOffset]; - hashedSignature[allocatorAndTimestampOffset + 1] = (byte)((secret[secretAllocatorAndTimeStampBytesOffset + 1] & 0xf0) | (yearsSince2024 >> 4 & 0x3)); - hashedSignature[allocatorAndTimestampOffset + 2] = (byte)(yearsSince2024 << 6 | zeroIndexedMonth); + hashedSignature[allocatorAndTimestampOffset] = secret[secretAllocatorAndTimeStampBytesOffset]; + hashedSignature[allocatorAndTimestampOffset + 1] = (byte)((secret[secretAllocatorAndTimeStampBytesOffset + 1] & 0xf0) | (yearsSince2024 >> 4 & 0x3)); + hashedSignature[allocatorAndTimestampOffset + 2] = (byte)(yearsSince2024 << 6 | zeroIndexedMonth); - int secretProviderSignatureBytesOffset = secretAllocatorAndTimeStampBytesOffset + 3; - int providerSignatureBytesOffset = allocatorAndTimestampOffset + 3; - Array.Copy(secret, secretProviderSignatureBytesOffset, hashedSignature, providerSignatureBytesOffset, 3); + int secretProviderSignatureBytesOffset = secretAllocatorAndTimeStampBytesOffset + 3; + int providerSignatureBytesOffset = allocatorAndTimestampOffset + 3; + Array.Copy(secret, secretProviderSignatureBytesOffset, hashedSignature, providerSignatureBytesOffset, 3); - Span toChecksum = new Span(hashedSignature, 0, providerSignatureBytesOffset + 3); + Span toChecksum = new Span(hashedSignature, 0, providerSignatureBytesOffset + 3); - byte[] crc32Bytes = new byte[4]; - Utilities.ComputeCrc32Hash(toChecksum, crc32Bytes); - - int crc32HashOffset = providerSignatureBytesOffset + 3; - Array.Copy(crc32Bytes, 0, hashedSignature, crc32HashOffset, 3); + byte[] crc32Bytes = new byte[4]; + Utilities.ComputeCrc32Hash(toChecksum, crc32Bytes); - return hashedSignature; - } + int crc32HashOffset = providerSignatureBytesOffset + 3; + Array.Copy(crc32Bytes, 0, hashedSignature, crc32HashOffset, 3); - public bool CompareHash(byte[] candidateHash, byte[] derivationInput, byte[] secret, int secretEntropyInBytes) - { - byte[] computedHash = GenerateHashedSignatureBytes(derivationInput, secret, secretEntropyInBytes); - return CryptographicOperations.FixedTimeEquals(computedHash, candidateHash); - } + return hashedSignature; + } + + public bool CompareHash(byte[] candidateHash, byte[] derivationInput, byte[] secret, int secretEntropyInBytes) + { + byte[] computedHash = GenerateHashedSignatureBytes(derivationInput, secret, secretEntropyInBytes); + return CryptographicOperations.FixedTimeEquals(computedHash, candidateHash); } -} +} \ No newline at end of file diff --git a/src/CSharp/CommonAnnotatedSecurityKeys/CaskExtensionMethods.cs b/src/CSharp/CommonAnnotatedSecurityKeys/CaskExtensionMethods.cs index 3cf12e2..407d6f2 100644 --- a/src/CSharp/CommonAnnotatedSecurityKeys/CaskExtensionMethods.cs +++ b/src/CSharp/CommonAnnotatedSecurityKeys/CaskExtensionMethods.cs @@ -1,23 +1,22 @@ // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace CommonAnnotatedSecurityKeys +namespace CommonAnnotatedSecurityKeys; + +internal static class CaskExtensionMethods { - internal static class CaskExtensionMethods + internal static string FromUrlSafe(this string base64) { - internal static string FromUrlSafe(this string base64) - { - return base64.Replace('-', '+').Replace('_', '/'); - } + return base64.Replace('-', '+').Replace('_', '/'); + } - internal static string ToUrlSafe(this string base64) - { - return base64.Replace('+', '-').Replace('/', '_'); - } + internal static string ToUrlSafe(this string base64) + { + return base64.Replace('+', '-').Replace('/', '_'); + } - internal static int RoundUpToMultipleOf(this int value, int multiple) - { - return (value + multiple - 1) / multiple * multiple; - } + internal static int RoundUpToMultipleOf(this int value, int multiple) + { + return (value + multiple - 1) / multiple * multiple; } -} +} \ No newline at end of file diff --git a/src/CSharp/CommonAnnotatedSecurityKeys/CaskUtilityApi.cs b/src/CSharp/CommonAnnotatedSecurityKeys/CaskUtilityApi.cs index ea23fd2..6870dcc 100644 --- a/src/CSharp/CommonAnnotatedSecurityKeys/CaskUtilityApi.cs +++ b/src/CSharp/CommonAnnotatedSecurityKeys/CaskUtilityApi.cs @@ -7,90 +7,89 @@ using System.Text; using System.Text.RegularExpressions; -namespace CommonAnnotatedSecurityKeys +namespace CommonAnnotatedSecurityKeys; + +public class CaskUtilityApi : ICaskUtilityApi { - public class CaskUtilityApi : ICaskUtilityApi - { - [ThreadStatic] - private static Lazy caskConstants = - new(() => new CaskUtilityApi()); + [ThreadStatic] + private static Lazy caskConstants = + new(() => new CaskUtilityApi()); - [ThreadStatic] - private static readonly Lazy crc32 = - new(() => new Crc32()); + [ThreadStatic] + private static readonly Lazy crc32 = + new(() => new Crc32()); - [ThreadStatic] - private static readonly Lazy sha256 = - new(() => SHA256.Create()); + [ThreadStatic] + private static readonly Lazy sha256 = + new(() => SHA256.Create()); - [ThreadStatic] - private static readonly Lazy caskHmac256 = - new(() => new HMACSHA256(Encoding.UTF8.GetBytes("Cask_v1"))); + [ThreadStatic] + private static readonly Lazy caskHmac256 = + new(() => new HMACSHA256(Encoding.UTF8.GetBytes("Cask_v1"))); - [ThreadStatic] - private static readonly Lazy rng = - new(() => RandomNumberGenerator.Create()); + [ThreadStatic] + private static readonly Lazy rng = + new(() => RandomNumberGenerator.Create()); - public static string CaskSignature => "JQQJ"; + public static string CaskSignature => "JQQJ"; - public static byte[] CaskSignatureBytes => Convert.FromBase64String(CaskSignature); + public static byte[] CaskSignatureBytes => Convert.FromBase64String(CaskSignature); - public static readonly Regex CaskKeyRegex = - new Regex("(^|[^A-Za-z0-9+/-_])([A-Za-z0-9-_]{4}){6,}JQQJ[A-Za-z0-9-_]{12}($|[^A-Za-z0-9+/-_])", - RegexOptions.Compiled | RegexOptions.ExplicitCapture | RegexOptions.CultureInvariant); + public static readonly Regex CaskKeyRegex = + new Regex("(^|[^A-Za-z0-9+/-_])([A-Za-z0-9-_]{4}){6,}JQQJ[A-Za-z0-9-_]{12}($|[^A-Za-z0-9+/-_])", + RegexOptions.Compiled | RegexOptions.ExplicitCapture | RegexOptions.CultureInvariant); - public static ICaskUtilityApi Instance + public static ICaskUtilityApi Instance + { + get { - get + if (caskConstants.Value == null) { - if (caskConstants.Value == null) - { - caskConstants = new Lazy(() => new CaskUtilityApi()); - } - return caskConstants.Value; + caskConstants = new Lazy(() => new CaskUtilityApi()); } - set { caskConstants = new (() => value); } + return caskConstants.Value; } + set { caskConstants = new(() => value); } + } - public static Crc32 Crc32 - { - get { return crc32.Value; } - } + public static Crc32 Crc32 + { + get { return crc32.Value; } + } - public static SHA256 Sha256 - { - get { return sha256.Value; } - } + public static SHA256 Sha256 + { + get { return sha256.Value; } + } - public static HMACSHA256 Hmac256 - { - get { return caskHmac256.Value; } - } + public static HMACSHA256 Hmac256 + { + get { return caskHmac256.Value; } + } - public static RandomNumberGenerator Rng - { - get { return rng.Value; } - } + public static RandomNumberGenerator Rng + { + get { return rng.Value; } + } - private static readonly char[] orderedUrlSafeBase64Characters = new[] { - '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', '-', '_' }; + private static readonly char[] orderedUrlSafeBase64Characters = new[] { + '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', '-', '_' }; - public static ReadOnlySpan OrderedUrlSafeBase64Characters => orderedUrlSafeBase64Characters; + public static ReadOnlySpan OrderedUrlSafeBase64Characters => orderedUrlSafeBase64Characters; - public static readonly ISet UrlSafeBase64Characters = orderedUrlSafeBase64Characters.ToImmutableHashSet(); + public static readonly ISet UrlSafeBase64Characters = orderedUrlSafeBase64Characters.ToImmutableHashSet(); - public virtual DateTimeOffset GetCurrentDateTimeUtc() - { - return DateTimeOffset.UtcNow; - } + public virtual DateTimeOffset GetCurrentDateTimeUtc() + { + return DateTimeOffset.UtcNow; + } - public virtual void ComputeCrc32Hash(ReadOnlySpan toChecksum, Span destination) - { - Crc32.Reset(); - Crc32.Append(toChecksum); - Crc32.GetHashAndReset(destination); - } + public virtual void ComputeCrc32Hash(ReadOnlySpan toChecksum, Span destination) + { + Crc32.Reset(); + Crc32.Append(toChecksum); + Crc32.GetHashAndReset(destination); } -} +} \ No newline at end of file diff --git a/src/CSharp/CommonAnnotatedSecurityKeys/ICask.cs b/src/CSharp/CommonAnnotatedSecurityKeys/ICask.cs index eef5c13..d4949e4 100644 --- a/src/CSharp/CommonAnnotatedSecurityKeys/ICask.cs +++ b/src/CSharp/CommonAnnotatedSecurityKeys/ICask.cs @@ -1,30 +1,27 @@ // Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. // See LICENSE file in the project root for full license information. -namespace CommonAnnotatedSecurityKeys -{ - public interface ICask - { - ICaskUtilityApi Utilities { get; set; } - - bool IsCask(string keyOrHash); +namespace CommonAnnotatedSecurityKeys; - bool IsCask(byte[] keyOrHashBytes); +public interface ICask +{ + ICaskUtilityApi Utilities { get; set; } - string GenerateKey(string providerSignature, - string allocatorCode, - string? reserved = null, - int secretEntropyInBytes = 32); + bool IsCask(string keyOrHash); - string GenerateHash(byte[] derivationInput, - byte[] secret, - int secretEntropyInBytes = 32); + bool IsCask(byte[] keyOrHashBytes); - bool CompareHash(byte[] candidateHash, - byte[] derivationInput, - byte[] secret, - int secretEntropyInBytes = 32); - } -} + string GenerateKey(string providerSignature, + string allocatorCode, + string? reserved = null, + int secretEntropyInBytes = 32); + string GenerateHash(byte[] derivationInput, + byte[] secret, + int secretEntropyInBytes = 32); + bool CompareHash(byte[] candidateHash, + byte[] derivationInput, + byte[] secret, + int secretEntropyInBytes = 32); +} \ No newline at end of file diff --git a/src/CSharp/CommonAnnotatedSecurityKeys/ICaskUtilityApi.cs b/src/CSharp/CommonAnnotatedSecurityKeys/ICaskUtilityApi.cs index 12af31c..1274eee 100644 --- a/src/CSharp/CommonAnnotatedSecurityKeys/ICaskUtilityApi.cs +++ b/src/CSharp/CommonAnnotatedSecurityKeys/ICaskUtilityApi.cs @@ -1,14 +1,11 @@ // Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. // See LICENSE file in the project root for full license information. -namespace CommonAnnotatedSecurityKeys -{ - public interface ICaskUtilityApi - { - DateTimeOffset GetCurrentDateTimeUtc(); - - void ComputeCrc32Hash(ReadOnlySpan toChecksum, Span destination); - } -} +namespace CommonAnnotatedSecurityKeys; +public interface ICaskUtilityApi +{ + DateTimeOffset GetCurrentDateTimeUtc(); + void ComputeCrc32Hash(ReadOnlySpan toChecksum, Span destination); +} \ No newline at end of file diff --git a/src/CSharp/CommonAnnotatedSecurityKeys/Notes.cs b/src/CSharp/CommonAnnotatedSecurityKeys/Notes.cs deleted file mode 100644 index bd1b87b..0000000 --- a/src/CSharp/CommonAnnotatedSecurityKeys/Notes.cs +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -/* -namespace CommonAnnotatedSecurityKeys -{ - internal class Notes - { - public string GenerateKey(string providerSignature, string allocatorCode, string reserved = null, int randomBytesCount = 32) - { - ValidateArguments(providerSignature, allocatorCode, reserved, randomBytesCount); - - // Ensure that the randomBytesCount is a multiple of 3. We keep all data - // aligned along a 3-byte boundary to ensure consistent base64 encoding - // in the key for fixed components. - randomBytesCount = ((randomBytesCount + 2) / 3) * 3; - byte[] randomBytes = new byte[randomBytesCount]; - s_rng.GetBytes(randomBytes); - string randomComponent = Convert.ToBase64String(randomBytes).Replace('+', '-').Replace('/', '_'); - - char yearsSince2024 = (char)('A' + DateTime.UtcNow.Year - 2024); - char zeroIndexedMonth = (char)('A' + DateTime.UtcNow.Month - 1); - - string key = $"{randomComponent}{reserved}JQQJ{allocatorCode}{yearsSince2024}{zeroIndexedMonth}{providerSignature}"; - - key = key.Replace('-', '+').Replace('_', '/'); - byte[] hash = hmac.ComputeHash(Convert.FromBase64String(key)); - - return $"{key}{Convert.ToBase64String(hash, 0, 3)}"; - - } - } -} -*/ diff --git a/src/CSharp/CommonAnnotatedSecurityKeys/Polyfill/CryptographicOperations.cs b/src/CSharp/CommonAnnotatedSecurityKeys/Polyfill/CryptographicOperations.cs index 692812b..56ebeaf 100644 --- a/src/CSharp/CommonAnnotatedSecurityKeys/Polyfill/CryptographicOperations.cs +++ b/src/CSharp/CommonAnnotatedSecurityKeys/Polyfill/CryptographicOperations.cs @@ -7,53 +7,52 @@ #if NETFRAMEWORK using System.Runtime.CompilerServices; -namespace System.Security.Cryptography -{ - internal static class CryptographicOperations - { - /// - /// Determine the equality of two byte sequences in an amount of time which depends on - /// the length of the sequences, but not the values. - /// - /// The first buffer to compare. - /// The second buffer to compare. - /// - /// true if and have the same - /// values for and the same contents, false - /// otherwise. - /// - /// - /// This method compares two buffers' contents for equality in a manner which does not - /// leak timing information, making it ideal for use within cryptographic routines. - /// This method will short-circuit and return false only if - /// and have different lengths. - /// - /// Fixed-time behavior is guaranteed in all other cases, including if - /// and reference the same address. - /// - [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] - public static bool FixedTimeEquals(ReadOnlySpan left, ReadOnlySpan right) - { - // NoOptimization because we want this method to be exactly as non-short-circuiting - // as written. - // - // NoInlining because the NoOptimization would get lost if the method got inlined. +namespace System.Security.Cryptography; - if (left.Length != right.Length) - { - return false; - } +internal static class CryptographicOperations +{ + /// + /// Determine the equality of two byte sequences in an amount of time which depends on + /// the length of the sequences, but not the values. + /// + /// The first buffer to compare. + /// The second buffer to compare. + /// + /// true if and have the same + /// values for and the same contents, false + /// otherwise. + /// + /// + /// This method compares two buffers' contents for equality in a manner which does not + /// leak timing information, making it ideal for use within cryptographic routines. + /// This method will short-circuit and return false only if + /// and have different lengths. + /// + /// Fixed-time behavior is guaranteed in all other cases, including if + /// and reference the same address. + /// + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] + public static bool FixedTimeEquals(ReadOnlySpan left, ReadOnlySpan right) + { + // NoOptimization because we want this method to be exactly as non-short-circuiting + // as written. + // + // NoInlining because the NoOptimization would get lost if the method got inlined. - int length = left.Length; - int accum = 0; + if (left.Length != right.Length) + { + return false; + } - for (int i = 0; i < length; i++) - { - accum |= left[i] - right[i]; - } + int length = left.Length; + int accum = 0; - return accum == 0; + for (int i = 0; i < length; i++) + { + accum |= left[i] - right[i]; } + + return accum == 0; } } #endif \ No newline at end of file diff --git a/src/CSharp/CommonAnnotatedSecurityKeys/Properties/AssemblyInfo.cs b/src/CSharp/CommonAnnotatedSecurityKeys/Properties/AssemblyInfo.cs index cbca6cb..16defb4 100644 --- a/src/CSharp/CommonAnnotatedSecurityKeys/Properties/AssemblyInfo.cs +++ b/src/CSharp/CommonAnnotatedSecurityKeys/Properties/AssemblyInfo.cs @@ -2,4 +2,4 @@ [assembly: InternalsVisibleTo("Tests.CommonAnnotatedSecurityKeys, PublicKey=0024000004800000940000000602000000240000525341310004000001000100433fbf156abe9718142bdbd48a440e779a1b708fd21486ee0ae536f4c548edf8a7185c1e3ac89ceef76c15b8cc2497906798779a59402f9b9e27281fb15e7111566cdc9a9f8326301d45320623c5222089cf4d0013f365ae729fb0a9c9d15138042825cd511a0f3d4887a7b92f4c2749f81b410813d297b73244cf64995effb1")] [assembly: InternalsVisibleTo("Cask.Cli, PublicKey=0024000004800000940000000602000000240000525341310004000001000100433fbf156abe9718142bdbd48a440e779a1b708fd21486ee0ae536f4c548edf8a7185c1e3ac89ceef76c15b8cc2497906798779a59402f9b9e27281fb15e7111566cdc9a9f8326301d45320623c5222089cf4d0013f365ae729fb0a9c9d15138042825cd511a0f3d4887a7b92f4c2749f81b410813d297b73244cf64995effb1")] -[assembly: InternalsVisibleTo("Cask.Benchmarks, PublicKey=0024000004800000940000000602000000240000525341310004000001000100433fbf156abe9718142bdbd48a440e779a1b708fd21486ee0ae536f4c548edf8a7185c1e3ac89ceef76c15b8cc2497906798779a59402f9b9e27281fb15e7111566cdc9a9f8326301d45320623c5222089cf4d0013f365ae729fb0a9c9d15138042825cd511a0f3d4887a7b92f4c2749f81b410813d297b73244cf64995effb1")] +[assembly: InternalsVisibleTo("Cask.Benchmarks, PublicKey=0024000004800000940000000602000000240000525341310004000001000100433fbf156abe9718142bdbd48a440e779a1b708fd21486ee0ae536f4c548edf8a7185c1e3ac89ceef76c15b8cc2497906798779a59402f9b9e27281fb15e7111566cdc9a9f8326301d45320623c5222089cf4d0013f365ae729fb0a9c9d15138042825cd511a0f3d4887a7b92f4c2749f81b410813d297b73244cf64995effb1")] \ No newline at end of file diff --git a/src/CSharp/Test/Tests.CommonAnnotatedSecurityKeys/CSharpCaskTests.cs b/src/CSharp/Test/Tests.CommonAnnotatedSecurityKeys/CSharpCaskTests.cs index 8810ebc..117350b 100644 --- a/src/CSharp/Test/Tests.CommonAnnotatedSecurityKeys/CSharpCaskTests.cs +++ b/src/CSharp/Test/Tests.CommonAnnotatedSecurityKeys/CSharpCaskTests.cs @@ -2,12 +2,11 @@ using CommonAnnotatedSecurityKeys; -namespace Tests.CommonAnnotatedSecurityKeys +namespace Tests.CommonAnnotatedSecurityKeys; + +public class CSharpCaskTests : CaskTestsBase { - public class CSharpCaskTests : CaskTestsBase + public CSharpCaskTests() : base(new Cask()) { - public CSharpCaskTests() : base(new Cask()) - { - } } -} +} \ No newline at end of file diff --git a/src/CSharp/Test/Tests.CommonAnnotatedSecurityKeys/CaskSecretsTests.cs b/src/CSharp/Test/Tests.CommonAnnotatedSecurityKeys/CaskSecretsTests.cs index 89dc2dd..d19cbe4 100644 --- a/src/CSharp/Test/Tests.CommonAnnotatedSecurityKeys/CaskSecretsTests.cs +++ b/src/CSharp/Test/Tests.CommonAnnotatedSecurityKeys/CaskSecretsTests.cs @@ -1,167 +1,166 @@ // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using System; - using CommonAnnotatedSecurityKeys; +using System; + using Xunit; -namespace Tests.CommonAnnotatedSecurityKeys +namespace Tests.CommonAnnotatedSecurityKeys; + +public abstract class CaskTestsBase { - public abstract class CaskTestsBase + protected CaskTestsBase(ICask cask) { - protected CaskTestsBase(ICask cask) - { - Cask = cask; - } + Cask = cask; + } - protected ICask Cask { get; } + protected ICask Cask { get; } - [Theory, InlineData(16), InlineData(32), InlineData(64)] - public void CaskSecrets_IsCask(int secretEntropyInBytes) - { - string key = Cask.GenerateKey(providerSignature: "TEST", - allocatorCode: "88", - reserved: null, - secretEntropyInBytes); + [Theory, InlineData(16), InlineData(32), InlineData(64)] + public void CaskSecrets_IsCask(int secretEntropyInBytes) + { + string key = Cask.GenerateKey(providerSignature: "TEST", + allocatorCode: "88", + reserved: null, + secretEntropyInBytes); - IsCaskValidate(Cask, key); - } + IsCaskValidate(Cask, key); + } - [Theory, InlineData(16), InlineData(32), InlineData(64)] - public void CaskSecrets_GenerateKey_Basic(int secretEntropyInBytes) - { + [Theory, InlineData(16), InlineData(32), InlineData(64)] + public void CaskSecrets_GenerateKey_Basic(int secretEntropyInBytes) + { - string key = Cask.GenerateKey(providerSignature: "TEST", - allocatorCode: "88", - reserved: "ABCD", - secretEntropyInBytes); + string key = Cask.GenerateKey(providerSignature: "TEST", + allocatorCode: "88", + reserved: "ABCD", + secretEntropyInBytes); - byte[] keyBytes = Convert.FromBase64String(key.FromUrlSafe()); - Assert.True(keyBytes.Length % 3 == 0, "'GenerateKey' output wasn't aligned on a 3-byte boundary."); + byte[] keyBytes = Convert.FromBase64String(key.FromUrlSafe()); + Assert.True(keyBytes.Length % 3 == 0, "'GenerateKey' output wasn't aligned on a 3-byte boundary."); - IsCaskValidate(Cask, key); - - } + IsCaskValidate(Cask, key); - [Theory] - [InlineData(16, 2023), InlineData(16, 2088)] - [InlineData(32, 2023), InlineData(32, 2088)] - [InlineData(64, 2023), InlineData(64, 2088)] - public void CaskSecrets_GenerateKey_InvalidTimestamps(int secretEntropyInBytes, int invalidYear) + } + + [Theory] + [InlineData(16, 2023), InlineData(16, 2088)] + [InlineData(32, 2023), InlineData(32, 2088)] + [InlineData(64, 2023), InlineData(64, 2088)] + public void CaskSecrets_GenerateKey_InvalidTimestamps(int secretEntropyInBytes, int invalidYear) + { + // The CASK standard timestamp is only valid from 2024 - 2087 + // (where the base64-encoded character 'A' indicates 2024, and + // the last valid base64 character '_' indicates 2087. + + var caskUtilities = Cask.Utilities; + var testCaskUtilityApi = new TestCaskUtilityApi(); + Cask.Utilities = testCaskUtilityApi; + try { - // The CASK standard timestamp is only valid from 2024 - 2087 - // (where the base64-encoded character 'A' indicates 2024, and - // the last valid base64 character '_' indicates 2087. - - var caskUtilities = Cask.Utilities; - var testCaskUtilityApi = new TestCaskUtilityApi(); - Cask.Utilities = testCaskUtilityApi; - try + for (int month = 0; month < 12; month++) { - for (int month = 0; month < 12; month++) - { - testCaskUtilityApi.GetCurrentDateTimeUtcFunc = - () => new DateTimeOffset(new DateOnly(invalidYear, 1 + month, 1), default, default); + testCaskUtilityApi.GetCurrentDateTimeUtcFunc = + () => new DateTimeOffset(new DateOnly(invalidYear, 1 + month, 1), default, default); - var action = () => Cask.GenerateKey(providerSignature: "TEST", - allocatorCode: "88", - reserved: "ABCD", - secretEntropyInBytes); + var action = () => Cask.GenerateKey(providerSignature: "TEST", + allocatorCode: "88", + reserved: "ABCD", + secretEntropyInBytes); - Assert.Throws(action); - } - } - finally - { - Cask.Utilities = caskUtilities; + Assert.Throws(action); } } + finally + { + Cask.Utilities = caskUtilities; + } + } - [Theory, InlineData(16), InlineData(32), InlineData(64)] - public void CaskSecrets_GenerateKey_ValidTimestamps(int secretEntropyInBytes) + [Theory, InlineData(16), InlineData(32), InlineData(64)] + public void CaskSecrets_GenerateKey_ValidTimestamps(int secretEntropyInBytes) + { + var testCaskUtilityApi = new TestCaskUtilityApi(); + CaskUtilityApi.Instance = testCaskUtilityApi; + + try { - var testCaskUtilityApi = new TestCaskUtilityApi(); - CaskUtilityApi.Instance = testCaskUtilityApi; - try + // Every year from 2024 - 2087 should produce a valid key. + // We trust that the CASK standard will be long dead by + // 2087 or perhaps simply all or most programmers will be. + for (int year = 0; year < 64; year++) { - - // Every year from 2024 - 2087 should produce a valid key. - // We trust that the CASK standard will be long dead by - // 2087 or perhaps simply all or most programmers will be. - for (int year = 0; year < 64; year++) + for (int month = 0; month < 12; month++) { - for (int month = 0; month < 12; month++) - { - testCaskUtilityApi.GetCurrentDateTimeUtcFunc = - () => new DateTimeOffset(new DateOnly(2024 + year, 1 + month, 1), default, default); - - string key = Cask.GenerateKey(providerSignature: "TEST", - allocatorCode: "88", - reserved: "ABCD", - secretEntropyInBytes); + testCaskUtilityApi.GetCurrentDateTimeUtcFunc = + () => new DateTimeOffset(new DateOnly(2024 + year, 1 + month, 1), default, default); - IsCaskValidate(Cask, key); - } + string key = Cask.GenerateKey(providerSignature: "TEST", + allocatorCode: "88", + reserved: "ABCD", + secretEntropyInBytes); + IsCaskValidate(Cask, key); } - } - finally - { - CaskUtilityApi.Instance = null!; + } } - - private void IsCaskValidate(ICask cask, string key) + finally { - // Positive test cases. - Assert.True(cask.IsCask(key), $"'GenerateKey' output failed 'IsCask(string)': {key}"); + CaskUtilityApi.Instance = null!; + } + } - byte[] keyBytes = Convert.FromBase64String(key.FromUrlSafe()); - Assert.True(cask.IsCask(keyBytes), $"'GenerateKey' output failed 'IsCask(byte[]): {key}'."); + private void IsCaskValidate(ICask cask, string key) + { + // Positive test cases. + Assert.True(cask.IsCask(key), $"'GenerateKey' output failed 'IsCask(string)': {key}"); - // Now we will modify the CASK standard fixed signature only ('JQQJ'). - // We will recompute the checksum and replace it, to ensure that it - // is the signature check, and not the checksum hash, that - // invalidates the secret. + byte[] keyBytes = Convert.FromBase64String(key.FromUrlSafe()); + Assert.True(cask.IsCask(keyBytes), $"'GenerateKey' output failed 'IsCask(byte[]): {key}'."); - int signatureIndex = key.LastIndexOf("JQQJ"); - for (int i = 0; i < 4; i++) - { - // Cycle through XQQJ, JXQJ, JQXJ, and JQQX. - string modifiedKey = $"{key.Substring(0, signatureIndex + i)}X{key.Substring(signatureIndex + i + 1)}"; + // Now we will modify the CASK standard fixed signature only ('JQQJ'). + // We will recompute the checksum and replace it, to ensure that it + // is the signature check, and not the checksum hash, that + // invalidates the secret. + + int signatureIndex = key.LastIndexOf("JQQJ"); + for (int i = 0; i < 4; i++) + { + // Cycle through XQQJ, JXQJ, JQXJ, and JQQX. + string modifiedKey = $"{key.Substring(0, signatureIndex + i)}X{key.Substring(signatureIndex + i + 1)}"; - Span toChecksum = new Span(keyBytes, 0, keyBytes.Length - 3); + Span toChecksum = new Span(keyBytes, 0, keyBytes.Length - 3); - byte[] crc32Bytes = new byte[4]; - CaskUtilityApi.Instance.ComputeCrc32Hash(toChecksum, crc32Bytes); + byte[] crc32Bytes = new byte[4]; + CaskUtilityApi.Instance.ComputeCrc32Hash(toChecksum, crc32Bytes); - string checksum = Convert.ToBase64String(crc32Bytes).ToUrlSafe().Substring(0, 4); - modifiedKey = $"{modifiedKey.Substring(0, modifiedKey.Length - 4)}{checksum}"; + string checksum = Convert.ToBase64String(crc32Bytes).ToUrlSafe().Substring(0, 4); + modifiedKey = $"{modifiedKey.Substring(0, modifiedKey.Length - 4)}{checksum}"; - Assert.False(cask.IsCask(modifiedKey), $"'IsCask(string)' unexpectedly succeeded with modified 'JQQJ' signature: {modifiedKey}"); + Assert.False(cask.IsCask(modifiedKey), $"'IsCask(string)' unexpectedly succeeded with modified 'JQQJ' signature: {modifiedKey}"); - keyBytes = Convert.FromBase64String(modifiedKey.FromUrlSafe()); - Assert.False(cask.IsCask(keyBytes), $"'IsCask(byte[])' unexpectedly succeeded with modified 'JQQJ' signature: {modifiedKey}"); - } + keyBytes = Convert.FromBase64String(modifiedKey.FromUrlSafe()); + Assert.False(cask.IsCask(keyBytes), $"'IsCask(byte[])' unexpectedly succeeded with modified 'JQQJ' signature: {modifiedKey}"); + } - // Having established that the key is a CASK secret, we now will modify - // every character in the key, which should invalidate the checksum. + // Having established that the key is a CASK secret, we now will modify + // every character in the key, which should invalidate the checksum. - for (int i = 0; i < key.Length; i++) - { - char replacement = key[i] == '-' ? '_' : '-'; - string modifiedKey = $"{key.Substring(0, i)}{replacement}{key.Substring(i + 1)}"; + for (int i = 0; i < key.Length; i++) + { + char replacement = key[i] == '-' ? '_' : '-'; + string modifiedKey = $"{key.Substring(0, i)}{replacement}{key.Substring(i + 1)}"; - bool result = cask.IsCask(modifiedKey); - Assert.False(result, $"'IsCask(string)' unexpectedly succeeded after invalidating checksum: {modifiedKey}. Original key was: {key}"); + bool result = cask.IsCask(modifiedKey); + Assert.False(result, $"'IsCask(string)' unexpectedly succeeded after invalidating checksum: {modifiedKey}. Original key was: {key}"); - keyBytes = Convert.FromBase64String(modifiedKey.FromUrlSafe()); - result = cask.IsCask(keyBytes); - Assert.False(result, $"'IsCask(byte[])' unexpectedly succeeded after invalidating checksum: {modifiedKey}. Original key was: {key}"); - } + keyBytes = Convert.FromBase64String(modifiedKey.FromUrlSafe()); + result = cask.IsCask(keyBytes); + Assert.False(result, $"'IsCask(byte[])' unexpectedly succeeded after invalidating checksum: {modifiedKey}. Original key was: {key}"); } } } \ No newline at end of file diff --git a/src/CSharp/Test/Tests.CommonAnnotatedSecurityKeys/TestCaskUtilityApi.cs b/src/CSharp/Test/Tests.CommonAnnotatedSecurityKeys/TestCaskUtilityApi.cs index 7534101..9c056c6 100644 --- a/src/CSharp/Test/Tests.CommonAnnotatedSecurityKeys/TestCaskUtilityApi.cs +++ b/src/CSharp/Test/Tests.CommonAnnotatedSecurityKeys/TestCaskUtilityApi.cs @@ -5,29 +5,28 @@ using System; -namespace Tests.CommonAnnotatedSecurityKeys +namespace Tests.CommonAnnotatedSecurityKeys; + +internal class TestCaskUtilityApi : CaskUtilityApi { - internal class TestCaskUtilityApi : CaskUtilityApi + public Func? GetCurrentDateTimeUtcFunc; + public Action? ComputeCrc32HashAction = null; // Not currently assigned anywhere else, redundant null assignment to silence compiler warning. + + public override DateTimeOffset GetCurrentDateTimeUtc() { - public Func? GetCurrentDateTimeUtcFunc; - public Action? ComputeCrc32HashAction = null; // Not currently assigned anywhere else, redundant null assignment to silence compiler warning. + return GetCurrentDateTimeUtcFunc == null + ? base.GetCurrentDateTimeUtc() + : GetCurrentDateTimeUtcFunc(); + } - public override DateTimeOffset GetCurrentDateTimeUtc() + public override void ComputeCrc32Hash(ReadOnlySpan toChecksum, Span destination) + { + if (ComputeCrc32HashAction != null) { - return GetCurrentDateTimeUtcFunc == null - ? base.GetCurrentDateTimeUtc() - : GetCurrentDateTimeUtcFunc(); + ComputeCrc32HashAction(toChecksum.ToArray(), destination.ToArray()); + return; } - public override void ComputeCrc32Hash(ReadOnlySpan toChecksum, Span destination) - { - if (ComputeCrc32HashAction != null) - { - ComputeCrc32HashAction(toChecksum.ToArray(), destination.ToArray()); - return; - } - - base.ComputeCrc32Hash(toChecksum, destination); - } + base.ComputeCrc32Hash(toChecksum, destination); } -} +} \ No newline at end of file