Permalink
Browse files

Initial commit

  • Loading branch information...
0 parents commit 9055be700f2579f74bc8d360dc75f414748b080d @shuber shuber committed Jan 8, 2009
Showing with 358 additions and 0 deletions.
  1. +2 −0 CHANGELOG
  2. +20 −0 MIT-LICENSE
  3. +2 −0 README.markdown
  4. +22 −0 Rakefile
  5. +10 −0 lib/attr_encrypted.rb
  6. +28 −0 lib/huberry/active_record.rb
  7. +77 −0 lib/huberry/class.rb
  8. +31 −0 lib/huberry/object.rb
  9. +11 −0 test/active_record_test.rb
  10. +146 −0 test/attr_encrypted_test.rb
  11. +9 −0 test/test_helper.rb
@@ -0,0 +1,2 @@
+2009-01-07 - Sean Huber (shuber@huberry.com)
+ * Initial commit
@@ -0,0 +1,20 @@
+Copyright (c) 2008 Sean Huber - shuber@huberry.com
+
+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.
@@ -0,0 +1,2 @@
+attr_encrypted
+==============
@@ -0,0 +1,22 @@
+require 'rake'
+require 'rake/testtask'
+require 'rake/rdoctask'
+
+desc 'Default: run unit tests.'
+task :default => :test
+
+desc 'Test the attr_encrypted gem.'
+Rake::TestTask.new(:test) do |t|
+ t.libs << 'lib'
+ t.pattern = 'test/**/*_test.rb'
+ t.verbose = true
+end
+
+desc 'Generate documentation for the attr_encrypted gem.'
+Rake::RDocTask.new(:rdoc) do |rdoc|
+ rdoc.rdoc_dir = 'rdoc'
+ rdoc.title = 'attr_encrypted'
+ rdoc.options << '--line-numbers' << '--inline-source'
+ rdoc.rdoc_files.include('README.markdown')
+ rdoc.rdoc_files.include('lib/**/*.rb')
+end
@@ -0,0 +1,10 @@
+require 'huberry/class'
+Class.send :include, Huberry::Class
+
+require 'huberry/object'
+Object.send :include, Huberry::Object
+
+if defined?(ActiveRecord)
+ require 'huberry/active_record'
+ ActiveRecord::Base.extend Huberry::ActiveRecord
+end
@@ -0,0 +1,28 @@
+module Huberry
+ module ActiveRecord
+ def self.extended(base)
+ base.alias_method_chain :method_missing, :attr_encrypted
+ end
+
+ protected
+
+ def attr_encrypted(*attrs)
+ options = { :encode => true, :marshal => true }.merge(attrs.last.is_a?(Hash) ? attrs.pop : {})
+ super *(attrs << options)
+ end
+
+ def method_missing_with_attr_encrypted(method, *args, &block)
+ if match = /^(find|scoped)_(all_by|by)_([_a-zA-Z]\w*)$/.match(method.to_s)
+ attribute_names = extract_attribute_names_from_match(match)
+ attribute_names.each_with_index do |attribute, index|
+ if attr_encrypted?(attribute)
+ args[index] = send("encrypt_#{attribute}", args[index])
+ attribute_names[index] = encrypted_attributes[attribute]
+ end
+ end
+ method = "#{$1}_#{$2}_#{attribute_names.join('_and_')}".to_sym
+ end
+ method_missing_without_attr_encrypted(method, *args, &block)
+ end
+ end
+end
@@ -0,0 +1,77 @@
+module Huberry
+ module Class
+ protected
+ def attr_encrypted(*attrs)
+ options = {
+ :prefix => 'encrypted_',
+ :suffix => '',
+ :encryptor => Huberry::Encryptor,
+ :encrypt_method => :encrypt,
+ :decrypt_method => :decrypt,
+ :encode => false,
+ :marshal => false
+ }.merge(attr_encrypted_options).merge(attrs.last.is_a?(Hash) ? attrs.pop : {})
+
+ attrs.each do |attribute|
+ encrypted_attribute_name = options[:attribute].nil? ? options[:prefix].to_s + attribute.to_s + options[:suffix].to_s : options[:attribute].to_s
+
+ encrypted_attributes[attribute.to_s] = encrypted_attribute_name
+
+ attr_accessor encrypted_attribute_name.to_sym unless instance_methods.include?(encrypted_attribute_name)
+
+ define_class_method "encrypt_#{attribute}" do |value|
+ if value.nil?
+ encrypted_value = nil
+ else
+ value = Marshal.dump(value) if options[:marshal]
+ encrypted_value = options[:encryptor].send options[:encrypt_method], options.merge(:value => value)
+ encrypted_value = [encrypted_value].pack('m*') if options[:encode]
+ end
+ encrypted_value
+ end
+
+ define_class_method "decrypt_#{attribute}" do |encrypted_value|
+ if encrypted_value.nil?
+ decrypted_value = nil
+ else
+ encrypted_value = encrypted_value.unpack('m*').to_s if options[:encode]
+ decrypted_value = options[:encryptor].send(options[:decrypt_method], options.merge(:value => encrypted_value))
+ decrypted_value = Marshal.load(decrypted_value) if options[:marshal]
+ end
+ decrypted_value
+ end
+
+ define_method "#{attribute}" do
+ value = read_attribute(attribute)
+ encrypted_value = read_attribute(encrypted_attribute_name)
+ original_key = options[:key]
+ options[:key] = self.class.send :evaluate_attr_encrypted_key, options[:key], self
+ value = write_attribute(attribute, self.class.send("decrypt_#{attribute}".to_sym, encrypted_value)) if value.nil? && !encrypted_value.nil?
+ options[:key] = original_key
+ value
+ end
+
+ define_method "#{attribute}=" do |value|
+ original_key = options[:key]
+ options[:key] = self.class.send :evaluate_attr_encrypted_key, options[:key], self
+ write_attribute(encrypted_attribute_name, self.class.send("encrypt_#{attribute}".to_sym, value))
+ options[:key] = original_key
+ write_attribute(attribute, nil)
+ end
+ end
+ end
+
+ # Evaluates encryption keys specified as symbols (representing instance methods) or procs
+ # If the key is not a symbol or proc then the original key is returned
+ def evaluate_attr_encrypted_key(key, object)
+ case key
+ when Symbol
+ object.send(key)
+ when Proc
+ key.call(object)
+ else
+ key
+ end
+ end
+ end
+end
@@ -0,0 +1,31 @@
+module Huberry
+ module Object
+ def self.included(base)
+ base.class_eval do
+ extend ClassMethods
+ cattr_accessor :attr_encrypted_options, :encrypted_attributes
+ self.attr_encrypted_options = {}
+ self.encrypted_attributes = {}
+ end
+ end
+
+ def read_attribute(attribute)
+ instance_variable_get("@#{attribute}")
+ end
+
+ def write_attribute(attribute, value)
+ instance_variable_set("@#{attribute}", value)
+ end
+
+ module ClassMethods
+ def attr_encrypted?(attribute)
+ encrypted_attributes.keys.include?(attribute.to_s)
+ end
+
+ def inherited(base)
+ base.attr_encrypted_options = self.attr_encrypted_options.nil? ? {} : self.attr_encrypted_options.dup
+ base.encrypted_attributes = self.encrypted_attributes.nil? ? {} : self.encrypted_attributes.dup
+ end
+ end
+ end
+end
@@ -0,0 +1,11 @@
+require File.dirname(__FILE__) + '/test_helper'
+gem 'activerecord'
+require 'active_record'
+
+class AttrEncryptedTest < Test::Unit::TestCase
+
+ def test_this_gem
+ flunk
+ end
+
+end
@@ -0,0 +1,146 @@
+require File.dirname(__FILE__) + '/test_helper'
+
+class SillyEncryptor
+ def self.silly_encrypt(options)
+ (options[:value] + options[:some_arg]).reverse
+ end
+
+ def self.silly_decrypt(options)
+ options[:value].reverse.gsub(/#{options[:some_arg]}$/, '')
+ end
+end
+
+class User
+ self.attr_encrypted_options[:key] = Proc.new { |user| user.class.to_s } # default key
+
+ attr_encrypted :email, :without_encoding, :key => 'secret key'
+ attr_encrypted :password, :prefix => 'crypted_', :suffix => '_test'
+ attr_encrypted :ssn, :key => :salt, :attribute => 'ssn_encrypted'
+ attr_encrypted :credit_card, :encryptor => SillyEncryptor, :encrypt_method => :silly_encrypt, :decrypt_method => :silly_decrypt, :some_arg => 'test'
+ attr_encrypted :with_encoding, :key => 'secret key', :encode => true
+ attr_encrypted :with_marshaling, :key => 'secret key', :marshal => true
+ attr_accessor :salt
+
+ def initialize
+ self.salt = Time.now.to_i.to_s
+ end
+end
+
+class AttrEncryptedTest < Test::Unit::TestCase
+
+ def test_should_store_email_in_encrypted_attributes
+ assert User.encrypted_attributes.include?('email')
+ end
+
+ def test_should_not_store_salt_in_encrypted_attributes
+ assert !User.encrypted_attributes.include?('salt')
+ end
+
+ def test_attr_encrypted_should_return_true_for_email
+ assert User.attr_encrypted?('email')
+ end
+
+ def test_attr_encrypted_should_return_false_for_salt
+ assert !User.attr_encrypted?('salt')
+ end
+
+ def test_should_generate_an_encrypted_attribute
+ assert User.new.respond_to?(:encrypted_email)
+ end
+
+ def test_should_generate_an_encrypted_attribute_with_a_prefix_and_suffix
+ assert User.new.respond_to?(:crypted_password_test)
+ end
+
+ def test_should_generate_an_encrypted_attribute_with_the_attribute_option
+ assert User.new.respond_to?(:ssn_encrypted)
+ end
+
+ def test_should_not_encrypt_nil_value
+ assert_nil User.encrypt_email(nil)
+ end
+
+ def test_should_encrypt_email
+ assert_not_nil User.encrypt_email('test@example.com')
+ assert_not_equal 'test@example.com', User.encrypt_email('test@example.com')
+ end
+
+ def test_should_encrypt_email_when_modifying_the_attr_writer
+ @user = User.new
+ assert_nil @user.encrypted_email
+ @user.email = 'test@example.com'
+ assert_not_nil @user.encrypted_email
+ assert_equal User.encrypt_email('test@example.com'), @user.encrypted_email
+ end
+
+ def test_should_not_decrypt_nil_value
+ assert_nil User.decrypt_email(nil)
+ end
+
+ def test_should_decrypt_email
+ encrypted_email = User.encrypt_email('test@example.com')
+ assert_not_equal 'test@test.com', encrypted_email
+ assert_equal 'test@example.com', User.decrypt_email(encrypted_email)
+ end
+
+ def test_should_decrypt_email_when_reading
+ @user = User.new
+ assert_nil @user.email
+ @user.encrypted_email = User.encrypt_email('test@example.com')
+ assert_equal 'test@example.com', @user.email
+ end
+
+ def test_should_encrypt_with_encoding
+ assert_equal User.encrypt_with_encoding('test'), [User.encrypt_without_encoding('test')].pack('m*')
+ end
+
+ def test_should_decrypt_with_encoding
+ encrypted = User.encrypt_with_encoding('test')
+ assert_equal 'test', User.decrypt_with_encoding(encrypted)
+ assert_equal User.decrypt_with_encoding(encrypted), User.decrypt_without_encoding(encrypted.unpack('m*').to_s)
+ end
+
+ def test_should_encrypt_with_marshaling
+ @user = User.new
+ @user.with_marshaling = [1, 2, 3]
+ assert_not_nil @user.encrypted_with_marshaling
+ assert_equal User.encrypt_with_marshaling([1, 2, 3]), @user.encrypted_with_marshaling
+ end
+
+ def test_should_decrypt_with_marshaling
+ encrypted = User.encrypt_with_marshaling([1, 2, 3])
+ @user = User.new
+ assert_nil @user.with_marshaling
+ @user.encrypted_with_marshaling = encrypted
+ assert_equal [1, 2, 3], @user.with_marshaling
+ end
+
+ def test_should_use_custom_encryptor_and_crypt_method_names_and_arguments
+ assert_equal SillyEncryptor.silly_encrypt(:value => 'testing', :some_arg => 'test'), User.encrypt_credit_card('testing')
+ end
+
+ def test_should_evaluate_a_key_passed_as_a_symbol
+ @user = User.new
+ assert_nil @user.ssn_encrypted
+ @user.ssn = 'testing'
+ assert_not_nil @user.ssn_encrypted
+ assert_equal Huberry::Encryptor.encrypt(:value => 'testing', :key => @user.salt), @user.ssn_encrypted
+ end
+
+ def test_should_evaluate_a_key_passed_as_a_proc
+ @user = User.new
+ assert_nil @user.crypted_password_test
+ @user.password = 'testing'
+ assert_not_nil @user.crypted_password_test
+ assert_equal Huberry::Encryptor.encrypt(:value => 'testing', :key => 'User'), @user.crypted_password_test
+ end
+
+ def test_should_use_options_found_in_the_attr_encrypted_options_attribute
+ @user = User.new
+ assert_nil @user.crypted_password_test
+ @user.password = 'testing'
+ assert_not_nil @user.crypted_password_test
+ assert_equal Huberry::Encryptor.encrypt(:value => 'testing', :key => 'User'), @user.crypted_password_test
+ end
+
+end
@@ -0,0 +1,9 @@
+require 'test/unit'
+
+require 'rubygems'
+gem 'shuber-eigenclass'
+gem 'shuber-encryptor'
+require 'eigenclass'
+require 'encryptor'
+
+require File.dirname(__FILE__) + '/../lib/attr_encrypted'

0 comments on commit 9055be7

Please sign in to comment.