├── 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 | [](https://hub.docker.com/r/basistheory/azure-keyvault-emulator)
7 | [](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 |
--------------------------------------------------------------------------------