portunus 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +3 -0
  3. data/.nvmrc +1 -0
  4. data/.travis.yml +27 -0
  5. data/CODE_OF_CONDUCT.md +74 -0
  6. data/Gemfile +6 -0
  7. data/Gemfile.lock +189 -0
  8. data/LICENSE.txt +21 -0
  9. data/README.md +289 -0
  10. data/Rakefile +2 -0
  11. data/bin/console +14 -0
  12. data/bin/setup +8 -0
  13. data/lib/Rakefile +4 -0
  14. data/lib/generators/install_generator.rb +26 -0
  15. data/lib/generators/templates/create_portunus.rb.erb +28 -0
  16. data/lib/portunus/configuration.rb +49 -0
  17. data/lib/portunus/data_encryption_key.rb +25 -0
  18. data/lib/portunus/data_key_generator.rb +55 -0
  19. data/lib/portunus/encryptable.rb +42 -0
  20. data/lib/portunus/encrypters/open_ssl_aes.rb +58 -0
  21. data/lib/portunus/encrypters/time.rb +1 -0
  22. data/lib/portunus/field_configurer.rb +157 -0
  23. data/lib/portunus/hasher.rb +9 -0
  24. data/lib/portunus/master_key.rb +15 -0
  25. data/lib/portunus/railtie.rb +14 -0
  26. data/lib/portunus/rotators/dek.rb +69 -0
  27. data/lib/portunus/rotators/kek.rb +51 -0
  28. data/lib/portunus/storage_adaptors/credentials.rb +48 -0
  29. data/lib/portunus/storage_adaptors/environment.rb +67 -0
  30. data/lib/portunus/tasks/generate_keys.rake +36 -0
  31. data/lib/portunus/tasks/rotate_keys.rake +36 -0
  32. data/lib/portunus/type_caster.rb +37 -0
  33. data/lib/portunus/type_casters/boolean.rb +39 -0
  34. data/lib/portunus/type_casters/date.rb +29 -0
  35. data/lib/portunus/type_casters/date_time.rb +29 -0
  36. data/lib/portunus/type_casters/float.rb +29 -0
  37. data/lib/portunus/type_casters/integer.rb +29 -0
  38. data/lib/portunus/type_casters/string.rb +29 -0
  39. data/lib/portunus/version.rb +3 -0
  40. data/lib/portunus.rb +39 -0
  41. data/portunus.gemspec +51 -0
  42. data/tmp/log/development.log +0 -0
  43. metadata +255 -0
