diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..33b2aea --- /dev/null +++ b/.editorconfig @@ -0,0 +1,109 @@ +# EditorConfig is awesome: http://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Don't use tabs for indentation. +[*] +indent_style = space +# (Please don't specify an indent_size here; that has too many unintended consequences.) + +# Code files +[*.{cs,csx,vb,vbx}] +indent_size = 4 +insert_final_newline = true +charset = utf-8-bom + +# Xml project files +[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] +indent_size = 2 + +# Xml config files +[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}] +indent_size = 2 + +# JSON files +[*.json] +indent_size = 2 + +# Dotnet code style settings: +[*.{cs,vb}] +# Sort using and Import directives with System.* appearing first +dotnet_sort_system_directives_first = true +# Avoid "this." and "Me." if not necessary +dotnet_style_qualification_for_field = false:suggestion +dotnet_style_qualification_for_property = false:suggestion +dotnet_style_qualification_for_method = false:suggestion +dotnet_style_qualification_for_event = false:suggestion + +# Use language keywords instead of framework type names for type references +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion +dotnet_style_predefined_type_for_member_access = true:suggestion + +# Suggest more modern language features when available +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_explicit_tuple_names = true:suggestion + +# Prefix private members with underscore +dotnet_naming_rule.private_members_with_underscore.symbols = private_fields +dotnet_naming_rule.private_members_with_underscore.style = prefix_underscore +dotnet_naming_rule.private_members_with_underscore.severity = suggestion + +dotnet_naming_symbols.private_fields.applicable_kinds = field +dotnet_naming_symbols.private_fields.applicable_accessibilities = private + +dotnet_naming_style.prefix_underscore.capitalization = camel_case +dotnet_naming_style.prefix_underscore.required_prefix = _ + +# Async methods should have "Async" suffix +dotnet_naming_rule.async_methods_end_in_async.symbols = any_async_methods +dotnet_naming_rule.async_methods_end_in_async.style = end_in_async +dotnet_naming_rule.async_methods_end_in_async.severity = suggestion + +dotnet_naming_symbols.any_async_methods.applicable_kinds = method +dotnet_naming_symbols.any_async_methods.applicable_accessibilities = * +dotnet_naming_symbols.any_async_methods.required_modifiers = async + +dotnet_naming_style.end_in_async.required_prefix = +dotnet_naming_style.end_in_async.required_suffix = Async +dotnet_naming_style.end_in_async.capitalization = pascal_case +dotnet_naming_style.end_in_async.word_separator = + +# CSharp code style settings: +[*.cs] +# Prefer "var" everywhere +csharp_style_var_for_built_in_types = true:suggestion +csharp_style_var_when_type_is_apparent = true:suggestion +csharp_style_var_elsewhere = true:suggestion + +# Prefer method-like constructs to have a expression-body +csharp_style_expression_bodied_methods = true:none +csharp_style_expression_bodied_constructors = true:none +csharp_style_expression_bodied_operators = true:none + +# Prefer property-like constructs to have an expression-body +csharp_style_expression_bodied_properties = true:none +csharp_style_expression_bodied_indexers = true:none +csharp_style_expression_bodied_accessors = true:none + +# Suggest more modern language features when available +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion +csharp_style_throw_expression = true:suggestion +csharp_style_conditional_delegate_call = true:suggestion + +# Newline settings +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true + +# All files +[*] +guidelines = 120 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..1d01269 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +*.sh eol=lf +.dockerignore eol=lf +dockerfile eol=lf \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f10b60c --- /dev/null +++ b/.gitignore @@ -0,0 +1,219 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +build/ +bld/ +[Bb]in/ +[Oo]bj/ + +# Visual Studo 2015 cache/options directory +.vs/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opensdf +*.sdf +*.cachefile + +# Visual Studio profiler +*.psess +*.vsp +*.vspx + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding addin-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj +PublishProfiles/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config + +# Windows Azure Build Output +csx/ +*.build.csdef + +# Windows Store app package directory +AppPackages/ + +# Others +*.[Cc]ache +ClientBin/ +[Ss]tyle[Cc]op.* +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Other +project.lock.json +*.jfm +mail_dist/ +*.refactorlog +*.scmp +src/Core/Properties/launchSettings.json +*.override.env +**/*.DS_Store +src/Admin/wwwroot/lib +src/Admin/wwwroot/css +.vscode/* +**/.vscode/* +bitwarden_license/src/Portal/wwwroot/lib +bitwarden_license/src/Portal/wwwroot/css +bitwarden_license/src/Sso/wwwroot/lib +bitwarden_license/src/Sso/wwwroot/css +.github/test/build.secrets +**/CoverageOutput/ +.idea/* +**/**.swp diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..fd037c0 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,182 @@ +BITWARDEN LICENSE AGREEMENT +Version 1, 4 September 2020 + +PLEASE CAREFULLY READ THIS BITWARDEN LICENSE AGREEMENT ("AGREEMENT"). THIS +AGREEMENT CONSTITUTES A LEGALLY BINDING AGREEMENT BETWEEN YOU AND BITWARDEN, +INC. ("BITWARDEN") AND GOVERNS YOUR USE OF THE COMMERCIAL MODULES (DEFINED +BELOW). BY COPYING OR USING THE COMMERCIAL MODULES, YOU AGREE TO THIS AGREEMENT. +IF YOU DO NOT AGREE WITH THIS AGREEMENT, YOU MAY NOT COPY OR USE THE COMMERCIAL +MODULES. IF YOU ARE COPYING OR USING THE COMMERCIAL MODULES ON BEHALF OF A LEGAL +ENTITY, YOU REPRESENT AND WARRANT THAT YOU HAVE AUTHORITY TO AGREE TO THIS +AGREEMENT ON BEHALF OF SUCH ENTITY. IF YOU DO NOT HAVE SUCH AUTHORITY, DO NOT +COPY OR USE THE COMMERCIAL MODULES IN ANY MANNER. + +This Agreement is entered into by and between Bitwarden and you, or the legal +entity on behalf of whom you are acting (as applicable, "You" or "Your"). + +1. DEFINITIONS + +"Bitwarden Software" means the Bitwarden server software, libraries, and +Commercial Modules. + +"Commercial Modules" means the modules designed to work with and enhance the +Bitwarden Software to which this Agreement is linked, referenced, or appended. + +2. LICENSES, RESTRICTIONS AND THIRD PARTY CODE + +2.1 Commercial Module License. Subject to Your compliance with this Agreement, +Bitwarden hereby grants to You a limited, non-exclusive, non-transferable, +royalty-free license to use the Commercial Modules for the sole purposes of +internal development and internal testing, and only in a non-production +environment. + +2.2 Reservation of Rights. As between Bitwarden and You, Bitwarden owns all +right, title and interest in and to the Bitwarden Software, and except as +expressly set forth in Sections 2.1, no other license to the Bitwarden Software +is granted to You under this Agreement, by implication, estoppel, or otherwise. + +2.3 Restrictions. You agree not to: (i) except as expressly permitted in +Section 2.1, sell, rent, lease, distribute, sublicense, loan or otherwise +transfer the Commercial Modules to any third party; (ii) alter or remove any +trademarks, service mark, and logo included with the Commercial Modules, or +(iii) use the Commercial Modules to create a competing product or service. +Bitwarden is not obligated to provide maintenance and support services for the +Bitwarden Software licensed under this Agreement. + +2.4 Third Party Software. The Commercial Modules may contain or be provided +with third party open source libraries, components, utilities and other open +source software (collectively, "Open Source Software"). Notwithstanding anything +to the contrary herein, use of the Open Source Software will be subject to the +license terms and conditions applicable to such Open Source Software. To the +extent any condition of this Agreement conflicts with any license to the Open +Source Software, the Open Source Software license will govern with respect to +such Open Source Software only. + +2.5 This Agreement does not grant any rights in the trademarks, service marks, or +logos of any Contributor (except as may be necessary to comply with the notice +requirements in Section 2.3), and use of any Bitwarden trademarks must comply with +Bitwarden Trademark Guidelines +. + +3. TERMINATION + +3.1 Termination. This Agreement will automatically terminate upon notice from +Bitwarden, which notice may be by email or posting in the location where the +Commercial Modules are made available. + +3.2 Effect of Termination. Upon any termination of this Agreement, for any +reason, You will promptly cease use of the Commercial Modules and destroy any +copies thereof. For the avoidance of doubt, termination of this Agreement will +not affect Your right to Bitwarden Software, other than the Commercial Modules, +made available pursuant to an Open Source Software license. + +3.3 Survival. Sections 1, 2.2 -2.4, 3.2, 3.3, 4, and 5 will survive any +termination of this Agreement. + +4. DISCLAIMER AND LIMITATION OF LIABILITY + +4.1 Disclaimer of Warranties. TO THE MAXIMUM EXTENT PERMITTED UNDER APPLICABLE +LAW, THE BITWARDEN SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED REGARDING OR RELATING TO THE BITWARDEN SOFTWARE, INCLUDING +ANY IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, +TITLE, AND NON-INFRINGEMENT. FURTHER, BITWARDEN DOES NOT WARRANT RESULTS OF USE +OR THAT THE BITWARDEN SOFTWARE WILL BE ERROR FREE OR THAT THE USE OF THE +BITWARDEN SOFTWARE WILL BE UNINTERRUPTED. + +4.2 Limitation of Liability. IN NO EVENT WILL BITWARDEN OR ITS LICENSORS BE +LIABLE TO YOU OR ANY THIRD PARTY UNDER THIS AGREEMENT FOR (I) ANY AMOUNTS IN +EXCESS OF US $25 OR (II) FOR ANY SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES OF +ANY KIND, INCLUDING FOR ANY LOSS OF PROFITS, LOSS OF USE, BUSINESS INTERRUPTION, +LOSS OF DATA, COST OF SUBSTITUTE GOODS OR SERVICES, WHETHER ALLEGED AS A BREACH +OF CONTRACT OR TORTIOUS CONDUCT, INCLUDING NEGLIGENCE, EVEN IF BITWARDEN HAS +BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +5. MISCELLANEOUS + +5.1 Assignment. You may not assign or otherwise transfer this Agreement or any +rights or obligations hereunder, in whole or in part, whether by operation of +law or otherwise, to any third party without Bitwarden's prior written consent. +Any purported transfer, assignment or delegation without such prior written +consent will be null and void and of no force or effect. Bitwarden may assign +this Agreement to any successor to its business or assets to which this +Agreement relates, whether by merger, sale of assets, sale of stock, +reorganization or otherwise. Subject to this Section 5.1, this Agreement will be +binding upon and inure to the benefit of the parties hereto, and their +respective successors and permitted assigns. + +5.2 Entire Agreement; Modification; Waiver. This Agreement represents the +entire agreement between the parties, and supersedes all prior agreements and +understandings, written or oral, with respect to the matters covered by this +Agreement, and is not intended to confer upon any third party any rights or +remedies hereunder. You acknowledge that You have not entered in this Agreement +based on any representations other than those contained herein. No modification +of or amendment to this Agreement, nor any waiver of any rights under this +Agreement, will be effective unless in writing and signed by both parties. The +waiver of one breach or default or any delay in exercising any rights will not +constitute a waiver of any subsequent breach or default. + +5.3 Governing Law. This Agreement will in all respects be governed by the laws +of the State of California without reference to its principles of conflicts of +laws. The parties hereby agree that all disputes arising out of this Agreement +will be subject to the exclusive jurisdiction of and venue in the federal and +state courts within Los Angeles County, California. You hereby consent to the +personal and exclusive jurisdiction and venue of these courts. The parties +hereby disclaim and exclude the application hereto of the United Nations +Convention on Contracts for the International Sale of Goods. + +5.4 Severability. If any provision of this Agreement is held invalid or +unenforceable under applicable law by a court of competent jurisdiction, it will +be replaced with the valid provision that most closely reflects the intent of +the parties and the remaining provisions of the Agreement will remain in full +force and effect. + +5.5 Relationship of the Parties. Nothing in this Agreement is to be construed +as creating an agency, partnership, or joint venture relationship between the +parties hereto. Neither party will have any right or authority to assume or +create any obligations or to make any representations or warranties on behalf of +any other party, whether express or implied, or to bind the other party in any +respect whatsoever. + +5.6 Notices. All notices permitted or required under this Agreement will be in +writing and will be deemed to have been given when delivered in person +(including by overnight courier), or three (3) business days after being mailed +by first class, registered or certified mail, postage prepaid, to the address of +the party specified in this Agreement or such other address as either party may +specify in writing. + +5.7 U.S. Government Restricted Rights. If Commercial Modules is being licensed +by the U.S. Government, the Commercial Modules is deemed to be "commercial +computer software" and "commercial computer documentation" developed exclusively +at private expense, and (a) if acquired by or on behalf of a civilian agency, +will be subject solely to the terms of this computer software license as +specified in 48 C.F.R. 12.212 of the Federal Acquisition Regulations and its +successors; and (b) if acquired by or on behalf of units of the Department of +Defense ("DOD") will be subject to the terms of this commercial computer +software license as specified in 48 C.F.R. 227.7202-2, DOD FAR Supplement and +its successors. + +5.8 Injunctive Relief. A breach or threatened breach by You of Section 2 may +cause irreparable harm for which damages at law may not provide adequate relief, +and therefore Bitwarden will be entitled to seek injunctive relief in any +applicable jurisdiction without being required to post a bond. + +5.9 Export Law Assurances. You understand that the Commercial Modules is +subject to export control laws and regulations. You may not download or +otherwise export or re-export the Commercial Modules or any underlying +information or technology except in full compliance with all applicable laws and +regulations, in particular, but without limitation, United States export control +laws. None of the Commercial Modules or any underlying information or technology +may be downloaded or otherwise exported or re- exported: (a) into (or to a +national or resident of) any country to which the United States has embargoed +goods; or (b) to anyone on the U.S. Treasury Department's list of specially +designated nationals or the U.S. Commerce Department's list of prohibited +countries or debarred or denied persons or entities. You hereby agree to the +foregoing and represents and warrants that You are not located in, under control +of, or a national or resident of any such country or on any such list. + +5.10 Construction. The titles and section headings used in this Agreement are +for ease of reference only and will not be used in the interpretation or +construction of this Agreement. No rule of construction resolving any ambiguity +in favor of the non-drafting party will be applied hereto. The word "including", +when used herein, is illustrative rather than exclusive and means "including, +without limitation." diff --git a/bitwarden-crypto-agent.sln b/bitwarden-crypto-agent.sln new file mode 100644 index 0000000..b589def --- /dev/null +++ b/bitwarden-crypto-agent.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.31205.134 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CryptoAgent", "src\CryptoAgent\CryptoAgent.csproj", "{CAD440BF-93C9-4DEC-B083-99FD49B50429}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {CAD440BF-93C9-4DEC-B083-99FD49B50429}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CAD440BF-93C9-4DEC-B083-99FD49B50429}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CAD440BF-93C9-4DEC-B083-99FD49B50429}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CAD440BF-93C9-4DEC-B083-99FD49B50429}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {40AD45DE-FF2B-4E64-9451-F368AA7C01E9} + EndGlobalSection +EndGlobal diff --git a/src/CryptoAgent/Controllers/MiscController.cs b/src/CryptoAgent/Controllers/MiscController.cs new file mode 100644 index 0000000..7b42229 --- /dev/null +++ b/src/CryptoAgent/Controllers/MiscController.cs @@ -0,0 +1,35 @@ +using Bit.CryptoAgent.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using System; +using System.Threading.Tasks; + +namespace Bit.CryptoAgent.Controllers +{ + public class MiscController : Controller + { + private readonly IRsaKeyService _rsaKeyService; + + public MiscController( + IRsaKeyService rsaKeyService) + { + _rsaKeyService = rsaKeyService; + } + + [HttpGet("~/alive")] + [HttpGet("~/now")] + [AllowAnonymous] + public DateTime GetAlive() + { + return DateTime.UtcNow; + } + + [HttpGet("~/public-key")] + [AllowAnonymous] + public async Task GetPublicKey() + { + var key = await _rsaKeyService.GetPublicKeyAsync(); + return new OkObjectResult(new { PublicKey = Convert.ToBase64String(key) }); + } + } +} diff --git a/src/CryptoAgent/Controllers/UserKeysController.cs b/src/CryptoAgent/Controllers/UserKeysController.cs new file mode 100644 index 0000000..4cca2f8 --- /dev/null +++ b/src/CryptoAgent/Controllers/UserKeysController.cs @@ -0,0 +1,84 @@ +using Bit.CryptoAgent.Models; +using Bit.CryptoAgent.Repositories; +using Bit.CryptoAgent.Services; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using System; +using System.Threading.Tasks; + +namespace Bit.CryptoAgent.Controllers +{ + [Route("user-keys")] + public class UserKeysController : Controller + { + private readonly ILogger _logger; + private readonly ICryptoService _cryptoService; + private readonly IUserKeyRepository _userKeyRepository; + + public UserKeysController( + ILogger logger, + IUserKeyRepository userKeyRepository, + ICryptoService cryptoService) + { + _logger = logger; + _cryptoService = cryptoService; + _userKeyRepository = userKeyRepository; + } + + [HttpPost("{userId}/get")] + public async Task Get(Guid userId, [FromBody] UserKeyGetRequestModel model) + { + var publicKey = Convert.FromBase64String(model.PublicKey); + var user = await _userKeyRepository.ReadAsync(userId); + if (user == null) + { + return new NotFoundResult(); + } + user.LastAccessDate = DateTime.UtcNow; + await _userKeyRepository.UpdateAsync(user); + var key = await _cryptoService.AesDecryptAsync(user.Key); + var encKey = await _cryptoService.RsaEncryptAsync(key, publicKey); + var response = new UserKeyResponseModel + { + Key = Convert.ToBase64String(encKey) + }; + return new JsonResult(response); + } + + [HttpPost("{userId}")] + public async Task Post(Guid userId, [FromBody] UserKeyRequestModel model) + { + var user = await _userKeyRepository.ReadAsync(userId); + if (user != null) + { + return new BadRequestResult(); + } + var key = await _cryptoService.RsaDecryptAsync(Convert.FromBase64String(model.Key)); + user = new UserKeyModel + { + Id = userId, + Key = await _cryptoService.AesEncryptToB64Async(key) + }; + await _userKeyRepository.CreateAsync(user); + return new OkResult(); + } + + [HttpPut("{userId}")] + public async Task Put(Guid userId, [FromBody] UserKeyRequestModel model) + { + var user = await _userKeyRepository.ReadAsync(userId); + if (user != null) + { + return new BadRequestResult(); + } + var key = await _cryptoService.RsaDecryptAsync(Convert.FromBase64String(model.Key)); + user = new UserKeyModel + { + Id = userId, + Key = await _cryptoService.AesEncryptToB64Async(key) + }; + await _userKeyRepository.UpdateAsync(user); + return new OkResult(); + } + } +} diff --git a/src/CryptoAgent/CryptoAgent.csproj b/src/CryptoAgent/CryptoAgent.csproj new file mode 100644 index 0000000..7720812 --- /dev/null +++ b/src/CryptoAgent/CryptoAgent.csproj @@ -0,0 +1,18 @@ + + + + net5.0 + Bit.CryptoAgent + 116f49e5-0b50-4080-856f-7e812413e723 + + + + + + + + + + + + diff --git a/src/CryptoAgent/CryptoAgentSettings.cs b/src/CryptoAgent/CryptoAgentSettings.cs new file mode 100644 index 0000000..1383255 --- /dev/null +++ b/src/CryptoAgent/CryptoAgentSettings.cs @@ -0,0 +1,53 @@ +namespace Bit.CryptoAgent +{ + public class CryptoAgentSettings + { + public DatabaseSettings Database { get; set; } + public CertificateSettings Certificate { get; set; } + public RsaKeySettings RsaKey { get; set; } + + public class CertificateSettings + { + // Filesystem + public string FilesystemPath { get; set; } + public string FilesystemPassword { get; set; } + // Local store + public string StoreThumbprint { get; set; } + // Azure blob storage + public string AzureStorageConnectionString { get; set; } + public string AzureStorageContainer { get; set; } + public string AzureStorageFileName { get; set; } + public string AzureStorageFilePassword { get; set; } + // Azure key vault + public string AzureKeyvaultUri { get; set; } + public string AzureKeyvaultCertificateName { get; set; } + public string AzureKeyvaultAdTenantId { get; set; } + public string AzureKeyvaultAdAppId { get; set; } + public string AzureKeyvaultAdSecret { get; set; } + } + + public class RsaKeySettings + { + // Local certificate provider + public string Provider { get; set; } + // Azure key vault + public string AzureKeyvaultUri { get; set; } + public string AzureKeyvaultKeyName { get; set; } + public string AzureKeyvaultAdTenantId { get; set; } + public string AzureKeyvaultAdAppId { get; set; } + public string AzureKeyvaultAdSecret { get; set; } + // GCP... + // AWS... + // Hashicorp Vault... + // Other HSMs... + } + + public class DatabaseSettings + { + public string JsonFilePath { get; set; } + public string SqlServerConnectionString { get; set; } + public string MySqlConnectionString { get; set; } + public string PostgreSqlConnectionString { get; set; } + } + } +} diff --git a/src/CryptoAgent/Models/IStoredItem.cs b/src/CryptoAgent/Models/IStoredItem.cs new file mode 100644 index 0000000..6099ed9 --- /dev/null +++ b/src/CryptoAgent/Models/IStoredItem.cs @@ -0,0 +1,9 @@ +using System; + +namespace Bit.CryptoAgent.Models +{ + public interface IStoredItem where TId : IEquatable + { + public TId Id { get; set; } + } +} diff --git a/src/CryptoAgent/Models/UserKeyGetRequestModel.cs b/src/CryptoAgent/Models/UserKeyGetRequestModel.cs new file mode 100644 index 0000000..e60cef2 --- /dev/null +++ b/src/CryptoAgent/Models/UserKeyGetRequestModel.cs @@ -0,0 +1,11 @@ +using System; +using System.ComponentModel.DataAnnotations; + +namespace Bit.CryptoAgent.Models +{ + public class UserKeyGetRequestModel + { + [Required] + public string PublicKey { get; set; } + } +} diff --git a/src/CryptoAgent/Models/UserKeyModel.cs b/src/CryptoAgent/Models/UserKeyModel.cs new file mode 100644 index 0000000..aed2eeb --- /dev/null +++ b/src/CryptoAgent/Models/UserKeyModel.cs @@ -0,0 +1,17 @@ +using System; + +namespace Bit.CryptoAgent.Models +{ + public class UserKeyModel : BaseUserKeyModel, IStoredItem + { + public Guid Id { get; set; } + } + + public abstract class BaseUserKeyModel + { + public string Key { get; set; } + public DateTime CreationDate { get; set; } = DateTime.UtcNow; + public DateTime? RevisionDate { get; set; } + public DateTime? LastAccessDate { get; set; } + } +} diff --git a/src/CryptoAgent/Models/UserKeyRequestModel.cs b/src/CryptoAgent/Models/UserKeyRequestModel.cs new file mode 100644 index 0000000..be40d3a --- /dev/null +++ b/src/CryptoAgent/Models/UserKeyRequestModel.cs @@ -0,0 +1,11 @@ +using Bit.CryptoAgent.Services; +using System; +using System.Threading.Tasks; + +namespace Bit.CryptoAgent.Models +{ + public class UserKeyRequestModel + { + public string Key { get; set; } + } +} diff --git a/src/CryptoAgent/Models/UserKeyResponseModel.cs b/src/CryptoAgent/Models/UserKeyResponseModel.cs new file mode 100644 index 0000000..0bcfc48 --- /dev/null +++ b/src/CryptoAgent/Models/UserKeyResponseModel.cs @@ -0,0 +1,7 @@ +namespace Bit.CryptoAgent.Models +{ + public class UserKeyResponseModel + { + public string Key { get; set; } + } +} diff --git a/src/CryptoAgent/Program.cs b/src/CryptoAgent/Program.cs new file mode 100644 index 0000000..c662805 --- /dev/null +++ b/src/CryptoAgent/Program.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace Bit.CryptoAgent +{ + public class Program + { + public static void Main(string[] args) + { + Host + .CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }) + .Build() + .Run(); + } + } +} diff --git a/src/CryptoAgent/Properties/launchSettings.json b/src/CryptoAgent/Properties/launchSettings.json new file mode 100644 index 0000000..8dce948 --- /dev/null +++ b/src/CryptoAgent/Properties/launchSettings.json @@ -0,0 +1,28 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:10253", + "sslPort": 0 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "SsoAgent": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": true, + "applicationUrl": "http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/CryptoAgent/Repositories/IApplicationDataRepository.cs b/src/CryptoAgent/Repositories/IApplicationDataRepository.cs new file mode 100644 index 0000000..4416854 --- /dev/null +++ b/src/CryptoAgent/Repositories/IApplicationDataRepository.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; + +namespace Bit.CryptoAgent.Repositories +{ + public interface IApplicationDataRepository + { + Task ReadSymmetricKeyAsync(); + Task UpdateSymmetricKeyAsync(string key); + } +} \ No newline at end of file diff --git a/src/CryptoAgent/Repositories/IRepository.cs b/src/CryptoAgent/Repositories/IRepository.cs new file mode 100644 index 0000000..189385f --- /dev/null +++ b/src/CryptoAgent/Repositories/IRepository.cs @@ -0,0 +1,12 @@ +using System.Threading.Tasks; + +namespace Bit.CryptoAgent.Repositories +{ + public interface IRepository + { + Task CreateAsync(TItem item); + Task ReadAsync(TId id); + Task UpdateAsync(TItem item); + Task DeleteAsync(TId id); + } +} diff --git a/src/CryptoAgent/Repositories/IUserKeyRepository.cs b/src/CryptoAgent/Repositories/IUserKeyRepository.cs new file mode 100644 index 0000000..0f3dcfc --- /dev/null +++ b/src/CryptoAgent/Repositories/IUserKeyRepository.cs @@ -0,0 +1,9 @@ +using Bit.CryptoAgent.Models; +using System; + +namespace Bit.CryptoAgent.Repositories +{ + public interface IUserKeyRepository : IRepository + { + } +} diff --git a/src/CryptoAgent/Repositories/JsonFile/ApplicationDataRepository.cs b/src/CryptoAgent/Repositories/JsonFile/ApplicationDataRepository.cs new file mode 100644 index 0000000..cca2b35 --- /dev/null +++ b/src/CryptoAgent/Repositories/JsonFile/ApplicationDataRepository.cs @@ -0,0 +1,26 @@ +using JsonFlatFileDataStore; +using System.Threading.Tasks; + +namespace Bit.CryptoAgent.Repositories.JsonFile +{ + public class ApplicationDataRepository : IApplicationDataRepository + { + public ApplicationDataRepository(IDataStore dataStore) + { + DataStore = dataStore; + } + + protected IDataStore DataStore { get; private set; } + + public Task ReadSymmetricKeyAsync() + { + var item = DataStore.GetItem("symmetricKey"); + return Task.FromResult(item as string); + } + + public async Task UpdateSymmetricKeyAsync(string key) + { + await DataStore.ReplaceItemAsync("symmetricKey", key, true); + } + } +} diff --git a/src/CryptoAgent/Repositories/JsonFile/Repository.cs b/src/CryptoAgent/Repositories/JsonFile/Repository.cs new file mode 100644 index 0000000..98f717b --- /dev/null +++ b/src/CryptoAgent/Repositories/JsonFile/Repository.cs @@ -0,0 +1,49 @@ +using Bit.CryptoAgent.Models; +using JsonFlatFileDataStore; +using System; +using System.Linq; +using System.Threading.Tasks; + +namespace Bit.CryptoAgent.Repositories.JsonFile +{ + public class Repository : IRepository + where TId : IEquatable + where TItem : class, IStoredItem + { + public Repository( + IDataStore dataStore, + string collectionName) + { + DataStore = dataStore; + CollectionName = collectionName; + } + + protected IDataStore DataStore { get; private set; } + protected string CollectionName { get; private set; } + + public virtual async Task CreateAsync(TItem item) + { + var collection = DataStore.GetCollection(CollectionName); + await collection.InsertOneAsync(item); + } + + public virtual Task ReadAsync(TId id) + { + var collection = DataStore.GetCollection(CollectionName); + var item = collection.AsQueryable().FirstOrDefault(i => i.Id.Equals(id)); + return Task.FromResult(item); + } + + public virtual async Task UpdateAsync(TItem item) + { + var collection = DataStore.GetCollection(CollectionName); + await collection.ReplaceOneAsync(item.Id, item); + } + + public virtual async Task DeleteAsync(TId id) + { + var collection = DataStore.GetCollection(CollectionName); + await collection.DeleteOneAsync(id); + } + } +} diff --git a/src/CryptoAgent/Repositories/JsonFile/UserKeyRepository.cs b/src/CryptoAgent/Repositories/JsonFile/UserKeyRepository.cs new file mode 100644 index 0000000..15b1558 --- /dev/null +++ b/src/CryptoAgent/Repositories/JsonFile/UserKeyRepository.cs @@ -0,0 +1,37 @@ +using Bit.CryptoAgent.Models; +using JsonFlatFileDataStore; +using System; +using System.Threading.Tasks; + +namespace Bit.CryptoAgent.Repositories.JsonFile +{ + public class UserKeyRepository : Repository, IUserKeyRepository + { + public UserKeyRepository(IDataStore dataStore) + : base(dataStore, "userKey") + { } + + public override async Task CreateAsync(UserKeyModel item) + { + var collection = DataStore.GetCollection(CollectionName); + await collection.InsertOneAsync(new JsonUserKeyModel(item)); + } + + // New model is required since JsonFlatFileDataStore doesn't handle Guid id types + public class JsonUserKeyModel : BaseUserKeyModel + { + public JsonUserKeyModel() { } + + public JsonUserKeyModel(UserKeyModel model) + { + Id = model.Id.ToString(); + Key = model.Key; + CreationDate = model.CreationDate; + RevisionDate = model.RevisionDate; + LastAccessDate = model.LastAccessDate; + } + + public string Id { get; set; } + } + } +} diff --git a/src/CryptoAgent/Services/AzureKeyVaultCertificateProviderService.cs b/src/CryptoAgent/Services/AzureKeyVaultCertificateProviderService.cs new file mode 100644 index 0000000..9db3a59 --- /dev/null +++ b/src/CryptoAgent/Services/AzureKeyVaultCertificateProviderService.cs @@ -0,0 +1,54 @@ +using Azure.Identity; +using Azure.Security.KeyVault.Certificates; +using Azure.Security.KeyVault.Secrets; +using System; +using System.Security.Cryptography.X509Certificates; +using System.Threading.Tasks; + +namespace Bit.CryptoAgent.Services +{ + public class AzureKeyVaultCertificateProviderService : ICertificateProviderService + { + private readonly CryptoAgentSettings _settings; + + public AzureKeyVaultCertificateProviderService(CryptoAgentSettings settings) + { + _settings = settings; + } + + public async Task GetCertificateAsync() + { + var credential = new ClientSecretCredential(_settings.Certificate.AzureKeyvaultAdTenantId, + _settings.Certificate.AzureKeyvaultAdAppId, _settings.Certificate.AzureKeyvaultAdSecret); + var keyVaultUri = new Uri(_settings.Certificate.AzureKeyvaultUri); + + var certificateClient = new CertificateClient(keyVaultUri, credential); + var certificateResponse = await certificateClient.GetCertificateAsync( + _settings.Certificate.AzureKeyvaultCertificateName); + var certificate = certificateResponse.Value; + if (certificate.Policy?.Exportable == true && certificate.Policy?.KeyType == CertificateKeyType.Rsa) + { + var secretName = ParseSecretName(certificate.SecretId); + var secretClient = new SecretClient(keyVaultUri, credential); + var secretResponse = await secretClient.GetSecretAsync(secretName); + var secret = secretResponse.Value; + if (string.Equals(secret.Properties.ContentType, CertificateContentType.Pkcs12.ToString(), + StringComparison.InvariantCultureIgnoreCase)) + { + var pfxBytes = Convert.FromBase64String(secret.Value); + return new X509Certificate2(pfxBytes); + } + } + return null; + } + + private string ParseSecretName(Uri secretId) + { + if (secretId.Segments.Length < 3) + { + throw new InvalidOperationException($@"The secret ""{secretId}"" does not contain a valid name."); + } + return secretId.Segments[2].TrimEnd('/'); + } + } +} diff --git a/src/CryptoAgent/Services/AzureKeyVaultRsaKeyService.cs b/src/CryptoAgent/Services/AzureKeyVaultRsaKeyService.cs new file mode 100644 index 0000000..c84a4af --- /dev/null +++ b/src/CryptoAgent/Services/AzureKeyVaultRsaKeyService.cs @@ -0,0 +1,90 @@ +using Azure.Identity; +using Azure.Security.KeyVault.Keys; +using Azure.Security.KeyVault.Keys.Cryptography; +using System; +using System.Threading.Tasks; + +namespace Bit.CryptoAgent.Services +{ + public class AzureKeyVaultRsaKeyService : IRsaKeyService + { + private readonly CryptoAgentSettings _settings; + + private KeyVaultKey _key; + private CryptographyClient _cryptographyClient; + private ClientSecretCredential _credential; + + public AzureKeyVaultRsaKeyService( + CryptoAgentSettings settings) + { + _settings = settings; + } + + public async Task EncryptAsync(byte[] data) + { + var client = await GetCryptographyClientAsync(); + var result = await client.EncryptAsync(EncryptionAlgorithm.RsaOaep, data); + return result.Ciphertext; + } + + public async Task DecryptAsync(byte[] data) + { + var client = await GetCryptographyClientAsync(); + var result = await client.DecryptAsync(EncryptionAlgorithm.RsaOaep, data); + return result.Plaintext; + } + + public async Task SignAsync(byte[] data) + { + var client = await GetCryptographyClientAsync(); + var result = await client.SignAsync(SignatureAlgorithm.RS256, data); + return result.Signature; + } + + public async Task VerifyAsync(byte[] data, byte[] signature) + { + var client = await GetCryptographyClientAsync(); + var result = await client.VerifyDataAsync(SignatureAlgorithm.RS256, data, signature); + return result.IsValid; + } + + public async Task GetPublicKeyAsync() + { + var key = await GetKeyAsync(); + return key.Key.ToRSA().ExportRSAPublicKey(); + } + + private async Task GetCryptographyClientAsync() + { + if (_cryptographyClient == null) + { + var key = await GetKeyAsync(); + var credential = GetCredential(); + _cryptographyClient = new CryptographyClient(key.Id, credential); + } + return _cryptographyClient; + } + + private async Task GetKeyAsync() + { + if (_key == null) + { + var credential = GetCredential(); + var keyVaultUri = new Uri(_settings.RsaKey.AzureKeyvaultUri); + var keyClient = new KeyClient(keyVaultUri, credential); + _key = await keyClient.GetKeyAsync(_settings.RsaKey.AzureKeyvaultKeyName); + } + return _key; + } + + private ClientSecretCredential GetCredential() + { + if (_credential == null) + { + _credential = new ClientSecretCredential(_settings.RsaKey.AzureKeyvaultAdTenantId, + _settings.RsaKey.AzureKeyvaultAdAppId, _settings.RsaKey.AzureKeyvaultAdSecret); + } + return _credential; + } + } +} diff --git a/src/CryptoAgent/Services/AzureStorageCertificateProviderService.cs b/src/CryptoAgent/Services/AzureStorageCertificateProviderService.cs new file mode 100644 index 0000000..91a557c --- /dev/null +++ b/src/CryptoAgent/Services/AzureStorageCertificateProviderService.cs @@ -0,0 +1,32 @@ +using Azure.Storage.Blobs; +using System.IO; +using System.Security.Cryptography.X509Certificates; +using System.Threading.Tasks; + +namespace Bit.CryptoAgent.Services +{ + public class AzureStorageCertificateProviderService : ICertificateProviderService + { + private readonly CryptoAgentSettings _settings; + + public AzureStorageCertificateProviderService(CryptoAgentSettings settings) + { + _settings = settings; + } + + public async Task GetCertificateAsync() + { + var container = new BlobContainerClient(_settings.Certificate.AzureStorageConnectionString, + _settings.Certificate.AzureStorageContainer); + await container.CreateIfNotExistsAsync(); + var blobClient = container.GetBlobClient(_settings.Certificate.AzureStorageFileName); + if (await blobClient.ExistsAsync()) + { + using var stream = new MemoryStream(); + await blobClient.DownloadToAsync(stream); + return new X509Certificate2(stream.ToArray(), _settings.Certificate.AzureStorageFilePassword); + } + return null; + } + } +} diff --git a/src/CryptoAgent/Services/CryptoFunctionService.cs b/src/CryptoAgent/Services/CryptoFunctionService.cs new file mode 100644 index 0000000..5a221fe --- /dev/null +++ b/src/CryptoAgent/Services/CryptoFunctionService.cs @@ -0,0 +1,99 @@ +using System; +using System.Security.Cryptography; +using System.Threading.Tasks; + +namespace Bit.CryptoAgent.Services +{ + public class CryptoFunctionService : ICryptoFunctionService + { + public async Task AesGcmEncryptAsync(byte[] data, byte[] key) + { + using var aes = new AesGcm(key); + var iv = await GetRandomBytesAsync(AesGcm.NonceByteSizes.MaxSize); + var tag = new byte[AesGcm.TagByteSizes.MaxSize]; + var encData = new byte[data.Length]; + + aes.Encrypt(iv, data, encData, tag); + + var encResult = new byte[encData.Length + tag.Length + iv.Length]; + encData.CopyTo(encResult, 0); + tag.CopyTo(encResult, encData.Length); + iv.CopyTo(encResult, encData.Length + tag.Length); + + return encResult; + } + + public Task AesGcmDecryptAsync(byte[] data, byte[] key) + { + using var aes = new AesGcm(key); + var endDataLength = data.Length - AesGcm.TagByteSizes.MaxSize - AesGcm.NonceByteSizes.MaxSize; + var encData = new ArraySegment(data, 0, endDataLength); + var tag = new ArraySegment(data, endDataLength, AesGcm.TagByteSizes.MaxSize); + var iv = new ArraySegment(data, endDataLength + AesGcm.TagByteSizes.MaxSize, AesGcm.NonceByteSizes.MaxSize); + var plainData = new byte[endDataLength]; + + aes.Decrypt(iv, encData, tag, plainData); + + return Task.FromResult(plainData); + } + + public Task RsaEncryptAsync(byte[] data, byte[] publicKey) + { + using var rsa = RSA.Create(); + rsa.ImportSubjectPublicKeyInfo(publicKey, out var bytesRead); + return RsaEncryptAsync(data, rsa); + } + + public Task RsaEncryptAsync(byte[] data, RSA publicKey) + { + var encData = publicKey.Encrypt(data, RSAEncryptionPadding.OaepSHA1); + return Task.FromResult(encData); + } + + public Task RsaDecryptAsync(byte[] data, byte[] privateKey) + { + using var rsa = RSA.Create(); + rsa.ImportPkcs8PrivateKey(privateKey, out var bytesRead); + return RsaDecryptAsync(data, rsa); + } + + public Task RsaDecryptAsync(byte[] data, RSA privateKey) + { + var encData = privateKey.Decrypt(data, RSAEncryptionPadding.OaepSHA1); + return Task.FromResult(encData); + } + + public Task RsaVerifyAsync(byte[] data, byte[] signature, byte[] publicKey) + { + using var rsa = RSA.Create(); + rsa.ImportSubjectPublicKeyInfo(publicKey, out var bytesRead); + return RsaVerifyAsync(data, signature, rsa); + } + + public Task RsaVerifyAsync(byte[] data, byte[] signature, RSA publicKey) + { + var valid = publicKey.VerifyData(data, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + return Task.FromResult(valid); + } + + public Task RsaSignAsync(byte[] data, byte[] privateKey) + { + using var rsa = RSA.Create(); + rsa.ImportPkcs8PrivateKey(privateKey, out var bytesRead); + return RsaSignAsync(data, rsa); + } + + public Task RsaSignAsync(byte[] data, RSA privateKey) + { + var signature = privateKey.SignData(data, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + return Task.FromResult(signature); + } + + public Task GetRandomBytesAsync(int size) + { + var bytes = new byte[size]; + RandomNumberGenerator.Fill(bytes); + return Task.FromResult(bytes); + } + } +} diff --git a/src/CryptoAgent/Services/CryptoService.cs b/src/CryptoAgent/Services/CryptoService.cs new file mode 100644 index 0000000..f958968 --- /dev/null +++ b/src/CryptoAgent/Services/CryptoService.cs @@ -0,0 +1,162 @@ +using Bit.CryptoAgent.Repositories; +using System; +using System.Threading.Tasks; + +namespace Bit.CryptoAgent.Services +{ + public class CryptoService : ICryptoService + { + private readonly IRsaKeyService _rsaKeyService; + private readonly ICryptoFunctionService _cryptoFunctionService; + private readonly IApplicationDataRepository _applicationDataRepository; + + private byte[] _symmetricKey; + + public CryptoService( + IRsaKeyService rsaKeyService, + ICryptoFunctionService cryptoFunctionService, + IApplicationDataRepository applicationDataRepository) + { + _rsaKeyService = rsaKeyService; + _cryptoFunctionService = cryptoFunctionService; + _applicationDataRepository = applicationDataRepository; + } + + // AES Decrypt + + public async Task AesDecryptAsync(byte[] data, byte[] key = null) + { + if (data == null) + { + return null; + } + if (key == null) + { + key = await GetSymmetricKeyAsync(); + } + var plainData = await _cryptoFunctionService.AesGcmDecryptAsync(data, key); + return plainData; + } + + public async Task AesDecryptToB64Async(byte[] data, byte[] key = null) + { + var plainData = await AesDecryptAsync(data, key); + return Convert.ToBase64String(plainData); + } + + public async Task AesDecryptAsync(string b64Data, byte[] key = null) + { + var data = Convert.FromBase64String(b64Data); + var plainData = await AesDecryptAsync(data, key); + return plainData; + } + + public async Task AesDecryptToB64Async(string b64Data, byte[] key = null) + { + var data = Convert.FromBase64String(b64Data); + var plainData = await AesDecryptToB64Async(data, key); + return plainData; + } + + // AES Encrypt + + public async Task AesEncryptAsync(byte[] data, byte[] key = null) + { + if (data == null) + { + return null; + } + if (key == null) + { + key = await GetSymmetricKeyAsync(); + } + var encData = await _cryptoFunctionService.AesGcmEncryptAsync(data, key); + return encData; + } + + public async Task AesEncryptAsync(string b64Data, byte[] key = null) + { + var data = Convert.FromBase64String(b64Data); + var encData = await AesEncryptAsync(data, key); + return encData; + } + + public async Task AesEncryptToB64Async(byte[] data, byte[] key = null) + { + var encData = await AesEncryptAsync(data, key); + return Convert.ToBase64String(encData); + } + + public async Task AesEncryptToB64Async(string b64Data, byte[] key = null) + { + var encData = await AesEncryptAsync(b64Data, key); + return Convert.ToBase64String(encData); + } + + // RSA Encrypt + + public async Task RsaEncryptAsync(byte[] data, byte[] publicKey = null) + { + if (data == null) + { + return null; + } + if (publicKey == null) + { + return await _rsaKeyService.EncryptAsync(data); + } + var encData = await _cryptoFunctionService.RsaEncryptAsync(data, publicKey); + return encData; + } + + // RSA Decrypt + + public async Task RsaDecryptAsync(byte[] data) + { + if (data == null) + { + return null; + } + return await _rsaKeyService.DecryptAsync(data); + } + + // RSA Verify + + public async Task RsaVerifyAsync(byte[] data, byte[] signature, byte[] publicKey = null) + { + if (data == null || signature == null) + { + return false; + } + if (publicKey == null) + { + return await _rsaKeyService.VerifyAsync(data, signature); + } + return await _cryptoFunctionService.RsaVerifyAsync(data, signature, publicKey); + } + + // Helpers + + private async Task GetSymmetricKeyAsync() + { + if (_symmetricKey == null) + { + var encKey = await _applicationDataRepository.ReadSymmetricKeyAsync(); + if (encKey != null) + { + var decodedEncKey = Convert.FromBase64String(encKey); + _symmetricKey = await RsaDecryptAsync(decodedEncKey); + } + else + { + _symmetricKey = await _cryptoFunctionService.GetRandomBytesAsync(32); + var decodedEncKey = await RsaEncryptAsync(_symmetricKey); + encKey = Convert.ToBase64String(decodedEncKey); + await _applicationDataRepository.UpdateSymmetricKeyAsync(encKey); + } + } + + return _symmetricKey; + } + } +} diff --git a/src/CryptoAgent/Services/FilesystemCertificateProviderService.cs b/src/CryptoAgent/Services/FilesystemCertificateProviderService.cs new file mode 100644 index 0000000..75e78dd --- /dev/null +++ b/src/CryptoAgent/Services/FilesystemCertificateProviderService.cs @@ -0,0 +1,22 @@ +using System.Security.Cryptography.X509Certificates; +using System.Threading.Tasks; + +namespace Bit.CryptoAgent.Services +{ + public class FilesystemCertificateProviderService : ICertificateProviderService + { + private readonly CryptoAgentSettings _settings; + + public FilesystemCertificateProviderService(CryptoAgentSettings settings) + { + _settings = settings; + } + + public Task GetCertificateAsync() + { + var cert = new X509Certificate2(_settings.Certificate.FilesystemPath, + _settings.Certificate.FilesystemPassword); + return Task.FromResult(cert); + } + } +} diff --git a/src/CryptoAgent/Services/ICertificateProviderService.cs b/src/CryptoAgent/Services/ICertificateProviderService.cs new file mode 100644 index 0000000..f2c17e8 --- /dev/null +++ b/src/CryptoAgent/Services/ICertificateProviderService.cs @@ -0,0 +1,10 @@ +using System.Security.Cryptography.X509Certificates; +using System.Threading.Tasks; + +namespace Bit.CryptoAgent.Services +{ + public interface ICertificateProviderService + { + Task GetCertificateAsync(); + } +} diff --git a/src/CryptoAgent/Services/ICryptoFunctionService.cs b/src/CryptoAgent/Services/ICryptoFunctionService.cs new file mode 100644 index 0000000..81b4d86 --- /dev/null +++ b/src/CryptoAgent/Services/ICryptoFunctionService.cs @@ -0,0 +1,20 @@ +using System.Security.Cryptography; +using System.Threading.Tasks; + +namespace Bit.CryptoAgent.Services +{ + public interface ICryptoFunctionService + { + Task AesGcmDecryptAsync(byte[] data, byte[] key); + Task AesGcmEncryptAsync(byte[] data, byte[] key); + Task RsaDecryptAsync(byte[] data, byte[] privateKey); + Task RsaDecryptAsync(byte[] data, RSA privateKey); + Task RsaEncryptAsync(byte[] data, byte[] publicKey); + Task RsaEncryptAsync(byte[] data, RSA publicKey); + Task RsaSignAsync(byte[] data, byte[] privateKey); + Task RsaSignAsync(byte[] data, RSA privateKey); + Task RsaVerifyAsync(byte[] data, byte[] signature, byte[] publicKey); + Task RsaVerifyAsync(byte[] data, byte[] signature, RSA publicKey); + Task GetRandomBytesAsync(int size); + } +} \ No newline at end of file diff --git a/src/CryptoAgent/Services/ICryptoService.cs b/src/CryptoAgent/Services/ICryptoService.cs new file mode 100644 index 0000000..2751301 --- /dev/null +++ b/src/CryptoAgent/Services/ICryptoService.cs @@ -0,0 +1,19 @@ +using System.Threading.Tasks; + +namespace Bit.CryptoAgent.Services +{ + public interface ICryptoService + { + Task AesDecryptAsync(byte[] data, byte[] key = null); + Task AesDecryptAsync(string b64Data, byte[] key = null); + Task AesDecryptToB64Async(byte[] data, byte[] key = null); + Task AesDecryptToB64Async(string b64Data, byte[] key = null); + Task AesEncryptAsync(byte[] data, byte[] key = null); + Task AesEncryptAsync(string b64Data, byte[] key = null); + Task AesEncryptToB64Async(byte[] data, byte[] key = null); + Task AesEncryptToB64Async(string b64Data, byte[] key = null); + Task RsaEncryptAsync(byte[] data, byte[] publicKey = null); + Task RsaDecryptAsync(byte[] data); + Task RsaVerifyAsync(byte[] data, byte[] signature, byte[] publicKey = null); + } +} \ No newline at end of file diff --git a/src/CryptoAgent/Services/IRsaKeyService.cs b/src/CryptoAgent/Services/IRsaKeyService.cs new file mode 100644 index 0000000..d7d345f --- /dev/null +++ b/src/CryptoAgent/Services/IRsaKeyService.cs @@ -0,0 +1,13 @@ +using System.Threading.Tasks; + +namespace Bit.CryptoAgent.Services +{ + public interface IRsaKeyService + { + Task DecryptAsync(byte[] data); + Task EncryptAsync(byte[] data); + Task SignAsync(byte[] data); + Task VerifyAsync(byte[] data, byte[] signature); + Task GetPublicKeyAsync(); + } +} \ No newline at end of file diff --git a/src/CryptoAgent/Services/LocalCertificateRsaKeyService.cs b/src/CryptoAgent/Services/LocalCertificateRsaKeyService.cs new file mode 100644 index 0000000..8761295 --- /dev/null +++ b/src/CryptoAgent/Services/LocalCertificateRsaKeyService.cs @@ -0,0 +1,81 @@ +using System.Security.Cryptography.X509Certificates; +using System.Threading.Tasks; + +namespace Bit.CryptoAgent.Services +{ + public class LocalCertificateRsaKeyService : IRsaKeyService + { + private readonly ICertificateProviderService _certificateProviderService; + private readonly ICryptoFunctionService _cryptoFunctionService; + + private X509Certificate2 _certificate; + + public LocalCertificateRsaKeyService( + ICertificateProviderService certificateProviderService, + ICryptoFunctionService cryptoFunctionService) + { + _certificateProviderService = certificateProviderService; + _cryptoFunctionService = cryptoFunctionService; + } + + public async Task EncryptAsync(byte[] data) + { + if (data == null) + { + return null; + } + var encData = await _cryptoFunctionService.RsaEncryptAsync(data, await GetPublicKeyAsync()); + return encData; + } + + public async Task DecryptAsync(byte[] data) + { + if (data == null) + { + return null; + } + var plainData = await _cryptoFunctionService.RsaDecryptAsync(data, await GetPrivateKeyAsync()); + return plainData; + } + + public async Task SignAsync(byte[] data) + { + if (data == null) + { + return null; + } + return await _cryptoFunctionService.RsaSignAsync(data, await GetPrivateKeyAsync()); + } + + public async Task VerifyAsync(byte[] data, byte[] signature) + { + if (data == null || signature == null) + { + return false; + } + return await _cryptoFunctionService.RsaVerifyAsync(data, signature, await GetPublicKeyAsync()); + } + + public async Task GetPublicKeyAsync() + { + var certificate = await GetCertificateAsync(); + return certificate.GetRSAPublicKey().ExportRSAPublicKey(); + } + + private async Task GetCertificateAsync() + { + if (_certificate == null) + { + _certificate = await _certificateProviderService.GetCertificateAsync(); + } + + return _certificate; + } + + private async Task GetPrivateKeyAsync() + { + var certificate = await GetCertificateAsync(); + return certificate.GetRSAPrivateKey(); + } + } +} diff --git a/src/CryptoAgent/Services/StoreCertificateProviderService.cs b/src/CryptoAgent/Services/StoreCertificateProviderService.cs new file mode 100644 index 0000000..4220be5 --- /dev/null +++ b/src/CryptoAgent/Services/StoreCertificateProviderService.cs @@ -0,0 +1,38 @@ +using System.Security.Cryptography.X509Certificates; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace Bit.CryptoAgent.Services +{ + public class StoreCertificateProviderService : ICertificateProviderService + { + private readonly CryptoAgentSettings _settings; + + public StoreCertificateProviderService(CryptoAgentSettings settings) + { + _settings = settings; + } + + public Task GetCertificateAsync() + { + X509Certificate2 cert = null; + var certStore = new X509Store(StoreName.My, StoreLocation.CurrentUser); + certStore.Open(OpenFlags.ReadOnly); + var certCollection = certStore.Certificates.Find(X509FindType.FindByThumbprint, + CleanThumbprint(_settings.Certificate.StoreThumbprint), false); + if (certCollection.Count > 0) + { + cert = certCollection[0]; + } + certStore.Close(); + return Task.FromResult(cert); + } + + public static string CleanThumbprint(string thumbprint) + { + // Clean possible garbage characters from thumbprint copy/paste + // ref http://stackoverflow.com/questions/8448147/problems-with-x509store-certificates-find-findbythumbprint + return Regex.Replace(thumbprint, @"[^\da-fA-F]", string.Empty).ToUpper(); + } + } +} diff --git a/src/CryptoAgent/Startup.cs b/src/CryptoAgent/Startup.cs new file mode 100644 index 0000000..9902a07 --- /dev/null +++ b/src/CryptoAgent/Startup.cs @@ -0,0 +1,104 @@ +using Bit.CryptoAgent.Repositories; +using Bit.CryptoAgent.Services; +using JsonFlatFileDataStore; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using System; +using System.Globalization; + +namespace Bit.CryptoAgent +{ + public class Startup + { + public Startup(IWebHostEnvironment env, IConfiguration configuration) + { + CultureInfo.DefaultThreadCurrentCulture = new CultureInfo("en-US"); + Configuration = configuration; + Environment = env; + } + + public IConfiguration Configuration { get; } + public IWebHostEnvironment Environment { get; set; } + + public void ConfigureServices(IServiceCollection services) + { + var settings = new CryptoAgentSettings(); + ConfigurationBinder.Bind(Configuration.GetSection("CryptoAgentSettings"), settings); + services.AddSingleton(s => settings); + + var rsaKeyProvider = settings.RsaKey.Provider?.ToLowerInvariant(); + if (rsaKeyProvider == "certificate") + { + services.AddSingleton(); + + if (!string.IsNullOrWhiteSpace(settings.Certificate?.StoreThumbprint)) + { + services.AddSingleton(); + } + else if (!string.IsNullOrWhiteSpace(settings.Certificate?.FilesystemPath)) + { + services.AddSingleton(); + } + else if (!string.IsNullOrWhiteSpace(settings.Certificate?.AzureStorageConnectionString)) + { + services.AddSingleton(); + } + else if (!string.IsNullOrWhiteSpace(settings.Certificate?.AzureKeyvaultUri)) + { + services.AddSingleton(); + } + else + { + throw new Exception("No certificate provider configured."); + } + } + else if (rsaKeyProvider == "azure") + { + if (!string.IsNullOrWhiteSpace(settings.RsaKey?.AzureKeyvaultUri)) + { + services.AddSingleton(); + } + else + { + throw new Exception("No azure key vault configured."); + } + } + else + { + throw new Exception("Unknown rsa key provider."); + } + + services.AddSingleton(); + services.AddSingleton(); + + // JsonFlatFileDataStore + if (!string.IsNullOrWhiteSpace(settings.Database?.JsonFilePath)) + { + // Assign foobar to keyProperty in order to not use incrementing Id functionality + services.AddSingleton(new DataStore(settings.Database.JsonFilePath, keyProperty: "--foobar--")); + services.AddSingleton(); + services.AddSingleton(); + } + else + { + throw new Exception("No database configured."); + } + + services.AddControllers(); + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseRouting(); + app.UseEndpoints(endpoints => endpoints.MapDefaultControllerRoute()); + } + } +} diff --git a/src/CryptoAgent/appsettings.Development.json b/src/CryptoAgent/appsettings.Development.json new file mode 100644 index 0000000..8983e0f --- /dev/null +++ b/src/CryptoAgent/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/src/CryptoAgent/appsettings.json b/src/CryptoAgent/appsettings.json new file mode 100644 index 0000000..bbaf8db --- /dev/null +++ b/src/CryptoAgent/appsettings.json @@ -0,0 +1,15 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*", + "cryptoAgentSettings": { + "rsaKey": { + "provider": "certificate" + } + } +}