diff --git a/.editorconfig b/.editorconfig index 9e8d5cd..bf4269a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -4,18 +4,16 @@ root = true charset = utf-8 indent_style = space indent_size = 2 - -# .gitattributes is configured to make this work on Windows too, irrespective of git config -end_of_line = lf insert_final_newline = true +# *WARNING*: If you use the Visual Studio Designer to edit this file, it may +# change this to 'crlf'. Revert it back to 'unset' or x-plat things will +# break. +end_of_line = unset + [*.sln] -end_of_line = crlf charset = utf-8-bom -[*.lutconfig] -end_of_line = crlf - #### C# Coding Conventions #### [*.cs] indent_size = 4 @@ -171,6 +169,8 @@ dotnet_diagnostic.CA1303.severity = silent # CS1591: Missing XML comment for publicly visible type or member dotnet_diagnostic.CS1591.severity = silent +# IDE0072: Add missing cases +dotnet_diagnostic.IDE0072.severity = silent [*.{cs,vb}] #### .NET Coding Conventions #### @@ -424,3 +424,76 @@ dotnet_naming_style.s_camelcase.capitalization = camel_case dotnet_style_allow_multiple_blank_lines_experimental = true:silent dotnet_style_allow_statement_immediately_after_block_experimental = true:silent + +### C++ Coding Conventions ### +[*.{c,c++,cc,cpp,cppm,cxx,h,h++,hh,hpp,hxx,inl,ipp,ixx,tlh,tli}] +indent_size = 4 +cpp_generate_documentation_comments = xml +cpp_indent_braces = false +cpp_indent_multi_line_relative_to = statement_begin +cpp_indent_within_parentheses = indent +cpp_indent_preserve_within_parentheses = true +cpp_indent_case_contents = false +cpp_indent_case_labels = false +cpp_indent_case_contents_when_block = false +cpp_indent_lambda_braces_when_parameter = false +cpp_indent_goto_labels = none +cpp_indent_preprocessor = none +cpp_indent_access_specifiers = false +cpp_indent_namespace_contents = false +cpp_indent_preserve_comments = false +cpp_new_line_before_open_brace_namespace = ignore +cpp_new_line_before_open_brace_type = ignore +cpp_new_line_before_open_brace_function = ignore +cpp_new_line_before_open_brace_block = ignore +cpp_new_line_before_open_brace_lambda = ignore +cpp_new_line_scope_braces_on_separate_lines = false +cpp_new_line_close_brace_same_line_empty_type = true +cpp_new_line_close_brace_same_line_empty_function = true +cpp_new_line_before_catch = false +cpp_new_line_before_else = false +cpp_new_line_before_while_in_do_while = false +cpp_space_before_function_open_parenthesis = ignore +cpp_space_within_parameter_list_parentheses = false +cpp_space_between_empty_parameter_list_parentheses = false +cpp_space_after_keywords_in_control_flow_statements = true +cpp_space_within_control_flow_statement_parentheses = false +cpp_space_before_lambda_open_parenthesis = false +cpp_space_within_cast_parentheses = false +cpp_space_after_cast_close_parenthesis = false +cpp_space_within_expression_parentheses = false +cpp_space_before_block_open_brace = false +cpp_space_between_empty_braces = false +cpp_space_before_initializer_list_open_brace = false +cpp_space_within_initializer_list_braces = false +cpp_space_preserve_in_initializer_list = false +cpp_space_before_open_square_bracket = false +cpp_space_within_square_brackets = false +cpp_space_before_empty_square_brackets = false +cpp_space_between_empty_square_brackets = false +cpp_space_group_square_brackets = false +cpp_space_within_lambda_brackets = false +cpp_space_between_empty_lambda_brackets = false +cpp_space_before_comma = false +cpp_space_after_comma = true +cpp_space_remove_around_member_operators = false +cpp_space_before_inheritance_colon = false +cpp_space_before_constructor_colon = false +cpp_space_remove_before_semicolon = false +cpp_space_after_semicolon = false +cpp_space_remove_around_unary_operator = false +cpp_space_around_binary_operator = ignore +cpp_space_around_assignment_operator = ignore +cpp_space_pointer_reference_alignment = ignore +cpp_space_around_ternary_operator = ignore +cpp_use_unreal_engine_macro_formatting = false +cpp_wrap_preserve_blocks = never +cpp_include_cleanup_add_missing_error_tag_type = suggestion +cpp_include_cleanup_remove_unused_error_tag_type = dimmed +cpp_include_cleanup_optimize_unused_error_tag_type = suggestion +cpp_include_cleanup_sort_after_edits = false +cpp_sort_includes_error_tag_type = none +cpp_sort_includes_priority_case_sensitive = false +cpp_sort_includes_priority_style = quoted +cpp_includes_style = default +cpp_includes_use_forward_slash = false diff --git a/.gitattributes b/.gitattributes index 3d765eb..d8b06f7 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,9 +1,5 @@ -# Use unix line endings always, even on Windows -* text=auto eol=lf - -# Exceptions to above for files that VS saves with CRLF always -*.sln eol=crlf -*.lutconfig eol=crlf +# Normalize line endings to LF in repo, checkout CRLF on Windows +* text=auto # Allow comments in JSON in GitHub rendering *.json linguist-language=JSON-with-Comments diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index cc579c2..abd2642 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -28,10 +28,13 @@ jobs: run: dotnet restore src - name: Check Formatting - run: dotnet format --verify-no-changes src - + run: dotnet format --verify-no-changes src --verbosity diagnostic - name: Build run: dotnet build src -c ${{matrix.configuration}} --no-restore + - name: Build With C++ support + if: matrix.os == 'windows-latest' + run: msbuild src /p:Configuration=${{matrix.configuration}} /p:Platform=x64 + - name: Test run: dotnet test src -c ${{matrix.configuration}} --no-build diff --git a/.vscode/settings.json b/.vscode/settings.json index bf229fa..95ce982 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,7 +4,6 @@ "editor.insertSpaces": true, "editor.tabSize": 2, "files.encoding": "utf8", - "files.eol": "\n", "files.insertFinalNewline": true, "files.exclude": { "bld/**": true, diff --git a/src/Cask.sln b/src/Cask.sln index 342b4f3..1dbb83a 100644 --- a/src/Cask.sln +++ b/src/Cask.sln @@ -16,6 +16,8 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Build", "Build", "{0C3A2105-9369-461A-92AB-3D39CA120B83}" ProjectSection(SolutionItems) = preProject ..\.editorconfig = ..\.editorconfig + ..\.gitattributes = ..\.gitattributes + ..\.gitignore = ..\.gitignore Cask.lutconfig = Cask.lutconfig Directory.Build.props = Directory.Build.props Directory.Build.rsp = Directory.Build.rsp @@ -51,6 +53,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Workflows", "Workflows", "{ ..\.github\workflows\validate.yml = ..\.github\workflows\validate.yml EndProjectSection EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "libcask", "libcask\libcask.vcxproj", "{14013CD3-B963-4851-AA9A-7C7A2F110A52}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -73,6 +77,8 @@ Global {FB74046B-2FF6-4316-85B1-39A28D945A18}.Debug|Any CPU.Build.0 = Debug|Any CPU {FB74046B-2FF6-4316-85B1-39A28D945A18}.Release|Any CPU.ActiveCfg = Release|Any CPU {FB74046B-2FF6-4316-85B1-39A28D945A18}.Release|Any CPU.Build.0 = Release|Any CPU + {14013CD3-B963-4851-AA9A-7C7A2F110A52}.Debug|Any CPU.ActiveCfg = Debug|x64 + {14013CD3-B963-4851-AA9A-7C7A2F110A52}.Release|Any CPU.ActiveCfg = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Tests/Cask.Tests/Cask.Tests.csproj b/src/Tests/Cask.Tests/Cask.Tests.csproj index 0b91fc9..5a24d5a 100644 --- a/src/Tests/Cask.Tests/Cask.Tests.csproj +++ b/src/Tests/Cask.Tests/Cask.Tests.csproj @@ -1,9 +1,30 @@ net8.0;net472 + true + true + true + true + + True + true + + + + + + + + + + + + + + diff --git a/src/Tests/Cask.Tests/CppCaskTests.cs b/src/Tests/Cask.Tests/CppCaskTests.cs new file mode 100644 index 0000000..daa7dda --- /dev/null +++ b/src/Tests/Cask.Tests/CppCaskTests.cs @@ -0,0 +1,139 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Runtime.InteropServices; +using System.Text; + +using Xunit; + + +using static System.Runtime.InteropServices.UnmanagedType; + +[assembly: DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)] + +namespace CommonAnnotatedSecurityKeys.Tests; + +// WIP: These tests are disabled because the C++ implementation is stubbed out. +// To enable them, flip the return value of IsSupportedTestClass in +// TestFilter.cs. + + +// CA2101: Specify marshaling for P/Invoke string arguments +// Supppressed due to false positives: https://github.com/dotnet/roslyn-analyzers/issues/7502 +#pragma warning disable CA2101 + +// SYSLIB1054: Use 'LibraryImportAttribute' instead of 'DllImportAttribute' /o +// generate P/Invoke marshalling code at compile time This is very cool but +// would require additional work to switch between LibraryImport and DllImport +// based on target framework. +#pragma warning disable SYSLIB1054 + +public class CppCaskTests : CaskTestsBase +{ + public CppCaskTests() : base(new Implementation()) + { + } + + private sealed class Implementation : ICask + { + public bool CompareHash(string candidateHash, + byte[] derivationInput, + string secret, + int secretEntropyInBytes = 32) + { + return NativeMethods.Cask_CompareHash(candidateHash, derivationInput, derivationInput.Length, secret, secretEntropyInBytes); + } + + public string GenerateHash(byte[] derivationInput, + string secret, + int secretEntropyInBytes = 32) + { + int size = NativeMethods.Cask_GenerateHash(derivationInput, derivationInput.Length, secret, secretEntropyInBytes, null, 0); + byte[] bytes = new byte[size]; + size = NativeMethods.Cask_GenerateHash(derivationInput, derivationInput.Length, secret, secretEntropyInBytes, bytes, size); + Assert.True(size == bytes.Length, "Cask_GenerateKey did not use as many bytes as it said it would."); + return Encoding.UTF8.GetString(bytes, 0, size - 1); // - 1 to remove null terminator + } + + public string GenerateKey(string providerSignature, + string allocatorCode, + string? reserved = null, + int secretEntropyInBytes = 32) + { + int size = NativeMethods.Cask_GenerateKey(providerSignature, allocatorCode, reserved, secretEntropyInBytes, null, 0); + byte[] bytes = new byte[size]; + size = NativeMethods.Cask_GenerateKey(providerSignature, allocatorCode, reserved, secretEntropyInBytes, bytes, size); + Assert.True(size == bytes.Length, "Cask_GenerateKey did not use as many bytes as it said it would."); + return Encoding.UTF8.GetString(bytes, 0, size - 1); // -1 to remove null terminator + } + + public bool IsCask(string keyOrHash) + { + return NativeMethods.Cask_IsCask(keyOrHash); + } + + public bool IsCaskBytes(byte[] bytes) + { + return NativeMethods.Cask_IsCaskBytes(bytes, bytes.Length); + } + + Mock ICask.MockFillRandom(FillRandomAction fillRandom) + { + throw new NotImplementedException(); + } + + Mock ICask.MockUtcNow(UtcNowFunc getUtcNow) + { + throw new NotImplementedException(); + } + + private static class NativeMethods + { + [DllImport("libcask")] + [return: MarshalAs(I1)] + public static extern bool Cask_IsCask( + [MarshalAs(LPUTF8Str)] string keyOrHash); + + [DllImport("libcask")] + [return: MarshalAs(I1)] + public static extern bool Cask_IsCaskBytes( + byte[] keyOrHash, + int length); + + [DllImport("libcask")] + [return: MarshalAs(I1)] + public static extern bool Cask_CompareHash( + [MarshalAs(LPUTF8Str)] + string candidateHash, + byte[] derivationInput, + int derivationInputLength, + [MarshalAs(LPUTF8Str)] string secret, + int secretEntropyInBytes); + + [DllImport("libcask")] + public static extern int Cask_GenerateKey( + [MarshalAs(LPUTF8Str)] + string providerSignature, + [MarshalAs(LPUTF8Str)] + string allocatorCode, + [MarshalAs(LPUTF8Str)] + string? providerData, + int secretEntropyInBytes, + byte[]? output, + int outputCapacity); + + [DllImport("libcask")] + public static extern int Cask_GenerateHash( + byte[] derivationInput, + int derivationInputLength, + [MarshalAs(LPUTF8Str)] + string secret, + int secretEntropyInBytes, + byte[]? output, + int outputCapacity); + } + } +} + +#pragma warning restore CA2101 +#pragma warning restore SYSLIB1054 diff --git a/src/Tests/Cask.Tests/Properties/launchSettings.json b/src/Tests/Cask.Tests/Properties/launchSettings.json new file mode 100644 index 0000000..4397fe1 --- /dev/null +++ b/src/Tests/Cask.Tests/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "Cask.Tests": { + "commandName": "Project", + "nativeDebugging": true + } + } +} \ No newline at end of file diff --git a/src/Tests/Cask.Tests/TestFilter.cs b/src/Tests/Cask.Tests/TestFilter.cs new file mode 100644 index 0000000..1caa533 --- /dev/null +++ b/src/Tests/Cask.Tests/TestFilter.cs @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Runtime.InteropServices; + +using Xunit; +using Xunit.Abstractions; +using Xunit.Sdk; + +[assembly: TestFramework("CommonAnnotatedSecurityKeys.Tests.TestFilter", "Cask.Tests")] + +namespace CommonAnnotatedSecurityKeys.Tests; + +public sealed class TestFilter : XunitTestFramework +{ + public TestFilter(IMessageSink messageSink) : base(messageSink) { } + + protected override ITestFrameworkDiscoverer CreateDiscoverer(IAssemblyInfo assemblyInfo) + { + return new Discoverer(assemblyInfo, SourceInformationProvider, DiagnosticMessageSink); + } + + private static bool IsSupportedTestClass(ITypeInfo type) + { + if (type.Name.EndsWith($".{nameof(CppCaskTests)}", StringComparison.Ordinal)) + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + Console.WriteLine("INFO: Skipping C++ tests on non-Windows."); + return false; + } + + if (RuntimeInformation.OSArchitecture != Architecture.X64) + { + Console.WriteLine("INFO: Skipping C++ tests on non-x64 OS."); + return false; + } + + // WIP: Flip this to true to enable C++ tests. They are not yet + // enabled because they fail as the C++ implementation is + // stubbed out.. + return true; + } + + return true; + } + + private sealed class Discoverer : XunitTestFrameworkDiscoverer + { + public Discoverer( + IAssemblyInfo assemblyInfo, + ISourceInformationProvider sourceProvider, + IMessageSink diagnosticMessageSink, + IXunitTestCollectionFactory? collectionFactory = null) + : base(assemblyInfo, sourceProvider, diagnosticMessageSink, collectionFactory) { } + + protected override bool IsValidTestClass(ITypeInfo type) + { + return base.IsValidTestClass(type) && IsSupportedTestClass(type); + } + } +} diff --git a/src/libcask/.gitignore b/src/libcask/.gitignore new file mode 100644 index 0000000..6be6c65 --- /dev/null +++ b/src/libcask/.gitignore @@ -0,0 +1 @@ +libcask.sln diff --git a/src/libcask/cask.cpp b/src/libcask/cask.cpp new file mode 100644 index 0000000..2f157a8 --- /dev/null +++ b/src/libcask/cask.cpp @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +// WIP: This file implements the interop-friendly libcask API +// The implementation can use C++. + +#include + +#include "cask.h" +#include "cask_dependencies.h" + +CASK_API bool Cask_IsCask(const char* keyOrHash) +{ + return false; +} + +CASK_API bool Cask_IsCaskBytes(const uint8_t* keyOrHashBytes, + int32_t length) +{ + return false; +} + +CASK_API int32_t Cask_GenerateKey(const char* allocatorCode, + const char* providerSignature, + const char* providerData, + int32_t secretEntropyInBytes, + char* output, + int32_t outputSizeInBytes) +{ + // WIP: Demonstrating a calling pattern for caller to be able to ask for + // buffer size. We can mimic this in GenerateHash. This is always a + // challenge for C API and I'm open to other approaches. + + const char* key = "not_a_real_key"; + int32_t requiredSizeInBytes = int32_t(strlen(key) + 1); // +1 for null terminator + + if (output == nullptr) + { + // no buffer: return the minimum size of the buffer needed to succeed. + // caller can then allocate a buffer of that size and call again. + return requiredSizeInBytes; + } + + if (outputSizeInBytes < requiredSizeInBytes) + { + // buffer is too small: return 0 + return 0; + } + + // buffer is big enough: write to buffer and return the number of bytes written + strncpy_s(output, outputSizeInBytes, key, requiredSizeInBytes); + return requiredSizeInBytes; +} + +CASK_API int32_t Cask_GenerateHash(const uint8_t* derivationInputBytes, + const int32_t derivationInputLength, + const char* secret, + int32_t secretEntropyInBytes, + char* buffer, + int32_t bufferSize) +{ + return 0; +} + +CASK_API bool Cask_CompareHash(const char* candidateHash, + const uint8_t* derivationInputBytes, + const int32_t derivationInputLength, + const char* secret, + int32_t secretEntropyInBytes) +{ + return false; +} + diff --git a/src/libcask/cask.h b/src/libcask/cask.h new file mode 100644 index 0000000..811e086 --- /dev/null +++ b/src/libcask/cask.h @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +// WIP: This header wil define the interop-friendly libcask API that will be +// exported from .dll/.so. This surface area must remain C compatible and +// cannot use C++-only types and features + +#ifndef CASK_H +#define CASK_H + +#include +#include + +#ifdef __cplusplus +#define CASK_EXTERN_C extern "C" +#else +#define CASK_EXTERN_C +#endif + +#ifdef LIBCASK_EXPORTS +#define CASK_API CASK_EXTERN_C __declspec(dllexport) +#else +#define CASK_API CASK_EXTERN_C __declspec(dllimport) +#endif + +CASK_API bool Cask_IsCask(const char* keyOrHash); + +CASK_API bool Cask_IsCaskBytes(const uint8_t* keyOrHashBytes, + int32_t length); + +CASK_API int32_t Cask_GenerateKey(const char* allocatorCode, + const char* providerSignature, + const char* providerData, + int32_t secretEntropyInBytes, + char* buffer, + int32_t bufferSize); + +CASK_API int32_t Cask_GenerateHash(const uint8_t* derivationInputBytes, + const int32_t derivationInputLength, + const char* secret, + int32_t secretEntropyInBytes, + char* buffer, + int32_t bufferSize); + +CASK_API bool Cask_CompareHash(const char* candidateHash, + const uint8_t* derivationInputBytes, + const int32_t derivationInputLength, + const char* secret, + int32_t secretEntropyInBytes); + +#endif // CASK_H diff --git a/src/libcask/cask_cimport_test.c b/src/libcask/cask_cimport_test.c new file mode 100644 index 0000000..4b7386d --- /dev/null +++ b/src/libcask/cask_cimport_test.c @@ -0,0 +1,5 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +// Dummy file to validate cask.h can be #included from C +#include "cask.h" diff --git a/src/libcask/cask_dependencies.cpp b/src/libcask/cask_dependencies.cpp new file mode 100644 index 0000000..cde91f0 --- /dev/null +++ b/src/libcask/cask_dependencies.cpp @@ -0,0 +1,19 @@ + +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +// WIP: This is our reference implementation for dependencies. We can pull in +// external libraries of our choice here and only here. We can use C++ +// here. + +#include "cask_dependencies.h" + +std::string Cask::Base64UrlEncode(const std::span& bytes) +{ + return ""; +} + +int32_t Cask::ComputeCrc32(const std::span& bytes) +{ + return 0; +} diff --git a/src/libcask/cask_dependencies.h b/src/libcask/cask_dependencies.h new file mode 100644 index 0000000..9d73230 --- /dev/null +++ b/src/libcask/cask_dependencies.h @@ -0,0 +1,18 @@ +#pragma once + +// WIP: This header will define a facade over all dependencies that libcask has that +// are not provided by the C++ standard library. Our reference implementation +// will choose external depenencies to implement this. Someone can then take the +// reference source implementation, and replace/edit cask_dependencies.cpp. We +// can use C++ here. + +#include +#include +#include + +namespace Cask { + +std::string Base64UrlEncode(const std::span& bytes); +int32_t ComputeCrc32(const std::span& bytes); + +} // namespace Cask diff --git a/src/libcask/libcask.vcxproj b/src/libcask/libcask.vcxproj new file mode 100644 index 0000000..67bdc88 --- /dev/null +++ b/src/libcask/libcask.vcxproj @@ -0,0 +1,102 @@ + + + + + Debug + x64 + + + Release + x64 + + + + 17.0 + Win32Proj + {14013cd3-b963-4851-aa9a-7c7a2f110a52} + libcask + 10.0 + + + + DynamicLibrary + true + v143 + Unicode + + + DynamicLibrary + false + v143 + true + Unicode + + + + + + + + + + + + + + + $(SolutionDir)..\bld\bin\$(ShortProjectName)\release_x64\ + $(SolutionDir)..\bld\obj\$(ShortProjectName)\release_x64\/ + + + $(SolutionDir)..\bld\bin\$(ShortProjectName)\debug_x64\ + $(SolutionDir)..\bld\obj\$(ShortProjectName)\debug_x64\ + + + + Level3 + true + _DEBUG;LIBCASK_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + true + NotUsing + stdcpp20 + stdc17 + + + Windows + true + false + + + + + Level3 + true + true + true + NDEBUG;LIBCASK_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + true + NotUsing + stdcpp20 + stdc17 + + + Windows + true + true + true + false + + + + + + + + + + + + + + + \ No newline at end of file