@@ -0,0 +1,28 @@
1
+ class CreatePortunus < ActiveRecord::Migration<%= migration_version %>
2
+ def change
3
+ create_table :portunus_data_encryption_keys do |t|
4
+ t.string :encrypted_key, null: false
5
+ t.string :master_keyname, null: false
6
+ t.string :encryptable_type, null: false
7
+ t.integer :encryptable_id, null: false
8
+ t.datetime :last_dek_rotation
9
+ t.datetime :last_kek_rotation
10
+ t.timestamps
11
+ end
12
+
13
+ add_index(
14
+ :portunus_data_encryption_keys,
15
+ [:encryptable_id, :encryptable_type],
16
+ unique: true,
17
+ name: "portunus_dek_on_encryptable_id_and_encryptable_type"
18
+ )
19
+ add_index(
20
+ :portunus_data_encryption_keys,
21
+ :last_dek_rotation
22
+ )
23
+ add_index(
24
+ :portunus_data_encryption_keys,
25
+ :last_kek_rotation
26
+ )
27
+ end
28
+ end
@@ -0,0 +1,49 @@
1
+ module Portunus
2
+ class Configuration
3
+ attr_accessor :storage_adaptor, :encrypter, :max_key_duration
4
+
5
+ def initialize
6
+ @storage_adaptor = ::Portunus::StorageAdaptors::Credentials
7
+ @encrypter = ::Portunus::Encrypters::OpenSslAes
8
+ @keys_loaded = false
9
+ @master_key_names = []
10
+ @max_key_duration = 6.months
11
+ end
12
+
13
+ def load_keys
14
+ storage_adaptor.load
15
+ @keys_loaded = true
16
+ end
17
+
18
+ def add_key(key_name)
19
+ @master_key_names.push(key_name)
20
+ # we want to load all the names of the keys in the storage
21
+ # adaptor. Because we might need to search through an environment
22
+ # that has quite a few keys often for every key we want to load
23
+ # the valid keys
24
+ end
25
+
26
+ def keys_loaded?
27
+ @keys_loaded
28
+ end
29
+
30
+ def reset_master_keys
31
+ # Clear all master keys to empty, used for testing
32
+ @master_key_names = []
33
+ @keys_loaded = false
34
+ end
35
+
36
+ def reload_master_keys
37
+ # Perform a reload on the master keys. This is used in tests and to
38
+ # add new keys into the environment without rebooting the app.
39
+ @master_key_names = []
40
+ load_keys
41
+ end
42
+
43
+ def master_key_names
44
+ load_keys unless keys_loaded?
45
+
46
+ @master_key_names
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,25 @@
1
+ module Portunus
2
+ class DataEncryptionKey < ::ActiveRecord::Base
3
+ belongs_to :encryptable, polymorphic: true
4
+
5
+ def key
6
+ ::Portunus.configuration.encrypter.decrypt(
7
+ key: master_encryption_key.value,
8
+ value: encrypted_key
9
+ )
10
+ end
11
+
12
+ def master_keyname=(new_key_value)
13
+ @_master_encryption_key = nil
14
+ super(new_key_value)
15
+ end
16
+
17
+ private
18
+
19
+ def master_encryption_key
20
+ @_master_encryption_key ||= Portunus.configuration.storage_adaptor.lookup(
21
+ master_keyname.to_sym
22
+ )
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,55 @@
1
+ module Portunus
2
+ class DataKeyGenerator
3
+ def self.generate(encrypted_object)
4
+ new(encrypted_object: encrypted_object).generate
5
+ end
6
+
7
+ def initialize(
8
+ encrypted_object:,
9
+ encrypter: ::Portunus.configuration.encrypter,
10
+ key_finder: Portunus.configuration.storage_adaptor
11
+ )
12
+ @encrypter = encrypter
13
+ @key_finder = key_finder
14
+ @encrypted_object = encrypted_object
15
+ end
16
+
17
+ def generate
18
+ dek = encrypted_object.build_data_encryption_key(
19
+ encrypted_key: new_encrypted_key,
20
+ master_keyname: master_keyname
21
+ )
22
+
23
+ if dek.key != new_plaintext_key
24
+ raise ::Portunus::Error.new(
25
+ "Dek Key creation failed: Decrypted key does not match the original"
26
+ )
27
+ end
28
+
29
+ dek
30
+ end
31
+
32
+ private
33
+
34
+ attr_reader :encrypted_object, :encrypter, :key_finder
35
+
36
+ def new_encrypted_key
37
+ @_new_encrypted_key ||= encrypter.encrypt(
38
+ key: master_encryption_key.value, value: new_plaintext_key
39
+ )
40
+ end
41
+
42
+ def new_plaintext_key
43
+ # this will be a base64 encoded key
44
+ @_new_key ||= encrypter.generate_key
45
+ end
46
+
47
+ def master_keyname
48
+ @_master_keyname ||= key_finder.list.sample
49
+ end
50
+
51
+ def master_encryption_key
52
+ @_master_encryption_key = key_finder.lookup(master_keyname)
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,42 @@
1
+ module Portunus
2
+ module Encryptable
3
+ extend ActiveSupport::Concern
4
+
5
+ class_methods do
6
+ def encrypted_fields_list
7
+ @_encrypted_fields_list ||= []
8
+ end
9
+
10
+ def encrypted_fields(*fields)
11
+ fields.map do |field|
12
+ ::Portunus::FieldConfigurer.for(self, field)
13
+ end
14
+ end
15
+ end
16
+
17
+ included do
18
+ before_validation :hash_encrypted_fields
19
+
20
+ has_one(
21
+ :data_encryption_key,
22
+ as: :encryptable,
23
+ class_name: "::Portunus::DataEncryptionKey"
24
+ )
25
+ end
26
+
27
+ private
28
+
29
+ def hash_encrypted_fields
30
+ self.class.encrypted_fields_list.each do |field|
31
+ hashed_field_name = "hashed_#{field}".to_sym
32
+
33
+ if respond_to?(hashed_field_name)
34
+ write_attribute(
35
+ hashed_field_name,
36
+ ::Portunus::Hasher.for(send(field.to_sym))
37
+ )
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,58 @@
1
+ module Portunus
2
+ module Encrypters
3
+ class OpenSslAes
4
+ def self.encrypt(key:, value:)
5
+ new(key: key, value: value).encrypt
6
+ end
7
+
8
+ def self.generate_key
9
+ cipher = OpenSSL::Cipher::AES256.new :CBC
10
+ cipher.encrypt
11
+ Base64.strict_encode64(cipher.random_key)
12
+ end
13
+
14
+ def self.decrypt(key:, value:)
15
+ new(key: key, value: value).decrypt
16
+ end
17
+
18
+ def initialize(key:, value:)
19
+ @key = Base64.strict_decode64(key)
20
+ @value = value
21
+ end
22
+
23
+ def encrypt
24
+ cipher = OpenSSL::Cipher::AES256.new :CBC
25
+ cipher.encrypt
26
+ iv = cipher.random_iv
27
+
28
+ cipher.key = key
29
+ cipher.iv = iv
30
+
31
+ encrypted_output = cipher.update(value) + cipher.final
32
+
33
+ [
34
+ Base64.strict_encode64(iv),
35
+ Base64.strict_encode64(encrypted_output)
36
+ ].join("$")
37
+ end
38
+
39
+ def decrypt
40
+ iv, encrypted_text = value.split("$")
41
+
42
+ decipher = OpenSSL::Cipher::AES256.new :CBC
43
+ decipher.decrypt
44
+
45
+ decipher.iv = Base64.strict_decode64(iv) # previously saved
46
+ decipher.key = key
47
+
48
+ output = decipher.update(Base64.strict_decode64(encrypted_text)) + decipher.final
49
+
50
+ output
51
+ end
52
+
53
+ private
54
+
55
+ attr_reader :key, :value
56
+ end
57
+ end
58
+ end
@@ -0,0 +1 @@
1
+ # DELETE ME, USE DATETIME AND NOT TIME CLASS
@@ -0,0 +1,157 @@
1
+ module Portunus
2
+ class FieldConfigurer
3
+ def self.for(object, field)
4
+ new(object: object, field: field).setup
5
+ end
6
+
7
+ def initialize(object:, field:)
8
+ require "pry"
9
+ @object = object
10
+
11
+ if field.is_a?(Hash)
12
+ @field = field.keys.first.to_sym
13
+ @field_options = default_field_options.merge(field[field.keys.first])
14
+ else
15
+ @field = field
16
+ @field_options = default_field_options
17
+ end
18
+ end
19
+
20
+ def setup
21
+ # add the field to a configuration so we can easily determine if it's
22
+ # a field we need to attempt to encrypt and decrypt. We need this in
23
+ # the `field_before_typecast` method since the normal accessors are
24
+ # not used after a validation fail
25
+ register_field
26
+
27
+ # setup the methods on the model itself
28
+ define_instance_methods
29
+ end
30
+
31
+ private
32
+
33
+ attr_accessor :field, :field_options, :object
34
+
35
+ def default_field_options
36
+ {
37
+ type: :string
38
+ }
39
+ end
40
+
41
+ def instance_methods_for_model
42
+ [
43
+ :define_getter,
44
+ :define_setter,
45
+ :define_before_type_cast,
46
+ :define_association
47
+ ]
48
+ end
49
+
50
+ def define_instance_methods
51
+ instance_methods_for_model.map do |method|
52
+ send(method)
53
+ end
54
+ end
55
+
56
+ def define_setter
57
+ l_field_options = field_options
58
+
59
+ # Override the setter of the field to do the encryption
60
+ object.define_method "#{field}=" do |value, &block|
61
+ if value.present?
62
+ dek = data_encryption_key
63
+
64
+ encrypted_value = ::Portunus.
65
+ configuration.
66
+ encrypter.
67
+ encrypt(
68
+ value: Portunus::TypeCaster.cast(
69
+ value: value,
70
+ type: l_field_options[:type]
71
+ ),
72
+ key: dek.key
73
+ )
74
+ end
75
+
76
+ super(encrypted_value)
77
+ end
78
+ end
79
+
80
+ def define_getter
81
+ # This is required to force the proper scope in this context.
82
+ l_field = field
83
+ l_field_options = field_options
84
+
85
+ object.define_method(l_field.to_sym) do
86
+ value = read_attribute(l_field.to_sym)
87
+
88
+ if value.present?
89
+ dek = data_encryption_key
90
+
91
+ decrypted_value = ::Portunus.
92
+ configuration.
93
+ encrypter.
94
+ decrypt(value: value, key: dek.key)
95
+
96
+ Portunus::TypeCaster.uncast(
97
+ value: decrypted_value,
98
+ type: l_field_options[:type]
99
+ )
100
+ else
101
+ nil
102
+ end
103
+ end
104
+ end
105
+
106
+ def define_before_type_cast
107
+ l_field = field
108
+ l_field_options = field_options
109
+
110
+ object.define_method "#{l_field}_before_type_cast" do
111
+ value = super()
112
+ encrypted = self.class.encrypted_fields_list.include?(l_field.to_sym)
113
+
114
+ if encrypted && value.present?
115
+ dek = data_encryption_key
116
+ decrypted_value = ::Portunus.
117
+ configuration.
118
+ encrypter.
119
+ decrypt(value: value, key: dek.key)
120
+
121
+ value = ::Portunus::TypeCaster.uncast(
122
+ value: decrypted_value,
123
+ type: l_field_options[:type]
124
+ )
125
+ end
126
+
127
+ return value
128
+ end
129
+ end
130
+
131
+ def define_association
132
+ # setup a lazy instantiaion of the DEK so that we don't need to worry
133
+ # about building it for every type of model
134
+ object.define_method :data_encryption_key do
135
+ # this is to determine if a data encryption key is present
136
+ # if not it will lazily create one and fill out the attribute
137
+ # field on the model containing the key
138
+ result = super()
139
+
140
+ if result.blank?
141
+ # self here is the model including encryptable. We pass this
142
+ # so we can call the rails build_data_encryption_key on the
143
+ # model and set up polymorphic columns automatically
144
+ dek = ::Portunus::DataKeyGenerator.generate(self)
145
+ dek
146
+ else
147
+ result
148
+ end
149
+ end
150
+ end
151
+
152
+ def register_field
153
+ # Register the field so we can look it up later
154
+ object.encrypted_fields_list << field
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,9 @@
1
+ module Portunus
2
+ class Hasher
3
+ def self.for(input)
4
+ return nil if input.blank?
5
+
6
+ Digest::SHA2.new(512).hexdigest(input)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,15 @@
1
+ module Portunus
2
+ class MasterKey
3
+ include ActiveModel::Model
4
+
5
+ attr_accessor :name, :value, :enabled, :created_at
6
+
7
+ def self.load
8
+ Portunus.configuration.storage_adaptor.load
9
+ end
10
+
11
+ def self.lookup(key_name)
12
+ Portunus.configuration.storage_adaptor.lookup(key_name)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,14 @@
1
+ # lib/railtie.rb
2
+ require "portunus"
3
+ require "rails"
4
+
5
+ module Portunus
6
+ class Railtie < Rails::Railtie
7
+ railtie_name :portunus
8
+
9
+ rake_tasks do
10
+ path = File.expand_path(__dir__)
11
+ Dir.glob("#{path}/tasks/**/*.rake").each { |f| load f }
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,69 @@
1
+ module Portunus
2
+ module Rotators
3
+ class Dek
4
+ def self.for(data_encryption_key)
5
+ new(data_encryption_key).rotate
6
+ end
7
+
8
+ def initialize(data_encryption_key)
9
+ @data_encryption_key = data_encryption_key
10
+ end
11
+
12
+ def rotate
13
+ encryptable = data_encryption_key.encryptable
14
+
15
+ encryptable.class.encrypted_fields_list.map do |field_name|
16
+ field_value_map[field_name.to_sym] = encryptable.send(field_name.to_sym)
17
+ end
18
+
19
+ data_encryption_key.encrypted_key = new_encrypted_key
20
+
21
+ field_value_map.map do |field_name, value|
22
+ encryptable.send("#{field_name}=".to_sym, value)
23
+ end
24
+
25
+ ActiveRecord::Base.transaction do
26
+ encryptable.save
27
+ data_encryption_key.last_dek_rotation = DateTime.now
28
+ data_encryption_key.save
29
+ end
30
+
31
+ true
32
+ rescue StandardError => error
33
+ raise ::Portunus::Error.new(
34
+ "Rotating DEK failed: #{error.full_message}"
35
+ )
36
+ end
37
+
38
+ private
39
+
40
+ attr_reader :data_encryption_key
41
+
42
+ def storage_adaptor
43
+ ::Portunus.configuration.storage_adaptor
44
+ end
45
+
46
+ def encrypter
47
+ ::Portunus.configuration.encrypter
48
+ end
49
+
50
+ def field_value_map
51
+ @_field_value_map ||= {}
52
+ end
53
+
54
+ def master_key
55
+ storage_adaptor.lookup(data_encryption_key.master_keyname)
56
+ end
57
+
58
+ def new_plaintext_key
59
+ @_new_plaintext_key ||= encrypter.generate_key
60
+ end
61
+
62
+ def new_encrypted_key
63
+ encrypter.encrypt(
64
+ key: master_key.value, value: new_plaintext_key
65
+ )
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,51 @@
1
+ module Portunus
2
+ module Rotators
3
+ class Kek
4
+ def self.for(data_encryption_key)
5
+ new(data_encryption_key).rotate
6
+ end
7
+
8
+ def initialize(data_encryption_key)
9
+ @data_encryption_key = data_encryption_key
10
+ @unencrypted_dek = data_encryption_key.key
11
+ end
12
+
13
+ def rotate
14
+ data_encryption_key.master_keyname = new_master_key_name
15
+ data_encryption_key.encrypted_key = encrypted_dek_with_new_master
16
+ data_encryption_key.last_kek_rotation = DateTime.now
17
+ data_encryption_key.save!
18
+ end
19
+
20
+ private
21
+
22
+ attr_reader :data_encryption_key, :unencrypted_dek
23
+
24
+ def encrypted_dek_with_new_master
25
+ Portunus.configuration.encrypter.encrypt(
26
+ key: new_master_key.value,
27
+ value: unencrypted_dek
28
+ )
29
+ end
30
+
31
+ def new_master_key
32
+ @_new_master_key ||= ::Portunus.configuration.storage_adaptor.lookup(
33
+ new_master_key_name.to_sym
34
+ )
35
+ end
36
+
37
+ def wrapped_current_master_key
38
+ [data_encryption_key.master_keyname]
39
+ end
40
+
41
+ def master_keys
42
+ Portunus.configuration.storage_adaptor.list
43
+ end
44
+
45
+ def new_master_key_name
46
+ @_new_master_key_name ||= (master_keys - wrapped_current_master_key).
47
+ sample
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,48 @@
1
+ module Portunus
2
+ module StorageAdaptors
3
+ class Credentials
4
+ def self.for(data_encryption_key)
5
+ self.lookup(data_encryption_key.master_keyname)
6
+ end
7
+
8
+ def self.load
9
+ key_names = Rails.application.credentials.portunus.keys
10
+
11
+ key_names.map do |key_name|
12
+ ::Portunus.configuration.add_key(key_name)
13
+ end
14
+ end
15
+
16
+ # Required method
17
+ def self.lookup(key_name)
18
+ master_key = Rails.application.credentials.portunus[key_name.to_sym]
19
+
20
+ MasterKey.new(
21
+ enabled: master_key[:enabled],
22
+ name: key_name,
23
+ value: master_key[:key],
24
+ created_at: master_key[:created_at]
25
+ )
26
+ rescue StandardError
27
+ raise ::Portunus::Error.new("Portunus: Master key not found")
28
+ end
29
+
30
+ # Required method
31
+ def self.list
32
+ key_names = Rails.application.credentials.portunus.keys
33
+ # reject any disabled keys so we no longer utilize them for new
34
+ # deks
35
+ key_names.reject! do |key_name|
36
+ Rails.application.credentials.portunus[key_name][:enabled] == false
37
+ end
38
+
39
+ if key_names.length == 0
40
+ raise ::Portunus::Error.new("No valid master keys configured")
41
+ end
42
+
43
+ key_names
44
+ end
45
+ end
46
+ end
47
+ end
48
+
@@ -0,0 +1,67 @@
1
+ module Portunus
2
+ module StorageAdaptors
3
+ class Environment
4
+ def self.for(data_encryption_key)
5
+ self.lookup(data_encryption_key.master_keyname)
6
+ end
7
+
8
+ def self.key_map
9
+ @@key_map ||= {}
10
+ end
11
+
12
+ def self.reset_key_map
13
+ @@key_map = {}
14
+ end
15
+
16
+ def self.load
17
+ key_names = ENV.keys.select { |key| key.start_with?("PORTUNUS_") }
18
+
19
+ key_names.map do |name|
20
+ _portunus, key_name, key_type = name.split("_")
21
+
22
+ if self.key_map[key_name.to_sym].blank?
23
+ self.key_map[key_name.to_sym] = {}
24
+ end
25
+
26
+ self.key_map[key_name.to_sym][key_type.to_sym] = ENV.fetch(name)
27
+ end
28
+
29
+ true
30
+ rescue StandardError => error
31
+ raise ::Portunus::Error.new(
32
+ "Portunus: Master keys failed to load: #{error.full_message}"
33
+ )
34
+ end
35
+
36
+ def self.lookup(key_name)
37
+ master_key = self.key_map[key_name.to_sym]
38
+
39
+ MasterKey.new(
40
+ enabled: master_key[:ENABLED],
41
+ name: key_name,
42
+ value: master_key[:KEY],
43
+ created_at: master_key[:CREATED]
44
+ )
45
+ rescue StandardError
46
+ raise ::Portunus::Error.new("Portunus: Master key not found")
47
+ end
48
+
49
+ def self.list
50
+ # Select only enabled keys
51
+ key_names = self.key_map.keys.map do |keyname|
52
+ keyname if self.key_map[keyname][:ENABLED] == "true"
53
+ end.compact
54
+
55
+ if key_names.length == 0
56
+ raise ::Portunus::Error.new("No valid master keys configured")
57
+ end
58
+
59
+ key_names
60
+ end
61
+
62
+ private
63
+
64
+ attr_reader :data_encryption_key
65
+ end
66
+ end
67
+ end