encrypted_store 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|