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