encrypted_store 0.2.0
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.
- checksums.yaml +7 -0
- data/lib/generators/encrypted_store/encrypt_table/encrypt_table_generator.rb +12 -0
- data/lib/generators/encrypted_store/install/install_generator.rb +26 -0
- data/lib/generators/encrypted_store/install/templates/create_encryption_key_salts.rb +12 -0
- data/lib/generators/encrypted_store/install/templates/create_encryption_keys.rb +12 -0
- data/lib/generators/encrypted_store/install/templates/initializer.rb +6 -0
- data/lib/generators/encrypted_store/upgrade/ZeroOneFive/templates/upgrade_encryption_key_salts_to_015.rb +8 -0
- data/lib/generators/encrypted_store/upgrade/ZeroOneFive/templates/upgrade_encryption_keys_to_015.rb +8 -0
- data/lib/generators/encrypted_store/upgrade/ZeroOneFive/zero_one_five_generator.rb +24 -0
- data/lib/ribbon/encrypted_store.rb +33 -0
- data/lib/ribbon/encrypted_store/crypto_hash.rb +150 -0
- data/lib/ribbon/encrypted_store/errors.rb +16 -0
- data/lib/ribbon/encrypted_store/instance.rb +30 -0
- data/lib/ribbon/encrypted_store/mixins.rb +5 -0
- data/lib/ribbon/encrypted_store/mixins/active_record_mixin.rb +108 -0
- data/lib/ribbon/encrypted_store/mixins/active_record_mixin/encryption_key.rb +77 -0
- data/lib/ribbon/encrypted_store/mixins/active_record_mixin/encryption_key_salt.rb +28 -0
- data/lib/ribbon/encrypted_store/railtie.rb +23 -0
- data/lib/ribbon/encrypted_store/version.rb +5 -0
- data/lib/tasks/encrypted_store.rake +19 -0
- metadata +169 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 597b51a7a639daa48f0168cd455614d0badc530d
|
4
|
+
data.tar.gz: 0c3ac74f182c8825f0bf8a2cb40b5c6c1636e221
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: f7abe54066eb6000e60e38ac7e6bf8df46f3b5e50d6d85957b38c56436dde3221e18730a4d21a7076729e96b673f887325856b419cda5be666b02c506168db56
|
7
|
+
data.tar.gz: 8a280ee382d0de6e35f34bbe3b9ec683f57583bddb6a3cd823f0beb10c52e4348f3724f25344b220cb2d836172a0fb8b5c353b80944b24ab12f95a44df69d858
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module Ribbon::EncryptedStore
|
2
|
+
module Generators
|
3
|
+
class EncryptTableGenerator < Rails::Generators::Base
|
4
|
+
source_root File.expand_path('../templates', __FILE__)
|
5
|
+
argument :table_name, :type => :string
|
6
|
+
|
7
|
+
def create_migrations
|
8
|
+
generate "migration", "add_encrypted_store_to_#{table_name} encryption_key_id:integer encrypted_store:binary"
|
9
|
+
end
|
10
|
+
end # EncryptTableGenerator
|
11
|
+
end # Generators
|
12
|
+
end # Ribbon::EncryptedStore
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'rails/generators/active_record'
|
2
|
+
|
3
|
+
module Ribbon::EncryptedStore
|
4
|
+
module Generators
|
5
|
+
class InstallGenerator < Rails::Generators::Base
|
6
|
+
include Rails::Generators::Migration
|
7
|
+
|
8
|
+
class << self
|
9
|
+
def next_migration_number(*args)
|
10
|
+
ActiveRecord::Generators::Base.next_migration_number(*args)
|
11
|
+
end
|
12
|
+
end # Class Methods
|
13
|
+
|
14
|
+
source_root File.expand_path("../templates", __FILE__)
|
15
|
+
|
16
|
+
def create_initializer
|
17
|
+
copy_file "initializer.rb", "config/initializers/encrypted_store.rb"
|
18
|
+
end
|
19
|
+
|
20
|
+
def create_migrations
|
21
|
+
migration_template 'create_encryption_keys.rb', 'db/migrate/create_encryption_keys.rb'
|
22
|
+
migration_template 'create_encryption_key_salts.rb', 'db/migrate/create_encryption_key_salts.rb'
|
23
|
+
end
|
24
|
+
end # InstallEncryptedStoreGenerator
|
25
|
+
end # Generators
|
26
|
+
end # Ribbon::EncryptedStore
|
@@ -0,0 +1,12 @@
|
|
1
|
+
class CreateEncryptionKeySalts < ActiveRecord::Migration
|
2
|
+
def change
|
3
|
+
create_table :encryption_key_salts do |t|
|
4
|
+
t.integer :encryption_key_id
|
5
|
+
t.binary :salt
|
6
|
+
|
7
|
+
t.timestamps
|
8
|
+
end
|
9
|
+
|
10
|
+
add_index :encryption_key_salts, [:encryption_key_id, :salt], unique: true
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,8 @@
|
|
1
|
+
class UpgradeEncryptionKeySaltsTo015 < ActiveRecord::Migration
|
2
|
+
def change
|
3
|
+
add_column :encryption_key_salts, :created_at, :datetime
|
4
|
+
add_column :encryption_key_salts, :updated_at, :datetime
|
5
|
+
|
6
|
+
add_index :encryption_key_salts, [:encryption_key_id, :salt], unique: true
|
7
|
+
end
|
8
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'rails/generators/active_record'
|
2
|
+
|
3
|
+
module Ribbon::EncryptedStore
|
4
|
+
module Generators
|
5
|
+
module Upgrade
|
6
|
+
class ZeroOneFiveGenerator < Rails::Generators::Base
|
7
|
+
include Rails::Generators::Migration
|
8
|
+
|
9
|
+
class << self
|
10
|
+
def next_migration_number(*args)
|
11
|
+
ActiveRecord::Generators::Base.next_migration_number(*args)
|
12
|
+
end
|
13
|
+
end # Class Methods
|
14
|
+
|
15
|
+
source_root File.expand_path("../templates", __FILE__)
|
16
|
+
|
17
|
+
def create_migrations
|
18
|
+
migration_template 'upgrade_encryption_keys_to_015.rb', 'db/migrate/upgrade_encryption_keys_to_015.rb'
|
19
|
+
migration_template 'upgrade_encryption_key_salts_to_015.rb', 'db/migrate/upgrade_encryption_key_salts_to_015.rb'
|
20
|
+
end
|
21
|
+
end # ZeroOneFiveGenerator
|
22
|
+
end # Upgrade
|
23
|
+
end # Generators
|
24
|
+
end # Ribbon::EncryptedStore
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'ribbon/encrypted_store/version'
|
2
|
+
require 'ribbon/config'
|
3
|
+
|
4
|
+
module Ribbon
|
5
|
+
module EncryptedStore
|
6
|
+
require 'ribbon/encrypted_store/railtie' if defined?(Rails)
|
7
|
+
autoload(:CryptoHash, 'ribbon/encrypted_store/crypto_hash')
|
8
|
+
autoload(:Instance, 'ribbon/encrypted_store/instance')
|
9
|
+
autoload(:Errors, 'ribbon/encrypted_store/errors')
|
10
|
+
autoload(:Mixins, 'ribbon/encrypted_store/mixins')
|
11
|
+
|
12
|
+
class << self
|
13
|
+
def included(base)
|
14
|
+
if defined?(ActiveRecord) && base < ActiveRecord::Base
|
15
|
+
base.send(:include, Mixins::ActiveRecordMixin)
|
16
|
+
else
|
17
|
+
raise Errors::UnsupportedModelError
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def method_missing(meth, *args, &block)
|
22
|
+
instance.send(meth, *args, &block)
|
23
|
+
end
|
24
|
+
|
25
|
+
def instance
|
26
|
+
@__instance ||= Instance.new
|
27
|
+
end
|
28
|
+
end # Class Methods
|
29
|
+
end # EncryptedStore
|
30
|
+
end # Ribbon
|
31
|
+
|
32
|
+
# Create a shortcut to the module
|
33
|
+
EncryptedStore = Ribbon::EncryptedStore
|
@@ -0,0 +1,150 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
require 'json'
|
3
|
+
require 'zlib'
|
4
|
+
|
5
|
+
module Ribbon::EncryptedStore
|
6
|
+
class CryptoHash < Hash
|
7
|
+
def initialize(data={})
|
8
|
+
super()
|
9
|
+
merge!(data)
|
10
|
+
end
|
11
|
+
|
12
|
+
##
|
13
|
+
# Encrypts the hash using the data encryption key and salt.
|
14
|
+
#
|
15
|
+
# Returns a blob:
|
16
|
+
# | Byte 0 | Byte 1 | Byte 2 | Bytes 3...S | Bytes S+1...E | Bytes E+1..E+4 |
|
17
|
+
# ------------------------------------------------------------------------------------------------
|
18
|
+
# | Version | Salt Length | Iteration Magnitude | Salt | Encrypted Data | CRC32 |
|
19
|
+
def encrypt(dek, salt, iter_mag=10)
|
20
|
+
return nil if empty?
|
21
|
+
raise Errors::InvalidSaltSize, 'too long' if salt.bytes.length > 255
|
22
|
+
|
23
|
+
key, iv = _keyiv_gen(dek, salt, iter_mag)
|
24
|
+
|
25
|
+
encryptor = OpenSSL::Cipher::AES256.new(:CBC).encrypt
|
26
|
+
encryptor.key = key
|
27
|
+
encryptor.iv = iv
|
28
|
+
|
29
|
+
data_packet = _encrypted_data_header_v2(salt, iter_mag) + encryptor.update(self.to_json) + encryptor.final
|
30
|
+
_append_crc32(data_packet)
|
31
|
+
end
|
32
|
+
|
33
|
+
class << self
|
34
|
+
def decrypt(dek, data)
|
35
|
+
return CryptoHash.new unless data
|
36
|
+
salt, iter_mag, data = _split_binary_data(data)
|
37
|
+
|
38
|
+
key, iv = _keyiv_gen(dek, salt, iter_mag)
|
39
|
+
|
40
|
+
decryptor = OpenSSL::Cipher::AES256.new(:CBC).decrypt
|
41
|
+
decryptor.key = key
|
42
|
+
decryptor.iv = iv
|
43
|
+
|
44
|
+
new_hash = JSON.parse(decryptor.update(data) + decryptor.final)
|
45
|
+
new_hash = Hash[new_hash.map { |k,v| [k.to_sym, v] }]
|
46
|
+
CryptoHash.new(new_hash)
|
47
|
+
end
|
48
|
+
|
49
|
+
def _keyiv_gen(key, salt, iter_mag)
|
50
|
+
if iter_mag == -1
|
51
|
+
raise Errors::InvalidKeySize, 'must be exactly 256 bits' unless key.bytes.length == 32
|
52
|
+
raise Errors::InvalidSaltSize, 'must be exactly 128 bits' unless salt.bytes.length == 16
|
53
|
+
iv = salt
|
54
|
+
else
|
55
|
+
digest = OpenSSL::Digest::SHA256.new
|
56
|
+
key_and_iv = OpenSSL::PKCS5.pbkdf2_hmac(key, salt, 1 << iter_mag, 48, digest)
|
57
|
+
|
58
|
+
key = key_and_iv[0..31]
|
59
|
+
iv = key_and_iv[32..-1]
|
60
|
+
end
|
61
|
+
|
62
|
+
[key, iv]
|
63
|
+
end
|
64
|
+
|
65
|
+
def _split_binary_data(encrypted_data)
|
66
|
+
# Split encrypted data and CRC
|
67
|
+
bytes = encrypted_data.bytes
|
68
|
+
|
69
|
+
version = bytes[0]
|
70
|
+
version_method = "_split_binary_data_v#{version}"
|
71
|
+
|
72
|
+
if respond_to?(version_method)
|
73
|
+
send(version_method, encrypted_data)
|
74
|
+
else
|
75
|
+
raise Errors::UnsupportedVersionError, "Unsupported encrypted data version: #{version}"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def _split_binary_data_v1(encrypted_data)
|
80
|
+
bytes = encrypted_data.bytes
|
81
|
+
salt_length = bytes[1]
|
82
|
+
|
83
|
+
salt_start_index = 2
|
84
|
+
salt_end_index = salt_start_index + salt_length - 1
|
85
|
+
salt = bytes[salt_start_index..salt_end_index].pack('c*')
|
86
|
+
data = bytes[salt_end_index+1..-5].pack('c*')
|
87
|
+
|
88
|
+
crc = bytes[-4..-1]
|
89
|
+
raise Errors::ChecksumFailedError unless crc == _calc_crc32(encrypted_data[0..-5]).bytes
|
90
|
+
|
91
|
+
[salt, 12, data]
|
92
|
+
end
|
93
|
+
|
94
|
+
def _split_binary_data_v2(encrypted_data)
|
95
|
+
bytes = encrypted_data.bytes
|
96
|
+
salt_length = bytes[1]
|
97
|
+
iter_mag = bytes[2].chr.unpack('c').first
|
98
|
+
|
99
|
+
salt_start_index = 3
|
100
|
+
salt_end_index = salt_start_index + salt_length - 1
|
101
|
+
salt = bytes[salt_start_index..salt_end_index].pack('c*')
|
102
|
+
data = bytes[salt_end_index+1..-5].pack('c*')
|
103
|
+
|
104
|
+
crc = bytes[-4..-1]
|
105
|
+
raise Errors::ChecksumFailedError unless crc == _calc_crc32(encrypted_data[0..-5]).bytes
|
106
|
+
|
107
|
+
[salt, iter_mag, data]
|
108
|
+
end
|
109
|
+
|
110
|
+
|
111
|
+
def _calc_crc32(data)
|
112
|
+
[Zlib.crc32(data)].pack('N')
|
113
|
+
end
|
114
|
+
end # Class Methods
|
115
|
+
|
116
|
+
private
|
117
|
+
|
118
|
+
##
|
119
|
+
# Generates the version 1 encrypted data header:
|
120
|
+
# | Byte 0 | Byte 1 | Bytes 2...S
|
121
|
+
# ---------------------------------------------------
|
122
|
+
# | Version | Salt Length | Salt
|
123
|
+
#
|
124
|
+
def _encrypted_data_header_v1(salt)
|
125
|
+
"\x01" + salt.bytes.length.chr + salt
|
126
|
+
end
|
127
|
+
|
128
|
+
##
|
129
|
+
# Generates the version 2 encrypted data header:
|
130
|
+
# | Byte 0 | Byte 1 | Byte 2 | Bytes 3...S
|
131
|
+
# ----------------------------------------------------------------------
|
132
|
+
# | Version | Salt Length | Iteration Magnitude | Salt
|
133
|
+
#
|
134
|
+
def _encrypted_data_header_v2(salt, iter_mag)
|
135
|
+
"\x02" + salt.bytes.length.chr + [iter_mag].pack('c') + salt
|
136
|
+
end
|
137
|
+
|
138
|
+
def _keyiv_gen(key, salt, iter_mag)
|
139
|
+
self.class._keyiv_gen(key, salt, iter_mag)
|
140
|
+
end
|
141
|
+
|
142
|
+
def _append_crc32(data)
|
143
|
+
data + _calc_crc32(data)
|
144
|
+
end
|
145
|
+
|
146
|
+
def _calc_crc32(data)
|
147
|
+
self.class._calc_crc32(data)
|
148
|
+
end
|
149
|
+
end # CryptoHash
|
150
|
+
end # Ribbon::EncryptedStore
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Ribbon::EncryptedStore
|
2
|
+
module Errors
|
3
|
+
class Error < StandardError; end
|
4
|
+
|
5
|
+
# General Errors
|
6
|
+
class GeneralError < Error; end
|
7
|
+
class UnsupportedModelError < GeneralError; end
|
8
|
+
|
9
|
+
# CryptoHash Errors
|
10
|
+
class CryptoHashError < Error; end
|
11
|
+
class ChecksumFailedError < CryptoHashError; end
|
12
|
+
class InvalidSaltSize < CryptoHashError; end
|
13
|
+
class InvalidKeySize < CryptoHashError; end
|
14
|
+
class UnsupportedVersionError < CryptoHashError; end
|
15
|
+
end # Errors
|
16
|
+
end # Ribbon::EncryptedStore
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Ribbon::EncryptedStore
|
2
|
+
class Instance
|
3
|
+
def config(&block)
|
4
|
+
(@__config ||= Ribbon::Config.new).tap { |config|
|
5
|
+
if block_given?
|
6
|
+
config.define(&block)
|
7
|
+
end
|
8
|
+
}
|
9
|
+
end
|
10
|
+
|
11
|
+
##
|
12
|
+
# Preloads the most recent `amount` keys.
|
13
|
+
def preload_keys(amount=12)
|
14
|
+
keys = Mixins::ActiveRecordMixin.preload_keys(amount)
|
15
|
+
keys.each { |k| (@_decrypted_keys ||= {})[k.id] = k.decrypted_key }
|
16
|
+
end
|
17
|
+
|
18
|
+
def decrypt_key(dek, primary=false)
|
19
|
+
config.decrypt_key? ? config.decrypt_key.last.call(dek, primary) : dek
|
20
|
+
end
|
21
|
+
|
22
|
+
def encrypt_key(dek, primary=false)
|
23
|
+
config.encrypt_key? ? config.encrypt_key.last.call(dek, primary) : dek
|
24
|
+
end
|
25
|
+
|
26
|
+
def retrieve_dek(key_model, key_id)
|
27
|
+
(@_decrypted_keys ||= {})[key_id] ||= key_model.find(key_id).decrypted_key
|
28
|
+
end
|
29
|
+
end # Instance
|
30
|
+
end # Ribbon::EncryptedStore
|
@@ -0,0 +1,108 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
|
3
|
+
module Ribbon::EncryptedStore
|
4
|
+
module Mixins
|
5
|
+
module ActiveRecordMixin
|
6
|
+
autoload(:EncryptionKeySalt, 'ribbon/encrypted_store/mixins/active_record_mixin/encryption_key_salt')
|
7
|
+
autoload(:EncryptionKey, 'ribbon/encrypted_store/mixins/active_record_mixin/encryption_key')
|
8
|
+
|
9
|
+
class << self
|
10
|
+
def included(base)
|
11
|
+
base.before_save(:_encrypted_store_save)
|
12
|
+
base.extend(ClassMethods)
|
13
|
+
end
|
14
|
+
|
15
|
+
def descendants
|
16
|
+
Rails.application.eager_load! if defined?(Rails) && Rails.application
|
17
|
+
ActiveRecord::Base.descendants.select { |model| model < Mixins::ActiveRecordMixin }
|
18
|
+
end
|
19
|
+
|
20
|
+
def descendants?
|
21
|
+
!descendants.empty?
|
22
|
+
end
|
23
|
+
|
24
|
+
##
|
25
|
+
# Preloads the most recent `amount` keys.
|
26
|
+
def preload_keys(amount)
|
27
|
+
EncryptionKey.preload(amount) if descendants?
|
28
|
+
end
|
29
|
+
end # Module Methods
|
30
|
+
|
31
|
+
module ClassMethods
|
32
|
+
def _encrypted_store_data
|
33
|
+
@_encrypted_store_data ||= {}
|
34
|
+
end
|
35
|
+
|
36
|
+
def attr_encrypted(*args)
|
37
|
+
# Store attrs in class data
|
38
|
+
_encrypted_store_data[:encrypted_attributes] = args.map(&:to_sym)
|
39
|
+
|
40
|
+
args.each { |arg|
|
41
|
+
define_method(arg) { _encrypted_store_get(arg) }
|
42
|
+
define_method("#{arg}=") { |value| _encrypted_store_set(arg, value) }
|
43
|
+
}
|
44
|
+
end
|
45
|
+
end # ClassMethods
|
46
|
+
|
47
|
+
##
|
48
|
+
# Instance Methods
|
49
|
+
##
|
50
|
+
def reencrypt(encryption_key)
|
51
|
+
_crypto_hash
|
52
|
+
self.encryption_key_id = encryption_key.id
|
53
|
+
@_reencrypting = true
|
54
|
+
end
|
55
|
+
|
56
|
+
def reencrypt!(encryption_key)
|
57
|
+
reencrypt(encryption_key).tap { save! }
|
58
|
+
end
|
59
|
+
|
60
|
+
def _encrypted_store_data
|
61
|
+
self.class._encrypted_store_data
|
62
|
+
end
|
63
|
+
|
64
|
+
def _encryption_key_id
|
65
|
+
self.encryption_key_id ||= EncryptionKey.primary_encryption_key.id
|
66
|
+
end
|
67
|
+
|
68
|
+
def _crypto_hash
|
69
|
+
@_crypto_hash ||= CryptoHash.decrypt(_decrypted_key, self.encrypted_store)
|
70
|
+
end
|
71
|
+
|
72
|
+
def _decrypted_key
|
73
|
+
EncryptedStore.retrieve_dek(EncryptionKey, _encryption_key_id)
|
74
|
+
end
|
75
|
+
|
76
|
+
def _encrypted_store_get(field)
|
77
|
+
_crypto_hash[field]
|
78
|
+
end
|
79
|
+
|
80
|
+
def _encrypted_store_set(field, value)
|
81
|
+
attribute_will_change!(field)
|
82
|
+
_crypto_hash[field] = value
|
83
|
+
end
|
84
|
+
|
85
|
+
def _encrypted_store_save
|
86
|
+
if !(self.changed.map(&:to_sym) & _encrypted_store_data[:encrypted_attributes]).empty? || @_reencrypting
|
87
|
+
# Obtain a lock without overriding attribute values for this record.
|
88
|
+
record = self.class.unscoped { self.class.lock.find(id) } unless new_record?
|
89
|
+
|
90
|
+
unless @_reencrypting
|
91
|
+
self.encryption_key_id = record.encryption_key_id if record && record.encryption_key_id
|
92
|
+
end
|
93
|
+
|
94
|
+
iter_mag = Ribbon::EncryptedStore.config.iteration_magnitude? ?
|
95
|
+
Ribbon::EncryptedStore.config.iteration_magnitude :
|
96
|
+
-1
|
97
|
+
|
98
|
+
@_reencrypting = false
|
99
|
+
self.encrypted_store = _crypto_hash.encrypt(
|
100
|
+
_decrypted_key,
|
101
|
+
EncryptionKeySalt.generate_salt(_encryption_key_id),
|
102
|
+
iter_mag
|
103
|
+
)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end # ActiveRecordMixin
|
107
|
+
end # Mixins
|
108
|
+
end # Ribbon::EncryptedStore
|
@@ -0,0 +1,77 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
require 'base64'
|
3
|
+
|
4
|
+
module Ribbon::EncryptedStore
|
5
|
+
module Mixins
|
6
|
+
module ActiveRecordMixin
|
7
|
+
class EncryptionKey < ActiveRecord::Base
|
8
|
+
validates_uniqueness_of :primary, if: :primary
|
9
|
+
|
10
|
+
class << self
|
11
|
+
def primary_encryption_key
|
12
|
+
new_key unless _has_primary?
|
13
|
+
where(primary: true).last || last
|
14
|
+
end
|
15
|
+
|
16
|
+
def new_key(custom_key=nil)
|
17
|
+
dek = custom_key || SecureRandom.random_bytes(32)
|
18
|
+
|
19
|
+
transaction {
|
20
|
+
_has_primary? && where(primary: true).first.update_attributes(primary: false)
|
21
|
+
_create_primary_key(dek)
|
22
|
+
}
|
23
|
+
end
|
24
|
+
|
25
|
+
def retire_keys(key_ids=[])
|
26
|
+
pkey = primary_encryption_key
|
27
|
+
|
28
|
+
ActiveRecordMixin.descendants.each { |model|
|
29
|
+
records = key_ids.empty? ? model.where("encryption_key_id != ?", pkey.id)
|
30
|
+
: model.where("encryption_key_id IN (?)", key_ids)
|
31
|
+
records.each { |record| record.reencrypt!(pkey) }
|
32
|
+
}
|
33
|
+
|
34
|
+
pkey
|
35
|
+
end
|
36
|
+
|
37
|
+
##
|
38
|
+
# Preload the most recent `amount` keys.
|
39
|
+
def preload(amount)
|
40
|
+
primary_encryption_key # Ensure there's at least a primary key
|
41
|
+
order(:created_at).limit(amount)
|
42
|
+
end
|
43
|
+
|
44
|
+
def rotate_keys
|
45
|
+
new_key
|
46
|
+
retire_keys
|
47
|
+
end
|
48
|
+
|
49
|
+
def _has_primary?
|
50
|
+
where(primary: true).exists?
|
51
|
+
end
|
52
|
+
|
53
|
+
def _get_table_models
|
54
|
+
Rails.application.eager_load! if defined?(Rails) && Rails.application
|
55
|
+
ActiveRecord::Base.descendants
|
56
|
+
end
|
57
|
+
|
58
|
+
def _get_models_with_encrypted_store
|
59
|
+
_get_table_models.select { |model| model < Mixins::ActiveRecordMixin }
|
60
|
+
end
|
61
|
+
|
62
|
+
def _create_primary_key(dek)
|
63
|
+
self.new.tap { |key|
|
64
|
+
key.dek = EncryptedStore.encrypt_key(dek, true)
|
65
|
+
key.primary = true
|
66
|
+
key.save!
|
67
|
+
}
|
68
|
+
end
|
69
|
+
end # Class Methods
|
70
|
+
|
71
|
+
def decrypted_key
|
72
|
+
EncryptedStore.decrypt_key(self.dek, self.primary)
|
73
|
+
end
|
74
|
+
end # EncryptionKey
|
75
|
+
end # ActiveRecordMixin
|
76
|
+
end # Mixins
|
77
|
+
end # Ribbon::EncryptedStore
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
|
3
|
+
module Ribbon::EncryptedStore
|
4
|
+
module Mixins
|
5
|
+
module ActiveRecordMixin
|
6
|
+
class EncryptionKeySalt < ActiveRecord::Base
|
7
|
+
validates :salt, uniqueness: {scope: :encryption_key_id}
|
8
|
+
|
9
|
+
class << self
|
10
|
+
def generate_salt(encryption_key_id)
|
11
|
+
loop do
|
12
|
+
salt = SecureRandom.random_bytes(16)
|
13
|
+
begin
|
14
|
+
salt_record = self.new
|
15
|
+
salt_record.encryption_key_id = encryption_key_id
|
16
|
+
salt_record.salt = salt
|
17
|
+
salt_record.save!
|
18
|
+
return salt
|
19
|
+
rescue ActiveRecord::RecordNotUnique => e
|
20
|
+
next
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end # Class Methods
|
25
|
+
end # EncryptionKeySalt
|
26
|
+
end # ActiveRecordMixin
|
27
|
+
end # Mixins
|
28
|
+
end # Ribbon::EncryptedStore
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'ribbon/encrypted_store'
|
2
|
+
require 'rails'
|
3
|
+
require 'rails/generators'
|
4
|
+
|
5
|
+
module Ribbon
|
6
|
+
module EncryptedStore
|
7
|
+
class Railtie < Rails::Railtie
|
8
|
+
railtie_name :encrypted_store
|
9
|
+
|
10
|
+
rake_tasks do
|
11
|
+
Dir[
|
12
|
+
File.expand_path("../../../tasks", __FILE__) + '/**/*.rake'
|
13
|
+
].each { |rake_file| load rake_file }
|
14
|
+
end
|
15
|
+
|
16
|
+
generators do
|
17
|
+
Dir[
|
18
|
+
File.expand_path("../../../generators", __FILE__) + '/**/*.rb'
|
19
|
+
].each { |generator| require generator }
|
20
|
+
end
|
21
|
+
end # Railtie
|
22
|
+
end # EncryptedStore
|
23
|
+
end # Ribbon
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'ribbon/encrypted_store'
|
2
|
+
|
3
|
+
namespace :encrypted_store do
|
4
|
+
task :new_key, [:custom_key] => :environment do |t, args|
|
5
|
+
new_key = EncryptedStore::Mixins::ActiveRecordMixin::EncryptionKey.new_key(args[:custom_key])
|
6
|
+
puts "Created new primary key: #{new_key.id}"
|
7
|
+
end
|
8
|
+
|
9
|
+
task :retire_keys, [:key_ids] => :environment do |t, args|
|
10
|
+
key_ids = (args[:key_ids] && args[:key_ids].split(" ")) || []
|
11
|
+
new_primary_key = EncryptedStore::Mixins::ActiveRecordMixin::EncryptionKey.retire_keys(key_ids)
|
12
|
+
puts "Retired key_ids: #{key_ids} and reencrypted records with primary key: #{new_primary_key.id}"
|
13
|
+
end
|
14
|
+
|
15
|
+
task :rotate_keys => :environment do |t, args|
|
16
|
+
new_primary_key = EncryptedStore::Mixins::ActiveRecordMixin::EncryptionKey.rotate_keys
|
17
|
+
puts "Retired all key_ids and reencrypted records with new primary key: #{new_primary_key.id}"
|
18
|
+
end
|
19
|
+
end
|
metadata
ADDED
@@ -0,0 +1,169 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: encrypted_store
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Robert Honer
|
8
|
+
- Kayvon Ghaffari
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2015-10-03 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: bcrypt
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - "~>"
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: 3.1.3
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 3.1.3
|
24
|
+
type: :runtime
|
25
|
+
prerelease: false
|
26
|
+
version_requirements: !ruby/object:Gem::Requirement
|
27
|
+
requirements:
|
28
|
+
- - "~>"
|
29
|
+
- !ruby/object:Gem::Version
|
30
|
+
version: 3.1.3
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 3.1.3
|
34
|
+
- !ruby/object:Gem::Dependency
|
35
|
+
name: ribbon-config
|
36
|
+
requirement: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 0.1.0
|
41
|
+
type: :runtime
|
42
|
+
prerelease: false
|
43
|
+
version_requirements: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 0.1.0
|
48
|
+
- !ruby/object:Gem::Dependency
|
49
|
+
name: database_cleaner
|
50
|
+
requirement: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
type: :development
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: rspec
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
type: :development
|
70
|
+
prerelease: false
|
71
|
+
version_requirements: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
- !ruby/object:Gem::Dependency
|
77
|
+
name: rspec-rails
|
78
|
+
requirement: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
type: :development
|
84
|
+
prerelease: false
|
85
|
+
version_requirements: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
- !ruby/object:Gem::Dependency
|
91
|
+
name: pg
|
92
|
+
requirement: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
type: :development
|
98
|
+
prerelease: false
|
99
|
+
version_requirements: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
- !ruby/object:Gem::Dependency
|
105
|
+
name: rails
|
106
|
+
requirement: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: 4.0.0
|
111
|
+
type: :development
|
112
|
+
prerelease: false
|
113
|
+
version_requirements: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: 4.0.0
|
118
|
+
description: Provides the EncryptedStore mixin
|
119
|
+
email:
|
120
|
+
- robert@ribbonpayments.com
|
121
|
+
- kayvon@ribbonpayments.com
|
122
|
+
executables: []
|
123
|
+
extensions: []
|
124
|
+
extra_rdoc_files: []
|
125
|
+
files:
|
126
|
+
- lib/generators/encrypted_store/encrypt_table/encrypt_table_generator.rb
|
127
|
+
- lib/generators/encrypted_store/install/install_generator.rb
|
128
|
+
- lib/generators/encrypted_store/install/templates/create_encryption_key_salts.rb
|
129
|
+
- lib/generators/encrypted_store/install/templates/create_encryption_keys.rb
|
130
|
+
- lib/generators/encrypted_store/install/templates/initializer.rb
|
131
|
+
- lib/generators/encrypted_store/upgrade/ZeroOneFive/templates/upgrade_encryption_key_salts_to_015.rb
|
132
|
+
- lib/generators/encrypted_store/upgrade/ZeroOneFive/templates/upgrade_encryption_keys_to_015.rb
|
133
|
+
- lib/generators/encrypted_store/upgrade/ZeroOneFive/zero_one_five_generator.rb
|
134
|
+
- lib/ribbon/encrypted_store.rb
|
135
|
+
- lib/ribbon/encrypted_store/crypto_hash.rb
|
136
|
+
- lib/ribbon/encrypted_store/errors.rb
|
137
|
+
- lib/ribbon/encrypted_store/instance.rb
|
138
|
+
- lib/ribbon/encrypted_store/mixins.rb
|
139
|
+
- lib/ribbon/encrypted_store/mixins/active_record_mixin.rb
|
140
|
+
- lib/ribbon/encrypted_store/mixins/active_record_mixin/encryption_key.rb
|
141
|
+
- lib/ribbon/encrypted_store/mixins/active_record_mixin/encryption_key_salt.rb
|
142
|
+
- lib/ribbon/encrypted_store/railtie.rb
|
143
|
+
- lib/ribbon/encrypted_store/version.rb
|
144
|
+
- lib/tasks/encrypted_store.rake
|
145
|
+
homepage: http://github.com/ribbon/encrypted_store
|
146
|
+
licenses:
|
147
|
+
- BSD
|
148
|
+
metadata: {}
|
149
|
+
post_install_message:
|
150
|
+
rdoc_options: []
|
151
|
+
require_paths:
|
152
|
+
- lib
|
153
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
154
|
+
requirements:
|
155
|
+
- - ">="
|
156
|
+
- !ruby/object:Gem::Version
|
157
|
+
version: '0'
|
158
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
159
|
+
requirements:
|
160
|
+
- - ">="
|
161
|
+
- !ruby/object:Gem::Version
|
162
|
+
version: '0'
|
163
|
+
requirements: []
|
164
|
+
rubyforge_project:
|
165
|
+
rubygems_version: 2.4.3
|
166
|
+
signing_key:
|
167
|
+
specification_version: 4
|
168
|
+
summary: Provides the EncryptedStore mixin
|
169
|
+
test_files: []
|