├── azure-keyvault-emulator.pfx ├── local-certs ├── azure-keyvault-emulator.pfx ├── azure-keyvault-emulator.crt └── azure-keyvault-emulator.key ├── .github ├── dependabot.yml ├── CODEOWNERS ├── ISSUE_TEMPLATE.md ├── workflows │ └── pull-request.yml └── PULL_REQUEST_TEMPLATE.md ├── .dockerignore ├── scripts ├── stopdocker.sh ├── startdocker.sh ├── stopservice.sh ├── dependencycheck.sh ├── release.sh ├── verify.sh ├── checks │ ├── dotnetcheck.sh │ ├── dockercomposecheck.sh │ └── dockercheck.sh ├── acceptancetest.sh ├── serviceup.sh └── importcert.sh ├── AzureKeyVaultEmulator ├── appsettings.json ├── Keys │ ├── Constants │ │ └── EncryptionAlgorithms.cs │ ├── Models │ │ ├── KeyOperationResult.cs │ │ ├── KeyOperationParameters.cs │ │ ├── KeyResponse.cs │ │ ├── KeyAttributesModel.cs │ │ ├── CreateKeyModel.cs │ │ └── JsonWebKeyModel.cs │ ├── Factories │ │ └── RsaKeyFactory.cs │ ├── Controllers │ │ └── KeysController.cs │ └── Services │ │ └── KeyVaultKeyService.cs ├── appsettings.Development.json ├── Secrets │ ├── Models │ │ ├── SecretAttributesModel.cs │ │ ├── SecretResponse.cs │ │ └── SetSecretModel.cs │ ├── Services │ │ └── KeyVaultSecretService.cs │ └── Controllers │ │ └── SecretsController.cs ├── AzureKeyVaultEmulator.csproj ├── Program.cs ├── Properties │ └── launchSettings.json └── Startup.cs ├── makefile ├── docker-compose.yml ├── Dockerfile ├── package.json ├── .releaserc.json ├── AzureKeyVaultEmulator.AcceptanceTests ├── Helpers │ └── LocalTokenCredential.cs ├── AzureKeyVaultEmulator.AcceptanceTests.csproj └── Secrets │ ├── CreateSecretTests.cs │ └── GetSecretTests.cs ├── azure-keyvault-emulator.crt ├── .gitignore ├── CHANGELOG.md ├── azure-keyvault-emulator.key ├── AzureKeyVaultEmulator.sln ├── CODE_OF_CONDUCT.md ├── README.md └── LICENSE /azure-keyvault-emulator.pfx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Basis-Theory/azure-keyvault-emulator/HEAD/azure-keyvault-emulator.pfx -------------------------------------------------------------------------------- /local-certs/azure-keyvault-emulator.pfx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Basis-Theory/azure-keyvault-emulator/HEAD/local-certs/azure-keyvault-emulator.pfx -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "nuget" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # directories 2 | **/bin/ 3 | **/obj/ 4 | **/out/ 5 | 6 | # files 7 | Dockerfile* 8 | **/*.trx 9 | **/*.md 10 | **/*.ps1 11 | **/*.cmd 12 | **/*.sh 13 | global.json 14 | -------------------------------------------------------------------------------- /scripts/stopdocker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | current_directory="$PWD" 5 | 6 | cd $(dirname $0)/.. 7 | 8 | docker compose down 9 | 10 | result=$? 11 | 12 | cd "$current_directory" 13 | 14 | exit $result -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # This is a comment. 2 | # Each line is a file pattern followed by one or more owners. 3 | 4 | # These owners will be the default owners for everything in 5 | # the repo. 6 | * @basis-theory/engineers 7 | -------------------------------------------------------------------------------- /scripts/startdocker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | current_directory="$PWD" 5 | 6 | cd $(dirname $0)/.. 7 | 8 | docker compose up -d --build 9 | 10 | result=$? 11 | 12 | cd "$current_directory" 13 | 14 | exit $result -------------------------------------------------------------------------------- /scripts/stopservice.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | current_directory="$PWD" 5 | 6 | cd $(dirname $0)/.. 7 | 8 | docker stop keyvault-emulator 9 | 10 | result=$? 11 | 12 | cd "$current_directory" 13 | 14 | exit $result 15 | -------------------------------------------------------------------------------- /scripts/dependencycheck.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # ensure that your machine is set up to build software 5 | my_directory=$(dirname $0) 6 | 7 | for filename in $my_directory/checks/*; do 8 | chmod +x $filename 9 | $filename 10 | done -------------------------------------------------------------------------------- /scripts/release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | current_directory="$PWD" 5 | 6 | cd $(dirname $0)/.. 7 | 8 | yarn install --frozen-lockfile 9 | yarn release 10 | 11 | result=$? 12 | 13 | cd "$current_directory" 14 | 15 | exit $result 16 | -------------------------------------------------------------------------------- /AzureKeyVaultEmulator/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Information", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*" 10 | } 11 | -------------------------------------------------------------------------------- /AzureKeyVaultEmulator/Keys/Constants/EncryptionAlgorithms.cs: -------------------------------------------------------------------------------- 1 | namespace AzureKeyVaultEmulator.Keys.Constants 2 | { 3 | public static class EncryptionAlgorithms 4 | { 5 | public const string RSA1_5 = "RSA1_5"; 6 | public const string RSA_OAEP = "RSA-OAEP"; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | MAKEFLAGS += --silent 2 | 3 | verify: 4 | ./scripts/verify.sh 5 | 6 | acceptance-test: 7 | ./scripts/acceptancetest.sh 8 | 9 | start-docker: 10 | ./scripts/startdocker.sh 11 | 12 | stop-docker: 13 | ./scripts/stopdocker.sh 14 | 15 | stop-service: 16 | ./scripts/stopservice.sh 17 | 18 | release: 19 | ./scripts/release.sh 20 | -------------------------------------------------------------------------------- /scripts/verify.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # verify the software 5 | 6 | current_directory="$PWD" 7 | 8 | cd $(dirname $0) 9 | 10 | time { 11 | ./dependencycheck.sh 12 | ./importcert.sh 13 | ./stopdocker.sh 14 | ./startdocker.sh 15 | ./serviceup.sh 16 | ./acceptancetest.sh 17 | } 18 | 19 | cd "$current_directory" 20 | -------------------------------------------------------------------------------- /AzureKeyVaultEmulator/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Information", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | }, 8 | "EventLog": { 9 | "LogLevel": { 10 | "Default": "Trace" 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /AzureKeyVaultEmulator/Keys/Models/KeyOperationResult.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace AzureKeyVaultEmulator.Keys.Models 4 | { 5 | public class KeyOperationResult 6 | { 7 | [JsonPropertyName("kid")] 8 | public string KeyIdentifier { get; set; } 9 | 10 | [JsonPropertyName("value")] 11 | public string Data { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /AzureKeyVaultEmulator/Keys/Models/KeyOperationParameters.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace AzureKeyVaultEmulator.Keys.Models 4 | { 5 | public class KeyOperationParameters 6 | { 7 | [JsonPropertyName("alg")] 8 | public string Algorithm { get; set; } 9 | 10 | [JsonPropertyName("value")] 11 | public string Data { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /AzureKeyVaultEmulator/Keys/Factories/RsaKeyFactory.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Cryptography; 2 | 3 | namespace AzureKeyVaultEmulator.Keys.Factories 4 | { 5 | public static class RsaKeyFactory 6 | { 7 | private const int DefaultKeySize = 2048; 8 | 9 | public static RSA CreateRsaKey(int? keySize) 10 | { 11 | return RSA.Create(keySize ?? DefaultKeySize); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | For additional support or to discuss issues/features, please reach out to us on our [Community](https://community.basistheory.com) or via email at [support@basistheory.com](mailto:support@basistheory.com) 2 | 3 | ## Expected Behavior 4 | - 5 | 6 | ## Actual Behavior 7 | - 8 | 9 | ## Steps to Reproduce the Problem 10 | 1. 11 | 1. 12 | 1. 13 | 14 | ## Specifications 15 | - Image Version: 16 | - Docker Version: 17 | - .NET Version: 18 | -------------------------------------------------------------------------------- /AzureKeyVaultEmulator/Keys/Models/KeyResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace AzureKeyVaultEmulator.Keys.Models 4 | { 5 | public class KeyResponse 6 | { 7 | [JsonPropertyName("key")] 8 | public JsonWebKeyModel Key { get; set; } 9 | 10 | [JsonPropertyName("attributes")] 11 | public KeyAttributesModel Attributes { get; set; } 12 | 13 | [JsonPropertyName("tags")] 14 | public object Tags { get; set; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /AzureKeyVaultEmulator/Secrets/Models/SecretAttributesModel.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace AzureKeyVaultEmulator.Secrets.Models 4 | { 5 | public class SecretAttributesModel 6 | { 7 | [JsonPropertyName("enabled")] 8 | public bool Enabled { get; set; } 9 | 10 | [JsonPropertyName("exp")] 11 | public int Expiration { get; set; } 12 | 13 | [JsonPropertyName("nbf")] 14 | public int NotBefore { get; set; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /scripts/checks/dotnetcheck.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Checking .NET..." 4 | EXPECTED_DOTNET="6.0.x" 5 | EXPECTED_DOTNET_REGEX="6\.0\.([0-9]*)" 6 | 7 | while IFS= read -r line 8 | do 9 | version="$(cut -d ' ' -f 1 <<< "$line")" 10 | if [[ $version =~ $EXPECTED_DOTNET_REGEX ]]; then 11 | echo ".NET is OK" 12 | exit 0 13 | fi 14 | done <<<"$(dotnet --list-sdks)" 15 | 16 | echo "Please Install .NET $EXPECTED_DOTNET SDK from here: https://dotnet.microsoft.com/download/dotnet/6.0" 17 | exit 1 18 | -------------------------------------------------------------------------------- /AzureKeyVaultEmulator/AzureKeyVaultEmulator.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /scripts/acceptancetest.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | current_directory="$PWD" 5 | 6 | cd $(dirname $0)/.. 7 | 8 | echo "Running acceptance tests..." 9 | 10 | dotnet restore -v=quiet 11 | dotnet build AzureKeyVaultEmulator.AcceptanceTests/AzureKeyVaultEmulator.AcceptanceTests.csproj --no-restore -c Release -v=minimal 12 | dotnet test AzureKeyVaultEmulator.AcceptanceTests/AzureKeyVaultEmulator.AcceptanceTests.csproj --no-restore --no-build -c Release -v=normal 13 | 14 | result=$? 15 | 16 | cd "$current_directory" 17 | 18 | exit $result 19 | -------------------------------------------------------------------------------- /AzureKeyVaultEmulator/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using Microsoft.Extensions.Hosting; 3 | 4 | namespace AzureKeyVaultEmulator 5 | { 6 | public static class Program 7 | { 8 | public static void Main(string[] args) 9 | { 10 | CreateHostBuilder(args).Build().Run(); 11 | } 12 | 13 | private static IHostBuilder CreateHostBuilder(string[] args) => 14 | Host.CreateDefaultBuilder(args) 15 | .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); }); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.github/workflows/pull-request.yml: -------------------------------------------------------------------------------- 1 | name: Pull Request 2 | 3 | on: 4 | pull_request: 5 | branches: [master] 6 | 7 | jobs: 8 | build: 9 | environment: PR 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | 15 | - name: Setup .NET 16 | uses: actions/setup-dotnet@v1 17 | with: 18 | dotnet-version: 6.0.x 19 | 20 | - name: Add hosts to /etc/hosts 21 | run: | 22 | echo "127.0.0.1 localhost.vault.azure.net" | sudo tee -a /etc/hosts 23 | 24 | - name: Verify 25 | run: make verify 26 | -------------------------------------------------------------------------------- /AzureKeyVaultEmulator/Secrets/Models/SecretResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace AzureKeyVaultEmulator.Secrets.Models 5 | { 6 | public class SecretResponse 7 | { 8 | [JsonPropertyName("id")] 9 | public Uri Id { get; set; } 10 | 11 | [JsonPropertyName("value")] 12 | public string Value { get; set; } 13 | 14 | [JsonPropertyName("attributes")] 15 | public SecretAttributesModel Attributes { get; set; } 16 | 17 | [JsonPropertyName("tags")] 18 | public object Tags { get; set; } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.9' 2 | 3 | services: 4 | keyvault-emulator: 5 | image: azure-keyvault-emulator:latest 6 | build: 7 | context: . 8 | hostname: azure-keyvault-emulator.vault.azure.net 9 | ports: 10 | - 5551:5551 11 | - 5550:5550 12 | volumes: 13 | - $PWD/local-certs:/https:ro 14 | environment: 15 | - ASPNETCORE_ENVIRONMENT=Development 16 | - ASPNETCORE_URLS=https://+:5551 17 | - ASPNETCORE_Kestrel__Certificates__Default__KeyPath=/https/azure-keyvault-emulator.key 18 | - ASPNETCORE_Kestrel__Certificates__Default__Path=/https/azure-keyvault-emulator.crt 19 | -------------------------------------------------------------------------------- /scripts/serviceup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | RED="\033[1;31m" 4 | GREEN="\033[1;32m" 5 | NOCOLOR="\033[0m" 6 | 7 | i=0 8 | timeout=30000 9 | 10 | # continue until $n equals 5 11 | while [ $i -le $timeout ] 12 | do 13 | status=$(curl -s -o /dev/null -i -w "%{http_code}" https://localhost:5551/) 14 | 15 | if [ $status == "404" ] 16 | then 17 | echo -e "${GREEN}✓${NOCOLOR} KeyVault Emulator is ready" 18 | exit 0 19 | else 20 | echo -e "❌ KeyVault Emulator is not ready" 21 | 22 | sleep 3 23 | i=$(( i+3000 )) # increments $n 24 | fi 25 | done 26 | 27 | echo 'Health check did not pass within timeout' 28 | exit 1 29 | -------------------------------------------------------------------------------- /AzureKeyVaultEmulator/Secrets/Models/SetSecretModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace AzureKeyVaultEmulator.Secrets.Models 5 | { 6 | public class SetSecretModel 7 | { 8 | [JsonPropertyName("value")] 9 | [Required] 10 | public string Value { get; set; } 11 | 12 | [JsonPropertyName("contentType")] 13 | public string ContentType { get; set; } 14 | 15 | [JsonPropertyName("attributes")] 16 | public SecretAttributesModel SecretAttributes { get; set; } 17 | 18 | [JsonPropertyName("tags")] 19 | public object Tags { get; set; } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/sdk:6.0-alpine AS builder 2 | WORKDIR /app 3 | 4 | COPY *.sln . 5 | COPY */*.csproj ./ 6 | RUN for file in $(ls *.csproj); do mkdir -p ./${file%.*}/ && mv $file ./${file%.*}/; done 7 | 8 | RUN dotnet restore 9 | 10 | COPY . . 11 | RUN dotnet publish AzureKeyVaultEmulator/AzureKeyVaultEmulator.csproj -c Release -o publish --no-restore 12 | 13 | ######################################## 14 | 15 | FROM mcr.microsoft.com/dotnet/aspnet:6.0-alpine 16 | WORKDIR /app 17 | 18 | RUN apk add --no-cache icu-libs tzdata 19 | 20 | ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false 21 | 22 | COPY --from=builder /app/publish . 23 | 24 | ENTRYPOINT ["dotnet", "AzureKeyVaultEmulator.dll"] 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "basistheory-azure-keyvault-emulator", 3 | "version": "1.3.0", 4 | "description": "Basis Theory Azure KeyVault Emulator", 5 | "repository": "https://github.com/Basis-Theory/azure-keyvault-emulator", 6 | "author": "Basis Theory", 7 | "license": "MIT", 8 | "private": true, 9 | "scripts": { 10 | "release": "semantic-release" 11 | }, 12 | "devDependencies": { 13 | "@semantic-release/changelog": "^5.0.1", 14 | "@semantic-release/commit-analyzer": "^8.0.1", 15 | "@semantic-release/exec": "^5.0.0", 16 | "@semantic-release/git": "^9.0.0", 17 | "@semantic-release/github": "^7.2.3", 18 | "@semantic-release/release-notes-generator": "^9.0.3", 19 | "semantic-release": "^19.0.3" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /scripts/importcert.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | current_directory="$PWD" 5 | 6 | cd $(dirname $0)/.. 7 | 8 | if [ "$(uname)" == "Darwin" ]; then 9 | if ! security find-certificate -c azure-keyvault-emulator > /dev/null; then 10 | sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain local-certs/azure-keyvault-emulator.crt 11 | else 12 | echo "azure-keyvault-emulator certificate already installed" 13 | fi 14 | elif [ "$(expr substr $(uname -s) 1 5)" == "Linux" ]; then 15 | sudo cp local-certs/azure-keyvault-emulator.crt /usr/local/share/ca-certificates/azure-keyvault-emulator.crt 16 | sudo update-ca-certificates 17 | fi 18 | 19 | result=$? 20 | 21 | cd "$current_directory" 22 | 23 | exit $result 24 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | ## Description 3 | 4 | - 5 | 6 | 7 | ## Testing required outside of automated testing? 8 | 9 | - [ ] Not Applicable 10 | 11 | 12 | ### Screenshots (if appropriate): 13 | 14 | - [ ] Not Applicable 15 | 16 | 17 | ## Rollback / Rollforward Procedure 18 | 19 | - [ ] Roll Forward 20 | - [ ] Roll Back 21 | 22 | ## Reviewer Checklist 23 | 24 | - [ ] Description of Change 25 | - [ ] Description of outside testing if applicable. 26 | - [ ] Description of Roll Forward / Backward Procedure 27 | - [ ] Documentation updated for Change 28 | -------------------------------------------------------------------------------- /scripts/checks/dockercomposecheck.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Checking Docker Compose..." 4 | 5 | EXPECTED_COMPOSE_REGEX=^[1-9][0-9]*\.[0-9]+\.[0-9]+ 6 | DISPLAY_COMPOSE_REGEX=$(sed -e 's|\\\([.+?*()]\)|\1|g' -e 's|[.+?]\*|*|g' <<<${EXPECTED_COMPOSE_REGEX}) 7 | 8 | version=$(docker compose version --short 2>&1) 9 | 10 | if [[ "$version" =~ $EXPECTED_COMPOSE_REGEX ]]; then 11 | echo "Docker Compose version \"${version}\" (matching required \"${DISPLAY_COMPOSE_REGEX}\") is OK" 12 | else 13 | echo "Current Docker Compose version is \"${version}\". Please Install Docker Compose matching \"${DISPLAY_COMPOSE_REGEX}\"" 14 | echo "Docker Compose comes with Docker Desktop which can be downloaded here: https://www.docker.com/products/docker-desktop" 15 | exit 1 16 | fi 17 | -------------------------------------------------------------------------------- /scripts/checks/dockercheck.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Checking Docker..." 4 | 5 | EXPECTED_DOCKER="^[1-9][0-9]+\.[0-9]+\.[0-9]*" 6 | version=$(docker version --format '{{.Client.Version}}' 2>&1) 7 | 8 | if [[ $version == *"docker.sock"* ]]; then 9 | echo "Your user does not have permission to run docker commands, and we don't want you to run this with sudo permissions. Go here and read: http://askubuntu.com/questions/477551/how-can-i-use-docker-without-sudo" 10 | echo "Probably your user should be a member of the 'docker' UNIX group. Run \`sudo adduser $(whoami) docker\` and then fully log out and log back in." 11 | exit 1 12 | fi 13 | 14 | if [[ "$version" =~ $EXPECTED_DOCKER ]]; then 15 | echo "Docker is OK" 16 | else 17 | echo "Please Install Docker via Docker Desktop; https://www.docker.com/products/docker-desktop" 18 | exit 1 19 | fi 20 | -------------------------------------------------------------------------------- /AzureKeyVaultEmulator/Keys/Models/KeyAttributesModel.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace AzureKeyVaultEmulator.Keys.Models 4 | { 5 | public class KeyAttributesModel 6 | { 7 | [JsonPropertyName("created")] 8 | public int Created { get; set; } 9 | 10 | [JsonPropertyName("enabled")] 11 | public bool Enabled { get; set; } 12 | 13 | [JsonPropertyName("exp")] 14 | public int Expiration { get; set; } 15 | 16 | [JsonPropertyName("nbf")] 17 | public int NotBefore { get; set; } 18 | 19 | [JsonPropertyName("recoverableDays")] 20 | public int RecoverableDays { get; set; } 21 | 22 | [JsonPropertyName("recoveryLevel")] 23 | public string RecoveryLevel { get; set; } 24 | 25 | [JsonPropertyName("updated")] 26 | public int Updated { get; set; } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /AzureKeyVaultEmulator/Keys/Models/CreateKeyModel.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.ComponentModel.DataAnnotations; 3 | using System.Text.Json.Serialization; 4 | 5 | namespace AzureKeyVaultEmulator.Keys.Models 6 | { 7 | public class CreateKeyModel 8 | { 9 | [JsonPropertyName("kty")] 10 | [Required] 11 | public string KeyType { get; set; } 12 | 13 | [JsonPropertyName("attributes")] 14 | public KeyAttributesModel KeyAttributes { get; set; } 15 | 16 | [JsonPropertyName("crv")] 17 | public string KeyCurveName { get; set; } 18 | 19 | [JsonPropertyName("key_ops")] 20 | public List KeyOperations { get; set; } 21 | 22 | [JsonPropertyName("key_size")] 23 | public int? KeySize { get; set; } 24 | 25 | [JsonPropertyName("tags")] 26 | public object Tags { get; set; } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "branches": [ 3 | "master" 4 | ], 5 | "plugins": [ 6 | "@semantic-release/commit-analyzer", 7 | "@semantic-release/release-notes-generator", 8 | "@semantic-release/changelog", 9 | [ 10 | "@semantic-release/exec", 11 | { 12 | "prepareCmd": "npm version ${nextRelease.version} --no-git-tag-version --allow-same-version" 13 | } 14 | ], 15 | [ 16 | "@semantic-release/exec", 17 | { 18 | "prepareCmd": "echo \"::set-output name=version::${nextRelease.version}\"" 19 | } 20 | ], 21 | [ 22 | "@semantic-release/github" 23 | ], 24 | [ 25 | "@semantic-release/git", 26 | { 27 | "assets": [ 28 | "package.json", 29 | "CHANGELOG.md" 30 | ], 31 | "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}" 32 | } 33 | ] 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /AzureKeyVaultEmulator/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:16083", 8 | "sslPort": 44395 9 | } 10 | }, 11 | "profiles": { 12 | "IIS Express": { 13 | "commandName": "IISExpress", 14 | "launchBrowser": true, 15 | "launchUrl": "swagger", 16 | "environmentVariables": { 17 | "ASPNETCORE_ENVIRONMENT": "Development" 18 | } 19 | }, 20 | "AzureKeyVaultEmulator": { 21 | "commandName": "Project", 22 | "dotnetRunMessages": "true", 23 | "launchBrowser": false, 24 | "launchUrl": "swagger", 25 | "applicationUrl": "https://localhost:5551;http://localhost:5550", 26 | "environmentVariables": { 27 | "ASPNETCORE_ENVIRONMENT": "Development" 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /AzureKeyVaultEmulator.AcceptanceTests/Helpers/LocalTokenCredential.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Azure.Core; 5 | 6 | namespace AzureKeyVaultEmulator.AcceptanceTests.Helpers 7 | { 8 | public class LocalTokenCredential : TokenCredential 9 | { 10 | public override ValueTask GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken) 11 | { 12 | return new(new AccessToken("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE4OTAyMzkwMjIsImlzcyI6Imh0dHBzOi8vbG9jYWxob3N0OjUwMDEvIn0.bHLeGTRqjJrmIJbErE-1Azs724E5ibzvrIc-UQL6pws", DateTimeOffset.MaxValue)); 13 | } 14 | 15 | public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken) 16 | { 17 | return new("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE4OTAyMzkwMjIsImlzcyI6Imh0dHBzOi8vbG9jYWxob3N0OjUwMDEvIn0.bHLeGTRqjJrmIJbErE-1Azs724E5ibzvrIc-UQL6pws", DateTimeOffset.MaxValue); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /AzureKeyVaultEmulator.AcceptanceTests/AzureKeyVaultEmulator.AcceptanceTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | all 18 | 19 | 20 | runtime; build; native; contentfiles; analyzers; buildtransitive 21 | all 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /azure-keyvault-emulator.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFPDCCAySgAwIBAgIJAN6yZUBL6I4XMA0GCSqGSIb3DQEBCwUAMCIxIDAeBgNV 3 | BAMMF2F6dXJlLWtleXZhdWx0LWVtdWxhdG9yMB4XDTIyMTAwNDEzMzMyMloXDTMy 4 | MDcwMzEzMzMyMlowIjEgMB4GA1UEAwwXYXp1cmUta2V5dmF1bHQtZW11bGF0b3Iw 5 | ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC0NIpwcSq9bQiciTprOLGf 6 | 6TwqvdPAO1XlB3B6m0kVhD3wdj7+R3qePeuhFhAGUCczlbMqg47L2Vca6u7ieIna 7 | mPBA0G+fTk486T96qK26XUumVwdbrs5fNrWCCQlD9O5kV1pp2N3AQ6pi5FdMfOWD 8 | cSdvjRIZdhm/PVFWX2PD9xbyJIrPClIGeEcz/hzx4xeRSF4fRGrCaR8GAOJLbG1e 9 | si8SZIXer/bek0iSlrBCUDjcqFVRg3nFMrc85abO7ZDTHbDYxFbEAG+Jd1UXb/y4 10 | PJCOgK3LTRFprW0U9qPnyrrtHL+zsMCWX4nESeoqkwBsW3XBH/reer3HZOGEk7KJ 11 | OHxZWQGxl+dP+s3kGCjxBCtWbraceFlqbYVwqUG3zZc/M/0g6oNxRuCJS66oUq/5 12 | XnPZUoTvYDoblKFdlKt6ycNjENu2fDzOgsf6c+qNq+ZlT1CAcwR4NGW6lZ3V29+C 13 | cz/ECztfEwpbZdQFl5aCzMmZD5l3/K8HKFltyxQmtwS7K1cHVzxwMvSCVnV87op6 14 | EMpUHZ8895KDmZccb25O+B6pW2VEERCUIxC+O6Vb6M0ppUsfJnOd3Qut77NHYKkg 15 | FHCjetzPRX4zHuntpSKPZ4Ax5wexfm7tIbOqCI1VImEzVYdlO4chPA3ceREz4A4o 16 | eGs3zoJcJe7gVHOWisinYQIDAQABo3UwczBxBgNVHREEajBogglsb2NhbGhvc3SC 17 | F2F6dXJlLWtleXZhdWx0LWVtdWxhdG9yghlsb2NhbGhvc3QudmF1bHQuYXp1cmUu 18 | bmV0gidhenVyZS1rZXl2YXVsdC1lbXVsYXRvci52YXVsdC5henVyZS5uZXQwDQYJ 19 | KoZIhvcNAQELBQADggIBAC4tiVEKxmFUPRTsdejW8hETf2RLs2XZYLx7Nz0fPiJD 20 | xUi2Y39R3yHOrcxxOGMgEIg1HiuVALqWVfwTWhlFkChNDNIjknpMNvK1SONWpYcx 21 | 6RXg41M9N5myAdxrD5lrjsJ5/FlxYMPp5ONJ2g8Iu4iPpuHu6kpBnMLAXDsnsPzh 22 | fHgswJesE3AJlfEJi4Zftv0BPs3cpfrPYaAHCz9kHBsipKbKYc112wwArcqUh9T6 23 | BBu8yDjCmKxcrmo8JhSUu4qjBD+YjLyP5TjBIVF84cDHbw4OTFh0QWk5nntQg3sB 24 | 2vO5KJpcIIjQYMBrHalipj/tKZAVk3KT64QSBECCf+rxH8zDphED/fOmo55K3v0S 25 | Jrewsdxmuo7KBWzklZ9gzbIdow7/fen90QUl1v33C9cXIS5DfodstOsm10H4k34A 26 | pL05Uz1YYxtAT0ezPmwRKTC9bJ5C4INR+m++4YOtZwwXIXKM35mb6PTGhx8l4J/N 27 | yIzrK/kCbPMPxoH2qjvDuPPeNedccQS6ONaV5NLKE6CBmYIumqgKwEf/n6v/Ns62 28 | ka1ym8G5rIFwriE1vOj2GVOlnsh6jsKwfRq8WL+Xom1lCTsXORBS+zc3KT/gy74Q 29 | Dkm/ftCTgP+KQA5Of9P+wLmZSUuqGCUhFuDfna8vwBcRBeVYifqcmeu1nj7ejLzf 30 | -----END CERTIFICATE----- 31 | -------------------------------------------------------------------------------- /local-certs/azure-keyvault-emulator.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFPDCCAySgAwIBAgIJAN6yZUBL6I4XMA0GCSqGSIb3DQEBCwUAMCIxIDAeBgNV 3 | BAMMF2F6dXJlLWtleXZhdWx0LWVtdWxhdG9yMB4XDTIyMTAwNDEzMzMyMloXDTMy 4 | MDcwMzEzMzMyMlowIjEgMB4GA1UEAwwXYXp1cmUta2V5dmF1bHQtZW11bGF0b3Iw 5 | ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC0NIpwcSq9bQiciTprOLGf 6 | 6TwqvdPAO1XlB3B6m0kVhD3wdj7+R3qePeuhFhAGUCczlbMqg47L2Vca6u7ieIna 7 | mPBA0G+fTk486T96qK26XUumVwdbrs5fNrWCCQlD9O5kV1pp2N3AQ6pi5FdMfOWD 8 | cSdvjRIZdhm/PVFWX2PD9xbyJIrPClIGeEcz/hzx4xeRSF4fRGrCaR8GAOJLbG1e 9 | si8SZIXer/bek0iSlrBCUDjcqFVRg3nFMrc85abO7ZDTHbDYxFbEAG+Jd1UXb/y4 10 | PJCOgK3LTRFprW0U9qPnyrrtHL+zsMCWX4nESeoqkwBsW3XBH/reer3HZOGEk7KJ 11 | OHxZWQGxl+dP+s3kGCjxBCtWbraceFlqbYVwqUG3zZc/M/0g6oNxRuCJS66oUq/5 12 | XnPZUoTvYDoblKFdlKt6ycNjENu2fDzOgsf6c+qNq+ZlT1CAcwR4NGW6lZ3V29+C 13 | cz/ECztfEwpbZdQFl5aCzMmZD5l3/K8HKFltyxQmtwS7K1cHVzxwMvSCVnV87op6 14 | EMpUHZ8895KDmZccb25O+B6pW2VEERCUIxC+O6Vb6M0ppUsfJnOd3Qut77NHYKkg 15 | FHCjetzPRX4zHuntpSKPZ4Ax5wexfm7tIbOqCI1VImEzVYdlO4chPA3ceREz4A4o 16 | eGs3zoJcJe7gVHOWisinYQIDAQABo3UwczBxBgNVHREEajBogglsb2NhbGhvc3SC 17 | F2F6dXJlLWtleXZhdWx0LWVtdWxhdG9yghlsb2NhbGhvc3QudmF1bHQuYXp1cmUu 18 | bmV0gidhenVyZS1rZXl2YXVsdC1lbXVsYXRvci52YXVsdC5henVyZS5uZXQwDQYJ 19 | KoZIhvcNAQELBQADggIBAC4tiVEKxmFUPRTsdejW8hETf2RLs2XZYLx7Nz0fPiJD 20 | xUi2Y39R3yHOrcxxOGMgEIg1HiuVALqWVfwTWhlFkChNDNIjknpMNvK1SONWpYcx 21 | 6RXg41M9N5myAdxrD5lrjsJ5/FlxYMPp5ONJ2g8Iu4iPpuHu6kpBnMLAXDsnsPzh 22 | fHgswJesE3AJlfEJi4Zftv0BPs3cpfrPYaAHCz9kHBsipKbKYc112wwArcqUh9T6 23 | BBu8yDjCmKxcrmo8JhSUu4qjBD+YjLyP5TjBIVF84cDHbw4OTFh0QWk5nntQg3sB 24 | 2vO5KJpcIIjQYMBrHalipj/tKZAVk3KT64QSBECCf+rxH8zDphED/fOmo55K3v0S 25 | Jrewsdxmuo7KBWzklZ9gzbIdow7/fen90QUl1v33C9cXIS5DfodstOsm10H4k34A 26 | pL05Uz1YYxtAT0ezPmwRKTC9bJ5C4INR+m++4YOtZwwXIXKM35mb6PTGhx8l4J/N 27 | yIzrK/kCbPMPxoH2qjvDuPPeNedccQS6ONaV5NLKE6CBmYIumqgKwEf/n6v/Ns62 28 | ka1ym8G5rIFwriE1vOj2GVOlnsh6jsKwfRq8WL+Xom1lCTsXORBS+zc3KT/gy74Q 29 | Dkm/ftCTgP+KQA5Of9P+wLmZSUuqGCUhFuDfna8vwBcRBeVYifqcmeu1nj7ejLzf 30 | -----END CERTIFICATE----- 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ################### 2 | # compiled source # 3 | ################### 4 | *.com 5 | *.class 6 | *.dll 7 | *.exe 8 | *.pdb 9 | *.dll.config 10 | *.cache 11 | *.suo 12 | # Include dlls if they’re in the NuGet packages directory 13 | !/packages/*/lib/*.dll 14 | !/packages/*/lib/*/*.dll 15 | # Include dlls if they're in the CommonReferences directory 16 | !*CommonReferences/*.dll 17 | #################### 18 | # VS Upgrade stuff # 19 | #################### 20 | UpgradeLog.XML 21 | _UpgradeReport_Files/ 22 | ############### 23 | # Directories # 24 | ############### 25 | bin/ 26 | obj/ 27 | TestResults/ 28 | ################### 29 | # Web publish log # 30 | ################### 31 | *.Publish.xml 32 | ############# 33 | # Resharper # 34 | ############# 35 | /_ReSharper.* 36 | *.ReSharper.* 37 | ############ 38 | # Packages # 39 | ############ 40 | # it’s better to unpack these files and commit the raw source 41 | # git has its own built in compression methods 42 | *.7z 43 | *.dmg 44 | *.gz 45 | *.iso 46 | *.jar 47 | *.rar 48 | *.tar 49 | *.zip 50 | ###################### 51 | # Logs and databases # 52 | ###################### 53 | *.log 54 | *.sqlite 55 | # OS generated files # 56 | ###################### 57 | .DS_Store? 58 | .DS_Store 59 | ehthumbs.db 60 | Icon? 61 | Thumbs.db 62 | [Bb]in 63 | [Oo]bj 64 | [Tt]est[Rr]esults 65 | *.suo 66 | *.user 67 | *.[Cc]ache 68 | *[Rr]esharper* 69 | packages 70 | NuGet.exe 71 | _[Ss]cripts 72 | *.exe 73 | *.dll 74 | *.nupkg 75 | *.ncrunchsolution 76 | *.dot[Cc]over 77 | *.idea/ 78 | *.DS_Store 79 | **/wwwroot/lib/* 80 | **/wwwroot/dist/* 81 | .vs/ 82 | node_modules/ 83 | ci/pipeline-secrets.yml 84 | tf/.terraform/ 85 | tf/terraform.tfstate.d/ 86 | .vscode 87 | out/ 88 | *.txt 89 | 90 | keys/ 91 | !AzureKeyVaultEmulator/Keys 92 | publish/ 93 | sftp/ 94 | newrelic/ 95 | .idea/ 96 | # Fake GraphQL directives # 97 | ########################### 98 | _not-imported-dev.graphql 99 | local-certs/local* 100 | -------------------------------------------------------------------------------- /AzureKeyVaultEmulator.AcceptanceTests/Secrets/CreateSecretTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Azure.Security.KeyVault.Secrets; 4 | using AzureKeyVaultEmulator.AcceptanceTests.Helpers; 5 | using Xunit; 6 | 7 | namespace AzureKeyVaultEmulator.AcceptanceTests.Secrets 8 | { 9 | public class CreateSecretTests 10 | { 11 | private readonly SecretClient _secretClient; 12 | 13 | public CreateSecretTests() 14 | { 15 | _secretClient = new SecretClient(new Uri("https://localhost.vault.azure.net:5551/"), 16 | new LocalTokenCredential(), 17 | new SecretClientOptions 18 | { 19 | DisableChallengeResourceVerification = true 20 | }); 21 | } 22 | 23 | [Fact] 24 | public async Task ShouldBeAbleToCreateASecret() 25 | { 26 | var secret = new KeyVaultSecret(Guid.NewGuid().ToString(), Guid.NewGuid().ToString()) 27 | { 28 | Properties = 29 | { 30 | Enabled = true, 31 | ExpiresOn = DateTimeOffset.UtcNow.AddDays(1), 32 | NotBefore = DateTimeOffset.UtcNow, 33 | Tags = 34 | { 35 | { "environment", "local" }, 36 | { "testing", "true" } 37 | } 38 | } 39 | }; 40 | 41 | var result = await _secretClient.SetSecretAsync(secret); 42 | Assert.NotNull(result); 43 | 44 | KeyVaultSecret createdSecret = result.Value; 45 | Assert.NotNull(createdSecret); 46 | 47 | Assert.NotNull(createdSecret.Id); 48 | Assert.Equal(secret.Value, createdSecret.Value); 49 | Assert.Equal(secret.Properties.Enabled, createdSecret.Properties.Enabled); 50 | Assert.Equal(secret.Properties.ExpiresOn.Value.ToUnixTimeSeconds(), 51 | createdSecret.Properties.ExpiresOn.GetValueOrDefault().ToUnixTimeSeconds()); 52 | Assert.Equal(secret.Properties.NotBefore.Value.ToUnixTimeSeconds(), 53 | createdSecret.Properties.NotBefore.GetValueOrDefault().ToUnixTimeSeconds()); 54 | Assert.NotNull(createdSecret.Properties.Version); 55 | Assert.Equal("local", createdSecret.Properties.Tags["environment"]); 56 | Assert.Equal("true", createdSecret.Properties.Tags["testing"]); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /AzureKeyVaultEmulator/Secrets/Services/KeyVaultSecretService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using AzureKeyVaultEmulator.Secrets.Models; 4 | using Microsoft.AspNetCore.Http; 5 | 6 | namespace AzureKeyVaultEmulator.Secrets.Services 7 | { 8 | public interface IKeyVaultSecretService 9 | { 10 | SecretResponse Get(string name); 11 | SecretResponse Get(string name, string version); 12 | SecretResponse SetSecret(string name, SetSecretModel requestBody); 13 | } 14 | 15 | public class KeyVaultSecretService : IKeyVaultSecretService 16 | { 17 | private readonly IHttpContextAccessor _httpContextAccessor; 18 | private static readonly ConcurrentDictionary Secrets = new(); 19 | 20 | public KeyVaultSecretService(IHttpContextAccessor httpContextAccessor) 21 | { 22 | _httpContextAccessor = httpContextAccessor; 23 | } 24 | 25 | public SecretResponse Get(string name) 26 | { 27 | Secrets.TryGetValue(GetSecretCacheId(name), out var found); 28 | 29 | return found; 30 | } 31 | 32 | public SecretResponse Get(string name, string version) 33 | { 34 | Secrets.TryGetValue(GetSecretCacheId(name, version), out var found); 35 | 36 | return found; 37 | } 38 | 39 | public SecretResponse SetSecret(string name, SetSecretModel secret) 40 | { 41 | var version = Guid.NewGuid().ToString(); 42 | var secretUrl = new UriBuilder 43 | { 44 | Scheme = _httpContextAccessor.HttpContext.Request.Scheme, 45 | Host = _httpContextAccessor.HttpContext.Request.Host.Host, 46 | Port = _httpContextAccessor.HttpContext.Request.Host.Port ?? -1, 47 | Path = $"secrets/{name}/{version}" 48 | }; 49 | 50 | var response = new SecretResponse 51 | { 52 | Id = secretUrl.Uri, 53 | Value = secret.Value, 54 | Attributes = secret.SecretAttributes, 55 | Tags = secret.Tags 56 | }; 57 | 58 | Secrets.AddOrUpdate(GetSecretCacheId(name), response, (_, _) => response); 59 | Secrets.TryAdd(GetSecretCacheId(name, version), response); 60 | 61 | return response; 62 | } 63 | 64 | private static string GetSecretCacheId(string name, string version = null) => name + (version ?? ""); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /AzureKeyVaultEmulator/Secrets/Controllers/SecretsController.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using AzureKeyVaultEmulator.Keys.Models; 3 | using AzureKeyVaultEmulator.Secrets.Models; 4 | using AzureKeyVaultEmulator.Secrets.Services; 5 | using Microsoft.AspNetCore.Authorization; 6 | using Microsoft.AspNetCore.Http; 7 | using Microsoft.AspNetCore.Mvc; 8 | 9 | namespace AzureKeyVaultEmulator.Secrets.Controllers 10 | { 11 | [ApiController] 12 | [Route("secrets/{name}")] 13 | [Authorize] 14 | public class SecretsController : ControllerBase 15 | { 16 | private readonly IKeyVaultSecretService _keyVaultSecretService; 17 | 18 | public SecretsController(IKeyVaultSecretService keyVaultSecretService) 19 | { 20 | _keyVaultSecretService = keyVaultSecretService; 21 | } 22 | 23 | [HttpPut] 24 | [Produces("application/json")] 25 | [Consumes("application/json")] 26 | [ProducesResponseType(typeof(KeyResponse), StatusCodes.Status200OK)] 27 | public IActionResult SetSecret( 28 | [RegularExpression("[a-zA-Z0-9-]+")][FromRoute] string name, 29 | [FromQuery(Name = "api-version")] string apiVersion, 30 | [FromBody] SetSecretModel requestBody) 31 | { 32 | var secret = _keyVaultSecretService.SetSecret(name, requestBody); 33 | 34 | return Ok(secret); 35 | } 36 | 37 | [HttpGet("{version}")] 38 | [Produces("application/json")] 39 | [ProducesResponseType(typeof(KeyResponse), StatusCodes.Status200OK)] 40 | public IActionResult GetSecret( 41 | [FromRoute] string name, 42 | [FromRoute] string version, 43 | [FromQuery(Name = "api-version")] string apiVersion) 44 | { 45 | var secretResult = _keyVaultSecretService.Get(name, version); 46 | 47 | if (secretResult == null) return NotFound(); 48 | 49 | return Ok(secretResult); 50 | } 51 | 52 | [HttpGet] 53 | [Produces("application/json")] 54 | [ProducesResponseType(typeof(KeyResponse), StatusCodes.Status200OK)] 55 | public IActionResult GetSecret( 56 | [FromRoute] string name, 57 | [FromQuery(Name = "api-version")] string apiVersion) 58 | { 59 | var secretResult = _keyVaultSecretService.Get(name); 60 | 61 | if (secretResult == null) return NotFound(); 62 | 63 | return Ok(secretResult); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # [1.3.0](https://github.com/Basis-Theory/azure-keyvault-emulator/compare/v1.2.1...v1.3.0) (2024-10-29) 2 | 3 | 4 | ### Features 5 | 6 | * update secrets and workflows ([#147](https://github.com/Basis-Theory/azure-keyvault-emulator/issues/147)) ([e2a1edd](https://github.com/Basis-Theory/azure-keyvault-emulator/commit/e2a1edd23b9edf2482a1ecc89d8317d517306ef9)) 7 | 8 | ## [1.2.1](https://github.com/Basis-Theory/azure-keyvault-emulator/compare/v1.2.0...v1.2.1) (2024-05-21) 9 | 10 | 11 | ### Bug Fixes 12 | 13 | * use scope provided in the request context ([0b8e733](https://github.com/Basis-Theory/azure-keyvault-emulator/commit/0b8e733524226e2b626c31872e49c0d5144a318c)) 14 | 15 | # [1.2.0](https://github.com/Basis-Theory/azure-keyvault-emulator/compare/v1.1.3...v1.2.0) (2022-10-06) 16 | 17 | 18 | ### Features 19 | 20 | * upgrade deps and use valid keyvault domains ([#100](https://github.com/Basis-Theory/azure-keyvault-emulator/issues/100)) ([edc500b](https://github.com/Basis-Theory/azure-keyvault-emulator/commit/edc500be969ba564ec06735aadb026205d7dc8ff)) 21 | 22 | ## [1.1.3](https://github.com/Basis-Theory/azure-keyvault-emulator/compare/v1.1.2...v1.1.3) (2022-04-13) 23 | 24 | 25 | ### Bug Fixes 26 | 27 | * add security requirements to swagger and document Auth requirements ([#71](https://github.com/Basis-Theory/azure-keyvault-emulator/issues/71)) ([ecd25d1](https://github.com/Basis-Theory/azure-keyvault-emulator/commit/ecd25d1d3d2407105d2cff8f7eb00201eeaa2c1b)) 28 | 29 | ## [1.1.2](https://github.com/Basis-Theory/azure-keyvault-emulator/compare/v1.1.1...v1.1.2) (2022-03-28) 30 | 31 | 32 | ### Bug Fixes 33 | 34 | * **deps:** bump minimist from 1.2.5 to 1.2.6 ([#66](https://github.com/Basis-Theory/azure-keyvault-emulator/issues/66)) ([926fb11](https://github.com/Basis-Theory/azure-keyvault-emulator/commit/926fb115d149cf670c0be34540c4a704f6d04f73)) 35 | 36 | ## [1.1.1](https://github.com/Basis-Theory/azure-keyvault-emulator/compare/v1.1.0...v1.1.1) (2021-11-11) 37 | 38 | 39 | ### Bug Fixes 40 | 41 | * publish linux/amd64 and linux/arm64 image platforms ([#49](https://github.com/Basis-Theory/azure-keyvault-emulator/issues/49)) ([28dd42e](https://github.com/Basis-Theory/azure-keyvault-emulator/commit/28dd42e9e54ecaf01d3dab1eef5fead4261f27a1)) 42 | * publish multi-platfrom docker image with docker build-push-action ([#50](https://github.com/Basis-Theory/azure-keyvault-emulator/issues/50)) ([838168e](https://github.com/Basis-Theory/azure-keyvault-emulator/commit/838168e98fd8df4695447add71e8dbe97237a6db)) 43 | 44 | # [1.1.0](https://github.com/Basis-Theory/azure-keyvault-emulator/compare/v1.0.0...v1.1.0) (2021-11-11) 45 | 46 | 47 | ### Features 48 | 49 | * upgrade to .NET 6 and use docker image that supports both ARM and Intel CPU architectures ([#48](https://github.com/Basis-Theory/azure-keyvault-emulator/issues/48)) ([b8d5371](https://github.com/Basis-Theory/azure-keyvault-emulator/commit/b8d5371c8a44fef9be18d6616aa1dfa9c8b567e4)) 50 | 51 | # 1.0.0 (2021-08-07) 52 | 53 | 54 | ### Features 55 | 56 | * Initial commit ([6e7830e](https://github.com/Basis-Theory/azure-keyvault-emulator/commit/6e7830e25f6091385455b94af7213ae815723ed6)) 57 | -------------------------------------------------------------------------------- /azure-keyvault-emulator.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | 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 51 | V20NPv6LYUshyYwOo3z25EX57WbuMnuK 52 | -----END PRIVATE KEY----- 53 | -------------------------------------------------------------------------------- /local-certs/azure-keyvault-emulator.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | 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 51 | V20NPv6LYUshyYwOo3z25EX57WbuMnuK 52 | -----END PRIVATE KEY----- 53 | -------------------------------------------------------------------------------- /AzureKeyVaultEmulator/Keys/Controllers/KeysController.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using AzureKeyVaultEmulator.Keys.Models; 3 | using AzureKeyVaultEmulator.Keys.Services; 4 | using Microsoft.AspNetCore.Authorization; 5 | using Microsoft.AspNetCore.Http; 6 | using Microsoft.AspNetCore.Mvc; 7 | 8 | namespace AzureKeyVaultEmulator.Keys.Controllers 9 | { 10 | [ApiController] 11 | [Route("keys/{name}")] 12 | [Authorize] 13 | public class KeysController : ControllerBase 14 | { 15 | private readonly IKeyVaultKeyService _keyVaultKeyService; 16 | 17 | public KeysController(IKeyVaultKeyService keyVaultKeyService) 18 | { 19 | _keyVaultKeyService = keyVaultKeyService; 20 | } 21 | 22 | [HttpPost("create")] 23 | [Produces("application/json")] 24 | [Consumes("application/json")] 25 | [ProducesResponseType(typeof(KeyResponse), StatusCodes.Status200OK)] 26 | public IActionResult CreateKey( 27 | [RegularExpression("[a-zA-Z0-9-]+")][FromRoute] string name, 28 | [FromQuery(Name = "api-version")] string apiVersion, 29 | [FromBody] CreateKeyModel requestBody) 30 | { 31 | var createdKey = _keyVaultKeyService.CreateKey(name, requestBody); 32 | 33 | return Ok(createdKey); 34 | } 35 | 36 | [HttpGet("{version}")] 37 | [Produces("application/json")] 38 | [ProducesResponseType(typeof(KeyResponse), StatusCodes.Status200OK)] 39 | public IActionResult GetKey( 40 | [FromRoute] string name, 41 | [FromRoute] string version, 42 | [FromQuery(Name = "api-version")] string apiVersion) 43 | { 44 | var keyResult = _keyVaultKeyService.Get(name, version); 45 | 46 | if (keyResult == null) return NotFound(); 47 | 48 | return Ok(keyResult); 49 | } 50 | 51 | [HttpGet] 52 | [Produces("application/json")] 53 | [ProducesResponseType(typeof(KeyResponse), StatusCodes.Status200OK)] 54 | public IActionResult GetKey( 55 | [FromRoute] string name, 56 | [FromQuery(Name = "api-version")] string apiVersion) 57 | { 58 | var keyResult = _keyVaultKeyService.Get(name); 59 | 60 | if (keyResult == null) return NotFound(); 61 | 62 | return Ok(keyResult); 63 | } 64 | 65 | [HttpPost("{version}/encrypt")] 66 | [Produces("application/json")] 67 | [Consumes("application/json")] 68 | public IActionResult Encrypt( 69 | [FromRoute] string name, 70 | [FromRoute] string version, 71 | [FromQuery(Name = "api-version")] string apiVersion, 72 | [FromBody] KeyOperationParameters keyOperationParameters) 73 | { 74 | var result = _keyVaultKeyService.Encrypt(name, version, keyOperationParameters); 75 | 76 | return Ok(result); 77 | } 78 | 79 | [HttpPost("{version}/decrypt")] 80 | [Produces("application/json")] 81 | [Consumes("application/json")] 82 | public IActionResult Decrypt( 83 | [FromRoute] string name, 84 | [FromRoute] string version, 85 | [FromQuery(Name = "api-version")] string apiVersion, 86 | [FromBody] KeyOperationParameters keyOperationParameters) 87 | { 88 | var result = _keyVaultKeyService.Decrypt(name, version, keyOperationParameters); 89 | 90 | return Ok(result); 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /AzureKeyVaultEmulator.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AzureKeyVaultEmulator", "AzureKeyVaultEmulator\AzureKeyVaultEmulator.csproj", "{ABAEE20D-4E4B-45CF-ABEA-A02263F63615}" 4 | EndProject 5 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AzureKeyVaultEmulator.AcceptanceTests", "AzureKeyVaultEmulator.AcceptanceTests\AzureKeyVaultEmulator.AcceptanceTests.csproj", "{181CD468-BBCA-424A-97A7-5ACC8331FE5A}" 6 | EndProject 7 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{C9BA3288-4BA1-4BA4-A7FB-3729ABFC8488}" 8 | ProjectSection(SolutionItems) = preProject 9 | makefile = makefile 10 | README.md = README.md 11 | .gitignore = .gitignore 12 | .dockerignore = .dockerignore 13 | Dockerfile = Dockerfile 14 | docker-compose.yml = docker-compose.yml 15 | .releaserc.json = .releaserc.json 16 | CODE_OF_CONDUCT.md = CODE_OF_CONDUCT.md 17 | LICENSE = LICENSE 18 | package.json = package.json 19 | CHANGELOG.md = CHANGELOG.md 20 | EndProjectSection 21 | EndProject 22 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Scripts", "Scripts", "{EEEEDF54-ADE9-447E-8ED7-36A9E3CC0FD0}" 23 | ProjectSection(SolutionItems) = preProject 24 | scripts\acceptancetest.sh = scripts\acceptancetest.sh 25 | scripts\dependencycheck.sh = scripts\dependencycheck.sh 26 | scripts\serviceup.sh = scripts\serviceup.sh 27 | scripts\startdocker.sh = scripts\startdocker.sh 28 | scripts\stopdocker.sh = scripts\stopdocker.sh 29 | scripts\stopservice.sh = scripts\stopservice.sh 30 | scripts\verify.sh = scripts\verify.sh 31 | scripts\release.sh = scripts\release.sh 32 | scripts\importcert.sh = scripts\importcert.sh 33 | EndProjectSection 34 | EndProject 35 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Pipeline", "Pipeline", "{B4453CBE-DBCF-4CBF-B6DC-B951DA62668A}" 36 | ProjectSection(SolutionItems) = preProject 37 | .github\workflows\pull-request.yml = .github\workflows\pull-request.yml 38 | .github\workflows\verify.yml = .github\workflows\verify.yml 39 | .github\dependabot.yml = .github\dependabot.yml 40 | .github\CODEOWNERS = .github\CODEOWNERS 41 | .github\ISSUE_TEMPLATE.md = .github\ISSUE_TEMPLATE.md 42 | .github\PULL_REQUEST_TEMPLATE.md = .github\PULL_REQUEST_TEMPLATE.md 43 | EndProjectSection 44 | EndProject 45 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Checks", "Checks", "{9D107928-7669-4B0C-9291-F5318F9F334B}" 46 | ProjectSection(SolutionItems) = preProject 47 | scripts\checks\dockercheck.sh = scripts\checks\dockercheck.sh 48 | scripts\checks\dockercomposecheck.sh = scripts\checks\dockercomposecheck.sh 49 | scripts\checks\dotnetcheck.sh = scripts\checks\dotnetcheck.sh 50 | EndProjectSection 51 | EndProject 52 | Global 53 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 54 | Debug|Any CPU = Debug|Any CPU 55 | Release|Any CPU = Release|Any CPU 56 | EndGlobalSection 57 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 58 | {ABAEE20D-4E4B-45CF-ABEA-A02263F63615}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 59 | {ABAEE20D-4E4B-45CF-ABEA-A02263F63615}.Debug|Any CPU.Build.0 = Debug|Any CPU 60 | {ABAEE20D-4E4B-45CF-ABEA-A02263F63615}.Release|Any CPU.ActiveCfg = Release|Any CPU 61 | {ABAEE20D-4E4B-45CF-ABEA-A02263F63615}.Release|Any CPU.Build.0 = Release|Any CPU 62 | {181CD468-BBCA-424A-97A7-5ACC8331FE5A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 63 | {181CD468-BBCA-424A-97A7-5ACC8331FE5A}.Debug|Any CPU.Build.0 = Debug|Any CPU 64 | {181CD468-BBCA-424A-97A7-5ACC8331FE5A}.Release|Any CPU.ActiveCfg = Release|Any CPU 65 | {181CD468-BBCA-424A-97A7-5ACC8331FE5A}.Release|Any CPU.Build.0 = Release|Any CPU 66 | EndGlobalSection 67 | GlobalSection(NestedProjects) = preSolution 68 | {9D107928-7669-4B0C-9291-F5318F9F334B} = {EEEEDF54-ADE9-447E-8ED7-36A9E3CC0FD0} 69 | EndGlobalSection 70 | EndGlobal 71 | -------------------------------------------------------------------------------- /AzureKeyVaultEmulator/Keys/Services/KeyVaultKeyService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using AzureKeyVaultEmulator.Keys.Factories; 4 | using AzureKeyVaultEmulator.Keys.Models; 5 | using Microsoft.AspNetCore.Http; 6 | using Microsoft.IdentityModel.Tokens; 7 | 8 | namespace AzureKeyVaultEmulator.Keys.Services 9 | { 10 | public interface IKeyVaultKeyService 11 | { 12 | KeyResponse Get(string name); 13 | KeyResponse Get(string name, string version); 14 | KeyResponse CreateKey(string name, CreateKeyModel key); 15 | 16 | KeyOperationResult Encrypt(string name, string version, KeyOperationParameters keyOperationParameters); 17 | KeyOperationResult Decrypt(string keyName, string keyVersion, KeyOperationParameters keyOperationParameters); 18 | } 19 | 20 | public class KeyVaultKeyService : IKeyVaultKeyService 21 | { 22 | private readonly IHttpContextAccessor _httpContextAccessor; 23 | private static readonly ConcurrentDictionary Keys = new(); 24 | 25 | public KeyVaultKeyService(IHttpContextAccessor httpContextAccessor) 26 | { 27 | _httpContextAccessor = httpContextAccessor; 28 | } 29 | 30 | public KeyResponse Get(string name) 31 | { 32 | Keys.TryGetValue(GetCacheId(name), out var found); 33 | 34 | return found; 35 | } 36 | 37 | public KeyResponse Get(string name, string version) 38 | { 39 | Keys.TryGetValue(GetCacheId(name, version), out var found); 40 | 41 | return found; 42 | } 43 | 44 | public KeyResponse CreateKey(string name, CreateKeyModel key) 45 | { 46 | JsonWebKeyModel jsonWebKeyModel; 47 | switch (key.KeyType) 48 | { 49 | case "RSA": 50 | var rsaKey = RsaKeyFactory.CreateRsaKey(key.KeySize); 51 | jsonWebKeyModel = new JsonWebKeyModel(rsaKey); 52 | break; 53 | 54 | default: 55 | throw new NotImplementedException($"KeyType {key.KeyType} is not supported"); 56 | } 57 | 58 | var version = Guid.NewGuid().ToString(); 59 | var keyUrl = new UriBuilder 60 | { 61 | Scheme = _httpContextAccessor.HttpContext.Request.Scheme, 62 | Host = _httpContextAccessor.HttpContext.Request.Host.Host, 63 | Port = _httpContextAccessor.HttpContext.Request.Host.Port ?? -1, 64 | Path = $"keys/{name}/{version}" 65 | }; 66 | 67 | jsonWebKeyModel.KeyName = name; 68 | jsonWebKeyModel.KeyVersion = version; 69 | jsonWebKeyModel.KeyIdentifier = keyUrl.Uri.ToString(); 70 | jsonWebKeyModel.KeyOperations = key.KeyOperations; 71 | 72 | var response = new KeyResponse 73 | { 74 | Key = jsonWebKeyModel, 75 | Attributes = key.KeyAttributes, 76 | Tags = key.Tags 77 | }; 78 | 79 | Keys.AddOrUpdate(GetCacheId(name), response, (_, _) => response); 80 | Keys.TryAdd(GetCacheId(name, version), response); 81 | 82 | return response; 83 | } 84 | 85 | public KeyOperationResult Encrypt(string name, string version, KeyOperationParameters keyOperationParameters) 86 | { 87 | if (!Keys.TryGetValue(GetCacheId(name, version), out var foundKey)) 88 | throw new Exception("Key not found"); 89 | 90 | var encrypted = Base64UrlEncoder.Encode(foundKey.Key.Encrypt(keyOperationParameters)); 91 | 92 | return new KeyOperationResult 93 | { 94 | KeyIdentifier = foundKey.Key.KeyIdentifier, 95 | Data = encrypted 96 | }; 97 | } 98 | 99 | public KeyOperationResult Decrypt(string keyName, string keyVersion, KeyOperationParameters keyOperationParameters) 100 | { 101 | if (!Keys.TryGetValue(GetCacheId(keyName, keyVersion), out var foundKey)) 102 | throw new Exception("Key not found"); 103 | 104 | var decrypted = foundKey.Key.Decrypt(keyOperationParameters); 105 | 106 | return new KeyOperationResult 107 | { 108 | KeyIdentifier = foundKey.Key.KeyIdentifier, 109 | Data = decrypted 110 | }; 111 | } 112 | 113 | private static string GetCacheId(string name, string version = null) => name + (version ?? ""); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /AzureKeyVaultEmulator/Keys/Models/JsonWebKeyModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Security.Cryptography; 4 | using System.Text.Json.Serialization; 5 | using AzureKeyVaultEmulator.Keys.Constants; 6 | using Microsoft.IdentityModel.Tokens; 7 | 8 | namespace AzureKeyVaultEmulator.Keys.Models 9 | { 10 | public class JsonWebKeyModel 11 | { 12 | [JsonPropertyName("crv")] 13 | public string KeyCurve { get; set; } 14 | 15 | [JsonPropertyName("d")] 16 | [JsonIgnore] 17 | public string D { get; set; } 18 | 19 | [JsonPropertyName("dp")] 20 | public string Dp { get; set; } 21 | 22 | [JsonPropertyName("dq")] 23 | public string Dq { get; set; } 24 | 25 | [JsonPropertyName("e")] 26 | public string E { get; set; } 27 | 28 | [JsonPropertyName("k")] 29 | public string K { get; set; } 30 | 31 | [JsonPropertyName("key_hsm")] 32 | public string KeyHsm { get; set; } 33 | 34 | [JsonPropertyName("key_ops")] 35 | public List KeyOperations { get; set; } 36 | 37 | [JsonPropertyName("kty")] 38 | public string KeyType { get; set; } 39 | 40 | [JsonPropertyName("kid")] 41 | public string KeyIdentifier { get; set; } 42 | 43 | [JsonIgnore] 44 | public string KeyName { get; set; } 45 | 46 | [JsonIgnore] 47 | public string KeyVersion { get; set; } 48 | 49 | [JsonPropertyName("n")] 50 | public string N { get; set; } 51 | 52 | [JsonPropertyName("p")] 53 | public string P { get; set; } 54 | 55 | [JsonPropertyName("q")] 56 | public string Q { get; set; } 57 | 58 | [JsonPropertyName("qi")] 59 | public string Qi { get; set; } 60 | 61 | [JsonPropertyName("x")] 62 | public string x { get; set; } 63 | 64 | [JsonPropertyName("y")] 65 | public string y { get; set; } 66 | 67 | private readonly RSA _rsaKey; 68 | private readonly RSAParameters _rsaParameters; 69 | 70 | public JsonWebKeyModel() 71 | { 72 | } 73 | 74 | public JsonWebKeyModel(RSA rsaKey) 75 | { 76 | _rsaKey = rsaKey; 77 | _rsaParameters = rsaKey.ExportParameters(true); 78 | D = Base64UrlEncoder.Encode(_rsaParameters.D); 79 | Dp = Base64UrlEncoder.Encode(_rsaParameters.DP); 80 | Dq = Base64UrlEncoder.Encode(_rsaParameters.DQ); 81 | E = Base64UrlEncoder.Encode(_rsaParameters.Exponent); 82 | D = Base64UrlEncoder.Encode(_rsaParameters.D); 83 | KeyType = "RSA"; 84 | N = Base64UrlEncoder.Encode(_rsaParameters.Modulus); 85 | P = Base64UrlEncoder.Encode(_rsaParameters.P); 86 | Q = Base64UrlEncoder.Encode(_rsaParameters.Q); 87 | Qi = Base64UrlEncoder.Encode(_rsaParameters.InverseQ); 88 | } 89 | 90 | public byte[] Encrypt(KeyOperationParameters data) 91 | { 92 | return data.Algorithm switch 93 | { 94 | EncryptionAlgorithms.RSA1_5 => RsaEncrypt(data.Data, RSAEncryptionPadding.Pkcs1), 95 | EncryptionAlgorithms.RSA_OAEP => RsaEncrypt(data.Data, RSAEncryptionPadding.OaepSHA1), 96 | _ => throw new NotImplementedException($"Algorithm '{data.Algorithm}' does not support Encryption") 97 | }; 98 | } 99 | 100 | private byte[] RsaEncrypt(string plaintext, RSAEncryptionPadding padding) 101 | { 102 | using var rsaAlg = new RSACryptoServiceProvider(_rsaKey.KeySize); 103 | rsaAlg.ImportParameters(_rsaParameters); 104 | return rsaAlg.Encrypt(Base64UrlEncoder.DecodeBytes(plaintext), padding); 105 | } 106 | 107 | public string Decrypt(KeyOperationParameters data) 108 | { 109 | return data.Algorithm switch 110 | { 111 | EncryptionAlgorithms.RSA1_5 => RsaDecrypt(data.Data, RSAEncryptionPadding.Pkcs1), 112 | EncryptionAlgorithms.RSA_OAEP => RsaDecrypt(data.Data, RSAEncryptionPadding.OaepSHA1), 113 | _ => throw new NotImplementedException($"Algorithm '{data.Algorithm}' does not support Decryption") 114 | }; 115 | } 116 | 117 | private string RsaDecrypt(string ciphertext, RSAEncryptionPadding padding) 118 | { 119 | using var rsaAlg = new RSACryptoServiceProvider(_rsaKey.KeySize); 120 | rsaAlg.ImportParameters(_rsaParameters); 121 | return Base64UrlEncoder.Encode(rsaAlg.Decrypt(Base64UrlEncoder.DecodeBytes(ciphertext), padding)); 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /AzureKeyVaultEmulator/Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IdentityModel.Tokens.Jwt; 3 | using System.Text.Json.Serialization; 4 | using System.Threading.Tasks; 5 | using AzureKeyVaultEmulator.Keys.Services; 6 | using AzureKeyVaultEmulator.Secrets.Services; 7 | using Microsoft.AspNetCore.Authentication.JwtBearer; 8 | using Microsoft.AspNetCore.Builder; 9 | using Microsoft.AspNetCore.Hosting; 10 | using Microsoft.Extensions.Configuration; 11 | using Microsoft.Extensions.DependencyInjection; 12 | using Microsoft.Extensions.Hosting; 13 | using Microsoft.IdentityModel.Tokens; 14 | using Microsoft.OpenApi.Models; 15 | 16 | namespace AzureKeyVaultEmulator 17 | { 18 | public class Startup 19 | { 20 | private readonly IConfiguration _configuration; 21 | 22 | public Startup(IConfiguration configuration) 23 | { 24 | _configuration = configuration; 25 | } 26 | 27 | public void ConfigureServices(IServiceCollection services) 28 | { 29 | services.AddControllers() 30 | .AddJsonOptions(o => 31 | { 32 | o.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; 33 | }); 34 | 35 | services.AddSwaggerGen(c => 36 | { 37 | c.SwaggerDoc("v1", new OpenApiInfo { Title = "Azure KeyVault Emulator", Version = "v1" }); 38 | c.AddSecurityDefinition("JWT", new OpenApiSecurityScheme 39 | { 40 | Name = "Authorization", 41 | In = ParameterLocation.Header, 42 | Type = SecuritySchemeType.Http, 43 | Description = "JWT Authorization header using the Bearer scheme. Use 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE4OTAyMzkwMjIsImlzcyI6Imh0dHBzOi8vbG9jYWxob3N0OjUwMDEvIn0.bHLeGTRqjJrmIJbErE-1Azs724E5ibzvrIc-UQL6pws'", 44 | Scheme = JwtBearerDefaults.AuthenticationScheme, 45 | }); 46 | c.AddSecurityRequirement(new OpenApiSecurityRequirement 47 | { 48 | { 49 | new OpenApiSecurityScheme 50 | { 51 | Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "JWT" } 52 | }, 53 | Array.Empty() 54 | } 55 | }); 56 | }); 57 | 58 | services.AddHttpContextAccessor(); 59 | services.AddScoped(); 60 | services.AddScoped(); 61 | 62 | services.AddAuthentication(x => 63 | { 64 | x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; 65 | x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; 66 | }) 67 | .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, x => 68 | { 69 | x.TokenValidationParameters = new TokenValidationParameters 70 | { 71 | ValidIssuer = "https://localhost:5001/", 72 | ValidateIssuer = false, 73 | ValidateAudience = false, 74 | ValidateLifetime = false, 75 | RequireSignedTokens = false, 76 | ValidateIssuerSigningKey = false, 77 | TryAllIssuerSigningKeys = false, 78 | SignatureValidator = (token, _) => new JwtSecurityToken(token) 79 | }; 80 | 81 | x.Events = new JwtBearerEvents 82 | { 83 | OnChallenge = context => 84 | { 85 | var requestHostSplit = context.Request.Host.ToString().Split(".", 2); 86 | var scope = $"https://{requestHostSplit[^1]}/.default"; 87 | context.Response.Headers.Remove("WWW-Authenticate"); 88 | context.Response.Headers["WWW-Authenticate"] = $"Bearer authorization=\"https://localhost:5001/foo/bar\", scope=\"{scope}\", resource=\"https://vault.azure.net\""; 89 | return Task.CompletedTask; 90 | } 91 | }; 92 | }); 93 | } 94 | 95 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 96 | { 97 | if (env.IsDevelopment()) 98 | { 99 | app.UseDeveloperExceptionPage(); 100 | app.UseSwagger(); 101 | app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "Azure KeyVault Emulator v1")); 102 | } 103 | 104 | app.UseHttpsRedirection(); 105 | 106 | app.UseRouting(); 107 | 108 | app.UseAuthentication(); 109 | app.UseAuthorization(); 110 | 111 | app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /AzureKeyVaultEmulator.AcceptanceTests/Secrets/GetSecretTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Azure; 4 | using Azure.Security.KeyVault.Secrets; 5 | using AzureKeyVaultEmulator.AcceptanceTests.Helpers; 6 | using Xunit; 7 | 8 | namespace AzureKeyVaultEmulator.AcceptanceTests.Secrets 9 | { 10 | public class GetSecretTests 11 | { 12 | private readonly SecretClient _secretClient; 13 | 14 | public GetSecretTests() 15 | { 16 | _secretClient = new SecretClient(new Uri("https://localhost.vault.azure.net:5551/"), new LocalTokenCredential()); 17 | } 18 | 19 | [Fact] 20 | public async Task ShouldBeAbleToGetLatestSecretVersionByName() 21 | { 22 | var expectedName = Guid.NewGuid().ToString(); 23 | 24 | var secret1 = await CreateSecret(expectedName); 25 | var actualLatest = await _secretClient.GetSecretAsync(expectedName); 26 | Assert.Equal(secret1.Value.Id, actualLatest.Value.Id); 27 | Assert.Equal(secret1.Value.Value, actualLatest.Value.Value); 28 | Assert.Equal(secret1.Value.Properties.Enabled, actualLatest.Value.Properties.Enabled); 29 | Assert.Equal(secret1.Value.Properties.NotBefore, actualLatest.Value.Properties.NotBefore); 30 | Assert.Equal(secret1.Value.Properties.ExpiresOn, actualLatest.Value.Properties.ExpiresOn); 31 | Assert.Equal(secret1.Value.Properties.Version, actualLatest.Value.Properties.Version); 32 | 33 | var secret2 = await CreateSecret(expectedName); 34 | actualLatest = await _secretClient.GetSecretAsync(expectedName); 35 | Assert.Equal(secret2.Value.Id, actualLatest.Value.Id); 36 | Assert.Equal(secret2.Value.Value, actualLatest.Value.Value); 37 | Assert.Equal(secret2.Value.Properties.Enabled, actualLatest.Value.Properties.Enabled); 38 | Assert.Equal(secret2.Value.Properties.NotBefore, actualLatest.Value.Properties.NotBefore); 39 | Assert.Equal(secret2.Value.Properties.ExpiresOn, actualLatest.Value.Properties.ExpiresOn); 40 | Assert.Equal(secret2.Value.Properties.Version, actualLatest.Value.Properties.Version); 41 | } 42 | 43 | [Fact] 44 | public async Task ShouldBeAbleToGetSecretByNameAndVersion() 45 | { 46 | var expectedName = Guid.NewGuid().ToString(); 47 | var expectedSecret = await CreateSecret(expectedName); 48 | 49 | var actualLatestSecret = await _secretClient.GetSecretAsync(expectedName, expectedSecret.Value.Properties.Version); 50 | Assert.Equal(expectedSecret.Value.Id, actualLatestSecret.Value.Id); 51 | Assert.Equal(expectedSecret.Value.Properties.Version, actualLatestSecret.Value.Properties.Version); 52 | } 53 | 54 | [Fact] 55 | public async Task ShouldBeAbleToGetSecretByNameAndVersionWhenNewerVersionExists() 56 | { 57 | var expectedName = Guid.NewGuid().ToString(); 58 | var expected = await CreateSecret(expectedName); 59 | 60 | await CreateSecret(expectedName); 61 | 62 | var actualLatest = await _secretClient.GetSecretAsync(expectedName, expected.Value.Properties.Version); 63 | Assert.Equal(expected.Value.Id, actualLatest.Value.Id); 64 | Assert.Equal(expected.Value.Properties.Version, actualLatest.Value.Properties.Version); 65 | } 66 | 67 | [Fact] 68 | public async Task ShouldThrowRequestFailedExceptionWhenNameDoesNotExist() 69 | { 70 | var name = Guid.NewGuid().ToString(); 71 | var exceptionThrown = false; 72 | 73 | try 74 | { 75 | await _secretClient.GetSecretAsync(name); 76 | } 77 | catch (RequestFailedException) 78 | { 79 | exceptionThrown = true; 80 | } 81 | 82 | Assert.True(exceptionThrown); 83 | } 84 | 85 | [Fact] 86 | public async Task ShouldThrowRequestFailedExceptionWhenGetSecretByNameAndVersionDoesNotExist() 87 | { 88 | var name = Guid.NewGuid().ToString(); 89 | var version = Guid.NewGuid().ToString(); 90 | var exceptionThrown = false; 91 | 92 | try 93 | { 94 | await _secretClient.GetSecretAsync(name, version); 95 | } 96 | catch (RequestFailedException) 97 | { 98 | exceptionThrown = true; 99 | } 100 | 101 | Assert.True(exceptionThrown); 102 | } 103 | 104 | private async Task> CreateSecret(string name) 105 | { 106 | return await _secretClient.SetSecretAsync(new KeyVaultSecret(name, Guid.NewGuid().ToString()) 107 | { 108 | Properties = 109 | { 110 | Enabled = true, 111 | ExpiresOn = DateTimeOffset.UtcNow.AddDays(1), 112 | NotBefore = DateTimeOffset.UtcNow, 113 | Tags = 114 | { 115 | {"environment", "local"}, 116 | {"testing", "true"} 117 | } 118 | } 119 | }); 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributor Covenant Code of Conduct 3 | 4 | ## Our Pledge 5 | 6 | We as members, contributors, and leaders pledge to make participation in our 7 | community a harassment-free experience for everyone, regardless of age, body 8 | size, visible or invisible disability, ethnicity, sex characteristics, gender 9 | identity and expression, level of experience, education, socio-economic status, 10 | nationality, personal appearance, race, caste, color, religion, or sexual identity 11 | and orientation. 12 | 13 | We pledge to act and interact in ways that contribute to an open, welcoming, 14 | diverse, inclusive, and healthy community. 15 | 16 | ## Our Standards 17 | 18 | Examples of behavior that contributes to a positive environment for our 19 | community include: 20 | 21 | * Demonstrating empathy and kindness toward other people 22 | * Being respectful of differing opinions, viewpoints, and experiences 23 | * Giving and gracefully accepting constructive feedback 24 | * Accepting responsibility and apologizing to those affected by our mistakes, 25 | and learning from the experience 26 | * Focusing on what is best not just for us as individuals, but for the 27 | overall community 28 | 29 | Examples of unacceptable behavior include: 30 | 31 | * The use of sexualized language or imagery, and sexual attention or 32 | advances of any kind 33 | * Trolling, insulting or derogatory comments, and personal or political attacks 34 | * Public or private harassment 35 | * Publishing others' private information, such as a physical or email 36 | address, without their explicit permission 37 | * Other conduct which could reasonably be considered inappropriate in a 38 | professional setting 39 | 40 | ## Enforcement Responsibilities 41 | 42 | Community leaders are responsible for clarifying and enforcing our standards of 43 | acceptable behavior and will take appropriate and fair corrective action in 44 | response to any behavior that they deem inappropriate, threatening, offensive, 45 | or harmful. 46 | 47 | Community leaders have the right and responsibility to remove, edit, or reject 48 | comments, commits, code, wiki edits, issues, and other contributions that are 49 | not aligned to this Code of Conduct, and will communicate reasons for moderation 50 | decisions when appropriate. 51 | 52 | ## Scope 53 | 54 | This Code of Conduct applies within all community spaces, and also applies when 55 | an individual is officially representing the community in public spaces. 56 | Examples of representing our community include using an official e-mail address, 57 | posting via an official social media account, or acting as an appointed 58 | representative at an online or offline event. 59 | 60 | ## Enforcement 61 | 62 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 63 | reported to the community leaders responsible for enforcement at 64 | [support@basistheory.com](mailto:support@basistheory.com?subject=Conduct). 65 | All complaints will be reviewed and investigated promptly and fairly. 66 | 67 | All community leaders are obligated to respect the privacy and security of the 68 | reporter of any incident. 69 | 70 | ## Enforcement Guidelines 71 | 72 | Community leaders will follow these Community Impact Guidelines in determining 73 | the consequences for any action they deem in violation of this Code of Conduct: 74 | 75 | ### 1. Correction 76 | 77 | **Community Impact**: Use of inappropriate language or other behavior deemed 78 | unprofessional or unwelcome in the community. 79 | 80 | **Consequence**: A private, written warning from community leaders, providing 81 | clarity around the nature of the violation and an explanation of why the 82 | behavior was inappropriate. A public apology may be requested. 83 | 84 | ### 2. Warning 85 | 86 | **Community Impact**: A violation through a single incident or series 87 | of actions. 88 | 89 | **Consequence**: A warning with consequences for continued behavior. No 90 | interaction with the people involved, including unsolicited interaction with 91 | those enforcing the Code of Conduct, for a specified period of time. This 92 | includes avoiding interactions in community spaces as well as external channels 93 | like social media. Violating these terms may lead to a temporary or 94 | permanent ban. 95 | 96 | ### 3. Temporary Ban 97 | 98 | **Community Impact**: A serious violation of community standards, including 99 | sustained inappropriate behavior. 100 | 101 | **Consequence**: A temporary ban from any sort of interaction or public 102 | communication with the community for a specified period of time. No public or 103 | private interaction with the people involved, including unsolicited interaction 104 | with those enforcing the Code of Conduct, is allowed during this period. 105 | Violating these terms may lead to a permanent ban. 106 | 107 | ### 4. Permanent Ban 108 | 109 | **Community Impact**: Demonstrating a pattern of violation of community 110 | standards, including sustained inappropriate behavior, harassment of an 111 | individual, or aggression toward or disparagement of classes of individuals. 112 | 113 | **Consequence**: A permanent ban from any sort of public interaction within 114 | the community. 115 | 116 | ## Attribution 117 | 118 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 119 | version 2.1, available at 120 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 121 | 122 | Community Impact Guidelines were inspired by 123 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 124 | 125 | For answers to common questions about this code of conduct, see the FAQ at 126 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available 127 | at [https://www.contributor-covenant.org/translations][translations]. 128 | 129 | [homepage]: https://www.contributor-covenant.org 130 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 131 | [Mozilla CoC]: https://github.com/mozilla/diversity 132 | [FAQ]: https://www.contributor-covenant.org/faq 133 | [translations]: https://www.contributor-covenant.org/translations 134 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DEPRECATION NOTICE 2 | As of `10/29/2024` Basis Theory is no longer maintaining this repository. 3 | 4 | ## Azure KeyVault Emulator 5 | 6 | [![Version](https://img.shields.io/docker/pulls/basistheory/azure-keyvault-emulator.svg)](https://hub.docker.com/r/basistheory/azure-keyvault-emulator) 7 | [![Verify](https://github.com/Basis-Theory/azure-keyvault-emulator/actions/workflows/verify.yml/badge.svg)](https://github.com/Basis-Theory/azure-keyvault-emulator/actions/workflows/verify.yml) 8 | 9 | The [Basis Theory](https://basistheory.com/) Azure KeyVault Emulator to mock interactions with Azure KeyVault using the official Azure KeyVault client 10 | 11 | ## Supported Operations 12 | 13 | ### Keys 14 | 15 | #### RSA 16 | 17 | - Create Key 18 | - Get Key 19 | - Get Key by Version 20 | - Encrypt 21 | - Decrypt 22 | - Supported [Algorithms](https://docs.microsoft.com/en-us/rest/api/keyvault/decrypt/decrypt#jsonwebkeyencryptionalgorithm) 23 | - `RSA1_5` 24 | - `RSA-OAEP` 25 | 26 | ### Secrets 27 | 28 | - Set 29 | - Get Secret 30 | - Get Secret by Version 31 | 32 | ## Requirements 33 | 34 | ### HTTPS 35 | 36 | Azure KeyClient and SecretClient require HTTPS communication with a KeyVault instance. 37 | When accessing the emulator on `localhost`, configure a trusted TLS certificate with [dotnet dev-certs](https://docs.microsoft.com/en-us/dotnet/core/additional-tools/self-signed-certificates-guide#with-dotnet-dev-certs). 38 | 39 | For accessing the emulator with a hostname other than `localhost`, a self-signed certificate needs to be generated and trusted by the client. See [Adding to docker-compose](#adding-to-docker-compose) for further instructions. 40 | 41 | ### AuthN/AuthZ 42 | 43 | Azure KeyClient and SecretClient use a [ChallengeBasedAuthenticationPolicy](https://github.com/Azure/azure-sdk-for-net/blob/b30fa6d0d402511fdf3270c5d1d9ae5dfa2a0340/sdk/keyvault/Azure.Security.KeyVault.Shared/src/ChallengeBasedAuthenticationPolicy.cs#L64-L66) 44 | to determine the authentication scheme used by the server. In order for the KeyVault Emulator to work with the Azure SDK, the emulator requires JWT authentication in the `Authorization` header with `Bearer` prefix. 45 | KeyVault Emulator only validates the JWT is well-formed. 46 | 47 | ```shell 48 | curl -X 'GET' \ 49 | 'https://localhost:5551/secrets/foo' \ 50 | -H 'accept: application/json' \ 51 | -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE4OTAyMzkwMjIsImlzcyI6Imh0dHBzOi8vbG9jYWxob3N0OjUwMDEvIn0.bHLeGTRqjJrmIJbErE-1Azs724E5ibzvrIc-UQL6pws' 52 | ``` 53 | 54 | ## Adding to docker-compose 55 | 56 | For the Azure KeyVault Emulator to be accessible from other containers in the same compose file, a new OpenSSL certificate has to be generated: 57 | 1. Replace `` and run the following script to generate a new public/private keypair: 58 | 59 | ``` 60 | openssl req \ 61 | -x509 \ 62 | -newkey rsa:4096 \ 63 | -sha256 \ 64 | -days 3560 \ 65 | -nodes \ 66 | -keyout .key \ 67 | -out .crt \ 68 | -subj '/CN=' \ 69 | -extensions san \ 70 | -config <( \ 71 | echo '[req]'; \ 72 | echo 'distinguished_name=req'; \ 73 | echo '[san]'; \ 74 | echo 'subjectAltName=DNS.1:localhost,DNS.2:,DNS.3:localhost.vault.azure.net,DNS.4:.vault.azure.net') 75 | ``` 76 | 77 | 1. Export a `.pks` formatted key using the public/private keypair generated in the previous step: 78 | 79 | ``` 80 | openssl pkcs12 -export -out .pfx \ 81 | -inkey .key \ 82 | -in .crt 83 | ``` 84 | 85 | 1. Trust the certificate in the login keychain 86 | 87 | ``` 88 | sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain .crt 89 | ``` 90 | 91 | 1. Add a service to docker-compose.yml for Azure KeyVault Emulator: 92 | 93 | ``` 94 | version: '3.9' 95 | 96 | services: 97 | ... 98 | azure-keyvault-emulator: 99 | image: basis-theory/azure-keyvault-emulator:latest 100 | hostname: .vault.azure.net 101 | ports: 102 | - 5001:5001 103 | - 5000:5000 104 | volumes: 105 | - :/https 106 | environment: 107 | - ASPNETCORE_URLS=https://+:5001;http://+:5000 108 | - ASPNETCORE_Kestrel__Certificates__Default__Path=/https/.pfx 109 | - KeyVault__Name= 110 | ``` 111 | 112 | 1. Modify the client application's entrypoint to add the self-signed certificate to the truststore. Example using docker-compose.yml to override the entrypoint: 113 | 114 | ``` 115 | version: '3.9' 116 | 117 | services: 118 | my-awesome-keyvault-client: 119 | container_name: my-awesome-client 120 | build: 121 | context: . 122 | depends_on: 123 | - azure-keyvault-emulator 124 | entrypoint: sh -c "cp /https/.crt /usr/local/share/ca-certificates/.crt && update-ca-certificates && exec " 125 | volumes: 126 | - :/https 127 | environment: 128 | - KeyVault__BaseUrl=https://.vault.azure.net:5001/ 129 | ``` 130 | 131 | 1. (Optional) Azure KeyVault SDKs verify the challenge resource URL as of v4.4.0 (read more [here](https://devblogs.microsoft.com/azure-sdk/guidance-for-applications-using-the-key-vault-libraries/)). 132 | To satisfy the new challenge resource verification requirements, do one of the following: 133 | 1. Use an emulator hostname that ends with `.vault.azure.net` (e.g. `localhost.vault.azure.net`). A new entry may need to be added to `/etc/hosts` to properly resolve DNS (i.e. `127.0.0.1 localhost.vault.azure.net`). 134 | 1. Set `DisableChallengeResourceVerification` to true in your client options to disable verification. 135 | ```csharp 136 | var client = new SecretClient( 137 | new Uri("https://localhost.vault.azure.net:5551/"), 138 | new LocalTokenCredential(), 139 | new SecretClientOptions 140 | { 141 | DisableChallengeResourceVerification = true 142 | }); 143 | ``` 144 | 145 | ## Development 146 | 147 | The provided scripts will check for all dependencies, start docker, build the solution, and run all tests. 148 | 149 | ### Dependencies 150 | - [Docker](https://www.docker.com/products/docker-desktop) 151 | - [Docker Compose](https://www.docker.com/products/docker-desktop) 152 | - [.NET 6](https://dotnet.microsoft.com/download/dotnet/6.0) 153 | 154 | ### Build the KeyVault emulator and run Tests 155 | 156 | Run the following command from the root of the project: 157 | 158 | ```sh 159 | make verify 160 | ``` 161 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2021 Basis Theory 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | --------------------------------------------------------------------------------