├── .circleci └── config.yml ├── .codeclimate ├── .gemnasium.yml ├── .github └── dependabot.yml ├── .rubocop_todo.yml ├── Gemfile ├── Gemfile.lock ├── README.md ├── kms-decrypt.rb ├── kms-encrypt.rb └── test.sh /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | orbs: 3 | aws-cli: circleci/aws-cli@5.1.1 4 | jobs: 5 | build: 6 | environment: 7 | AWS_REGION: us-east-1 8 | docker: 9 | - image: cimg/ruby:2.7.0 10 | steps: 11 | - checkout 12 | 13 | - aws-cli/setup: 14 | profile_name: OIDC-PROFILE 15 | role_arn: arn:aws:iam::186675908400:role/circleci-kms 16 | role_session_name: CircleCI 17 | 18 | # Restore bundle cache 19 | - restore_cache: 20 | key: bundle-{{ checksum "Gemfile.lock" }} 21 | 22 | # Bundle install dependencies 23 | - run: 24 | name: Bundler 25 | command: bundle install --path vendor/bundle 26 | 27 | # Store bundle cache 28 | - save_cache: 29 | name: Save bundles 30 | key: bundle-{{ checksum "Gemfile.lock" }} 31 | paths: 32 | - vendor/bundle 33 | 34 | - run: 35 | name: Run Test 36 | command: bash -x ./test.sh 37 | environment: 38 | AWS_PROFILE: OIDC-PROFILE 39 | 40 | workflows: 41 | aws-cli: 42 | jobs: 43 | - build: 44 | context: aws 45 | -------------------------------------------------------------------------------- /.codeclimate: -------------------------------------------------------------------------------- 1 | engines: 2 | rubocop: 3 | enabled: true 4 | markdownlint: 5 | enabled: true 6 | 7 | ratings: 8 | paths: 9 | - "**.rb" 10 | - "**.md" 11 | -------------------------------------------------------------------------------- /.gemnasium.yml: -------------------------------------------------------------------------------- 1 | project_name: aws-kms-ruby-encrypt-decrypt-example # A name to remember your project. 2 | project_slug: github.com/jhmartin/aws-kms-ruby-encrypt-decrypt-example # Unique slug for this project. Get it on the "project settings" page. 3 | project_branch: master # /!\ If you don't use git, remove this line 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: bundler 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "13:00" 8 | open-pull-requests-limit: 10 9 | ignore: 10 | - dependency-name: rubocop 11 | versions: 12 | - 1.12.0 13 | - 1.12.1 14 | -------------------------------------------------------------------------------- /.rubocop_todo.yml: -------------------------------------------------------------------------------- 1 | # This configuration was generated by 2 | # `rubocop --auto-gen-config` 3 | # on 2017-06-17 17:39:49 -0700 using RuboCop version 0.49.1. 4 | # The point is for the user to remove these configuration records 5 | # one by one as the offenses are removed from the code base. 6 | # Note that changes in the inspected code, or installation of new 7 | # versions of RuboCop, may require this file to be generated again. 8 | 9 | # Offense count: 4 10 | # Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns. 11 | # URISchemes: http, https 12 | inherit_from: .rubocop_todo.yml 13 | 14 | Metrics/LineLength: 15 | Max: 162 16 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | gem 'aws-sdk-kms', '~> 1' 6 | gem 'json' 7 | gem 'openssl' 8 | 9 | group :development do 10 | gem 'mdl' 11 | gem 'rubocop' 12 | end 13 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | ast (2.4.3) 5 | aws-eventstream (1.4.0) 6 | aws-partitions (1.1113.0) 7 | aws-sdk-core (3.225.1) 8 | aws-eventstream (~> 1, >= 1.3.0) 9 | aws-partitions (~> 1, >= 1.992.0) 10 | aws-sigv4 (~> 1.9) 11 | base64 12 | jmespath (~> 1, >= 1.6.1) 13 | logger 14 | aws-sdk-kms (1.104.0) 15 | aws-sdk-core (~> 3, >= 3.225.0) 16 | aws-sigv4 (~> 1.5) 17 | aws-sigv4 (1.12.0) 18 | aws-eventstream (~> 1, >= 1.0.2) 19 | base64 (0.3.0) 20 | chef-utils (18.3.0) 21 | concurrent-ruby 22 | concurrent-ruby (1.2.2) 23 | jmespath (1.6.2) 24 | json (2.12.2) 25 | kramdown (2.4.0) 26 | rexml 27 | kramdown-parser-gfm (1.1.0) 28 | kramdown (~> 2.0) 29 | language_server-protocol (3.17.0.5) 30 | lint_roller (1.1.0) 31 | logger (1.7.0) 32 | mdl (0.13.0) 33 | kramdown (~> 2.3) 34 | kramdown-parser-gfm (~> 1.1) 35 | mixlib-cli (~> 2.1, >= 2.1.1) 36 | mixlib-config (>= 2.2.1, < 4) 37 | mixlib-shellout 38 | mixlib-cli (2.1.8) 39 | mixlib-config (3.0.27) 40 | tomlrb 41 | mixlib-shellout (3.2.7) 42 | chef-utils 43 | openssl (3.3.0) 44 | parallel (1.27.0) 45 | parser (3.3.8.0) 46 | ast (~> 2.4.1) 47 | racc 48 | prism (1.4.0) 49 | racc (1.8.1) 50 | rainbow (3.1.1) 51 | regexp_parser (2.10.0) 52 | rexml (3.3.9) 53 | rubocop (1.76.1) 54 | json (~> 2.3) 55 | language_server-protocol (~> 3.17.0.2) 56 | lint_roller (~> 1.1.0) 57 | parallel (~> 1.10) 58 | parser (>= 3.3.0.2) 59 | rainbow (>= 2.2.2, < 4.0) 60 | regexp_parser (>= 2.9.3, < 3.0) 61 | rubocop-ast (>= 1.45.0, < 2.0) 62 | ruby-progressbar (~> 1.7) 63 | unicode-display_width (>= 2.4.0, < 4.0) 64 | rubocop-ast (1.45.1) 65 | parser (>= 3.3.7.2) 66 | prism (~> 1.4) 67 | ruby-progressbar (1.13.0) 68 | tomlrb (2.0.3) 69 | unicode-display_width (3.1.4) 70 | unicode-emoji (~> 4.0, >= 4.0.4) 71 | unicode-emoji (4.0.4) 72 | 73 | PLATFORMS 74 | ruby 75 | 76 | DEPENDENCIES 77 | aws-sdk-kms (~> 1) 78 | json 79 | mdl 80 | openssl 81 | rubocop 82 | 83 | BUNDLED WITH 84 | 2.1.4 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # aws-kms-ruby-enrypt-decrypt-example 2 | 3 | [![CircleCI](https://circleci.com/gh/jhmartin/aws-kms-ruby-encrypt-decrypt-example.svg?style=svg)](https://circleci.com/gh/jhmartin/aws-kms-ruby-encrypt-decrypt-example) 4 | [![Code Climate](https://codeclimate.com/github/jhmartin/aws-kms-ruby-encrypt-decrypt-example/badges/gpa.svg)](https://codeclimate.com/github/jhmartin/aws-kms-ruby-encrypt-decrypt-example) 5 | 6 | Sample using AWS Key Management Service (assuming IAM roles) to generate a data 7 | key, encrypt an object with that key, then decrypt it. 8 | 9 | The data key response contains a cleartext key and an encrypted key. The 10 | cleartext key is used to encrypt the contents and then discarded. The 11 | ciphertext and encrypted key are stored together. Then KMS is asked to decrypt 12 | the data-key, and the resulting cleartext-key is used against the ciphertext to 13 | decrypt back to cleartext. 14 | 15 | Sample ciphertext file looks like: 16 | 17 | ```JSON 18 | { 19 | "ciphertext": "U2FsdGVkX1/J92QzTjh7E+u8mHLoruXfNgFxRVzHXDh/QwV7gtuO+KODk8aZ\ng2jktXbHnY1V1YcH1g6whGZgAPUksG2VGvKLBNKXbFbigPRd6JUSNhLUkbho\nCKWS7vmH1om15ZGjqMEqhNKvCJN1bUTfb6cbyxDdYhe0nUIKNlbl1v5KRHOp\nyoeBLIHlrdGe/KhjAWrbtehTLYdlbfWLxWcprxekB0jhGHBb0QGOgqRmuWq1\npDJJjkeQZlWcT9Q4lBU1CXMxFdibE3DzuWtMsFXTZIN3CPNphZ0TIs+xxh5A\nwGaoZd3STjyAISenK8L4YK22HnM7nb9TfdPK77gYgWM51HI65cNB/XIPm4fs\nDQUU8ZV0dhSGwD65+Mw9ZsbXjemwFDyoI4r16Luu0KEBRBVZS99BZwlXrI72\n1LwI1s3/8lddfGGyrfyQf7biXsulVtx6llCwZOId4HxvjwOIo+9FG7t0dndA\n8vZ//ZdCNvLMDiAxnVL/2uL15wXU+L9uxl+NJgJP9OshmujN0u/zMFa0pk8+\nl8Yu9nB62rf+tk8m8JcpgPrwkOMUkQxz9OPzUYLaSNglwtOGkjHZ1iAipdCg\nAyw5pIUCRH1EBT9T5enOFz8N5Lus4BPjcL2nE9kmTL3OnN/TjSNY1hnYjC+p\nhb8k9Qe18MmyysuQfF1oYZLq6RIVtXgD73y4wWBVWvcUXVidZDMOQjp6bRKa\noqoQzRglTuNP+vhgTYN1R7s9D46fVLRTqvlDeKmwuG5GZ59ZsFoaz6rAhzCy\nYJnmFpOC6Q==\n", 20 | "datakey": "CiAZM5lfpml79/xq2DrOPUKm4aSyNamrxnGq6oBiEkJ3yBKnAQEBAwB4GTOZX6Zpe/f8atg6zj1CpuGksjWpq8ZxquqAYhJCd8gAAAB+MHwGCSqGSIb3DQEHBqBvMG0CAQAwaAYJKoZIhvcNAQcBMB4GCWCGSAFlAwQBLjARBAxDH8IEyvf8fr3qtQYCARCAO7elZFDPuqPXJzjP5iciFabj5820Q6ZTdnZvdWyCZMhRyx0qQtoQL7tDVVMGH3yrlNY909grcx1nERWe" 21 | } 22 | ``` 23 | 24 | ## Usage 25 | 26 | 1. Create a AWS KMS master key (currently costs $1/mo) and record its keyid. 27 | 1. Set the KEYID environmental variable to the keyid or key alias. 28 | 1. Add an IAM user or role as 'key usage' authorized user. 29 | 1. Configure the AWS credentials as you normally would 30 | (I used an IAM instance role rather than explicit access keys) 31 | 1. Call kms-encrypt.rb with an input file and 32 | redirect the output to a ciphertext file. 33 | 1. Call kms-decrypt.rb with a reference to the ciphertext file. 34 | 35 | ``` 36 | $ kms-encrypt.rb /input/file/here |tee ciphertext.out 37 | { 38 | "ciphertext": "4Bt7uXIVJ2zI5qQ6bphykjK6mFydpc9V4G0G6pi8GZxiWuNSw8J09v99i30=", 39 | "datakey": "AQIDAHg3ZY/4pziRas3bdm9zX/i4bWa8VnJyUyzRQ9PyByYRIQEqR6pqy6ND0ZelFNfH2SMqAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMlu6UTpFela9WZaaQAgEQgDuYQItbLfsSMj1kr/YrU0/Doi2G9Hj9WsrgYHP3BaegE6SHxkUY6c4DQG84HP/IFwR9KJmNTgcToksK8Q==" 40 | } 41 | $ kms-decrypt.rb ciphertext.out 42 | FOO 43 | ``` 44 | 45 | -------------------------------------------------------------------------------- /kms-decrypt.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | # frozen_string_literal: true 3 | 4 | require 'rubygems' 5 | require 'aws-sdk-kms' 6 | require 'pp' 7 | require 'base64' 8 | require 'openssl' 9 | require 'json' 10 | 11 | if ARGV[0].nil? 12 | puts 'Usage: kms-decrypt.rb INPUTFILE' 13 | exit 1 14 | end 15 | 16 | contents = JSON.parse(IO.read(ARGV[0])) 17 | kms = Aws::KMS::Client.new(region: 'us-east-1') 18 | datakey = Base64.strict_decode64(contents['datakey']) 19 | 20 | cleartextkey = kms.decrypt(ciphertext_blob: datakey, 21 | encryption_context: { 22 | 'KeyType' => 'Some descriptive text here' 23 | }) 24 | 25 | alg = 'AES-256-CBC' 26 | decode_cipher = OpenSSL::Cipher.new(alg) 27 | decode_cipher.decrypt 28 | decode_cipher.key = cleartextkey.plaintext 29 | decode_cipher.iv = Base64.strict_decode64(contents['iv']) 30 | 31 | plaintext = decode_cipher.update( 32 | Base64.strict_decode64(contents['ciphertext']) 33 | ) + decode_cipher.final 34 | puts plaintext 35 | -------------------------------------------------------------------------------- /kms-encrypt.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | # frozen_string_literal: true 3 | 4 | require 'pp' 5 | require 'rubygems' 6 | require 'openssl' 7 | require 'aws-sdk-kms' 8 | require 'base64' 9 | require 'json' 10 | 11 | if ARGV[0].nil? 12 | puts 'Usage: encrypt.rb INPUTFILE' 13 | exit 1 14 | end 15 | 16 | # This uses the envelope-encryption model. 17 | # The cleartext is not sent to AWS KMS, 18 | # rather an encryption key is generated by KMS 19 | # and stored alongside the ciphertext. 20 | # keyid looks like 6d83e627-bf08-1111-9999-23b0a0df19b1 21 | keyid = ENV['KEYID'] 22 | plaintext = IO.read(ARGV[0]) 23 | 24 | kms = Aws::KMS::Client.new(region: 'us-east-1') 25 | 26 | # Encryption_context can be thought of as part of the key; 27 | # it must match on both the encrypt and decrypt end. 28 | kmsresponse = kms.generate_data_key( 29 | key_id: keyid, 30 | encryption_context: { 'KeyType' => 'Some descriptive text here' }, 31 | key_spec: 'AES_256' 32 | ) 33 | alg = 'AES-256-CBC' 34 | iv = OpenSSL::Cipher.new(alg).random_iv 35 | aes = OpenSSL::Cipher.new(alg) 36 | aes.encrypt 37 | aes.key = kmsresponse['plaintext'] 38 | aes.iv = iv 39 | 40 | cipher = aes.update(plaintext) 41 | cipher << aes.final 42 | 43 | cipher64 = Base64.strict_encode64(cipher) 44 | outputhash = { 'ciphertext' => cipher64, 45 | 'iv' => Base64.strict_encode64(iv), 46 | 'datakey' => Base64.strict_encode64( 47 | kmsresponse.ciphertext_blob 48 | ) } 49 | puts JSON.pretty_generate(outputhash) 50 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -ex 3 | echo FOO > test.input 4 | bundle exec ruby ./kms-encrypt.rb test.input | tee test.encrypted 5 | bundle exec ruby ./kms-decrypt.rb test.encrypted | tee test.decrypted 6 | diff -y test.decrypted test.input 7 | --------------------------------------------------------------------------------