kms-tools 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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