kms-tools 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,62 @@
1
+ module KmsTools
2
+ # Namespace for CLI specfic classes in kms-tools
3
+ module CLI
4
+ # Class for handling encrypt operations from the CLI
5
+ class Encrypt
6
+ # Set up encryption handler
7
+ #
8
+ # @param [Hash] global_options
9
+ # @param [Hash] options
10
+ # @option global_options [String] :master_key Master key to use if provided by CLI options
11
+ # @option global_options [String] :profile AWS credential profile to us if provided by CLI options
12
+ # @option global_options [String] :region AWS region to use if provided by CLI options
13
+ # @option options [String] :data_key Existing data key to use if provided by CLI options
14
+ def initialize(global_options, options)
15
+ @enc = KmsTools::Encrypter.new(global_options)
16
+ @output = options[:output]
17
+ abort "You must specify a key alias as a flag when encrypting stdin!" unless STDIN.tty? || global_options[:k]
18
+ @enc.use_key_alias = Helpers.select_key(@enc) if @enc.master_key.nil?
19
+ end
20
+
21
+ # Handle encrypt command
22
+ #
23
+ # @param [String] source Plaintext source to encrypt. Can be a string for direct encryption or a path to a file to encrypt
24
+ # @return [String] Returns Base64 encoded ciphertext if a string is provided
25
+ # @return [String] Returns path to encrypted file if a source path is provided
26
+ def encrypt(source)
27
+ if File.file?(source)
28
+ save_path = encrypt_file(source)
29
+ Output.say("Encrypted file saved to: #{save_path}")
30
+ elsif source.is_a?(String)
31
+ encrypted_response = @enc.encrypt_string(source)
32
+ if STDIN.tty?
33
+ Output.say("Encrypted ciphertext (copy all text without surrounding whitespace):\n\n#{encrypted_response}\n\n")
34
+ else
35
+ print encrypted_response
36
+ end
37
+ else
38
+ raise "Unknown input to encrypt..."
39
+ end
40
+
41
+ # clear source from memory for good measure
42
+ source = nil
43
+ end
44
+
45
+ # Encrypt a file from source path
46
+ #
47
+ # @param [String] source path to file to encrypt
48
+ # @return [String] path to encrypted file
49
+ def encrypt_file(source)
50
+ ef = KmsTools::EncryptedFile.new(encrypter: @enc)
51
+ ef.create_from_file(source)
52
+ save_path = Helpers::get_save_path({
53
+ :prompt => "Save encrytped file to",
54
+ :suggested_path => File.absolute_path(source.sub File.extname(source), ".kms")
55
+ })
56
+ ef.save_encrypted(save_path)
57
+ save_path
58
+ end
59
+
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,30 @@
1
+ module KmsTools
2
+ module CLI
3
+ # General purpose helper functions for the CLI
4
+ class Helpers
5
+
6
+ # Prompts the user to select a key alias from a list of key aliases available to current credentials
7
+ #
8
+ # @param [Object] kms KMS client object
9
+ # @return [String] key alias
10
+ def self.select_key(kms)
11
+ Output.select_from_list("Choose which key alias to use as the base Customer Master Key:", kms.available_aliases)
12
+ end
13
+
14
+ # Prompts the user for a path to save a file. Optionally providers a default suggestion.
15
+ #
16
+ # @param [Hash] params
17
+ # @option params [String] :prompt Prompt text to display
18
+ # @option params [String] :suggested_path Optional path to suggest to user and use a default if no additional input is given
19
+ # @return [String] path
20
+ def self.get_save_path(params)
21
+ prompt = params[:prompt].nil? ? "Save to" : params[:prompt]
22
+ prompt << " (#{params[:suggested_path]})" if params[:suggested_path]
23
+ prompt << ": "
24
+ entered_path = KmsTools::CLI::Output.ask(prompt, String)
25
+ entered_path.empty? ? params[:suggested_path] : entered_path
26
+ end
27
+
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,61 @@
1
+ require 'highline'
2
+
3
+ module KmsTools
4
+ module CLI
5
+ # Helper functions for terminal interaction
6
+ module Output
7
+ def self.terminal
8
+ HighLine.color_scheme = color_scheme
9
+ @@terminal ||= HighLine.new
10
+ end
11
+
12
+ def self.color_scheme
13
+ @@color_scheme ||= HighLine::ColorScheme.new(
14
+ :normal => [],
15
+ :error => [:bold, :red],
16
+ :warning => [:bold, :yellow],
17
+ :verbose => [:bold, :magenta],
18
+ :debug => [:bold, :cyan],
19
+ :success => [:bold, :green],
20
+ :addition => [:bold, :green],
21
+ :removal => [:bold, :red],
22
+ :modification => [:bold, :yellow],
23
+ )
24
+ end
25
+
26
+ def self.say(msg, log_style=:normal)
27
+ terminal.say format(msg, log_style)
28
+ end
29
+
30
+ def self.format(msg, log_style=:normal)
31
+ if $color
32
+ terminal.color(msg.to_s, log_style)
33
+ else
34
+ msg
35
+ end
36
+ end
37
+
38
+ def self.say_verbose(msg)
39
+ terminal.say format(msg.to_s, 'verbose') if $verbose
40
+ end
41
+
42
+ def self.say_debug(msg, log_style=:debug)
43
+ terminal.say format(msg.to_s, log_style) if $debug
44
+ end
45
+
46
+ def self.ask(*args, &block)
47
+ terminal.ask(*args, &block)
48
+ end
49
+
50
+ def self.select_from_list(prompt, options)
51
+ puts prompt
52
+ options.each_with_index do |key, index|
53
+ puts "#{index+1}. #{key}"
54
+ end
55
+ choice = Output.ask("? ", Integer) { |q| q.in = 1..options.length }
56
+ options[choice-1]
57
+ end
58
+
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,82 @@
1
+ module KmsTools
2
+ # Provides low-level decryption functionality for kms-tools
3
+ #
4
+ # @author Matt Kriegers
5
+ class Decrypter < KmsTools::Base
6
+
7
+ # Decrypt base64 encoded ciphertext that was encrypted directly with a customer master key
8
+ # @param str [String] Base64 encoded ciphertext
9
+ # @return [String] Binary plaintext
10
+ def decrypt_string(str)
11
+ kms.decrypt({:ciphertext_blob => from_64(str)}).plaintext
12
+ end
13
+
14
+ # Decrypt a blob using private keys
15
+ #
16
+ # @param [Hash] params
17
+ # @option params [String] :cipher OpenSSL cipher used for encryption
18
+ # @option params [String] :encrypted_key Encrypted private key
19
+ # @option params [String] :encrypted_iv Encrypted initialization vector
20
+ # @option params [String] :encrypted_data Ciphertext data blob
21
+ # @return [String] Binary plaintext
22
+ def decrypt_with_data_key(params)
23
+ cipher = OpenSSL::Cipher.new(params[:cipher])
24
+ cipher.decrypt
25
+ cipher.key = decrypt_string(params[:encrypted_key])
26
+ cipher.iv = decrypt_string(params[:encrypted_iv])
27
+ decrypted_data = cipher.update(params[:encrypted_data]) + cipher.final
28
+
29
+ raise "File integrity check failed!" unless integrity_verified?(decrypted_data, params[:checksum])
30
+
31
+ decrypted_data
32
+ end
33
+
34
+ # Decrypt a stream using private keys
35
+ #
36
+ # @param [Hash] params
37
+ # @option params [String] :cipher OpenSSL cipher used for encryption
38
+ # @option params [String] :encrypted_key Encrypted private key
39
+ # @option params [String] :encrypted_iv Encrypted initialization vector
40
+ # @option params [Stream] :in Input stream to read ciphertext data from
41
+ # @option params [Integer] :position Optional file position marking beginning of ciphertext
42
+ # @option params [Stream] :out Stream to to write plaintext output to
43
+ # @option params [String] :checksum Optional SHA1 checksum to verify integrity of decrypted data
44
+ def stream_decrypt_with_data_key(params)
45
+ # set up cipher
46
+ cipher = OpenSSL::Cipher.new(params[:cipher])
47
+ cipher.decrypt
48
+ cipher.key = decrypt_string(params[:encrypted_key])
49
+ cipher.iv = decrypt_string(params[:encrypted_iv])
50
+
51
+ sha1 = Digest::SHA1.new if params[:checksum]
52
+
53
+ # write the output stream
54
+ chunk = ""
55
+ params[:in].seek(params[:position], IO::SEEK_SET) if params[:position]
56
+ while params[:in].read(STREAM_CHUNK_SIZE, chunk)
57
+ decrypted_chunk = cipher.update(chunk)
58
+ sha1.update(decrypted_chunk) if params[:checksum]
59
+ params[:out] << decrypted_chunk
60
+ end
61
+
62
+ final = cipher.final
63
+ sha1.update(final) if params[:checksum]
64
+
65
+ if params[:checksum]
66
+ raise "Decrypted data stream failed checksum verification!" unless params[:checksum].eql? sha1.hexdigest
67
+ end
68
+
69
+ params[:out] << final
70
+ true
71
+ end
72
+
73
+ # Verify data blob against known hash
74
+ #
75
+ # @param [String] data Data to verify
76
+ # @param [String] checksum Known SHA1 hash
77
+ def integrity_verified?(data, checksum)
78
+ Digest::SHA1.hexdigest(data).eql? checksum
79
+ end
80
+
81
+ end
82
+ end
@@ -0,0 +1,184 @@
1
+ require 'yaml'
2
+ require 'digest'
3
+
4
+ module KmsTools
5
+ # Interacts with files and streams for encryption and decryption of large blobs of data
6
+ #
7
+ # @author Matt Krieger
8
+ class EncryptedFile
9
+ # Number of bytes allocated at the beginning of a KMS file noting metadata size. 7 bytes limits metadata to 9.9 MB, which is way too much. Don't use that much.
10
+ META_SIZE_HEADER_LENGTH = 7
11
+
12
+ # Default cipher for local encryption
13
+ DEFAULT_CIPHER = 'aes-256-cbc'
14
+
15
+ # Minimum required metadata elements for a KMS file to be valid
16
+ KMS_REQUIRED_ELEMENTS = %i(encrypted_key encrypted_iv cipher original_extension checksum)
17
+
18
+ # Creats an EncryptedFile object.
19
+ #
20
+ # @param [Hash] options
21
+ # @option options [Object] :encrypter existing (Encrypter) object with options set
22
+ # @option options [String] :path Path to an encrypted file to load on initialization
23
+ def initialize(options = {})
24
+ @enc = options[:encrypter] || KmsTools::Encrypter.new
25
+ @dec = KmsTools::Decrypter.new
26
+ @kms_meta = {}
27
+
28
+ if options[:path]
29
+ if File.readable?(options[:path])
30
+ load_encrypted_file(options[:path])
31
+ else
32
+ raise "#{options[:path]} is not a readable!"
33
+ end
34
+ end
35
+ end
36
+
37
+ # Generate metadata from a plaintext file and prepare for encryption
38
+ #
39
+ # @param [String] path Path to plaintext file
40
+ # @return [Hash] KMS file metadata
41
+ def create_from_file(path)
42
+ @decrypted_source = path
43
+ @kms_meta[:arn] = @enc.master_key_arn
44
+ @kms_meta[:checksum] = file_sha(path)
45
+ @kms_meta[:original_extension] = File.extname(path)
46
+ set_up_encryption_params
47
+ @kms_meta
48
+ end
49
+
50
+ # Read a KMS encrypted file and populate object metadata from file headers
51
+ #
52
+ # @param [String] path Path to encrypted file
53
+ # @return [Hash] KMS file metadata
54
+ def load_encrypted_file(path)
55
+ meta_size = IO.binread(path, META_SIZE_HEADER_LENGTH).to_i
56
+ kms_yaml = YAML::load(IO.binread(path, meta_size, META_SIZE_HEADER_LENGTH))
57
+ if is_valid_kms_file?(kms_yaml)
58
+ @kms_meta = kms_yaml
59
+ @encrypted_file_path = path
60
+ @encrypted_data_start = META_SIZE_HEADER_LENGTH + meta_size
61
+ else
62
+ raise "#{path} is not a valid KMS file!"
63
+ end
64
+ @kms_meta
65
+ end
66
+
67
+ # Get the encrypted key of the current file, generate a new one if not present
68
+ #
69
+ # @return [string] Base64 encoded encrypted data key
70
+ def encrypted_key
71
+ @kms_meta[:encrypted_key] ||= @enc.new_encrypted_key
72
+ end
73
+
74
+ # Get the encrypted initialization vector of the current file, generate a new one if not present
75
+ #
76
+ # @return [string] Base64 encoded encrypted data key
77
+ def encrypted_iv
78
+ @kms_meta[:encrypted_iv] ||= @enc.new_encrypted_key
79
+ end
80
+
81
+ # Get the original extension of the encrypted file
82
+ #
83
+ # @return [string] file extension
84
+ def original_extension
85
+ @kms_meta[:original_extension]
86
+ end
87
+
88
+ def encrypted_data
89
+ @kms_meta[:encrypted_data]
90
+ end
91
+
92
+ # Get the cipher used for the encrypted file or set the default
93
+ #
94
+ # @return [string] OpenSSL cipher
95
+ def cipher
96
+ @kms_meta[:cipher] ||= DEFAULT_CIPHER
97
+ end
98
+
99
+ # Get the SHA1 checksum of the decrypted data
100
+ #
101
+ # @return [string] hex encoded SHA1 checksum
102
+ def checksum
103
+ @kms_meta[:checksum]
104
+ end
105
+
106
+ # Generate encryption key, iv, and cipher if they do not exist
107
+ # @return [nil]
108
+ def set_up_encryption_params
109
+ encrypted_key
110
+ encrypted_iv
111
+ cipher
112
+ return nil
113
+ end
114
+
115
+ # Save encrypted data with KMS headers to the provided path
116
+ #
117
+ # @param [String] path Path to save encrypted file
118
+ # @return [Boolean] returns true on success
119
+ def save_encrypted(path)
120
+ kms_yaml = @kms_meta.to_yaml
121
+ meta_size = kms_yaml.bytesize.to_s.rjust(META_SIZE_HEADER_LENGTH, "0")
122
+ infile = File.open(@decrypted_source, 'rb')
123
+ outfile = File.open(path, 'wb+')
124
+ outfile << meta_size
125
+ outfile << kms_yaml
126
+ @enc.stream_encrypt_with_data_key({
127
+ in: infile,
128
+ out: outfile,
129
+ encrypted_iv: encrypted_iv,
130
+ encrypted_key: encrypted_key,
131
+ cipher: cipher
132
+ })
133
+
134
+ outfile.close
135
+ true
136
+ end
137
+
138
+ # Save decrypted data to the provided path
139
+ #
140
+ # @param [String] path Path to save decrypted file
141
+ # @return [Boolean] returns true on success
142
+ def save_decrypted(path)
143
+ path << @kms_meta[:original_extension] unless File.extname(path) == @kms_meta[:original_extension]
144
+ infile = File.open(@encrypted_file_path, 'rb')
145
+ outfile = File.open(path, 'wb+')
146
+ @dec.stream_decrypt_with_data_key({
147
+ in: infile,
148
+ position: @encrypted_data_start,
149
+ out: outfile,
150
+ encrypted_iv: encrypted_iv,
151
+ encrypted_key: encrypted_key,
152
+ cipher: cipher,
153
+ checksum: checksum
154
+ })
155
+
156
+ outfile.close
157
+ true
158
+ end
159
+
160
+ # Verify YAML header of a KMS file to encure necessary information is present to decrypt
161
+ #
162
+ # @param [Object] yaml object containing KMS metadata
163
+ # @return [Boolean]
164
+ def is_valid_kms_file?(yaml)
165
+ KMS_REQUIRED_ELEMENTS.all? { |e| yaml.has_key? e }
166
+ end
167
+
168
+ # Calculate SHA of a given file by chunks
169
+ #
170
+ # @param [String] path Path to file
171
+ # @return [String] Hex encoded SHA1 hash
172
+ def file_sha(path)
173
+ sha1 = Digest::SHA1.new
174
+ file = File.open(path,'rb')
175
+ chunk = ""
176
+ while file.read(STREAM_CHUNK_SIZE, chunk)
177
+ sha1.update(chunk)
178
+ end
179
+
180
+ sha1.hexdigest
181
+ end
182
+
183
+ end
184
+ end
@@ -0,0 +1,96 @@
1
+ module KmsTools
2
+ # Provides low-level encryption functionality for kms-tools
3
+ #
4
+ # @author Matt Krieger
5
+ class Encrypter < KmsTools::Base
6
+ # Size limit for encrypting data directly using {http://docs.aws.amazon.com/sdkforruby/api/Aws/KMS/Client.html#encrypt-instance_method Aws::KMS::Client.encrypt}
7
+ STRING_SIZE_LIMIT = 4096
8
+
9
+ # Key spec to use by default unless overridden
10
+ DEFAULT_KEY_SPEC = 'AES_256'
11
+
12
+ # Encrypt a string up 4KB in size
13
+ # @param str [String] String to encrypt
14
+ # @return [String] Base64 encoded ciphertext
15
+ def encrypt_string(str)
16
+ to_s64(kms_encrypt(str).ciphertext_blob)
17
+ end
18
+
19
+ # Call {http://docs.aws.amazon.com/sdkforruby/api/Aws/KMS/Client.html#encrypt-instance_method Aws::KMS::Client.encrypt} using object master_key
20
+ # @param str [String] String to encrypt
21
+ # @return [Object] {http://docs.aws.amazon.com/sdkforruby/api/Aws/KMS/Types/EncryptResponse.html Aws::KMS::Types::EncryptResponse}
22
+ def kms_encrypt(str)
23
+ kms.encrypt({:key_id => master_key, :plaintext => str})
24
+ end
25
+
26
+ # Encrypt a blob using private keys
27
+ #
28
+ # @param [Hash] params
29
+ # @option params [String] :cipher OpenSSL cipher to use for encryption
30
+ # @option params [String] :encrypted_key Encrypted private key
31
+ # @option params [String] :encrypted_iv Encrypted initialization vector
32
+ # @option params [String] :data Plaintext data blob
33
+ # @return [String] Binary encrypted ciphertext
34
+ def encrypt_with_data_key(params)
35
+ d = KmsTools::Decrypter.new()
36
+ cipher = OpenSSL::Cipher.new(params[:cipher])
37
+ cipher.encrypt
38
+ cipher.key = d.decrypt_string(params[:encrypted_key])
39
+ cipher.iv = d.decrypt_string(params[:encrypted_iv])
40
+ encrypted_data = cipher.update(params[:data]) + cipher.final
41
+ end
42
+
43
+
44
+ # Encrypt a stream using private keys
45
+ #
46
+ # @param [Hash] params
47
+ # @option params [String] :cipher OpenSSL cipher to use for encryption
48
+ # @option params [String] :encrypted_key Encrypted private key
49
+ # @option params [String] :encrypted_iv Encrypted initialization vector
50
+ # @option params [Stream] :in Input stream to read plaintext data from
51
+ # @option params [Stream] :out Stream to to write encrypted output to
52
+ def stream_encrypt_with_data_key(params)
53
+ d = KmsTools::Decrypter.new()
54
+
55
+ # set up cipher
56
+ cipher = OpenSSL::Cipher.new(params[:cipher])
57
+ cipher.encrypt
58
+ cipher.key = d.decrypt_string(params[:encrypted_key])
59
+ cipher.iv = d.decrypt_string(params[:encrypted_iv])
60
+
61
+ # write the output stream
62
+ buf = ""
63
+ params[:in].seek(params[:position], :SET) if params[:position]
64
+ while params[:in].read(STREAM_CHUNK_SIZE, buf)
65
+ params[:out] << cipher.update(buf)
66
+ end
67
+ params[:out] << cipher.final
68
+
69
+ # return true if nothing errored out
70
+ true
71
+ end
72
+
73
+ # Generate a data key to use for local symmetric encryption
74
+ # @return [Object] {http://docs.aws.amazon.com/sdkforruby/api/Aws/KMS/Types/GenerateDataKeyResponse.html Aws::KMS::Types::GenerateDataKeyResponse}
75
+ def new_key
76
+ kms.generate_data_key({
77
+ :key_id => master_key,
78
+ :key_spec => key_spec
79
+ })
80
+ end
81
+
82
+ # Generate Base64 encoded encrypted data key to use for local symmetric encryption
83
+ # @return [String] Base64 encoded encrypted data key
84
+ def new_encrypted_key
85
+ to_s64(new_key.ciphertext_blob)
86
+ end
87
+
88
+ # Key spec that will be used for data key creation
89
+ # @return [String] {http://docs.aws.amazon.com/sdkforruby/api/Aws/KMS/Types/GenerateDataKeyRequest.html#key_spec-instance_method AWS Key Spec}
90
+ def key_spec
91
+ @key_spec ||= DEFAULT_KEY_SPEC
92
+ end
93
+
94
+
95
+ end
96
+ end