Skip to content
Browse files

initial commit from svn

  • Loading branch information...
0 parents commit 5ba4d7b1eb1ab3b32099fb993ae7b4e3549fced6 @technoweenie committed Jun 10, 2008
58 CHANGELOG
@@ -0,0 +1,58 @@
+*0.3.1* (7 Jan 2006)
+
+* removed useless breakpoint [Solomon White]
+
+*0.3* (29 Oct 2005)
+
+* added rake task for generating asymmetric keys
+* Switch to migrations and schema for testing setup
+
+*0.2.9* (18 Sep 2005)
+
+* First RubyForge release.
+
+*0.2.8* (17 Sep 2005)
+
+* Added Active Record unit tests
+
+*0.2.7* (17 Sep 2005)
+
+* Added rdocs and stubs for AR unit tests
+
+*0.2.6* (2 Aug 2005)
+
+* Fixed generates_crypted so it adds attribute accessors
+
+*0.2.5* (27 Jul 2005)
+
+* Set ActiveRecord callback objects to only encrypt fields when they are not empty.
+
+*0.2.4* (11 Jul 2005)
+
+* Split ActiveRecord callback methods into their own classes.
+* Set AR virtual columns to fail silently on errors.
+
+*0.2.3* (11 Jul 2005)
+
+* Added ActiveRecord callback objects for SymmetricSentry and AsymmetricSentry. +one_way_encrypt+ is depreciated.
+* Readme doc added too
+
+*0.2.1* (9 Jul 2005)
+
+* vastly simplified one_way_encrypt at danp's suggestion. Use this in your model to try it out:
+
+ +one_way_encrypt :password*
+
+ That generates an SHA hash of model.password to model.crypted_password which is saved in the DB.
+ model.password is a virtual field. Continue using validates_confirmation_of for confirmation.
+
+
+*0.2* (9 Jul 2005)
+
+* added ActiveRecord::Base#one_way_encrypt class method to hash passwords with SHA
+* Renamed core classes to SymmetricSentry and AsymmetricSentry
+* Test Suite added
+
+*0.1*
+
+* Initial Import
20 MIT-LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2005 Rick Olson
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
94 README
@@ -0,0 +1,94 @@
+= Sentry lib - painless encryption library
+
+Sentry is a simple wrapper around the mostly undocumented OpenSSL encryption classes.
+For now, look at the pseudo test cases in sentry.rb until I can get more examples written out.
+
+== Resources
+
+Install
+
+* gem install sentry
+
+Rubyforge project
+
+* http://rubyforge.org/projects/sentry
+
+RDocs
+
+* http://sentry.rubyforge.org
+
+Subversion
+
+* http://techno-weenie.net/svn/projects/sentry
+
+Collaboa
+
+* http://collaboa.techno-weenie.net/repository/browse/sentry
+
+== Using with ActiveRecord
+
+I wrote this for the purpose of encrypting ActiveRecord attributes. Just <tt>require 'sentry'</tt>, and some new
+class methods will be available to you:
+
+=== generates_crypted
+
+ generates_crypted :password, :mode => :sha | :symmetric | :asymmetric
+
+This is the generic class method to use. Default mode is :sha.
+
+=== generates_crypted_hash_of
+
+ generates_crypted_hash_of :password
+
+This is a shortcut for using SHA encryption. No different than specifying <tt>generates_crypted :password</tt>. In the above
+example, model.password is a virtual field, and the SHA hash is saved to model.crypted_password
+
+=== asymmetrically_encrypts
+
+ asymmetrically_encrypts :password
+
+This is a shortcut for using an asymmetrical algorithm with a private/public key file. To use this, generate a public and
+private key with Sentry::AsymmetricalSentry.save_random_rsa_key(private_key_file, public_key_file). If you want to encrypt the
+private key file with a symmetrical algorithm, pass a secret key (neither the key nor the decrypted value will be stored).
+
+ Sentry::AsymmetricSentry.save_random_rsa_key(private_key_file, public_key_file, :key => 'secret_password')
+
+What that does, is requires you to pass in that same secret password when accesing the method.
+
+ class Model < ActiveRecord::Base
+ generates_crypted :password, :mode => :asymmetric
+ end
+
+ model.password = '5234523453425'
+ model.save # password is encrypted and saved to crypted_password in the database,
+ # model.password is cleared and becomes a virtual field.
+ model.password('secret_password')
+ => '5234523453425'
+
+The public and private key file names can be set in config/environment.rb
+
+ Sentry::AsymmetricSentry.default_public_key_file = "#{RAILS_ROOT}/config/public.key"
+ Sentry::AsymmetricSentry.default_private_key_file = "#{RAILS_ROOT}/config/private.key"
+
+If the private key was encrypted with the Sentry::AsymmetricalSentry#save_random_rsa_key, you must provide that same key
+when accessing the AR model.
+
+=== symmetrically_encrypts
+
+ symmetrically_encrypts :password
+
+This is a shortcut for using a symmetrical algorithm with a secret password to encrypt the field.
+
+ class Model < ActiveRecord::Base
+ generates_crypted :password, :mode => :symmetric
+ end
+
+ model.password = '5234523453425'
+ model.save # password is encrypted and saved to crypted_password in the database,
+ # model.password is cleared and becomes a virtual field.
+ model.password
+ => '5234523453425'
+
+The secret password can be set in config/environment.rb
+
+ Sentry::SymmetricSentry.default_key = "secret_password"
42 RUNNING_UNIT_TESTS
@@ -0,0 +1,42 @@
+== Creating the test database
+
+The default name for the test databases is "sentry_plugin_test". If you
+want to use another database name then be sure to update the connection
+adapter setups you want to test with in test/database.yml.
+
+Make sure that you create database objects with the same user that you specified in i
+database.yml otherwise (on Postgres, at least) tests for default values will fail.
+
+== Running with Rake
+
+The easiest way to run the unit tests is through Rake. The default task runs
+the entire test suite for the sqlite adapter. You can also run the suite on just
+one adapter by passing the DB environment variable.
+
+ rake test DB=mysql
+
+For more information, checkout the full array of rake tasks with "rake -T"
+
+Rake can be found at http://rake.rubyforge.org
+
+== Running by hand
+
+Unit tests are located in test directory. If you only want to run a single test suite,
+or don't want to bother with Rake, you can do so with something like:
+
+ cd test; DB=mysql ruby base_test.rb
+
+That'll run the base suite using the MySQL adapter. Change the adapter
+and test suite name as needed.
+
+== Faster tests
+
+If you are using a database that supports transactions, you can set the
+"AR_TX_FIXTURES" environment variable to "yes" to use transactional fixtures.
+This gives a very large speed boost. With rake:
+
+ rake AR_TX_FIXTURES=yes
+
+Or, by hand:
+
+ AR_TX_FIXTURES=yes ruby -I connections/native_sqlite3 base_test.rb
178 Rakefile
@@ -0,0 +1,178 @@
+require 'rubygems'
+
+Gem::manage_gems
+
+require 'rake/rdoctask'
+require 'rake/packagetask'
+require 'rake/gempackagetask'
+require 'rake/testtask'
+require 'rake/contrib/rubyforgepublisher'
+
+PKG_NAME = 'sentry'
+PKG_VERSION = '0.3.1'
+PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
+PROD_HOST = "technoweenie@bidwell.textdrive.com"
+RUBY_FORGE_PROJECT = 'sentry'
+RUBY_FORGE_USER = 'technoweenie'
+
+task :default => [:test]
+Rake::TestTask.new("test") do |t|
+ t.libs << "test"
+ t.pattern = "test/*_test.rb"
+ t.verbose = true
+end
+
+load 'tasks/sentry.rake'
+
+Rake::RDocTask.new do |rdoc|
+ rdoc.rdoc_dir = 'doc'
+ rdoc.title = "#{PKG_NAME} -- painless encryption for Active Record"
+ rdoc.options << '--line-numbers --inline-source --accessor cattr_accessor=object'
+ rdoc.template = "#{ENV['template']}.rb" if ENV['template']
+ rdoc.rdoc_files.include('README', 'CHANGELOG', 'RUNNING_UNIT_TESTS')
+ rdoc.rdoc_files.include('lib/**/*.rb')
+end
+
+spec = Gem::Specification.new do |s|
+ s.name = PKG_NAME
+ s.version = PKG_VERSION
+ s.platform = Gem::Platform::RUBY
+ s.summary = "Sentry provides painless encryption services with a wrapper around some OpenSSL classes"
+ s.files = FileList["{lib,test}/**/*"].to_a + %w(README MIT-LICENSE CHANGELOG RUNNING_UNIT_TESTS)
+ s.files.delete "test/sentry_plugin.sqlite.db"
+ s.files.delete "test/sentry_plugin.sqlite3.db"
+ s.require_path = 'lib'
+ s.autorequire = 'sentry'
+ s.has_rdoc = true
+ s.test_file = 'test/tests.rb'
+ s.author = "Rick Olson"
+ s.email = "technoweenie@gmail.com"
+ s.homepage = "http://techno-weenie.net"
+end
+
+Rake::GemPackageTask.new(spec) do |pkg|
+ pkg.need_tar = true
+end
+
+desc "Publish the API documentation"
+task :pdoc => [:rdoc] do
+ Rake::RubyForgePublisher.new(RUBY_FORGE_PROJECT, RUBY_FORGE_USER).upload
+end
+
+desc 'Publish the gem and API docs'
+task :publish => [:pdoc, :rubyforge_upload]
+
+desc "Publish the release files to RubyForge."
+task :rubyforge_upload => :package do
+ files = %w(gem tgz).map { |ext| "pkg/#{PKG_FILE_NAME}.#{ext}" }
+
+ if RUBY_FORGE_PROJECT then
+ require 'net/http'
+ require 'open-uri'
+
+ project_uri = "http://rubyforge.org/projects/#{RUBY_FORGE_PROJECT}/"
+ project_data = open(project_uri) { |data| data.read }
+ group_id = project_data[/[?&]group_id=(\d+)/, 1]
+ raise "Couldn't get group id" unless group_id
+
+ # This echos password to shell which is a bit sucky
+ if ENV["RUBY_FORGE_PASSWORD"]
+ password = ENV["RUBY_FORGE_PASSWORD"]
+ else
+ print "#{RUBY_FORGE_USER}@rubyforge.org's password: "
+ password = STDIN.gets.chomp
+ end
+
+ login_response = Net::HTTP.start("rubyforge.org", 80) do |http|
+ data = [
+ "login=1",
+ "form_loginname=#{RUBY_FORGE_USER}",
+ "form_pw=#{password}"
+ ].join("&")
+ http.post("/account/login.php", data)
+ end
+
+ cookie = login_response["set-cookie"]
+ raise "Login failed" unless cookie
+ headers = { "Cookie" => cookie }
+
+ release_uri = "http://rubyforge.org/frs/admin/?group_id=#{group_id}"
+ release_data = open(release_uri, headers) { |data| data.read }
+ package_id = release_data[/[?&]package_id=(\d+)/, 1]
+ raise "Couldn't get package id" unless package_id
+
+ first_file = true
+ release_id = ""
+
+ files.each do |filename|
+ basename = File.basename(filename)
+ file_ext = File.extname(filename)
+ file_data = File.open(filename, "rb") { |file| file.read }
+
+ puts "Releasing #{basename}..."
+
+ release_response = Net::HTTP.start("rubyforge.org", 80) do |http|
+ release_date = Time.now.strftime("%Y-%m-%d %H:%M")
+ type_map = {
+ ".zip" => "3000",
+ ".tgz" => "3110",
+ ".gz" => "3110",
+ ".gem" => "1400"
+ }; type_map.default = "9999"
+ type = type_map[file_ext]
+ boundary = "rubyqMY6QN9bp6e4kS21H4y0zxcvoor"
+
+ query_hash = if first_file then
+ {
+ "group_id" => group_id,
+ "package_id" => package_id,
+ "release_name" => PKG_FILE_NAME,
+ "release_date" => release_date,
+ "type_id" => type,
+ "processor_id" => "8000", # Any
+ "release_notes" => "",
+ "release_changes" => "",
+ "preformatted" => "1",
+ "submit" => "1"
+ }
+ else
+ {
+ "group_id" => group_id,
+ "release_id" => release_id,
+ "package_id" => package_id,
+ "step2" => "1",
+ "type_id" => type,
+ "processor_id" => "8000", # Any
+ "submit" => "Add This File"
+ }
+ end
+
+ query = "?" + query_hash.map do |(name, value)|
+ [name, URI.encode(value)].join("=")
+ end.join("&")
+
+ data = [
+ "--" + boundary,
+ "Content-Disposition: form-data; name=\"userfile\"; filename=\"#{basename}\"",
+ "Content-Type: application/octet-stream",
+ "Content-Transfer-Encoding: binary",
+ "", file_data, ""
+ ].join("\x0D\x0A")
+
+ release_headers = headers.merge(
+ "Content-Type" => "multipart/form-data; boundary=#{boundary}"
+ )
+
+ target = first_file ? "/frs/admin/qrs.php" : "/frs/admin/editrelease.php"
+ http.post(target + query, data, release_headers)
+ end
+
+ if first_file then
+ release_id = release_response.body[/release_id=(\d+)/, 1]
+ raise("Couldn't get release id") unless release_id
+ end
+
+ first_file = false
+ end
+ end
+end
1 init.rb
@@ -0,0 +1 @@
+require 'sentry'
79 lib/active_record/sentry.rb
@@ -0,0 +1,79 @@
+module ActiveRecord # :nodoc:
+ module Sentry
+ def self.included(base) # :nodoc:
+ base.extend ClassMethods
+ end
+
+ module ClassMethods
+ def generates_crypted(attr_name, options = {})
+ mode = options[:mode] || :sha
+ case mode
+ when :sha
+ generates_crypted_hash_of(attr_name)
+ when :asymmetric, :asymmetrical
+ asymmetrically_encrypts(attr_name)
+ when :symmetric, :symmetrical
+ symmetrically_encrypts(attr_name)
+ end
+ end
+
+ def generates_crypted_hash_of(attribute)
+ before_validation ::Sentry::ShaSentry.new(attribute)
+ attr_accessor attribute
+ end
+
+ def asymmetrically_encrypts(attr_name)
+ temp_sentry = ::Sentry::AsymmetricSentryCallback.new(attr_name)
+ before_validation temp_sentry
+ after_save temp_sentry
+
+ define_method(attr_name) do |*optional|
+ send("#{attr_name}!", *optional) rescue nil
+ end
+
+ define_method("#{attr_name}!") do |*optional|
+ return decrypted_values[attr_name] unless decrypted_values[attr_name].nil?
+ return nil if send("crypted_#{attr_name}").nil?
+ key = optional.shift
+ ::Sentry::AsymmetricSentry.decrypt_from_base64(send("crypted_#{attr_name}"), key)
+ end
+
+ define_method("#{attr_name}=") do |value|
+ decrypted_values[attr_name] = value
+ nil
+ end
+
+ private
+ define_method(:decrypted_values) do
+ @decrypted_values ||= {}
+ end
+ end
+
+ def symmetrically_encrypts(attr_name)
+ temp_sentry = ::Sentry::SymmetricSentryCallback.new(attr_name)
+ before_validation temp_sentry
+ after_save temp_sentry
+
+ define_method(attr_name) do
+ send("#{attr_name}!") rescue nil
+ end
+
+ define_method("#{attr_name}!") do
+ return decrypted_values[attr_name] unless decrypted_values[attr_name].nil?
+ return nil if send("crypted_#{attr_name}").nil?
+ ::Sentry::SymmetricSentry.decrypt_from_base64(send("crypted_#{attr_name}"))
+ end
+
+ define_method("#{attr_name}=") do |value|
+ decrypted_values[attr_name] = value
+ nil
+ end
+
+ private
+ define_method(:decrypted_values) do
+ @decrypted_values ||= {}
+ end
+ end
+ end
+ end
+end
46 lib/sentry.rb
@@ -0,0 +1,46 @@
+#Copyright (c) 2005 Rick Olson
+#
+#Permission is hereby granted, free of charge, to any person obtaining
+#a copy of this software and associated documentation files (the
+#"Software"), to deal in the Software without restriction, including
+#without limitation the rights to use, copy, modify, merge, publish,
+#distribute, sublicense, and/or sell copies of the Software, and to
+#permit persons to whom the Software is furnished to do so, subject to
+#the following conditions:
+#
+#The above copyright notice and this permission notice shall be
+#included in all copies or substantial portions of the Software.
+#
+#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+#EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+#MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+#NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+#LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+#OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+#WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+require 'openssl'
+require 'base64'
+require 'sentry/symmetric_sentry'
+require 'sentry/asymmetric_sentry'
+require 'sentry/sha_sentry'
+require 'sentry/symmetric_sentry_callback'
+require 'sentry/asymmetric_sentry_callback'
+
+module Sentry
+ class NoKeyError < StandardError
+ end
+ class NoPublicKeyError < StandardError
+ end
+ class NoPrivateKeyError < StandardError
+ end
+end
+
+begin
+ require 'active_record/sentry'
+ ActiveRecord::Base.class_eval do
+ include ActiveRecord::Sentry
+ end
+rescue NameError
+ nil
+end
144 lib/sentry/asymmetric_sentry.rb
@@ -0,0 +1,144 @@
+module Sentry
+ class AsymmetricSentry
+ attr_reader :private_key_file
+ attr_reader :public_key_file
+ attr_accessor :symmetric_algorithm
+ @@default_private_key_file = nil
+ @@default_public_key_file = nil
+ @@default_symmetric_algorithm = nil
+
+ # available options:
+ # * <tt>:private_key_file</tt> - encrypted private key file
+ # * <tt>:public_key_file</tt> - public key file
+ # * <tt>:symmetric_algorithm</tt> - algorithm to use for SymmetricSentry
+ def initialize(options = {})
+ @public_key = @private_key = nil
+ private_key_file = options[:private_key_file]
+ public_key_file = options[:public_key_file] || @@default_public_key_file
+ @symmetric_algorithm = options[:symmetric_algorithm] || @@default_symmetric_algorithm
+ end
+
+ def encrypt(data)
+ raise NoPublicKeyError unless public?
+ public_rsa.public_encrypt(data)
+ end
+
+ def encrypt_to_base64(data)
+ Base64.encode64(encrypt(data))
+ end
+
+ def decrypt(data, key = nil)
+ raise NoPrivateKeyError unless private?
+ private_rsa(key).private_decrypt(data)
+ end
+
+ def decrypt_from_base64(data, key = nil)
+ decrypt(Base64.decode64(data), key)
+ end
+
+ def private_key_file=(file)
+ @private_key_file = file and load_private_key
+ end
+
+ def public_key_file=(file)
+ @public_key_file = file and load_public_key
+ end
+
+ def public?
+ return true unless @public_key.nil?
+ load_public_key and return @public_key
+ end
+
+ def private?
+ return true unless @private_key.nil?
+ load_private_key and return @private_key
+ end
+
+ class << self
+ # * <tt>:key</tt> - secret password
+ # * <tt>:symmetric_algorithm</tt> - symmetrical algorithm to use
+ def save_random_rsa_key(private_key_file, public_key_file, options = {})
+ rsa = OpenSSL::PKey::RSA.new(512)
+ public_key = rsa.public_key
+ private_key = options[:key].to_s.empty? ?
+ rsa.to_s :
+ SymmetricSentry.new(:algorithm => options[:symmetric_algorithm]).encrypt_to_base64(rsa.to_s, options[:key])
+ File.open(public_key_file, 'w') { |f| f.write(public_key) }
+ File.open(private_key_file, 'w') { |f| f.write(private_key) }
+ end
+
+ def encrypt(data)
+ self.new.encrypt(data)
+ end
+
+ def encrypt_to_base64(data)
+ self.new.encrypt_to_base64(data)
+ end
+
+ def decrypt(data, key = nil)
+ self.new.decrypt(data, key)
+ end
+
+ def decrypt_from_base64(data, key = nil)
+ self.new.decrypt_from_base64(data, key)
+ end
+
+ # cattr_accessor would be lovely
+ def default_private_key_file
+ @@default_private_key_file
+ end
+
+ def default_private_key_file=(value)
+ @@default_private_key_file = value
+ end
+
+ def default_public_key_file
+ @@default_public_key_file
+ end
+
+ def default_public_key_file=(value)
+ @@default_public_key_file = value
+ end
+
+ def default_symmetric_algorithm
+ @@default_symmetric_algorithm
+ end
+
+ def default_symmetric_algorithm=(value)
+ @@default_symmetric_algorithm = value
+ end
+ end
+
+ private
+ def encryptor
+ @encryptor ||= SymmetricSentry.new(:algorithm => @symmetric_algorithm)
+ end
+
+ def load_private_key
+ @private_rsa = nil
+ @private_key_file ||= @@default_private_key_file
+ if @private_key_file and File.file?(@private_key_file)
+ @private_key = File.open(@private_key_file) { |f| f.read }
+ end
+ end
+
+ def load_public_key
+ @public_rsa = nil
+ @public_key_file ||= @@default_public_key_file
+ if @public_key_file and File.file?(@public_key_file)
+ @public_key = File.open(@public_key_file) { |f| f.read }
+ end
+ end
+
+ # retrieves private rsa from encrypted private key
+ def private_rsa(key = nil)
+ return @private_rsa ||= OpenSSL::PKey::RSA.new(@private_key) unless key
+ OpenSSL::PKey::RSA.new(encryptor.decrypt_from_base64(@private_key, key))
+ end
+
+ # retrieves public rsa
+ def public_rsa
+ @public_rsa ||= OpenSSL::PKey::RSA.new(@public_key)
+ end
+ end
+end
17 lib/sentry/asymmetric_sentry_callback.rb
@@ -0,0 +1,17 @@
+module Sentry
+ class AsymmetricSentryCallback
+ def initialize(attr_name)
+ @attr_name = attr_name
+ end
+
+ # Performs encryption on before_validation Active Record callback
+ def before_validation(model)
+ return if model.send(@attr_name).blank?
+ model.send("crypted_#{@attr_name}=", AsymmetricSentry.encrypt_to_base64(model.send(@attr_name)))
+ end
+
+ def after_save(model)
+ model.send("#{@attr_name}=", nil)
+ end
+ end
+end
41 lib/sentry/sha_sentry.rb
@@ -0,0 +1,41 @@
+require 'digest/sha1'
+module Sentry
+ class ShaSentry
+ @@salt = 'salt'
+ attr_accessor :salt
+
+ # Encrypts data using SHA.
+ def encrypt(data)
+ self.class.encrypt(data + salt.to_s)
+ end
+
+ # Initialize the class.
+ # Used by ActiveRecord::Base#generates_crypted to set up as a callback object for a model
+ def initialize(attribute = nil)
+ @attribute = attribute
+ end
+
+ # Performs encryption on before_validation Active Record callback
+ def before_validation(model)
+ return unless model.send(@attribute)
+ model.send("crypted_#{@attribute}=", encrypt(model.send(@attribute)))
+ end
+
+ class << self
+ # Gets the class salt value used when encrypting
+ def salt
+ @@salt
+ end
+
+ # Sets the class salt value used when encrypting
+ def salt=(value)
+ @@salt = value
+ end
+
+ # Encrypts the data
+ def encrypt(data)
+ Digest::SHA1.hexdigest(data + @@salt)
+ end
+ end
+ end
+end
79 lib/sentry/symmetric_sentry.rb
@@ -0,0 +1,79 @@
+module Sentry
+ class SymmetricSentry
+ @@default_algorithm = 'DES-EDE3-CBC'
+ @@default_key = nil
+ attr_accessor :algorithm
+ def initialize(options = {})
+ @algorithm = options[:algorithm] || @@default_algorithm
+ end
+
+ def encrypt(data, key = nil)
+ key = check_for_key!(key)
+ des = encryptor
+ des.encrypt(key)
+ data = des.update(data)
+ data << des.final
+ end
+
+ def encrypt_to_base64(text, key = nil)
+ Base64.encode64(encrypt(text, key))
+ end
+
+ def decrypt(data, key = nil)
+ key = check_for_key!(key)
+ des = encryptor
+ des.decrypt(key)
+ text = des.update(data)
+ text << des.final
+ end
+
+ def decrypt_from_base64(text, key = nil)
+ decrypt(Base64.decode64(text), key)
+ end
+
+ class << self
+ def default_algorithm
+ @@default_algorithm
+ end
+
+ def default_algorithm=(value)
+ @@default_algorithm = value
+ end
+
+ def default_key
+ @@default_key
+ end
+
+ def default_key=(value)
+ @@default_key = value
+ end
+
+ def encrypt(data, key = nil)
+ self.new.encrypt(data, key)
+ end
+
+ def encrypt_to_base64(text, key = nil)
+ self.new.encrypt_to_base64(text, key)
+ end
+
+ def decrypt(data, key = nil)
+ self.new.decrypt(data, key)
+ end
+
+ def decrypt_from_base64(text, key = nil)
+ self.new.decrypt_from_base64(text, key)
+ end
+ end
+
+ private
+ def encryptor
+ @encryptor ||= OpenSSL::Cipher::Cipher.new(@algorithm)
+ end
+
+ def check_for_key!(key)
+ valid_key = key || @@default_key
+ raise Sentry::NoKeyError if valid_key.nil?
+ valid_key
+ end
+ end
+end
17 lib/sentry/symmetric_sentry_callback.rb
@@ -0,0 +1,17 @@
+module Sentry
+ class SymmetricSentryCallback
+ def initialize(attr_name)
+ @attr_name = attr_name
+ end
+
+ # Performs encryption on before_validation Active Record callback
+ def before_validation(model)
+ return if model.send(@attr_name).blank?
+ model.send("crypted_#{@attr_name}=", SymmetricSentry.encrypt_to_base64(model.send(@attr_name)))
+ end
+
+ def after_save(model)
+ model.send("#{@attr_name}=", nil)
+ end
+ end
+end
9 tasks/sentry.rake
@@ -0,0 +1,9 @@
+require 'sentry'
+
+desc "Creates a private/public key for asymmetric encryption: rake sentry_key PUB=/path/to/public.key PRIV=/path/to/priv.key [KEY=secret]"
+task :sentry_key do
+ Sentry::AsymmetricSentry.save_random_rsa_key(
+ ENV['PRIV'] || 'private.key',
+ ENV['PUB'] || 'public.key',
+ :key => ENV['KEY'])
+end
33 test/abstract_unit.rb
@@ -0,0 +1,33 @@
+$:.unshift(File.dirname(__FILE__) + '/../lib')
+
+require 'rubygems'
+require 'test/unit'
+require 'active_record'
+require 'active_record/fixtures'
+require 'active_support/binding_of_caller'
+require 'active_support/breakpoint'
+require "#{File.dirname(__FILE__)}/../lib/sentry"
+
+config_location = File.dirname(__FILE__) + '/database.yml'
+
+config = YAML::load(IO.read(config_location))
+ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + "/debug.log")
+ActiveRecord::Base.establish_connection(config[ENV['DB'] || 'sqlite'])
+
+load(File.dirname(__FILE__) + "/schema.rb")
+
+Test::Unit::TestCase.fixture_path = File.dirname(__FILE__) + "/fixtures/"
+Test::Unit::TestCase.use_instantiated_fixtures = false
+Test::Unit::TestCase.use_transactional_fixtures = (ENV['AR_TX_FIXTURES'] == "yes")
+$LOAD_PATH.unshift(Test::Unit::TestCase.fixture_path)
+
+class Test::Unit::TestCase #:nodoc:
+ def create_fixtures(*table_names)
+ if block_given?
+ Fixtures.create_fixtures(Test::Unit::TestCase.fixture_path, table_names) { yield }
+ else
+ Fixtures.create_fixtures(Test::Unit::TestCase.fixture_path, table_names)
+ end
+ end
+end
+
61 test/asymmetric_sentry_callback_test.rb
@@ -0,0 +1,61 @@
+require 'abstract_unit'
+require 'fixtures/user'
+
+class AsymmetricSentryCallbackTest < Test::Unit::TestCase
+ fixtures :users
+
+ def setup
+ @str = 'sentry'
+ @key = 'secret'
+ @public_key_file = File.dirname(__FILE__) + '/keys/public'
+ @private_key_file = File.dirname(__FILE__) + '/keys/private'
+ @encrypted_public_key_file = File.dirname(__FILE__) + '/keys/encrypted_public'
+ @encrypted_private_key_file = File.dirname(__FILE__) + '/keys/encrypted_private'
+
+ @orig = 'sentry'
+ Sentry::AsymmetricSentry.default_public_key_file = @public_key_file
+ Sentry::AsymmetricSentry.default_private_key_file = @private_key_file
+ end
+
+ def test_should_encrypt_creditcard
+ u = User.create :login => 'jones'
+ u.creditcard = @orig
+ assert u.save
+ assert !u.crypted_creditcard.empty?
+ end
+
+ def test_should_decrypt_creditcard
+ assert_equal @orig, users(:user_1).creditcard
+ end
+
+ def test_should_not_decrypt_encrypted_creditcard_with_invalid_key
+ assert_nil users(:user_2).creditcard
+ assert_nil users(:user_2).creditcard(@key)
+ use_encrypted_keys
+ assert_nil users(:user_1).creditcard
+ end
+
+ def test_should_not_decrypt_encrypted_creditcard
+ use_encrypted_keys
+ assert_nil users(:user_2).creditcard
+ assert_nil users(:user_2).creditcard('other secret')
+ end
+
+ def test_should_encrypt_encrypted_creditcard
+ use_encrypted_keys
+ u = User.create :login => 'jones'
+ u.creditcard = @orig
+ assert u.save
+ assert !u.crypted_creditcard.empty?
+ end
+
+ def test_should_decrypt_encrypted_creditcard
+ use_encrypted_keys
+ assert_equal @orig, users(:user_2).creditcard(@key)
+ end
+
+ def use_encrypted_keys
+ Sentry::AsymmetricSentry.default_public_key_file = @encrypted_public_key_file
+ Sentry::AsymmetricSentry.default_private_key_file = @encrypted_private_key_file
+ end
+end
88 test/asymmetric_sentry_test.rb
@@ -0,0 +1,88 @@
+require 'abstract_unit'
+
+class AsymmetricSentryTest < Test::Unit::TestCase
+ def setup
+ @str = 'sentry'
+ @key = 'secret'
+ @public_key_file = File.dirname(__FILE__) + '/keys/public'
+ @private_key_file = File.dirname(__FILE__) + '/keys/private'
+ @encrypted_public_key_file = File.dirname(__FILE__) + '/keys/encrypted_public'
+ @encrypted_private_key_file = File.dirname(__FILE__) + '/keys/encrypted_private'
+ @sentry = Sentry::AsymmetricSentry.new
+
+ @orig = 'sentry'
+ @data = "vYfMxtVB8ezXmQKSNqTC9sPgi8TbsYRxWd7DVbpprzyuEdZ7gftJ/0IXsbXm\nXCU08bTAl0uEFm7dau+eJMXEJg==\n"
+ @encrypted_data = "q2obYAITmK93ylzVS01mJx1jSlnmylMX15nFpb4uKesVgnqvtzBRHZ/SK+Nm\nEzceIoAcJc3DHosVa4VUE/aK/A==\n"
+ Sentry::AsymmetricSentry.default_public_key_file = nil
+ Sentry::AsymmetricSentry.default_private_key_file = nil
+ end
+
+ def test_should_decrypt_files
+ set_key_files @public_key_file, @private_key_file
+ assert_equal @orig, @sentry.decrypt_from_base64(@data)
+ end
+
+ def test_should_decrypt_files_with_encrypted_key
+ set_key_files @encrypted_public_key_file, @encrypted_private_key_file
+ assert_equal @orig, @sentry.decrypt_from_base64(@encrypted_data, @key)
+ end
+
+ def test_should_read_key_files
+ assert !@sentry.public?
+ assert !@sentry.private?
+ set_key_files @public_key_file, @private_key_file
+ end
+
+ def test_should_read_encrypted_key_files
+ assert !@sentry.public?
+ assert !@sentry.private?
+ set_key_files @encrypted_public_key_file, @encrypted_private_key_file
+ end
+
+ def test_should_decrypt_files_with_default_key
+ set_default_key_files @public_key_file, @private_key_file
+ assert_equal @orig, @sentry.decrypt_from_base64(@data)
+ end
+
+ def test_should_decrypt_files_with_default_encrypted_key
+ set_default_key_files @encrypted_public_key_file, @encrypted_private_key_file
+ assert_equal @orig, @sentry.decrypt_from_base64(@encrypted_data, @key)
+ end
+
+ def test_should_decrypt_files_with_default_key_using_class_method
+ set_default_key_files @public_key_file, @private_key_file
+ assert_equal @orig, Sentry::AsymmetricSentry.decrypt_from_base64(@data)
+ end
+
+ def test_should_decrypt_files_with_default_encrypted_key_using_class_method
+ set_default_key_files @encrypted_public_key_file, @encrypted_private_key_file
+ assert_equal @orig, Sentry::AsymmetricSentry.decrypt_from_base64(@encrypted_data, @key)
+ end
+
+ def test_should_read_key_files_with_default_key
+ assert !@sentry.public?
+ assert !@sentry.private?
+ set_default_key_files @public_key_file, @private_key_file
+ end
+
+ def test_should_read_encrypted_key_files_with_default_key
+ assert !@sentry.public?
+ assert !@sentry.private?
+ set_default_key_files @encrypted_public_key_file, @encrypted_private_key_file
+ end
+
+ private
+ def set_key_files(public_key, private_key)
+ @sentry.public_key_file = public_key
+ @sentry.private_key_file = private_key
+ assert @sentry.private?
+ assert @sentry.public?
+ end
+
+ def set_default_key_files(public_key, private_key)
+ Sentry::AsymmetricSentry.default_public_key_file = public_key
+ Sentry::AsymmetricSentry.default_private_key_file = private_key
+ assert @sentry.private?
+ assert @sentry.public?
+ end
+end
18 test/database.yml
@@ -0,0 +1,18 @@
+sqlite:
+ :adapter: sqlite
+ :dbfile: sentry_plugin.sqlite.db
+sqlite3:
+ :adapter: sqlite3
+ :dbfile: sentry_plugin.sqlite3.db
+postgresql:
+ :adapter: postgresql
+ :username: postgres
+ :password: postgres
+ :database: sentry_plugin_test
+ :min_messages: ERROR
+mysql:
+ :adapter: mysql
+ :host: localhost
+ :username: rails
+ :password:
+ :database: sentry_plugin_test
25 test/fixtures/user.rb
@@ -0,0 +1,25 @@
+class User < ActiveRecord::Base
+ generates_crypted :creditcard, :mode => :asymmetric
+
+ def self.validates_password
+ validates_presence_of :crypted_password
+ validates_presence_of :password, :on => :create
+ validates_length_of :password, :in => 4..40
+ end
+end
+
+class ShaUser < User
+ validates_password
+ validates_confirmation_of :password
+ generates_crypted :password # sha is used by default
+end
+
+class DangerousUser < User # no password confirmation
+# validates_password
+ generates_crypted :password
+end
+
+class SymmetricUser < User
+ validates_password
+ generates_crypted :password, :mode => :symmetric
+end
11 test/fixtures/users.yml
@@ -0,0 +1,11 @@
+user_1:
+ id: 1
+ login: bob
+ crypted_password: "0XlmUuNpE2k=\n"
+ crypted_creditcard: "vYfMxtVB8ezXmQKSNqTC9sPgi8TbsYRxWd7DVbpprzyuEdZ7gftJ/0IXsbXm\nXCU08bTAl0uEFm7dau+eJMXEJg==\n"
+ type: SymmetricUser
+user_2:
+ id: 2
+ login: fred
+ crypted_creditcard: "q2obYAITmK93ylzVS01mJx1jSlnmylMX15nFpb4uKesVgnqvtzBRHZ/SK+Nm\nEzceIoAcJc3DHosVa4VUE/aK/A==\n"
+
12 test/keys/encrypted_private
@@ -0,0 +1,12 @@
+OBNa1q8kbx8pyZZjIpr/pZV0oulE2czh5JlPW/13XsBvoz+A2zxA9gchhi6c
+3yvfqgcZdojcsep+IiTqeg3gOPB2xNbedpP1lm+9tEfgdb9r1CLzRcURh7Hg
+ufWgyEkS0lloz/YLy4hg9YDKetFNF9fnrk3xVwZPwFVuk4l/Unw1FTXLHsrq
+KG27cR8mvNOow4bk4LVhk/avFSM85m3ITySEnyJsQQDzsI/RrWcQ7Js+8Ynv
+esN51E/T0CYtkMEne2zSaD5qUTJlQ7Qtn4UUeZkpYjn4xQZPxw4OjL6zofg7
+lsqElSv1/qP3QI8aKcQQklVsHRc5AgsxOFX4J6g6lo4kOGOwn0Ex8IRDfOej
+pq4SUDh9IXz+6FBieQrObB/xEsKysVwRSzXre6ObHlPFsigg5ekFPyCv5ZTz
+0iP8+xe/FJRrYdR3r3F5pRkOy0pw9EqlrLjmOx3/fgxhLq8FWmcSBbH3h3SG
+GkJlfHNjF77FTJjnHKzRS+5VpdW4IHbsjL+NlI1z9Ol//czYvSGv85NdJvkq
+PmH3o0+uYdwY5PeSMOPV21nJ3dwiKlm5IMFasL3C5yVJNVTVZTS7vWdcgZ4U
+XfWQ9Y266ibbqXPluv4nxt1+kgjxmPbjPdYrlB5t7a2+unzT3oE3f4VGOG+k
+YqFg0ErHN+fu
4 test/keys/encrypted_public
@@ -0,0 +1,4 @@
+-----BEGIN RSA PUBLIC KEY-----
+MEgCQQCvktJgveIcgTH98hAhMjo0g6/GVMJaYdUh+/zQn4RBWASRmwEfJqggsfKT
+pSNendZQMD8kKS8J1YTBr60ToM25AgMBAAE=
+-----END RSA PUBLIC KEY-----
9 test/keys/private
@@ -0,0 +1,9 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIBOwIBAAJBAL/xeY6aqFx6z1ThNOwgPgxv3tsonTlCj8VkN3Ikumg6SzBuLxlV
+i9gFQZ7K9Pv9o/7+xUTYODqBpVhwgLBeu2cCAwEAAQJAHyjFMfg7Yp/xLndMzxRA
+3mX+yJckRtpeWo31TktWE3syks1r9OrfmxKiStM9kFRubeBHTihZrW92TYkROLxh
+uQIhAPuftVTJZFDNxeYDKIMIMqwR8KZgtuf25cv4pTxYwPqLAiEAw0gNwDJHBkvo
+da4402pZNQmBA6qCSf0svDXqoEoaShUCIGBma340Oe6LJ0pb42Vv+pnZtazIWMq9
+2IQwmn1oM2bJAiEAhgP869mVRIzzi091UCG79tn+4DU0FPLasI+P5VD1mcECIQDb
+3ndvbPcElVvdJgabxyWJJsNtBBNZYPsuc6NrQyShOw==
+-----END RSA PRIVATE KEY-----
4 test/keys/public
@@ -0,0 +1,4 @@
+-----BEGIN RSA PUBLIC KEY-----
+MEgCQQC/8XmOmqhces9U4TTsID4Mb97bKJ05Qo/FZDdyJLpoOkswbi8ZVYvYBUGe
+yvT7/aP+/sVE2Dg6gaVYcICwXrtnAgMBAAE=
+-----END RSA PUBLIC KEY-----
10 test/schema.rb
@@ -0,0 +1,10 @@
+ActiveRecord::Schema.define(:version => 1) do
+
+ create_table "users", :force => true do |t|
+ t.column :crypted_password, :string, :limit => 255
+ t.column :crypted_creditcard, :string, :limit => 255
+ t.column :login, :string, :limit => 50
+ t.column :type, :string, :limit => 20
+ end
+
+end
31 test/sha_sentry_test.rb
@@ -0,0 +1,31 @@
+require 'abstract_unit'
+require 'fixtures/user'
+
+class ShaSentryTest < Test::Unit::TestCase
+ def setup
+ Sentry::ShaSentry.salt = 'salt'
+ end
+
+ def test_should_encrypt
+ assert_equal 'f438229716cab43569496f3a3630b3727524b81b', Sentry::ShaSentry.encrypt('test')
+ end
+
+ def test_should_encrypt_with_salt
+ Sentry::ShaSentry.salt = 'different salt'
+ assert_equal '18e3256d71529db8fa65b2eef24a69ddad7070f3', Sentry::ShaSentry.encrypt('test')
+ end
+
+ def test_should_encrypt_user_password
+ u = ShaUser.new :login => 'bob'
+ u.password = u.password_confirmation = 'test'
+ assert u.save
+ assert u.crypted_password = 'f438229716cab43569496f3a3630b3727524b81b'
+ end
+
+ def test_should_encrypt_user_password_without_confirmation
+ u = DangerousUser.new :login => 'bob'
+ u.password = 'test'
+ assert u.save
+ assert u.crypted_password = 'f438229716cab43569496f3a3630b3727524b81b'
+ end
+end
33 test/symmetric_sentry_callback_test.rb
@@ -0,0 +1,33 @@
+require 'abstract_unit'
+require 'fixtures/user'
+
+class SymmetricSentryCallbackTest < Test::Unit::TestCase
+ fixtures :users
+
+ def setup
+ @str = 'sentry'
+ Sentry::SymmetricSentry.default_key = @key = 'secret'
+ @encrypted = "0XlmUuNpE2k=\n"
+ end
+
+ def test_should_encrypt_user_password
+ u = SymmetricUser.new :login => 'bob'
+ u.password = @str
+ assert u.save
+ assert_equal @encrypted, u.crypted_password
+ end
+
+ def test_should_decrypted_user_password
+ assert_equal @str, users(:user_1).password
+ end
+
+ def test_should_return_nil_on_invalid_key
+ Sentry::SymmetricSentry.default_key = 'other secret'
+ assert_nil users(:user_1).password
+ end
+
+ def test_should_raise_error_on_invalid_key
+ Sentry::SymmetricSentry.default_key = 'other secret'
+ assert_raises(OpenSSL::CipherError) { users(:user_1).password! }
+ end
+end
37 test/symmetric_sentry_test.rb
@@ -0,0 +1,37 @@
+require 'abstract_unit'
+
+class SymmetricSentryTest < Test::Unit::TestCase
+ def setup
+ @str = 'sentry'
+ @key = 'secret'
+ @encrypted = "0XlmUuNpE2k=\n"
+ @sentry = Sentry::SymmetricSentry.new
+ Sentry::SymmetricSentry.default_key = nil
+ end
+
+ def test_should_encrypt
+ assert_equal @encrypted, @sentry.encrypt_to_base64(@str, @key)
+ end
+
+ def test_should_decrypt
+ assert_equal @str, @sentry.decrypt_from_base64(@encrypted, @key)
+ end
+
+ def test_should_encrypt_with_default_key
+ Sentry::SymmetricSentry.default_key = @key
+ assert_equal @encrypted, @sentry.encrypt_to_base64(@str)
+ end
+
+ def test_should_decrypt_with_default_key
+ Sentry::SymmetricSentry.default_key = @key
+ assert_equal @str, @sentry.decrypt_from_base64(@encrypted)
+ end
+
+ def test_should_raise_error_when_encrypt_with_no_key
+ assert_raises(Sentry::NoKeyError) { @sentry.encrypt_to_base64(@str) }
+ end
+
+ def test_should_raise_error_when_decrypt_with_no_key
+ assert_raises(Sentry::NoKeyError) { @sentry.decrypt_from_base64(@str) }
+ end
+end
2 test/tests.rb
@@ -0,0 +1,2 @@
+$:.unshift "../lib"
+Dir["**/*_test.rb"].each { |f| load f }

0 comments on commit 5ba4d7b

Please sign in to comment.
Something went wrong with that request. Please try again.