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