spikex-strongbox 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +22 -0
- data/README.textile +177 -0
- data/Rakefile +43 -0
- data/lib/strongbox.rb +69 -0
- data/lib/strongbox/lock.rb +85 -0
- data/rails/init.rb +1 -0
- data/test/database.yml +4 -0
- data/test/fixtures/keypair.pem +24 -0
- data/test/strongbox_test.rb +111 -0
- data/test/test_helper.rb +50 -0
- metadata +73 -0
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2009 Joseph A. Ilacqua, Jr
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person
|
4
|
+
obtaining a copy of this software and associated documentation
|
5
|
+
files (the "Software"), to deal in the Software without
|
6
|
+
restriction, including without limitation the rights to use,
|
7
|
+
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
8
|
+
copies of the Software, and to permit persons to whom the
|
9
|
+
Software is furnished to do so, subject to the following
|
10
|
+
conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be
|
13
|
+
included in all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
17
|
+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
18
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
19
|
+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
20
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
21
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
22
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
data/README.textile
ADDED
@@ -0,0 +1,177 @@
|
|
1
|
+
h1. Strongbox
|
2
|
+
|
3
|
+
Strongbox provides Public Key Encryption for ActiveRecord. By using a public key
|
4
|
+
sensitive information can be encrypted and stored automatically. Once stored a
|
5
|
+
password is required to access the information.
|
6
|
+
|
7
|
+
Because the largest amount of data that can practically be encrypted with a public
|
8
|
+
key is 245 byte, by default Strongbox uses a two layer approach. First it encrypts
|
9
|
+
the attribute using symmetric encryption with a randomly generated key and
|
10
|
+
initialization vector (IV) (which can just be through to as a second key), then it
|
11
|
+
encrypts those with the public key.
|
12
|
+
|
13
|
+
Strongbox stores the encrypted attribute in a database column by the same name, i.e.
|
14
|
+
if you tell Strongbox to encrypt "secret" then it will be store in "secret" in the
|
15
|
+
database, just as the unencrypted attribute would be. If symmetric encryption is used
|
16
|
+
(the default) two additional columns "secret_key" and "secret_iv" are needed as well.
|
17
|
+
|
18
|
+
The attribute is automatically encrypted simply by setting it:
|
19
|
+
|
20
|
+
user.secret = "Shhhhhhh..."
|
21
|
+
|
22
|
+
and decrypted by calling the "decrypt" method with the private key password.
|
23
|
+
|
24
|
+
plain_text = user.secret.decrypt 'letmein'
|
25
|
+
|
26
|
+
h2. Quick Start
|
27
|
+
|
28
|
+
In your model:
|
29
|
+
|
30
|
+
class User < ActiveRecord::Base
|
31
|
+
encrypt_with_public_key :secret,
|
32
|
+
:key_pair => File.join(RAILS_ROOT,'config','keypair.pem'),
|
33
|
+
end
|
34
|
+
|
35
|
+
In your migrations:
|
36
|
+
class AddSecretColumnsToUser < ActiveRecord::Migration
|
37
|
+
def self.up
|
38
|
+
add_column :users, :secret, binary
|
39
|
+
add_column :users, :secret_key, binary
|
40
|
+
add_column :users, :secret_iv, binary
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.down
|
44
|
+
remove_column :users, :secret
|
45
|
+
remove_column :users, :secret_key
|
46
|
+
remove_column :users, :secret_iv
|
47
|
+
end
|
48
|
+
|
49
|
+
Generate a key pair:
|
50
|
+
|
51
|
+
(Choose a strong password.)
|
52
|
+
openssl genrsa -des3 -out config/private.pem 2048
|
53
|
+
openssl rsa -in config/private.pem -out config/public.pem -outform PEM -pubout
|
54
|
+
cat config/private.pem config/public.pem >> config/keypair.pem
|
55
|
+
|
56
|
+
In your views and forms you don't need to do anything special to encrypt data. To
|
57
|
+
decrypt call:
|
58
|
+
|
59
|
+
user.secret.decrypt 'password'
|
60
|
+
|
61
|
+
h2. Gem installation (Rails 2.1+)
|
62
|
+
|
63
|
+
In config/environment.rb:
|
64
|
+
|
65
|
+
config.gem "spikex-strongbox",
|
66
|
+
:lib => 'strongbox',
|
67
|
+
:source => 'http://gems.github.com',
|
68
|
+
|
69
|
+
h2. Usage
|
70
|
+
|
71
|
+
_encrypt_with_public_key_ sets up the attribute it's called on for automatic
|
72
|
+
encryption. It's simplest form is:
|
73
|
+
|
74
|
+
class User < ActiveRecord::Base
|
75
|
+
encrypt_with_public_key :secret,
|
76
|
+
:key_pair => File.join(RAILS_ROOT,'config','keypair.pem')
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
Which will encrypt the attribute "secret". The attribute will be encrypted using
|
81
|
+
symmetric encryption with an automatically generated key and IV encrypted using the
|
82
|
+
public key. This requires three columns in the database "secret", "secret_key", and
|
83
|
+
"secret_iv" (see below).
|
84
|
+
|
85
|
+
Options to encrypt_with_public_key are:
|
86
|
+
|
87
|
+
:public_key - Path to the public key file. Overrides :keypair.
|
88
|
+
|
89
|
+
:private_key - Path to the private key file. Overrides :keypair.
|
90
|
+
|
91
|
+
:keypair - Path to a file containing both the public and private keys.
|
92
|
+
|
93
|
+
:symmetric :always/:never - Encrypt the date using symmetric encryption. The public
|
94
|
+
key is used to encrypt an automatically generated key and IV. This allows for large
|
95
|
+
amounts of data to be encrypted. The size of data that can be encrypted directly with
|
96
|
+
the public is limit to key size (in bytes) - 11. So a 2048 key can encrypt *245
|
97
|
+
bytes*. Defaults to *:always*
|
98
|
+
|
99
|
+
:symmetric_cipher - Cipher to use for symmetric encryption. Defaults to *'aes-256-cbc'*. Other ciphers support by OpenSSL may be used.
|
100
|
+
|
101
|
+
:base64 true/false - Use Base64 encoding to convert encrypted data to text. Use when
|
102
|
+
binary save data storage is not available. Defaults to *false*
|
103
|
+
|
104
|
+
:padding - Method used to pad data encrypted with the public key. Defaults to
|
105
|
+
RSA_PKCS1_PADDING. The default should be find unless you are dealing with legacy
|
106
|
+
data.
|
107
|
+
|
108
|
+
For example, encrypting a small attribute, providing only the public key for extra
|
109
|
+
security, and Base64 encoding the encrypted data:
|
110
|
+
|
111
|
+
class User < ActiveRecord::Base
|
112
|
+
validates_length_of :pin_code,
|
113
|
+
encrypt_with_public_key :pin_code, :is => 4
|
114
|
+
:symmetric => :never
|
115
|
+
:base64 => true
|
116
|
+
:public_key => File.join(RAILS_ROOT,'config','public.pem'),
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
h2. Key Generation
|
121
|
+
|
122
|
+
Generate a key pair:
|
123
|
+
|
124
|
+
openssl genrsa -des3 -out config/private.pem 2048
|
125
|
+
Generating RSA private key, 2048 bit long modulus
|
126
|
+
......+++
|
127
|
+
.+++
|
128
|
+
e is 65537 (0x10001)
|
129
|
+
Enter pass phrase for config/private.pem:
|
130
|
+
Verifying - Enter pass phrase for config/private.pem:
|
131
|
+
|
132
|
+
and extract the the public key:
|
133
|
+
|
134
|
+
openssl rsa -in config/private.pem -out config/public.pem -outform PEM -pubout
|
135
|
+
Enter pass phrase for config/private.pem:
|
136
|
+
writing RSA key
|
137
|
+
|
138
|
+
If you are going to leave the private key installed it's easiest to create a single
|
139
|
+
key pair file:
|
140
|
+
|
141
|
+
cat config/private.pem config/public.pem >> config/keypair.pem
|
142
|
+
|
143
|
+
Or, for added security, store the private key file else where, leaving only the public key.
|
144
|
+
|
145
|
+
h2. Table Creation
|
146
|
+
|
147
|
+
In it's default configuration Strongbox requires three columns, one the encrypted
|
148
|
+
data, one for the encrypted symmetric key, and one for the encrypted symmetric IV. If
|
149
|
+
symmetric encryption is disabled then only the columns for the data being encrypted
|
150
|
+
is needed.
|
151
|
+
|
152
|
+
If your underlying database allows, use the *binary* column type. If you must store
|
153
|
+
your data in text format be sure to enable Base64 encoding and to use the *text*
|
154
|
+
column type. The _string_ column type is likely to be too small to hold the encrypted
|
155
|
+
string.
|
156
|
+
|
157
|
+
h2. Security Caveats
|
158
|
+
|
159
|
+
If you don't encrypt your data, then an attacker only needs to steal that data to get
|
160
|
+
your secrets.
|
161
|
+
|
162
|
+
If encrypt your data using symmetric encrypts and a stored key, then the attacker
|
163
|
+
needs the data and the key stored on the server.
|
164
|
+
|
165
|
+
If you use public key encryption, the attacker needs the data, the private key, and
|
166
|
+
the password. This means the attacker has to sniff the password somehow, so that's
|
167
|
+
what you need to protect against.
|
168
|
+
|
169
|
+
h2. Authors
|
170
|
+
|
171
|
+
Spike Ilacqua
|
172
|
+
|
173
|
+
h2. Thanks
|
174
|
+
|
175
|
+
Strongbox's implementation drew inspiration from Thoughtbot's Paperclip gem
|
176
|
+
http://www.thoughtbot.com/projects/paperclip
|
177
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/testtask'
|
3
|
+
require 'rake/rdoctask'
|
4
|
+
|
5
|
+
$LOAD_PATH << File.join(File.dirname(__FILE__), 'lib')
|
6
|
+
require 'strongbox'
|
7
|
+
|
8
|
+
desc 'Default: run tests.'
|
9
|
+
task :default => :test
|
10
|
+
|
11
|
+
desc 'Test the strongbox gem.'
|
12
|
+
Rake::TestTask.new(:test) do |t|
|
13
|
+
t.libs << 'lib' << 'profile'
|
14
|
+
t.pattern = 'test/**/*_test.rb'
|
15
|
+
t.verbose = true
|
16
|
+
end
|
17
|
+
|
18
|
+
desc 'Generate documentation for the strongbox gem.'
|
19
|
+
Rake::RDocTask.new(:rdoc) do |rdoc|
|
20
|
+
rdoc.rdoc_dir = 'doc'
|
21
|
+
rdoc.title = 'Strongbox'
|
22
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
23
|
+
rdoc.rdoc_files.include('README*')
|
24
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
25
|
+
end
|
26
|
+
|
27
|
+
spec = Gem::Specification.new do |s|
|
28
|
+
s.name = "strongbox"
|
29
|
+
s.version = Strongbox::VERSION
|
30
|
+
s.summary = "Secures ActiveRecord fields with public key encryption."
|
31
|
+
s.authors = ["Spike Ilacqua"]
|
32
|
+
s.email = "spike@stuff-things.net"
|
33
|
+
s.homepage = "http://stuff-things.net/strongbox"
|
34
|
+
s.files = FileList["[A-Z]*", "{lib,rails,test}/**/*"]
|
35
|
+
s.add_development_dependency 'thoughtbot-shoulda'
|
36
|
+
end
|
37
|
+
|
38
|
+
desc "Generate a gemspec file for GitHub"
|
39
|
+
task :gemspec do
|
40
|
+
File.open("#{spec.name}.gemspec", 'w') do |f|
|
41
|
+
f.write spec.to_yaml
|
42
|
+
end
|
43
|
+
end
|
data/lib/strongbox.rb
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
require 'base64'
|
3
|
+
|
4
|
+
require 'strongbox/lock'
|
5
|
+
|
6
|
+
module Strongbox
|
7
|
+
|
8
|
+
VERSION = "0.1.1"
|
9
|
+
|
10
|
+
RSA_PKCS1_PADDING = OpenSSL::PKey::RSA::PKCS1_PADDING
|
11
|
+
RSA_SSLV23_PADDING = OpenSSL::PKey::RSA::SSLV23_PADDING
|
12
|
+
RSA_NO_PADDING = OpenSSL::PKey::RSA::NO_PADDING
|
13
|
+
RSA_PKCS1_OAEP_PADDING = OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING
|
14
|
+
|
15
|
+
class << self
|
16
|
+
# Provides for setting the default options for Strongbox
|
17
|
+
def options
|
18
|
+
@options ||= {
|
19
|
+
:base64 => false,
|
20
|
+
:symmetric => :always,
|
21
|
+
:padding => RSA_PKCS1_PADDING,
|
22
|
+
:symmetric_cipher => 'aes-256-cbc'
|
23
|
+
}
|
24
|
+
end
|
25
|
+
|
26
|
+
def included base #:nodoc:
|
27
|
+
base.extend ClassMethods
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class StrongboxError < StandardError #:nodoc:
|
32
|
+
end
|
33
|
+
|
34
|
+
module ClassMethods
|
35
|
+
# +encrypt_with_public_key+ gives the class it is called on an attribute that
|
36
|
+
# when assigned is automatically encrypted using a public key. This allows the
|
37
|
+
# unattended encryption of data, without exposing the information need to decrypt
|
38
|
+
# it (as would be the case when using symmetric key encryption alone). Small
|
39
|
+
# amounts of data may be encrypted directly with the public key. Larger data is
|
40
|
+
# encrypted using symmetric encryption. The encrypted data is stored in the
|
41
|
+
# database column of the same name as the attibute. If symmetric encryption is
|
42
|
+
# used (the default) additional column are need to store the generated password
|
43
|
+
# and IV.
|
44
|
+
def encrypt_with_public_key(name, options = {})
|
45
|
+
include InstanceMethods
|
46
|
+
|
47
|
+
options = options.symbolize_keys.reverse_merge Strongbox.options
|
48
|
+
|
49
|
+
define_method name do
|
50
|
+
@_lock ||= Lock.new(name, self, options)
|
51
|
+
end
|
52
|
+
|
53
|
+
define_method "#{name}=" do | plaintext |
|
54
|
+
@_lock ||= Lock.new(name, self, options)
|
55
|
+
@_lock.encrypt plaintext
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
module InstanceMethods
|
62
|
+
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
if Object.const_defined?("ActiveRecord")
|
67
|
+
ActiveRecord::Base.send(:include, Strongbox)
|
68
|
+
end
|
69
|
+
|
@@ -0,0 +1,85 @@
|
|
1
|
+
module Strongbox
|
2
|
+
# The Lock class encrypts and decrypts the protected attribute. It
|
3
|
+
# automatically encrypts the data when set and decrypts it when the private
|
4
|
+
# key password is provided.
|
5
|
+
class Lock
|
6
|
+
|
7
|
+
def initialize name, instance, options = {}
|
8
|
+
@name = name
|
9
|
+
@instance = instance
|
10
|
+
|
11
|
+
options = Strongbox.options.merge(options)
|
12
|
+
|
13
|
+
@base64 = options[:base64]
|
14
|
+
@public_key = options[:public_key] || options[:key_pair]
|
15
|
+
@private_key = options[:private_key] || options[:key_pair]
|
16
|
+
@padding = options[:padding]
|
17
|
+
@symmetric = options[:symmetric]
|
18
|
+
@symmetric_cipher = options[:symmetric_cipher]
|
19
|
+
@symmetric_key = options[:symmetric_key] || "#{name}_key"
|
20
|
+
@symmetric_iv = options[:symmetric_iv] || "#{name}_iv"
|
21
|
+
end
|
22
|
+
|
23
|
+
def encrypt plaintext
|
24
|
+
unless @public_key
|
25
|
+
raise StrongboxError.new("#{@instance.class} model does not have public key_file")
|
26
|
+
end
|
27
|
+
if !plaintext.blank?
|
28
|
+
# Using a blank password in OpenSSL::PKey::RSA.new prevents reading
|
29
|
+
# the private key if the file is a key pair
|
30
|
+
public_key = OpenSSL::PKey::RSA.new(File.read(@public_key),"")
|
31
|
+
if @symmetric == :always
|
32
|
+
cipher = OpenSSL::Cipher::Cipher.new(@symmetric_cipher)
|
33
|
+
cipher.encrypt
|
34
|
+
cipher.key = random_key = cipher.random_key
|
35
|
+
cipher.iv = random_iv = cipher.random_iv
|
36
|
+
|
37
|
+
ciphertext = cipher.update(plaintext)
|
38
|
+
ciphertext << cipher.final
|
39
|
+
|
40
|
+
@instance.write_attribute(@symmetric_key,public_key.public_encrypt(random_key,@padding))
|
41
|
+
@instance.write_attribute(@symmetric_iv,public_key.public_encrypt(random_iv,@padding))
|
42
|
+
else
|
43
|
+
ciphertext = public_key.public_encrypt(plaintext,@padding)
|
44
|
+
end
|
45
|
+
ciphertext = Base64.encode64(ciphertext) if @base64
|
46
|
+
@instance.write_attribute(@name,ciphertext)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Given the private key password decrypts the attribute. Will raise
|
51
|
+
# OpenSSL::PKey::RSAError if the password is wrong.
|
52
|
+
|
53
|
+
def decrypt password = ""
|
54
|
+
# Given a private key and a nil password OpenSSL::PKey::RSA.new() will
|
55
|
+
# *prompt* for a password, we default to an empty string to avoid that.
|
56
|
+
return "*encrypted*" if password.blank?
|
57
|
+
|
58
|
+
unless @private_key
|
59
|
+
raise StrongboxError.new("#{@instance.class} model does not have public key_file")
|
60
|
+
end
|
61
|
+
|
62
|
+
ciphertext = @instance.read_attribute(@name)
|
63
|
+
if ciphertext
|
64
|
+
ciphertext = Base64.decode64(ciphertext) if @base64
|
65
|
+
private_key = OpenSSL::PKey::RSA.new(File.read(@private_key),password)
|
66
|
+
if @symmetric == :always
|
67
|
+
cipher = OpenSSL::Cipher::Cipher.new('aes-256-cbc')
|
68
|
+
cipher.decrypt
|
69
|
+
cipher.key = private_key.private_decrypt(@instance.read_attribute(@symmetric_key),@padding)
|
70
|
+
cipher.iv = private_key.private_decrypt(@instance.read_attribute(@symmetric_iv),@padding)
|
71
|
+
plaintext = cipher.update(ciphertext)
|
72
|
+
plaintext << cipher.final
|
73
|
+
else
|
74
|
+
plaintext = private_key.private_decrypt(ciphertext,@padding)
|
75
|
+
end
|
76
|
+
else
|
77
|
+
nil
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def to_s
|
82
|
+
decrypt
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
data/rails/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__),'../init.rb')
|
data/test/database.yml
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
-----BEGIN RSA PRIVATE KEY-----
|
2
|
+
Proc-Type: 4,ENCRYPTED
|
3
|
+
DEK-Info: DES-EDE3-CBC,317921A00FB0882F
|
4
|
+
|
5
|
+
f+GWBkcLJLsBUElOEKhqrtYgT1X4nixaZHD5x0VhmW2FrREz4vcqXrxwLTaRQJK/
|
6
|
+
vHFJ/7IVmEHScwEognSfw/wX2HMIHczoQT3ugsa29Nt7t1VLGy9jvN1+1f+g90xe
|
7
|
+
02jC7CYEKUJ3agZPox49i0/UN9OCIgdtKfecdDHYWyziob8yYTsUdDGyAXlPv0Kx
|
8
|
+
0MPSCRDtEh4UJ2PIFyw2HowkYeNss6uIte9rxJGINI11D9vmXR0pH0XyCwHQn+2T
|
9
|
+
ScHWg8BJ1rkBKydbKQ4vnfhGMjG+bZyrJXrJSoazXroseuhHu8QRUONm5Kl/zW1f
|
10
|
+
GP1CjIfTCQQZECYIa2tXTFdL9y2ZOCn8xit57SwEpmJMvZC58PkQX5+/aHPcOXhl
|
11
|
+
YrF+6FEfNpdBz9PUmv4Af2kTa88xZqm1Q3GtTOk7wsJpfeTMhU71KjA1pL9xNPrT
|
12
|
+
DnKhtfLGvcgo8Z9BGOiLFe9uQvhhprX7isc1XdysbMigsVIWLvZp9RxRp/zAn7fy
|
13
|
+
y56C6mc3tUwcq89RcxAn+bC75gwZO/hyVrnkhManOMfHTEiZXVybU9Ril3SZ+ry6
|
14
|
+
8AxMid0ZWbbtCHdDc5rHfXsGeFhJZxBbg/WtMxBPGHNByqs8sWUM9Z8YoK8WMYxV
|
15
|
+
GvC9RB4m0jgA4S3MEOMmKOXDuJxa7IgTgApVmLPl+sDOHGK3xAItYJJawJqOZQ1f
|
16
|
+
r+x/8g19CuehuflCxDo+D4/RJMqkOEq+0FGUqI8lHv6vR6+YpkGdrQQXUohBy67f
|
17
|
+
3Qym1ztZ8ygsttgJwnhwAfMh8FdIrVJc7NZ8pDiBZbg=
|
18
|
+
-----END RSA PRIVATE KEY-----
|
19
|
+
-----BEGIN PUBLIC KEY-----
|
20
|
+
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC9F1ipsLL+V68bGSJFqFLQKgXq
|
21
|
+
Glyyplx0s9KxgLbmbDICXpV7DceKaIBUkPZDx2DrlvjZmG+rG5ehdWNI7q/hupao
|
22
|
+
NF0WzEiOp+30gISeyl81Z/NAmhcwcOnZpbS9nl4JLaWrN7iGC1geNBNDo+lVbsm1
|
23
|
+
O2+Tlt8rjHsNjzgIzQIDAQAB
|
24
|
+
-----END PUBLIC KEY-----
|
@@ -0,0 +1,111 @@
|
|
1
|
+
require 'test/test_helper'
|
2
|
+
|
3
|
+
class StrongboxTest < Test::Unit::TestCase
|
4
|
+
context "A Class with a secured field" do
|
5
|
+
setup do
|
6
|
+
rebuild_model :key_pair => File.join(FIXTURES_DIR,'keypair.pem')
|
7
|
+
end
|
8
|
+
|
9
|
+
should "not error when trying to also create a secure field" do
|
10
|
+
assert_nothing_raised do
|
11
|
+
Dummy.class_eval do
|
12
|
+
encrypt_with_public_key :secret,
|
13
|
+
:key_pair => File.join(FIXTURES_DIR,'keypair.pem')
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
context "that is valid" do
|
19
|
+
setup do
|
20
|
+
@dummy = Dummy.new
|
21
|
+
@dummy.secret = 'Shhhh'
|
22
|
+
@dummy.in_the_clear = 'Hey you guys!'
|
23
|
+
end
|
24
|
+
|
25
|
+
should "not change unencrypted fields" do
|
26
|
+
assert_equal 'Hey you guys!', @dummy.in_the_clear
|
27
|
+
end
|
28
|
+
|
29
|
+
should "return '*encrypted*' when locked" do
|
30
|
+
assert_equal "*encrypted*", @dummy.secret.decrypt
|
31
|
+
end
|
32
|
+
|
33
|
+
should "return secret when unlocked" do
|
34
|
+
assert_equal "Shhhh", @dummy.secret.decrypt('boost facile')
|
35
|
+
end
|
36
|
+
|
37
|
+
should "generate and store symmetric encryption key and IV" do
|
38
|
+
assert_not_nil @dummy.attributes['secret_key']
|
39
|
+
assert_not_nil @dummy.attributes['secret_iv']
|
40
|
+
end
|
41
|
+
|
42
|
+
should "raise on bad password" do
|
43
|
+
assert_raises(OpenSSL::PKey::RSAError) do
|
44
|
+
@dummy.secret.decrypt('letmein')
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
context "with symmetric encryption disabled" do
|
49
|
+
setup do
|
50
|
+
rebuild_class(:key_pair => File.join(FIXTURES_DIR,'keypair.pem'),
|
51
|
+
:symmetric => :never)
|
52
|
+
@dummy = Dummy.new
|
53
|
+
@dummy.secret = 'Shhhh'
|
54
|
+
end
|
55
|
+
|
56
|
+
should "return '*encrypted*' when locked" do
|
57
|
+
assert_equal "*encrypted*", @dummy.secret.decrypt
|
58
|
+
end
|
59
|
+
|
60
|
+
should "return secret when unlocked" do
|
61
|
+
assert_equal "Shhhh", @dummy.secret.decrypt('boost facile')
|
62
|
+
end
|
63
|
+
|
64
|
+
should "not generate and store symmetric encryption key and IV" do
|
65
|
+
assert_nil @dummy.attributes['secret_key']
|
66
|
+
assert_nil @dummy.attributes['secret_iv']
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
|
71
|
+
context "with Base64 encoding enabled" do
|
72
|
+
setup do
|
73
|
+
rebuild_class(:key_pair => File.join(FIXTURES_DIR,'keypair.pem'),
|
74
|
+
:base64 => true)
|
75
|
+
@dummy = Dummy.new
|
76
|
+
@dummy.secret = 'Shhhh'
|
77
|
+
end
|
78
|
+
|
79
|
+
should 'Base64 encode the ciphertext' do
|
80
|
+
# Base64 encoded text is limited to the charaters A–Z, a–z, and 0–9,
|
81
|
+
# and is padded with 0 to 2 equal-signs
|
82
|
+
assert @dummy.attributes['secret'] =~ /^[0-9A-Za-z+\/]+={0,2}$/
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
context "when a key_pair is not provided" do
|
89
|
+
setup do
|
90
|
+
rebuild_class
|
91
|
+
@dummy = Dummy.new
|
92
|
+
end
|
93
|
+
|
94
|
+
should "raise on encrypt" do
|
95
|
+
assert_raises(Strongbox::StrongboxError) do
|
96
|
+
@dummy.secret = 'Shhhh'
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
should "raise on decrypt with a password" do
|
101
|
+
assert_raises(Strongbox::StrongboxError) do
|
102
|
+
@dummy.secret.decrypt('boost facile')
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
should "return '*encrypted*' when still locked" do
|
107
|
+
assert_equal "*encrypted*", @dummy.secret.decrypt
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
ROOT = File.join(File.dirname(__FILE__), '..')
|
2
|
+
RAILS_ROOT = ROOT
|
3
|
+
$LOAD_PATH << File.join(ROOT, 'lib')
|
4
|
+
|
5
|
+
require 'rubygems'
|
6
|
+
require 'test/unit'
|
7
|
+
require 'activerecord'
|
8
|
+
gem 'thoughtbot-shoulda', ">= 2.9.0"
|
9
|
+
require 'shoulda'
|
10
|
+
begin require 'redgreen'; rescue LoadError; end
|
11
|
+
|
12
|
+
require 'strongbox'
|
13
|
+
|
14
|
+
ENV['RAILS_ENV'] ||= 'test'
|
15
|
+
|
16
|
+
FIXTURES_DIR = File.join(File.dirname(__FILE__), "fixtures")
|
17
|
+
config = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml'))
|
18
|
+
ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + "/debug.log")
|
19
|
+
ActiveRecord::Base.establish_connection(config['test'])
|
20
|
+
|
21
|
+
|
22
|
+
# rebuild_model and rebuild_class are borrowed directly from the Paperclip gem
|
23
|
+
#
|
24
|
+
# http://thoughtbot.com/projects/paperclip
|
25
|
+
|
26
|
+
# rebuild_model (re)creates a database table for our Dummy model.
|
27
|
+
# Call this to initial create a model, or to reset the database.
|
28
|
+
|
29
|
+
def rebuild_model options = {}
|
30
|
+
ActiveRecord::Base.connection.create_table :dummies, :force => true do |table|
|
31
|
+
table.string :in_the_clear
|
32
|
+
table.binary :secret
|
33
|
+
table.binary :secret_key
|
34
|
+
table.binary :secret_iv
|
35
|
+
end
|
36
|
+
rebuild_class options
|
37
|
+
end
|
38
|
+
|
39
|
+
# rebuild_class creates or replaces the Dummy ActiveRecord Model.
|
40
|
+
# Call this when changing the options to encrypt_with_public_key
|
41
|
+
|
42
|
+
def rebuild_class options = {}
|
43
|
+
ActiveRecord::Base.send(:include, Strongbox)
|
44
|
+
Object.send(:remove_const, "Dummy") rescue nil
|
45
|
+
Object.const_set("Dummy", Class.new(ActiveRecord::Base))
|
46
|
+
Dummy.class_eval do
|
47
|
+
include Strongbox
|
48
|
+
encrypt_with_public_key :secret, options
|
49
|
+
end
|
50
|
+
end
|
metadata
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: spikex-strongbox
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Spike Ilacqua
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-04-15 23:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: thoughtbot-shoulda
|
17
|
+
type: :development
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: "0"
|
24
|
+
version:
|
25
|
+
description:
|
26
|
+
email: spike@stuff-things.net
|
27
|
+
executables: []
|
28
|
+
|
29
|
+
extensions: []
|
30
|
+
|
31
|
+
extra_rdoc_files: []
|
32
|
+
|
33
|
+
files:
|
34
|
+
- LICENSE
|
35
|
+
- Rakefile
|
36
|
+
- README.textile
|
37
|
+
- lib/strongbox
|
38
|
+
- lib/strongbox/lock.rb
|
39
|
+
- lib/strongbox.rb
|
40
|
+
- rails/init.rb
|
41
|
+
- test/database.yml
|
42
|
+
- test/fixtures
|
43
|
+
- test/fixtures/keypair.pem
|
44
|
+
- test/strongbox_test.rb
|
45
|
+
- test/test_helper.rb
|
46
|
+
has_rdoc: false
|
47
|
+
homepage: http://stuff-things.net/strongbox
|
48
|
+
post_install_message:
|
49
|
+
rdoc_options: []
|
50
|
+
|
51
|
+
require_paths:
|
52
|
+
- lib
|
53
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
54
|
+
requirements:
|
55
|
+
- - ">="
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
version: "0"
|
58
|
+
version:
|
59
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
60
|
+
requirements:
|
61
|
+
- - ">="
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
version: "0"
|
64
|
+
version:
|
65
|
+
requirements: []
|
66
|
+
|
67
|
+
rubyforge_project:
|
68
|
+
rubygems_version: 1.2.0
|
69
|
+
signing_key:
|
70
|
+
specification_version: 2
|
71
|
+
summary: Secures ActiveRecord fields with public key encryption.
|
72
|
+
test_files: []
|
73
|
+
|