chintala-strongbox 0.6.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ test/debug.log
2
+ doc
3
+ /strongbox-*.gem
4
+ .bundle
5
+ Gemfile.lock
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in x.gemspec
4
+ gemspec
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,182 @@
1
+ h1. Strongbox
2
+
3
+ Strongbox provides Public Key Encryption for ActiveRecord. By using a public key sensitive information can be encrypted and stored automatically. Once stored a password is required to access the information.
4
+
5
+ Because the largest amount of data that can practically be encrypted with a public key is 245 byte, by default Strongbox uses a two layer approach. First it encrypts the attribute using symmetric encryption with a randomly generated key and initialization vector (IV) (which can just be thought of as a second key), then it encrypts those with the public key.
6
+
7
+ Strongbox stores the encrypted attribute in a database column by the same name, i.e. if you tell Strongbox to encrypt "secret" then it will be store in "secret" in the database, just as the unencrypted attribute would be. If symmetric encryption is used (the default) two additional columns "secret_key" and "secret_iv" are needed as well.
8
+
9
+ The attribute is automatically encrypted simply by setting it:
10
+
11
+ user.secret = "Shhhhhhh..."
12
+
13
+ and decrypted by calling the "decrypt" method with the private key password.
14
+
15
+ plain_text = user.secret.decrypt 'letmein'
16
+
17
+ h2. Quick Start
18
+
19
+ In your model:
20
+
21
+ bc. class User < ActiveRecord::Base
22
+ encrypt_with_public_key :secret,
23
+ :key_pair => File.join(RAILS_ROOT,'config','keypair.pem')
24
+ end
25
+
26
+ In your migrations:
27
+
28
+ bc. class AddSecretColumnsToUser < ActiveRecord::Migration
29
+ def self.up
30
+ add_column :users, :secret, :binary
31
+ add_column :users, :secret_key, :binary
32
+ add_column :users, :secret_iv, :binary
33
+ end
34
+ def self.down
35
+ remove_column :users, :secret
36
+ remove_column :users, :secret_key
37
+ remove_column :users, :secret_iv
38
+ end
39
+ end
40
+
41
+ Generate a key pair:
42
+
43
+ (Choose a strong password.)
44
+
45
+ bc. openssl genrsa -des3 -out config/private.pem 2048
46
+ openssl rsa -in config/private.pem -out config/public.pem -outform PEM -pubout
47
+ cat config/private.pem config/public.pem >> config/keypair.pem
48
+
49
+ In your views and forms you don't need to do anything special to encrypt data. To decrypt call:
50
+
51
+ bc. user.secret.decrypt 'password'
52
+
53
+ h2. Gem installation (Rails 2.1+)
54
+
55
+ In config/environment.rb:
56
+
57
+ bc. config.gem "strongbox"
58
+
59
+ h2. Usage
60
+
61
+ _encrypt_with_public_key_ sets up the attribute it's called on for automatic encryption. It's simplest form is:
62
+
63
+ bc. class User < ActiveRecord::Base
64
+ encrypt_with_public_key :secret,
65
+ :key_pair => File.join(RAILS_ROOT,'config','keypair.pem')
66
+ end
67
+
68
+ Which will encrypt the attribute "secret". The attribute will be encrypted using symmetric encryption with an automatically generated key and IV encrypted using the public key. This requires three columns in the database "secret", "secret_key", and "secret_iv" (see below).
69
+
70
+ Options to encrypt_with_public_key are:
71
+
72
+ :public_key - Public key. Overrides :key_pair. See Key Formats below.
73
+
74
+ :private_key - Private key. Overrides :key_pair.
75
+
76
+ :key_pair - Key pair, containing both the public and private keys.
77
+
78
+ :symmetric :always/:never - Encrypt the date using symmetric encryption. The public key is used to encrypt an automatically generated key and IV. This allows for large amounts of data to be encrypted. The size of data that can be encrypted directly with the public is limit to key size (in bytes) - 11. So a 2048 key can encrypt *245 bytes*. Defaults to *:always*.
79
+
80
+ :symmetric_cipher - Cipher to use for symmetric encryption. Defaults to *'aes-256-cbc'*. Other ciphers support by OpenSSL may be used.
81
+
82
+ :base64 true/false - Use Base64 encoding to convert encrypted data to text. Use when binary save data storage is not available. Defaults to *false*.
83
+
84
+ :padding - Method used to pad data encrypted with the public key. Defaults to *RSA_PKCS1_PADDING*. The default should be fine unless you are dealing with legacy data.
85
+
86
+ :ensure_required_columns - Make sure the required database column(s) exist. Defaults to *true*, set to false if you want to encrypt/decrypt data stored outside of the database.
87
+
88
+ For example, encrypting a small attribute, providing only the public key for extra security, and Base64 encoding the encrypted data:
89
+
90
+ bc. class User < ActiveRecord::Base
91
+ validates_length_of :pin_code, :is => 4
92
+ encrypt_with_public_key :pin_code,
93
+ :symmetric => :never,
94
+ :base64 => true,
95
+ :public_key => File.join(RAILS_ROOT,'config','public.pem')
96
+ end
97
+
98
+ Strongbox can encrypt muliple attributes. _encrypt_with_public_key_ accepts a list of attributes, assuming they will use the same options:
99
+
100
+ bc. class User < ActiveRecord::Base
101
+ encrypt_with_public_key :secret, :double_secret,
102
+ :key_pair => File.join(RAILS_ROOT,'config','keypair.pem')
103
+ end
104
+
105
+ If you need different options, call _encrypt_with_public_key_ for each attribute:
106
+
107
+ bc. class User < ActiveRecord::Base
108
+ encrypt_with_public_key :secret,
109
+ :key_pair => File.join(RAILS_ROOT,'config','keypair.pem')
110
+ encrypt_with_public_key :double_secret,
111
+ :key_pair => File.join(RAILS_ROOT,'config','another_key.pem')
112
+ end
113
+
114
+ h2 Key Formats
115
+
116
+ _:public_key_, _:private_key_, and _:key_pair_ can be in one of the following formats:
117
+
118
+ * A string containing path to a file. This is the default interpretation of a string.
119
+ * A string contanting a key in PEM format, needs to match this the regex /^-+BEGIN .* KEY-+$/
120
+ * A symbol naming a method to call. Can return any of the other valid key formats.
121
+ * A instance of OpenSSL::PKey::RSA. Must be unlocked to be used as the private key.
122
+
123
+ h2. Key Generation
124
+
125
+ h3. In the shell
126
+
127
+ Generate a key pair:
128
+
129
+ bc. openssl genrsa -des3 -out config/private.pem 2048
130
+ Generating RSA private key, 2048 bit long modulus
131
+ ......+++
132
+ .+++
133
+ e is 65537 (0x10001)
134
+ Enter pass phrase for config/private.pem:
135
+ Verifying - Enter pass phrase for config/private.pem:
136
+
137
+ and extract the the public key:
138
+
139
+ bc. openssl rsa -in config/private.pem -out config/public.pem -outform PEM -pubout
140
+ Enter pass phrase for config/private.pem:
141
+ writing RSA key
142
+
143
+ If you are going to leave the private key installed it's easiest to create a single key pair file:
144
+
145
+ bc. cat config/private.pem config/public.pem >> config/keypair.pem
146
+
147
+ Or, for added security, store the private key file else where, leaving only the public key.
148
+
149
+ h3. In code
150
+
151
+ bc. require 'openssl'
152
+ rsa_key = OpenSSL::PKey::RSA.new(2048)
153
+ cipher = OpenSSL::Cipher::Cipher.new('des3')
154
+ private_key = rsa_key.to_pem(cipher,'password')
155
+ public_key = rsa_key.public_key.to_pem
156
+ key_pair = private_key + public_key
157
+
158
+ _private_key_, _public_key_, and _key_pair_ are strings, store as you see fit.
159
+
160
+ h2. Table Creation
161
+
162
+ In it's default configuration Strongbox requires three columns, one the encrypted data, one for the encrypted symmetric key, and one for the encrypted symmetric IV. If symmetric encryption is disabled then only the columns for the data being encrypted is needed.
163
+
164
+ If your underlying database allows, use the *binary* column type. If you must store your data in text format be sure to enable Base64 encoding and to use the *text* column type. If you use a _string_ column and encrypt anything greater than 186 bytes (245 bytes if you don't enable Base64 encoding) *your data will be lost*.
165
+
166
+
167
+ h2. Security Caveats
168
+
169
+ If you don't encrypt your data, then an attacker only needs to steal that data to get your secrets.
170
+
171
+ If encrypt your data using symmetric encrypts and a stored key, then the attacker needs the data and the key stored on the server.
172
+
173
+ If you use public key encryption, the attacker needs the data, the private key, and the password. This means the attacker has to sniff the password somehow, so that's what you need to protect against.
174
+
175
+ h2. Authors
176
+
177
+ Spike Ilacqua
178
+
179
+ h2. Thanks
180
+
181
+ Strongbox's implementation drew inspiration from Thoughtbot's Paperclip gem http://www.thoughtbot.com/projects/paperclip
182
+
data/Rakefile ADDED
@@ -0,0 +1,39 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+
4
+ require 'rake'
5
+ require 'rake/testtask'
6
+ require 'rake/rdoctask'
7
+
8
+ $LOAD_PATH << File.join(File.dirname(__FILE__), 'lib')
9
+ require 'strongbox'
10
+
11
+ desc 'Default: run tests.'
12
+ task :default => :test
13
+
14
+ desc 'Test the strongbox gem.'
15
+ Rake::TestTask.new(:test) do |t|
16
+ t.libs << '.'
17
+ t.pattern = 'test/**/*_test.rb'
18
+ t.verbose = true
19
+ end
20
+
21
+ desc 'Generate documentation for the strongbox gem.'
22
+ Rake::RDocTask.new(:rdoc) do |rdoc|
23
+ rdoc.rdoc_dir = 'doc'
24
+ rdoc.title = 'Strongbox'
25
+ rdoc.options << '--line-numbers' << '--inline-source'
26
+ rdoc.rdoc_files.include('README*')
27
+ rdoc.rdoc_files.include('lib/**/*.rb')
28
+ end
29
+
30
+ desc "Generate a gemspec file for GitHub"
31
+ task :gemspec do
32
+ $spec = eval(File.read('strongbox.gemspec'))
33
+ $spec.validate
34
+ end
35
+
36
+ desc "Build the gem"
37
+ task :build => :gemspec do
38
+ Gem::Builder.new($spec).build
39
+ end
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'strongbox'
data/lib/strongbox.rb ADDED
@@ -0,0 +1,97 @@
1
+ require 'openssl'
2
+ require 'base64'
3
+
4
+ require 'strongbox/lock'
5
+
6
+ module Strongbox
7
+
8
+ VERSION = "0.6.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
+ :ensure_required_columns => true
24
+ }
25
+ end
26
+
27
+ def included base #:nodoc:
28
+ base.extend ClassMethods
29
+ if base.respond_to?(:class_attribute)
30
+ base.class_attribute :lock_options
31
+ end
32
+ end
33
+ end
34
+
35
+ class StrongboxError < StandardError #:nodoc:
36
+ end
37
+
38
+ module ClassMethods
39
+ # +encrypt_with_public_key+ gives the class it is called on an attribute that
40
+ # when assigned is automatically encrypted using a public key. This allows the
41
+ # unattended encryption of data, without exposing the information need to decrypt
42
+ # it (as would be the case when using symmetric key encryption alone). Small
43
+ # amounts of data may be encrypted directly with the public key. Larger data is
44
+ # encrypted using symmetric encryption. The encrypted data is stored in the
45
+ # database column of the same name as the attibute. If symmetric encryption is
46
+ # used (the default) additional column are need to store the generated password
47
+ # and IV.
48
+ #
49
+ # Last argument should be the options hash
50
+ # Argument 0..-2 contains columns to be encrypted
51
+ def encrypt_with_public_key(*args)
52
+ include InstanceMethods
53
+
54
+ options = args.delete_at(-1) || {}
55
+
56
+ unless options.is_a?(Hash)
57
+ args.push(options)
58
+ options = {}
59
+ end
60
+
61
+ if args.one?
62
+ name = args.first
63
+ else
64
+ return args.each { |name| encrypt_with_public_key(name, options) }
65
+ end
66
+
67
+ if respond_to?(:class_attribute)
68
+ self.lock_options = {} if lock_options.nil?
69
+ else
70
+ class_inheritable_reader :lock_options
71
+ write_inheritable_attribute(:lock_options, {}) if lock_options.nil?
72
+ end
73
+
74
+ lock_options[name] = options.symbolize_keys.reverse_merge Strongbox.options
75
+ define_method name do
76
+ lock_for(name)
77
+ end
78
+
79
+ define_method "#{name}=" do | plaintext |
80
+ lock_for(name).encrypt plaintext
81
+ end
82
+
83
+ end
84
+ end
85
+
86
+ module InstanceMethods
87
+ def lock_for name
88
+ @_locks ||= {}
89
+ @_locks[name] ||= Lock.new(name, self, self.class.lock_options[name])
90
+ end
91
+ end
92
+ end
93
+
94
+ if Object.const_defined?("ActiveRecord")
95
+ ActiveRecord::Base.send(:include, Strongbox)
96
+ end
97
+
@@ -0,0 +1,152 @@
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
+ @size = 0
12
+
13
+ options = Strongbox.options.merge(options)
14
+
15
+ @base64 = options[:base64]
16
+ @public_key = options[:public_key] || options[:key_pair]
17
+ @private_key = options[:private_key] || options[:key_pair]
18
+ @padding = options[:padding]
19
+ @symmetric = options[:symmetric]
20
+ @symmetric_cipher = options[:symmetric_cipher]
21
+ @symmetric_key = options[:symmetric_key] || "#{name}_key"
22
+ @symmetric_iv = options[:symmetric_iv] || "#{name}_iv"
23
+ @ensure_required_columns = options[:ensure_required_columns]
24
+ end
25
+
26
+ def encrypt plaintext
27
+ ensure_required_columns if @ensure_required_columns
28
+ unless @public_key
29
+ raise StrongboxError.new("#{@instance.class} model does not have public key_file")
30
+ end
31
+ if !plaintext.nil?
32
+ @size = plaintext.size # For validations
33
+ # Using a blank password in OpenSSL::PKey::RSA.new prevents reading
34
+ # the private key if the file is a key pair
35
+ public_key = get_rsa_key(@public_key,"")
36
+ if @symmetric == :always
37
+ cipher = OpenSSL::Cipher::Cipher.new(@symmetric_cipher)
38
+ cipher.encrypt
39
+ cipher.key = random_key = cipher.random_key
40
+ cipher.iv = random_iv = cipher.random_iv
41
+
42
+ ciphertext = cipher.update(plaintext)
43
+ ciphertext << cipher.final
44
+ encrypted_key = public_key.public_encrypt(random_key,@padding)
45
+ encrypted_iv = public_key.public_encrypt(random_iv,@padding)
46
+ if @base64
47
+ encrypted_key = Base64.encode64(encrypted_key)
48
+ encrypted_iv = Base64.encode64(encrypted_iv)
49
+ end
50
+ @instance[@symmetric_key] = encrypted_key
51
+ @instance[@symmetric_iv] = encrypted_iv
52
+ else
53
+ ciphertext = public_key.public_encrypt(plaintext,@padding)
54
+ end
55
+ ciphertext = Base64.encode64(ciphertext) if @base64
56
+ @instance[@name] = ciphertext
57
+ end
58
+ end
59
+
60
+ # Given the private key password decrypts the attribute. Will raise
61
+ # OpenSSL::PKey::RSAError if the password is wrong.
62
+
63
+ def decrypt password = nil, ciphertext = nil
64
+ # Given a private key and a nil password OpenSSL::PKey::RSA.new() will
65
+ # *prompt* for a password, we default to an empty string to avoid that.
66
+ ciphertext ||= @instance[@name]
67
+ return nil if ciphertext.nil?
68
+ return "" if ciphertext.empty?
69
+
70
+ return "*encrypted*" if password.nil?
71
+ unless @private_key
72
+ raise StrongboxError.new("#{@instance.class} model does not have private key_file")
73
+ end
74
+
75
+ if ciphertext
76
+ ciphertext = Base64.decode64(ciphertext) if @base64
77
+ private_key = get_rsa_key(@private_key,password)
78
+ if @symmetric == :always
79
+ random_key = @instance[@symmetric_key]
80
+ random_iv = @instance[@symmetric_iv]
81
+ if @base64
82
+ random_key = Base64.decode64(random_key)
83
+ random_iv = Base64.decode64(random_iv)
84
+ end
85
+ cipher = OpenSSL::Cipher::Cipher.new(@symmetric_cipher)
86
+ cipher.decrypt
87
+ cipher.key = private_key.private_decrypt(random_key,@padding)
88
+ cipher.iv = private_key.private_decrypt(random_iv,@padding)
89
+ plaintext = cipher.update(ciphertext)
90
+ plaintext << cipher.final
91
+ else
92
+ plaintext = private_key.private_decrypt(ciphertext,@padding)
93
+ end
94
+ else
95
+ nil
96
+ end
97
+ end
98
+
99
+ def to_s
100
+ decrypt
101
+ end
102
+
103
+ def to_json(options = nil)
104
+ to_s
105
+ end
106
+
107
+ # Needed for validations
108
+ def blank?
109
+ @instance[@name].blank?
110
+ end
111
+
112
+ def nil?
113
+ @instance[@name].nil?
114
+ end
115
+
116
+ def size
117
+ @size
118
+ end
119
+
120
+ def length
121
+ @size
122
+ end
123
+
124
+ def ensure_required_columns
125
+ columns = [@name.to_s]
126
+ columns += [@symmetric_key, @symmetric_iv] if @symmetric == :always
127
+ columns.each do |column|
128
+ unless @instance.class.column_names.include? column
129
+ raise StrongboxError.new("#{@instance.class} model does not have database column \"#{column}\"")
130
+ end
131
+ end
132
+ end
133
+
134
+ private
135
+ def get_rsa_key(key,password = '')
136
+ if key.is_a?(Proc)
137
+ key = key.call
138
+ end
139
+
140
+ if key.is_a?(Symbol)
141
+ key = @instance.send(key)
142
+ end
143
+
144
+ return key if key.is_a?(OpenSSL::PKey::RSA)
145
+
146
+ if key !~ /^-+BEGIN .* KEY-+$/
147
+ key = File.read(key)
148
+ end
149
+ return OpenSSL::PKey::RSA.new(key,password)
150
+ end
151
+ end
152
+ end