column_cryptor 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.
- data/MIT-LICENSE +20 -0
- data/README.md +51 -0
- data/Rakefile +30 -0
- data/lib/column_cryptor/core_ext.rb +1 -0
- data/lib/column_cryptor/decryptor.rb +48 -0
- data/lib/column_cryptor/encryptor.rb +43 -0
- data/lib/column_cryptor/version.rb +4 -0
- data/lib/column_cryptor.rb +87 -0
- data/lib/generators/column_cryptor/install/install_generator.rb +18 -0
- data/lib/generators/column_cryptor/install/templates/initializer.rb.erb +1 -0
- data/test/column_cryptor/decryptor_test.rb +31 -0
- data/test/column_cryptor/encryptor_test.rb +35 -0
- data/test/column_cryptor_test.rb +57 -0
- data/test/dummy/README.rdoc +261 -0
- data/test/dummy/Rakefile +7 -0
- data/test/dummy/app/assets/javascripts/application.js +15 -0
- data/test/dummy/app/assets/stylesheets/application.css +13 -0
- data/test/dummy/app/controllers/application_controller.rb +3 -0
- data/test/dummy/app/helpers/application_helper.rb +2 -0
- data/test/dummy/app/views/layouts/application.html.erb +14 -0
- data/test/dummy/config/application.rb +59 -0
- data/test/dummy/config/boot.rb +10 -0
- data/test/dummy/config/database.yml +25 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/development.rb +37 -0
- data/test/dummy/config/environments/production.rb +67 -0
- data/test/dummy/config/environments/test.rb +37 -0
- data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/test/dummy/config/initializers/inflections.rb +15 -0
- data/test/dummy/config/initializers/mime_types.rb +5 -0
- data/test/dummy/config/initializers/secret_token.rb +7 -0
- data/test/dummy/config/initializers/session_store.rb +8 -0
- data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/test/dummy/config/locales/en.yml +5 -0
- data/test/dummy/config/routes.rb +58 -0
- data/test/dummy/config.ru +4 -0
- data/test/dummy/log/test.log +0 -0
- data/test/dummy/public/404.html +26 -0
- data/test/dummy/public/422.html +26 -0
- data/test/dummy/public/500.html +25 -0
- data/test/dummy/public/favicon.ico +0 -0
- data/test/dummy/script/rails +6 -0
- data/test/shoulda_macros/encrypts.rb +37 -0
- data/test/test_helper.rb +20 -0
- metadata +221 -0
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2012 SCVNGR, Inc.
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
# column_cryptor
|
2
|
+
|
3
|
+
`column_cryptor` is a gem that makes it easy to encrypt/decrypt ActiveRecord columns using a private key. If you need to store some sensitive information in your database, then this makes it easy to keep it encrypted.
|
4
|
+
|
5
|
+
Note that `column_cryptor` is NOT a good solution for something like credit card information. Do everyone a favor and use a vault-like service. Instead, use `column_cryptor` to encrypt a user's phone number or other one-off bits of information you'd like to keep secret.
|
6
|
+
|
7
|
+
## Getting Started
|
8
|
+
|
9
|
+
Add to your Gemfile:
|
10
|
+
|
11
|
+
gem 'column_cryptor'
|
12
|
+
|
13
|
+
then run `bundle install`.
|
14
|
+
|
15
|
+
Next, use the generator to create an initializer with a random private key:
|
16
|
+
|
17
|
+
rails generate column_cryptor:install
|
18
|
+
|
19
|
+
This will create a file called `config/initializers/column_cryptor.rb` that looks something like:
|
20
|
+
|
21
|
+
ColumnCryptor.private_key = "ssWII/MrFX8EmHMjG/5+un0mnYF5UeG2k7ajSjaKayU=\n"
|
22
|
+
|
23
|
+
Those random characters represent a Base64-encoded private key suitable for encrypting and decrypting data using `column_cryptor`. It's recommended that you move that private key somewhere outside your code, such as to a yaml file or as an environment variable. Just be sure to set `ColumnCryptor.private_key` to your key.
|
24
|
+
|
25
|
+
## Encrypting some data
|
26
|
+
|
27
|
+
Once installed, you can then encrypt an ActiveRecord column like so:
|
28
|
+
|
29
|
+
class User < ActiveRecord::Base
|
30
|
+
encrypts :phone_number
|
31
|
+
end
|
32
|
+
|
33
|
+
Getters and setters will be created for each column, automatically encrypting/decrypting `phone_number`.
|
34
|
+
|
35
|
+
## Generating a new private key
|
36
|
+
|
37
|
+
You can create a new private key using the `new_key` method:
|
38
|
+
|
39
|
+
ColumnCryptor.new_key
|
40
|
+
|
41
|
+
This will return a Base64-encoded string representing a random private key. Be sure to leave it as-is (with the new-line at the end!) or ColumnCryptor won't know what do with it.
|
42
|
+
|
43
|
+
Note that once you've started using a private key, if you ever lose it, all of your encrypted data will be lost with it. You also can't change your private key on the fly: you would need to first decrypt all of your data and then re-encrypt with your new private key.
|
44
|
+
|
45
|
+
## Requirements
|
46
|
+
|
47
|
+
`column_cryptor` requires Ruby 1.9+, and Rails 3.0 or later. The tests are written with Test::Unit and shoulda.
|
48
|
+
|
49
|
+
## License
|
50
|
+
|
51
|
+
`column_cryptor` is written by Ryan Twomey and Costa Walcott, and is Copyright 2012 SCVNGR, Inc. It is free software, and may be redistributed under the terms specified in the MIT-LICENSE file.
|
data/Rakefile
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
|
3
|
+
require 'bundler'
|
4
|
+
Bundler::GemHelper.install_tasks
|
5
|
+
|
6
|
+
begin
|
7
|
+
require 'rdoc/task'
|
8
|
+
rescue LoadError
|
9
|
+
require 'rdoc/rdoc'
|
10
|
+
require 'rake/rdoctask'
|
11
|
+
RDoc::Task = Rake::RDocTask
|
12
|
+
end
|
13
|
+
|
14
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
15
|
+
rdoc.rdoc_dir = 'rdoc'
|
16
|
+
rdoc.title = 'ColumnCryptor'
|
17
|
+
rdoc.options << '--line-numbers'
|
18
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
19
|
+
end
|
20
|
+
|
21
|
+
require 'rake/testtask'
|
22
|
+
|
23
|
+
desc 'Test the paperclip plugin.'
|
24
|
+
Rake::TestTask.new(:test) do |t|
|
25
|
+
t.libs << 'lib' << 'profile'
|
26
|
+
t.pattern = 'test/**/*_test.rb'
|
27
|
+
t.verbose = true
|
28
|
+
end
|
29
|
+
|
30
|
+
task default: :test
|
@@ -0,0 +1 @@
|
|
1
|
+
ActiveRecord::Base.send :include, ColumnCryptor
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module ColumnCryptor
|
2
|
+
# Given some ciphertext, attempts to decrypt it. Requires that ColumnCryptor.private_key be
|
3
|
+
# previously set to the same private key used to create the ciphertext.
|
4
|
+
|
5
|
+
class Decryptor
|
6
|
+
# Returns a new ColumnCryptor::Decryptor object initialized with the given ciphertext.
|
7
|
+
def initialize(encrypted_data)
|
8
|
+
@encrypted_data = encrypted_data
|
9
|
+
end
|
10
|
+
|
11
|
+
# Returns decrypted version of the Base64-encoded ciphertext.
|
12
|
+
def decrypt
|
13
|
+
unless @encrypted_data.blank?
|
14
|
+
plaintext_data
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def decipher
|
21
|
+
@decipher ||= OpenSSL::Cipher::AES.new(256, :CBC).tap do |decipher|
|
22
|
+
decipher.decrypt
|
23
|
+
decipher.key = private_key
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def deciphertext
|
28
|
+
decipher.update(decoded_data) + decipher.final
|
29
|
+
end
|
30
|
+
|
31
|
+
def decoded_data
|
32
|
+
@decoded_data ||= Base64.decode64(@encrypted_data)
|
33
|
+
end
|
34
|
+
|
35
|
+
def initialization_vector
|
36
|
+
decoded_data.slice! 0, decipher.block_size
|
37
|
+
end
|
38
|
+
|
39
|
+
def plaintext_data
|
40
|
+
decipher.iv = initialization_vector
|
41
|
+
deciphertext
|
42
|
+
end
|
43
|
+
|
44
|
+
def private_key
|
45
|
+
Base64.decode64 ColumnCryptor.private_key
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module ColumnCryptor
|
2
|
+
# Given some plaintext, attempts to encrypt it. Requires that ColumnCryptor.private_key be set.
|
3
|
+
|
4
|
+
class Encryptor
|
5
|
+
# Returns a new ColumnCryptor::Encryptor object initialized with the given plaintext.
|
6
|
+
def initialize(plaintext)
|
7
|
+
@plaintext = plaintext
|
8
|
+
end
|
9
|
+
|
10
|
+
# Returns a Base64-encoded string of the encrypted (ciphertext) version of the plaintext.
|
11
|
+
def encrypt
|
12
|
+
unless @plaintext.blank?
|
13
|
+
Base64.encode64 encrypted_data
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def cipher
|
20
|
+
@cipher ||= OpenSSL::Cipher::AES.new(256, :CBC).tap do |cipher|
|
21
|
+
cipher.encrypt
|
22
|
+
cipher.key = private_key
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def ciphertext
|
27
|
+
cipher.update(@plaintext) + cipher.final
|
28
|
+
end
|
29
|
+
|
30
|
+
def encrypted_data
|
31
|
+
cipher.iv = initialization_vector
|
32
|
+
initialization_vector + ciphertext
|
33
|
+
end
|
34
|
+
|
35
|
+
def initialization_vector
|
36
|
+
@initial_vector ||= cipher.random_iv
|
37
|
+
end
|
38
|
+
|
39
|
+
def private_key
|
40
|
+
Base64.decode64 ColumnCryptor.private_key
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
# This gem makes it easy to encrypt and decrypt sensitive application data using ActiveRecord. It
|
2
|
+
# adds a getter and setter for each encryptable column, transparently encrypting before writing to
|
3
|
+
# the database, and decrypting when reading.
|
4
|
+
#
|
5
|
+
# Author:: Ryan Twomey (mailto:ryant@scvngr.com)
|
6
|
+
# Copyright:: Copyright (c) 2012 SCVNGR, Inc.
|
7
|
+
# License:: MIT
|
8
|
+
|
9
|
+
# Sets up ColumnCryptor and includes it on all ActiveRecord::Base objects.
|
10
|
+
|
11
|
+
require 'active_support/core_ext/string'
|
12
|
+
require 'base64'
|
13
|
+
require 'openssl'
|
14
|
+
|
15
|
+
require 'column_cryptor/decryptor'
|
16
|
+
require 'column_cryptor/encryptor'
|
17
|
+
require 'column_cryptor/version'
|
18
|
+
|
19
|
+
module ColumnCryptor
|
20
|
+
@@private_key = nil
|
21
|
+
|
22
|
+
#:nodoc:
|
23
|
+
def self.included(base)
|
24
|
+
base.extend ClassMethods
|
25
|
+
end
|
26
|
+
|
27
|
+
# Returns a randomly generated key, Base64 encoded, suitable for sending to
|
28
|
+
# ColumnCryptor.private_key=
|
29
|
+
def self.new_key
|
30
|
+
Base64.encode64 cipher.random_key
|
31
|
+
end
|
32
|
+
|
33
|
+
# Sets the globally-used private key for all encryption/decryption operations.
|
34
|
+
def self.private_key=(private_key)
|
35
|
+
@@private_key = private_key
|
36
|
+
end
|
37
|
+
|
38
|
+
# Returns the currently set private_key, or nil.
|
39
|
+
def self.private_key
|
40
|
+
@@private_key
|
41
|
+
end
|
42
|
+
|
43
|
+
module ClassMethods
|
44
|
+
# Define which columns to mark as being encrypted. For example, if your ActiveRecord model has
|
45
|
+
# two columns you'd like to encrypt named "address" and "phone_number", you would call this as:
|
46
|
+
#
|
47
|
+
# encrypts :address, :phone_number
|
48
|
+
#
|
49
|
+
# This will define two methods for each attribute: a getter (i.e. "address") and a setter
|
50
|
+
# (i.e. "address="). The getter will automatically attempt to decrypt the data, while the
|
51
|
+
# setter will attempt to encrypt the data. When ActiveRecord writes your record to the database,
|
52
|
+
# the values of these columns will be the ciphertext.
|
53
|
+
#
|
54
|
+
# Note that you must set ColumnCryptor.private_key prior to using these setters/getters.
|
55
|
+
def encrypts(*args)
|
56
|
+
if args.respond_to?(:each)
|
57
|
+
args.each do |attribute|
|
58
|
+
define_encryption_methods attribute
|
59
|
+
end
|
60
|
+
else
|
61
|
+
define_encryption_methods args
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def define_encryption_methods(attribute)
|
68
|
+
define_method attribute do
|
69
|
+
encrypted_data = send("#{attribute}_encrypted")
|
70
|
+
ColumnCryptor::Decryptor.new(encrypted_data).decrypt
|
71
|
+
end
|
72
|
+
|
73
|
+
define_method "#{attribute}=" do |new_value|
|
74
|
+
encrypted_data = ColumnCryptor::Encryptor.new(new_value).encrypt
|
75
|
+
send "#{attribute}_encrypted=", encrypted_data
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
def self.cipher
|
83
|
+
OpenSSL::Cipher::AES.new(256, :CBC)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
require 'column_cryptor/core_ext'
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'rails/generators/active_record'
|
2
|
+
|
3
|
+
module ColumnCryptor
|
4
|
+
# Creates an initializer with a random private key
|
5
|
+
class InstallGenerator < Rails::Generators::Base
|
6
|
+
desc 'Add the column_cryptor initializer'
|
7
|
+
|
8
|
+
#:nodoc:
|
9
|
+
def self.source_root
|
10
|
+
@source_root ||= File.expand_path('../templates', __FILE__)
|
11
|
+
end
|
12
|
+
|
13
|
+
#:nodoc:
|
14
|
+
def install_initializer
|
15
|
+
template 'initializer.rb.erb', 'config/initializers/column_cryptor.rb'
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
ColumnCryptor.private_key = <%= ColumnCryptor.new_key.inspect %>
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require './test/test_helper'
|
2
|
+
require 'base64'
|
3
|
+
|
4
|
+
class DecryptorTest < ActiveSupport::TestCase
|
5
|
+
context '#decrypt' do
|
6
|
+
setup do
|
7
|
+
ColumnCryptor.private_key = "dKY56fUpxMF2gmOxDzWoEm1z7bUJwe1jj9a2962T0GA=\n"
|
8
|
+
end
|
9
|
+
|
10
|
+
context 'with no data' do
|
11
|
+
setup do
|
12
|
+
@decryptor = ColumnCryptor::Decryptor.new(nil)
|
13
|
+
end
|
14
|
+
|
15
|
+
should 'return nil' do
|
16
|
+
assert_nil @decryptor.decrypt
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
context 'with encrypted data' do
|
21
|
+
setup do
|
22
|
+
decryptor = ColumnCryptor::Decryptor.new("xxl4Ny2fGdOKYaEXTDqdOWoNRNd4hT1xe9GlPQ7qWxE=\n")
|
23
|
+
@result = decryptor.decrypt
|
24
|
+
end
|
25
|
+
|
26
|
+
should 'decrypt to plaintext' do
|
27
|
+
assert_equal 'abc123', @result
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require './test/test_helper'
|
2
|
+
require 'base64'
|
3
|
+
|
4
|
+
class EncryptorTest < ActiveSupport::TestCase
|
5
|
+
context '#encrypt' do
|
6
|
+
setup do
|
7
|
+
ColumnCryptor.private_key = "dKY56fUpxMF2gmOxDzWoEm1z7bUJwe1jj9a2962T0GA=\n"
|
8
|
+
end
|
9
|
+
|
10
|
+
context 'with no data' do
|
11
|
+
setup do
|
12
|
+
@encryptor = ColumnCryptor::Encryptor.new(nil)
|
13
|
+
end
|
14
|
+
|
15
|
+
should 'return nil' do
|
16
|
+
assert_nil @encryptor.encrypt
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
context 'with plaintext data' do
|
21
|
+
setup do
|
22
|
+
cipher = OpenSSL::Cipher::AES.new(256, :CBC)
|
23
|
+
cipher.stubs random_iv: Base64.decode64('xxl4Ny2fGdOKYaEXTDqdOQ==\n')
|
24
|
+
OpenSSL::Cipher::AES.stubs new: cipher
|
25
|
+
|
26
|
+
@encryptor = ColumnCryptor::Encryptor.new('abc123')
|
27
|
+
@expected = "xxl4Ny2fGdOKYaEXTDqdOWoNRNd4hT1xe9GlPQ7qWxE=\n"
|
28
|
+
end
|
29
|
+
|
30
|
+
should 'encrypt data' do
|
31
|
+
assert_equal @expected, @encryptor.encrypt
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require './test/test_helper'
|
2
|
+
|
3
|
+
class ColumnCryptorTest < ActiveSupport::TestCase
|
4
|
+
context '.included' do
|
5
|
+
context 'on the class' do
|
6
|
+
setup do
|
7
|
+
@test_class = Class.new(ActiveRecord::Base)
|
8
|
+
end
|
9
|
+
|
10
|
+
should 'define encrypts method' do
|
11
|
+
assert @test_class.respond_to?(:encrypts)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
context 'on the instance' do
|
16
|
+
setup do
|
17
|
+
test_class = Class.new(ActiveRecord::Base) do
|
18
|
+
encrypts :secret_data
|
19
|
+
end
|
20
|
+
|
21
|
+
klass = Object.const_set('Dummy', test_class)
|
22
|
+
create_table 'dummies'
|
23
|
+
|
24
|
+
@test_object = klass.new
|
25
|
+
end
|
26
|
+
|
27
|
+
should 'define getter and setter methods' do
|
28
|
+
assert @test_object.respond_to?(:secret_data)
|
29
|
+
assert @test_object.respond_to?(:secret_data=)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
context '.new_key' do
|
35
|
+
setup do
|
36
|
+
key_str = 'abc123'
|
37
|
+
cipher = stub(random_key: key_str)
|
38
|
+
OpenSSL::Cipher::AES.stubs new: cipher
|
39
|
+
|
40
|
+
@expected = Base64.encode64(key_str)
|
41
|
+
end
|
42
|
+
|
43
|
+
should 'return expected key' do
|
44
|
+
assert_equal @expected, ColumnCryptor.new_key
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
context '.private_key' do
|
49
|
+
setup do
|
50
|
+
ColumnCryptor.private_key = 'abc123'
|
51
|
+
end
|
52
|
+
|
53
|
+
should 'return abc123' do
|
54
|
+
assert_equal 'abc123', ColumnCryptor.private_key
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|