cryptonite 0.0.4 → 0.1.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 +4 -4
- data/.rubocop.yml +63 -0
- data/.travis.yml +3 -0
- data/README.md +29 -0
- data/Rakefile +3 -3
- data/cryptonite.gemspec +17 -16
- data/lib/cryptonite.rb +47 -45
- data/lib/cryptonite/coder.rb +37 -0
- data/lib/cryptonite/key_extractor.rb +50 -0
- data/lib/cryptonite/version.rb +1 -1
- data/spec/cryptonite/coder_spec.rb +53 -0
- data/spec/cryptonite_spec.rb +54 -32
- data/spec/spec_helper.rb +4 -2
- metadata +21 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f702ef8016f18189a7874c4bf8dc927465620aaa
|
4
|
+
data.tar.gz: 8e53c002975cb9d83266b29c90d93bc39f395f2b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7917a42f42863c853e9cfc2c5aa8efe94b60960872d787332759ee719545b7b678acdc1d53eeb3b83e422a1e9ade109663598b93c27c9db53324a21a0200bc57
|
7
|
+
data.tar.gz: aa4615f0259f2e0b9b3498eec6b65fb802c8d1610f8ae399d58b75adb0eedd292466294fb636a7c2698b116c0d8ac0695f0a6f64daacdd37d73c1fc8afc2ca0c
|
data/.rubocop.yml
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
AllCops:
|
2
|
+
RunRailsCops: true
|
3
|
+
Exclude:
|
4
|
+
# Rubocop's default
|
5
|
+
- 'vendor/**/*'
|
6
|
+
# To ensure smooth `rake rails:update`, we exclude some of the Rails' generated files
|
7
|
+
# please add new Rails' generated files to this list after running `rake rails:update` (if any)
|
8
|
+
# please add your exclude specific for this project after this section
|
9
|
+
- 'bin/**/*'
|
10
|
+
- 'config/boot.rb'
|
11
|
+
- 'config/application.rb'
|
12
|
+
- 'config/environment.rb'
|
13
|
+
- 'config/environments/development.rb'
|
14
|
+
- 'config/environments/production.rb'
|
15
|
+
- 'config/environments/test.rb'
|
16
|
+
- 'config/initializers/assets.rb'
|
17
|
+
- 'config/initializers/backtrace_silencers.rb'
|
18
|
+
- 'config/initializers/cookies_serializer.rb'
|
19
|
+
- 'config/initializers/filter_parameter_logging.rb'
|
20
|
+
- 'config/initializers/inflections.rb'
|
21
|
+
- 'config/initializers/mime_types.rb'
|
22
|
+
- 'config/initializers/session_store.rb'
|
23
|
+
- 'config/initializers/wrap_parameters.rb'
|
24
|
+
- 'db/schema.rb'
|
25
|
+
- 'script/**/*'
|
26
|
+
# Common generated files (projects independent)
|
27
|
+
- 'config/unicorn.rb'
|
28
|
+
- 'config/initializers/devise.rb'
|
29
|
+
# Add your exclude files specific to this project here
|
30
|
+
- 'config/initializers/maps.rb'
|
31
|
+
|
32
|
+
Style/AlignParameters:
|
33
|
+
Enabled: true
|
34
|
+
EnforcedStyle: with_fixed_indentation
|
35
|
+
|
36
|
+
Style/Documentation:
|
37
|
+
Enabled: false
|
38
|
+
|
39
|
+
# If the project still supporting ruby 1.8, uncomment the following lines to force Rubocop
|
40
|
+
# to use old hash_rockets syntax, if left commented, Rubocop will force the newer ruby19 syntax
|
41
|
+
# Style/HashSyntax:
|
42
|
+
# EnforcedStyle: hash_rockets
|
43
|
+
|
44
|
+
Style/SingleSpaceBeforeFirstArg:
|
45
|
+
Enabled: false
|
46
|
+
|
47
|
+
##################### Metrics ##################################
|
48
|
+
|
49
|
+
Metrics/LineLength:
|
50
|
+
Max: 120
|
51
|
+
AllowURI: true
|
52
|
+
|
53
|
+
Metrics/MethodLength:
|
54
|
+
CountComments: false
|
55
|
+
Exclude:
|
56
|
+
- 'db/migrate/*'
|
57
|
+
|
58
|
+
|
59
|
+
# Project specific settings
|
60
|
+
|
61
|
+
Style/BlockComments:
|
62
|
+
Exclude:
|
63
|
+
- 'spec/spec_helper.rb'
|
data/.travis.yml
CHANGED
data/README.md
CHANGED
@@ -46,6 +46,35 @@ ActiveRecord methods that operate massively on records do not use the
|
|
46
46
|
serialization features and so encryption / decryption does not take place
|
47
47
|
there. This is by design.
|
48
48
|
|
49
|
+
*Note: Currently the independency on private key is not fully implemented.*
|
50
|
+
|
51
|
+
### Migration Helpers
|
52
|
+
|
53
|
+
In order to facilitate migration two module methods have been implemented that
|
54
|
+
operate independently from the ActiveRecord models. In a migration file the
|
55
|
+
`Cryptonite.encrypt_model_attributes` may be used to facilitate upwards
|
56
|
+
migration and `Cryptonite.decrypt_model_attributes` for downwards migration.
|
57
|
+
The parameters is the ActiveRecord model class and the exact same parameters as
|
58
|
+
the `attr_encrypted` method, e.g.
|
59
|
+
|
60
|
+
class ChangeSecretInUsers < ActiveRecord::Migration
|
61
|
+
def up
|
62
|
+
change_column :users, :secret, :text, limit: 4096
|
63
|
+
|
64
|
+
say_with_time "encrypt_user_secrets" do
|
65
|
+
Cryptonite.encrypt_model_attributes(User, :secret, keypair: 'private_key.pem', private_key_password: 'test')
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def down
|
70
|
+
say_with_time "decrypt_user_secrets" do
|
71
|
+
Cryptonite.decrypt_model_attributes(User, :secret, keypair: 'private_key.pem', private_key_password: 'test')
|
72
|
+
end
|
73
|
+
|
74
|
+
change_column :users, :secret, :string
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
49
78
|
## Key Generation
|
50
79
|
|
51
80
|
Generate a key pair:
|
data/Rakefile
CHANGED
data/cryptonite.gemspec
CHANGED
@@ -4,24 +4,25 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
4
|
require 'cryptonite/version'
|
5
5
|
|
6
6
|
Gem::Specification.new do |spec|
|
7
|
-
spec.name =
|
7
|
+
spec.name = 'cryptonite'
|
8
8
|
spec.version = Cryptonite::VERSION
|
9
|
-
spec.authors = [
|
10
|
-
spec.email = [
|
11
|
-
spec.summary =
|
12
|
-
spec.description =
|
13
|
-
spec.homepage =
|
14
|
-
spec.license =
|
9
|
+
spec.authors = ['GaggleAMP']
|
10
|
+
spec.email = ['info@gaggleamp.com']
|
11
|
+
spec.summary = 'Enables the encryption of specific ActiveRecord attributes.'
|
12
|
+
spec.description = 'Enables the encryption of specific ActiveRecord attributes.'
|
13
|
+
spec.homepage = 'https://github.com/GaggleAMP/cryptonite'
|
14
|
+
spec.license = 'MIT'
|
15
15
|
|
16
16
|
spec.files = `git ls-files -z`.split("\x0")
|
17
|
-
spec.executables = spec.files.grep(
|
18
|
-
spec.test_files = spec.files.grep(
|
19
|
-
spec.require_paths = [
|
17
|
+
spec.executables = spec.files.grep(/^bin\//) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(/^(test|spec|features)\//)
|
19
|
+
spec.require_paths = ['lib']
|
20
20
|
|
21
|
-
spec.add_development_dependency
|
22
|
-
spec.add_development_dependency
|
23
|
-
spec.add_development_dependency
|
24
|
-
spec.add_development_dependency
|
25
|
-
spec.
|
26
|
-
spec.add_dependency
|
21
|
+
spec.add_development_dependency 'bundler', '~> 1.6'
|
22
|
+
spec.add_development_dependency 'rake'
|
23
|
+
spec.add_development_dependency 'rspec', '~> 3.1'
|
24
|
+
spec.add_development_dependency 'sqlite3'
|
25
|
+
spec.add_development_dependency 'rubocop'
|
26
|
+
spec.add_dependency 'activerecord', '>= 4.0', '< 4.2'
|
27
|
+
spec.add_dependency 'activesupport', '>= 4.0', '< 4.2'
|
27
28
|
end
|
data/lib/cryptonite.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
require 'cryptonite/version'
|
2
2
|
|
3
|
-
require '
|
4
|
-
require '
|
3
|
+
require 'cryptonite/coder'
|
4
|
+
require 'cryptonite/key_extractor'
|
5
5
|
|
6
6
|
require 'active_support/concern'
|
7
7
|
require 'active_support/lazy_load_hooks'
|
@@ -18,75 +18,77 @@ module Cryptonite
|
|
18
18
|
end
|
19
19
|
|
20
20
|
module ClassMethods
|
21
|
+
include Cryptonite::KeyExtractor
|
22
|
+
|
21
23
|
# Attributes listed as encrypted will be transparently encrypted and
|
22
24
|
# decrypted in database operations.
|
23
25
|
def attr_encrypted(*attributes)
|
24
26
|
options = attributes.extract_options!
|
25
27
|
|
26
|
-
|
27
|
-
|
28
|
+
public_key = extract_public_key(options)
|
29
|
+
private_key = extract_private_key(options)
|
28
30
|
|
29
|
-
|
30
|
-
serialize attribute, Coder.new(@private_key || @public_key)
|
31
|
-
end
|
31
|
+
serialize_attributes_with_coder(attributes, private_key || public_key)
|
32
32
|
|
33
|
-
self._attr_encrypted = Set.new(attributes.map
|
33
|
+
self._attr_encrypted = Set.new(attributes.map(&:to_s)) + (_attr_encrypted || [])
|
34
34
|
end
|
35
35
|
|
36
36
|
# Returns an array of all the attributes that have been specified as encrypted.
|
37
37
|
def encrypted_attributes
|
38
|
-
|
38
|
+
_attr_encrypted
|
39
39
|
end
|
40
40
|
|
41
|
-
|
42
|
-
# Retrives an RSA key with multiple ways.
|
43
|
-
def get_rsa_key(key, password = nil)
|
44
|
-
return nil unless key
|
41
|
+
private
|
45
42
|
|
46
|
-
|
47
|
-
|
43
|
+
# Serializes all attributes with encryption coder.
|
44
|
+
def serialize_attributes_with_coder(attributes, key)
|
45
|
+
attributes.each do |attribute|
|
46
|
+
serialize attribute, Coder.new(key)
|
48
47
|
end
|
48
|
+
end
|
49
|
+
end
|
49
50
|
|
50
|
-
|
51
|
-
key = @instance.send(key)
|
52
|
-
end
|
51
|
+
module_function
|
53
52
|
|
54
|
-
|
53
|
+
# Encrypts attributes of a specific model. This method is indended for migration purposes. It takes the same arguments
|
54
|
+
# as the `attr_encrypted` class method.
|
55
|
+
def encrypt_model_attributes(model, *attributes) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
56
|
+
fail ArgumentError, "ActiveRecord::Base expected, got #{model.inspect}" unless model <= ActiveRecord::Base
|
55
57
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
key = File.read(key)
|
60
|
-
end
|
58
|
+
options = attributes.extract_options!
|
59
|
+
encrypted_attributes = attributes.map(&:to_s) & model.column_names
|
60
|
+
coder = Coder.new extract_public_key(options)
|
61
61
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
62
|
+
model.find_each do |record|
|
63
|
+
updated_columns =
|
64
|
+
encrypted_attributes.each_with_object({}) do |attribute, values|
|
65
|
+
values[attribute] = coder.encrypt(record.typecasted_attribute_value attribute)
|
66
|
+
end
|
67
|
+
|
68
|
+
record.update_columns(updated_columns)
|
67
69
|
end
|
68
70
|
end
|
69
71
|
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
end
|
72
|
+
# Decrypts attributes of a specific model. This method is indended for migration purposes. It takes the same arguments
|
73
|
+
# as the `attr_encrypted` class method. It requires a private key to decrypt the data.
|
74
|
+
def decrypt_model_attributes(model, *attributes) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
75
|
+
fail ArgumentError, "ActiveRecord::Base expected, got #{model.inspect}" unless model <= ActiveRecord::Base
|
75
76
|
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
77
|
+
options = attributes.extract_options!
|
78
|
+
encrypted_attributes = attributes.map(&:to_s) & model.column_names
|
79
|
+
coder = Coder.new extract_private_key(options)
|
80
|
+
|
81
|
+
model.find_each do |record|
|
82
|
+
updated_columns =
|
83
|
+
encrypted_attributes.each_with_object({}) do |attribute, values|
|
84
|
+
values[attribute] = coder.decrypt(record.read_attribute_before_type_cast attribute)
|
85
|
+
end
|
82
86
|
|
83
|
-
|
84
|
-
# environment.
|
85
|
-
def decrypt(value)
|
86
|
-
@key.private_decrypt(Base64.decode64(value)) if value
|
87
|
+
record.update_columns(updated_columns)
|
87
88
|
end
|
88
|
-
alias :load :decrypt
|
89
89
|
end
|
90
|
+
|
91
|
+
extend KeyExtractor
|
90
92
|
end
|
91
93
|
|
92
94
|
ActiveSupport.on_load :active_record do
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
require 'base64'
|
3
|
+
|
4
|
+
module Cryptonite
|
5
|
+
class Coder # :nodoc:
|
6
|
+
HEADER = "Cryptonite #{VERSION}: "
|
7
|
+
BASE64_REGEXP = %r{([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)}
|
8
|
+
SEMVER_REGEXP = /
|
9
|
+
\bv?(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.
|
10
|
+
(?:0|[1-9][0-9]*)(?:-[\da-z\-]+(?:\.[\da-z\-]+)*)?(?:\+[\da-z\-]+(?:\.[\da-z\-]+)*)?\b
|
11
|
+
/ix
|
12
|
+
REGEXP = /^Cryptonite #{SEMVER_REGEXP}: (?<value>#{BASE64_REGEXP})$/
|
13
|
+
|
14
|
+
def initialize(key)
|
15
|
+
fail ArgumentError unless key.is_a?(::OpenSSL::PKey::RSA)
|
16
|
+
@key = key
|
17
|
+
end
|
18
|
+
|
19
|
+
# Encrypts a value with public key encryption. Keys should be defined in
|
20
|
+
# environment.
|
21
|
+
def encrypt(value)
|
22
|
+
return unless value
|
23
|
+
fail ArgumentError, 'Value is already encrypted' if value.match(REGEXP)
|
24
|
+
HEADER + Base64.strict_encode64(@key.public_encrypt(value))
|
25
|
+
end
|
26
|
+
alias_method :dump, :encrypt
|
27
|
+
|
28
|
+
# Decrypts a value with public key encryption. Keys should be defined in
|
29
|
+
# environment.
|
30
|
+
def decrypt(value)
|
31
|
+
return unless value
|
32
|
+
fail ArgumentError, 'Value is not encrypted' unless value.match(REGEXP)
|
33
|
+
@key.private_decrypt(Base64.strict_decode64(Regexp.last_match(:value)))
|
34
|
+
end
|
35
|
+
alias_method :load, :decrypt
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Cryptonite
|
2
|
+
module KeyExtractor # :nodoc:
|
3
|
+
private
|
4
|
+
|
5
|
+
# Extracts public key from options or the environment.
|
6
|
+
def extract_public_key(options)
|
7
|
+
extract_key(options[:public_key] || options[:key_pair] || ENV['PUBLIC_KEY'])
|
8
|
+
end
|
9
|
+
|
10
|
+
# Extracts private key from options or the environment.
|
11
|
+
def extract_private_key(options)
|
12
|
+
extract_key(
|
13
|
+
options[:private_key] || options[:key_pair] || ENV['PRIVATE_KEY'],
|
14
|
+
options[:private_key_password] || ENV['PRIVATE_KEY_PASSWORD']
|
15
|
+
)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Retrives an RSA key with multiple ways.
|
19
|
+
def extract_key(key, password = nil)
|
20
|
+
return nil unless key
|
21
|
+
|
22
|
+
case key
|
23
|
+
when Proc then extract_key_from_proc(key, password)
|
24
|
+
when Symbol then extract_key_from_method(key, password)
|
25
|
+
when ::OpenSSL::PKey::RSA then key
|
26
|
+
else
|
27
|
+
key = retrieve_key_string_from_stream(key)
|
28
|
+
return ::OpenSSL::PKey::RSA.new(key) if password.nil?
|
29
|
+
::OpenSSL::PKey::RSA.new(key, password.to_s)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Retrives an RSA key with a `proc` block.
|
34
|
+
def extract_key_from_proc(proc, password = nil)
|
35
|
+
extract_key(proc.call, password)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Retrives an RSA key with a method symbol.
|
39
|
+
def extract_key_from_method(method, password = nil)
|
40
|
+
extract_key(@instance.send(method), password)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Retrives a key string from a stream.
|
44
|
+
def retrieve_key_string_from_stream(stream)
|
45
|
+
return stream.read if stream.respond_to?(:read)
|
46
|
+
return File.read(stream) if stream.to_s !~ /^-+BEGIN .* KEY-+$/
|
47
|
+
stream
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
data/lib/cryptonite/version.rb
CHANGED
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
require 'cryptonite/coder'
|
4
|
+
|
5
|
+
describe Cryptonite::Coder do
|
6
|
+
context 'with private key' do
|
7
|
+
subject do
|
8
|
+
Cryptonite::Coder.new(PRIVATE_FIXTURE_KEY)
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'encrypts a value' do
|
12
|
+
value = SecureRandom.hex(16)
|
13
|
+
|
14
|
+
expect(subject.encrypt value).not_to eq(value)
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'decrypts a value' do
|
18
|
+
value = SecureRandom.hex(16)
|
19
|
+
encrypted_value = Cryptonite::Coder::HEADER + Base64.strict_encode64(PUBLIC_FIXTURE_KEY.public_encrypt(value))
|
20
|
+
|
21
|
+
expect(subject.decrypt encrypted_value).to eq(value)
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'handles nil values' do
|
25
|
+
expect(subject.encrypt nil).to be_nil
|
26
|
+
expect(subject.decrypt nil).to be_nil
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
context 'with public key only' do
|
31
|
+
subject do
|
32
|
+
Cryptonite::Coder.new(PUBLIC_FIXTURE_KEY)
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'encrypts a value' do
|
36
|
+
value = SecureRandom.hex(16)
|
37
|
+
|
38
|
+
expect(subject.encrypt value).not_to eq(value)
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'cannot decrypt a value' do
|
42
|
+
value = SecureRandom.hex(16)
|
43
|
+
encrypted_value = Cryptonite::Coder::HEADER + Base64.strict_encode64(PUBLIC_FIXTURE_KEY.public_encrypt(value))
|
44
|
+
|
45
|
+
expect { subject.decrypt encrypted_value }.to raise_error(OpenSSL::PKey::RSAError)
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'handles nil values' do
|
49
|
+
expect(subject.encrypt nil).to be_nil
|
50
|
+
expect(subject.decrypt nil).to be_nil
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
data/spec/cryptonite_spec.rb
CHANGED
@@ -12,21 +12,22 @@ describe Cryptonite do
|
|
12
12
|
database: ':memory:')
|
13
13
|
|
14
14
|
::ActiveRecord::Schema.define do
|
15
|
-
create_table :sensitive_data, :
|
15
|
+
create_table :sensitive_data, force: true do |t|
|
16
16
|
t.column :secret, :text
|
17
|
+
t.column :another_secret, :text
|
17
18
|
end
|
18
19
|
end
|
19
20
|
end
|
20
21
|
|
21
|
-
subject
|
22
|
+
subject do
|
22
23
|
Class.new(ActiveRecord::Base) do
|
23
24
|
def self.table_name
|
24
|
-
|
25
|
-
end
|
25
|
+
'sensitive_data'
|
26
|
+
end
|
26
27
|
end
|
27
|
-
|
28
|
+
end
|
28
29
|
|
29
|
-
context
|
30
|
+
context 'with private key' do
|
30
31
|
before do
|
31
32
|
subject.tap { |obj| obj.attr_encrypted :secret, key_pair: PRIVATE_FIXTURE_KEY }
|
32
33
|
end
|
@@ -35,9 +36,7 @@ describe Cryptonite do
|
|
35
36
|
secret = SecureRandom.hex(16)
|
36
37
|
|
37
38
|
subject.new(secret: secret).tap do |instance|
|
38
|
-
expect(
|
39
|
-
instance.instance_variable_get(:@attributes).send(:fetch, 'secret').serialized_value
|
40
|
-
).not_to eq(secret)
|
39
|
+
expect(instance.typecasted_attribute_value 'secret').not_to eq(secret)
|
41
40
|
end
|
42
41
|
end
|
43
42
|
|
@@ -45,23 +44,21 @@ describe Cryptonite do
|
|
45
44
|
secret = SecureRandom.hex(16)
|
46
45
|
|
47
46
|
subject.new.tap do |instance|
|
48
|
-
instance.
|
47
|
+
instance.raw_write_attribute('secret', Cryptonite::Coder.new(PUBLIC_FIXTURE_KEY).encrypt(secret))
|
49
48
|
|
50
|
-
expect(instance.secret).to eq(secret)
|
49
|
+
expect(instance.read_attribute_before_type_cast 'secret').to eq(secret)
|
51
50
|
end
|
52
51
|
end
|
53
52
|
|
54
53
|
it 'handles nil values' do
|
55
54
|
subject.new(secret: nil).tap do |instance|
|
56
|
-
expect(
|
57
|
-
instance.instance_variable_get(:@attributes).send(:fetch, 'secret').serialized_value
|
58
|
-
).to be_nil
|
55
|
+
expect(instance.typecasted_attribute_value 'secret').to be_nil
|
59
56
|
end
|
60
57
|
|
61
58
|
subject.new.tap do |instance|
|
62
|
-
instance.
|
59
|
+
instance.raw_write_attribute('secret', nil)
|
63
60
|
|
64
|
-
expect(instance.secret).to be_nil
|
61
|
+
expect(instance.read_attribute_before_type_cast 'secret').to be_nil
|
65
62
|
end
|
66
63
|
end
|
67
64
|
|
@@ -69,16 +66,13 @@ describe Cryptonite do
|
|
69
66
|
secret = SecureRandom.hex(16)
|
70
67
|
|
71
68
|
subject.create(secret: secret).reload.tap do |instance|
|
72
|
-
expect(
|
73
|
-
|
74
|
-
).not_to eq(secret)
|
75
|
-
|
76
|
-
expect(instance.secret).to eq(secret)
|
69
|
+
expect(instance.typecasted_attribute_value 'secret').not_to eq(secret)
|
70
|
+
expect(instance.read_attribute_before_type_cast 'secret').to eq(secret)
|
77
71
|
end
|
78
72
|
end
|
79
73
|
end
|
80
74
|
|
81
|
-
context
|
75
|
+
context 'with public key only' do
|
82
76
|
before do
|
83
77
|
subject.tap { |obj| obj.attr_encrypted :secret, public_key: PUBLIC_FIXTURE_KEY }
|
84
78
|
end
|
@@ -87,9 +81,7 @@ describe Cryptonite do
|
|
87
81
|
secret = SecureRandom.hex(16)
|
88
82
|
|
89
83
|
subject.new(secret: secret).tap do |instance|
|
90
|
-
expect(
|
91
|
-
instance.instance_variable_get(:@attributes).send(:fetch, 'secret').serialized_value
|
92
|
-
).not_to eq(secret)
|
84
|
+
expect(instance.typecasted_attribute_value 'secret').not_to eq(secret)
|
93
85
|
end
|
94
86
|
end
|
95
87
|
|
@@ -97,24 +89,54 @@ describe Cryptonite do
|
|
97
89
|
secret = SecureRandom.hex(16)
|
98
90
|
|
99
91
|
subject.new.tap do |instance|
|
100
|
-
instance.
|
92
|
+
instance.raw_write_attribute('secret', Cryptonite::Coder.new(PUBLIC_FIXTURE_KEY).encrypt(secret))
|
101
93
|
|
102
|
-
expect{ instance.secret }.to raise_error OpenSSL::PKey::RSAError
|
94
|
+
expect { instance.read_attribute_before_type_cast 'secret' }.to raise_error OpenSSL::PKey::RSAError
|
103
95
|
end
|
104
96
|
end
|
105
97
|
|
106
98
|
it 'handles nil values' do
|
107
99
|
subject.new(secret: nil).tap do |instance|
|
108
|
-
expect(
|
109
|
-
instance.instance_variable_get(:@attributes).send(:fetch, 'secret').serialized_value
|
110
|
-
).to be_nil
|
100
|
+
expect(instance.typecasted_attribute_value 'secret').to be_nil
|
111
101
|
end
|
112
102
|
|
113
103
|
subject.new.tap do |instance|
|
114
|
-
instance.
|
104
|
+
instance.raw_write_attribute('secret', nil)
|
115
105
|
|
116
|
-
expect(instance.secret).to be_nil
|
106
|
+
expect(instance.read_attribute_before_type_cast 'secret').to be_nil
|
117
107
|
end
|
118
108
|
end
|
119
109
|
end
|
110
|
+
|
111
|
+
context 'during upwards migration' do
|
112
|
+
before do
|
113
|
+
@secret = SecureRandom.hex(16)
|
114
|
+
subject.create(secret: @secret)
|
115
|
+
|
116
|
+
subject.tap { |obj| obj.attr_encrypted :secret, key_pair: PRIVATE_FIXTURE_KEY }
|
117
|
+
end
|
118
|
+
|
119
|
+
it 'encrypts field in database' do
|
120
|
+
expect { subject.last.read_attribute_before_type_cast 'secret' }.to raise_error ArgumentError
|
121
|
+
|
122
|
+
Cryptonite.encrypt_model_attributes(subject, :secret, public_key: PUBLIC_FIXTURE_KEY)
|
123
|
+
|
124
|
+
expect(subject.last.read_attribute_before_type_cast 'secret').to eq(@secret)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
context 'during downwards migration' do
|
129
|
+
before do
|
130
|
+
@secret = SecureRandom.hex(16)
|
131
|
+
subject.dup.tap { |obj| obj.attr_encrypted :secret, key_pair: PRIVATE_FIXTURE_KEY }.create(secret: @secret)
|
132
|
+
end
|
133
|
+
|
134
|
+
it 'decrypts field in database' do
|
135
|
+
expect(subject.last.read_attribute 'secret').not_to eq(@secret)
|
136
|
+
|
137
|
+
Cryptonite.decrypt_model_attributes(subject, :secret, key_pair: PRIVATE_FIXTURE_KEY)
|
138
|
+
|
139
|
+
expect(subject.last.read_attribute 'secret').to eq(@secret)
|
140
|
+
end
|
141
|
+
end
|
120
142
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -88,6 +88,8 @@ RSpec.configure do |config|
|
|
88
88
|
=end
|
89
89
|
|
90
90
|
# Configure public key encryption for the Cryptonite concern.
|
91
|
-
::PUBLIC_FIXTURE_KEY =
|
92
|
-
|
91
|
+
::PUBLIC_FIXTURE_KEY =
|
92
|
+
OpenSSL::PKey::RSA.new(File.read(File.expand_path('../fixtures/keys/public.pem', __FILE__)))
|
93
|
+
::PRIVATE_FIXTURE_KEY =
|
94
|
+
OpenSSL::PKey::RSA.new(File.read(File.expand_path('../fixtures/keys/private.pem', __FILE__)), 'test')
|
93
95
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: cryptonite
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- GaggleAMP
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2015-01-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -66,6 +66,20 @@ dependencies:
|
|
66
66
|
- - ">="
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rubocop
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
69
83
|
- !ruby/object:Gem::Dependency
|
70
84
|
name: activerecord
|
71
85
|
requirement: !ruby/object:Gem::Requirement
|
@@ -115,6 +129,7 @@ extra_rdoc_files: []
|
|
115
129
|
files:
|
116
130
|
- ".gitignore"
|
117
131
|
- ".rspec"
|
132
|
+
- ".rubocop.yml"
|
118
133
|
- ".travis.yml"
|
119
134
|
- Gemfile
|
120
135
|
- LICENSE.txt
|
@@ -122,7 +137,10 @@ files:
|
|
122
137
|
- Rakefile
|
123
138
|
- cryptonite.gemspec
|
124
139
|
- lib/cryptonite.rb
|
140
|
+
- lib/cryptonite/coder.rb
|
141
|
+
- lib/cryptonite/key_extractor.rb
|
125
142
|
- lib/cryptonite/version.rb
|
143
|
+
- spec/cryptonite/coder_spec.rb
|
126
144
|
- spec/cryptonite_spec.rb
|
127
145
|
- spec/fixtures/keys/private.pem
|
128
146
|
- spec/fixtures/keys/public.pem
|
@@ -152,6 +170,7 @@ signing_key:
|
|
152
170
|
specification_version: 4
|
153
171
|
summary: Enables the encryption of specific ActiveRecord attributes.
|
154
172
|
test_files:
|
173
|
+
- spec/cryptonite/coder_spec.rb
|
155
174
|
- spec/cryptonite_spec.rb
|
156
175
|
- spec/fixtures/keys/private.pem
|
157
176
|
- spec/fixtures/keys/public.pem
|