diff --git a/Rakefile b/Rakefile index d2d7c1b..b20af24 100644 --- a/Rakefile +++ b/Rakefile @@ -28,6 +28,7 @@ task :default => :test desc "Test the #{spec.name} plugin." Rake::TestTask.new(:test) do |t| t.libs << 'lib' + t.libs << '.' t.test_files = spec.test_files t.verbose = true end diff --git a/lib/encrypted_strings/asymmetric_cipher.rb b/lib/encrypted_strings/asymmetric_cipher.rb index e24301a..9aa36a3 100644 --- a/lib/encrypted_strings/asymmetric_cipher.rb +++ b/lib/encrypted_strings/asymmetric_cipher.rb @@ -61,27 +61,36 @@ class << self attr_accessor :default_public_key_file end - # Private key used for decrypting data + # Private key file used for decrypting data attr_reader :private_key_file - # Public key used for encrypting data + # Public key file used for encrypting data attr_reader :public_key_file + # Private key used for decrypting data + dynamic_accessor :private_key + + # Public key used for encrypting data + dynamic_accessor :public_key + # The algorithm to use if the key files are encrypted themselves - attr_accessor :algorithm + dynamic_accessor :algorithm # The password used during symmetric decryption of the key files - attr_accessor :password + dynamic_accessor :password # Creates a new cipher that uses an asymmetric encryption strategy. # # Configuration options: - # * :private_key_file - Encrypted private key file + # * :private_key_file - Private key file (possibly encrypted) # * :public_key_file - Public key file + # * :private_key - Private key as String or OpenSSL::PKey::RSA + # * :public_key - Public key as String or OpenSSL::PKey::RSA, or :derived to derive it from the private key # * :password - The password to use in the symmetric cipher # * :algorithm - Algorithm to use symmetrically encrypted strings + # * :encrypt_with_private - Encrypt with the private instead of the public key def initialize(options = {}) - invalid_options = options.keys - [:private_key_file, :public_key_file, :algorithm, :password] + invalid_options = options.keys - [:private_key_file, :public_key_file, :private_key, :public_key, :algorithm, :password, :encrypt_with_private] raise ArgumentError, "Unknown key(s): #{invalid_options.join(", ")}" unless invalid_options.empty? options = { @@ -89,11 +98,13 @@ def initialize(options = {}) :public_key_file => AsymmetricCipher.default_public_key_file }.merge(options) - @public_key = @private_key = nil + @public_key = options[:public_key] + @private_key = options[:private_key] + @encrypt_with_private = options[:encrypt_with_private] self.private_key_file = options[:private_key_file] self.public_key_file = options[:public_key_file] - raise ArgumentError, 'At least one key file must be specified (:private_key_file or :public_key_file)' unless private_key_file || public_key_file + raise ArgumentError, 'At least one key or key file must be specified (:private_key, :public_key, :private_key_file or :public_key_file)' unless @private_key || @public_key || private_key_file || public_key_file self.algorithm = options[:algorithm] self.password = options[:password] @@ -101,22 +112,18 @@ def initialize(options = {}) super() end - # Encrypts the given data. If no public key file has been specified, then + # Encrypts the given data. If no public key has been specified, then # a NoPublicKeyError will be raised. def encrypt(data) - raise NoPublicKeyError, "Public key file: #{public_key_file}" unless public? - - encrypted_data = public_rsa.public_encrypt(data) - [encrypted_data].pack('m') + k = @encrypt_with_private ? :private : :public + perform k, :encrypt, data end - - # Decrypts the given data. If no private key file has been specified, then + + # Decrypts the given data. If no private key has been specified, then # a NoPrivateKeyError will be raised. def decrypt(data) - raise NoPrivateKeyError, "Private key file: #{private_key_file}" unless private? - - decrypted_data = data.unpack('m')[0] - private_rsa.private_decrypt(decrypted_data) + k = @encrypt_with_private ? :public : :private + perform k, :decrypt, data end # Sets the location of the private key and loads it @@ -131,55 +138,83 @@ def public_key_file=(file) # Does this cipher have a public key available? def public? - return true if @public_key - - load_public_key - !@public_key.nil? + !load_public_key.nil? end # Does this cipher have a private key available? def private? - return true if @private_key - - load_private_key - !@private_key.nil? + !load_private_key.nil? end private # Loads the private key from the configured file def load_private_key - @private_rsa = nil - - if private_key_file && File.file?(private_key_file) - @private_key = File.read(private_key_file) + @private_key ||= begin + @private_rsa = nil + + if private_key_file && File.file?(private_key_file) + File.read(private_key_file) + end end end # Loads the public key from the configured file def load_public_key - @public_rsa = nil - - if public_key_file && File.file?(public_key_file) - @public_key = File.read(public_key_file) + @public_key ||= begin + @public_rsa = nil + + if public_key_file && File.file?(public_key_file) + @public_key = File.read(public_key_file) + end end end # Retrieves the private RSA from the private key def private_rsa + @private_rsa = nil if @private_key.respond_to?(:call) if password options = {:password => password} options[:algorithm] = algorithm if algorithm - private_key = @private_key.decrypt(:symmetric, options) - OpenSSL::PKey::RSA.new(private_key) + pkey = @private_key.decrypt(:symmetric, options) + OpenSSL::PKey::RSA.new(pkey) else - @private_rsa ||= OpenSSL::PKey::RSA.new(@private_key) + @private_rsa ||= make_key(private_key) end end # Retrieves the public RSA def public_rsa - @public_rsa ||= OpenSSL::PKey::RSA.new(@public_key) + @public_rsa = nil if @public_key.respond_to?(:call) or + (@public_key == :derived and @private_key.respond_to?(:call)) + @public_rsa ||= if @public_key == :derived + private_rsa.public_key + else + make_key(public_key) + end + end + + def make_key(key) + if key.is_a?(OpenSSL::PKey::RSA) + key + else + OpenSSL::PKey::RSA.new(key) + end + end + + def perform(type, crypt, data) + data = data.unpack('m')[0] if crypt == :decrypt + result= get_key(type).send("#{type}_#{crypt}", data) + crypt == :encrypt ? [result].pack('m') : result + end + + def get_key(type) + unless send("#{type}?") + file = send("#{type}_key_file") + err = EncryptedStrings.const_get("No#{type.capitalize}KeyError") + raise err, "#{type.capitalize} key file: #{file}" + end + send("#{type}_rsa") end end end diff --git a/lib/encrypted_strings/cipher.rb b/lib/encrypted_strings/cipher.rb index 5966143..707c1ea 100644 --- a/lib/encrypted_strings/cipher.rb +++ b/lib/encrypted_strings/cipher.rb @@ -3,6 +3,15 @@ module EncryptedStrings # assumed to be able to decrypt strings. Note, however, that certain # encryption algorithms do not allow decryption. class Cipher + def self.dynamic_accessor(name) + eval <<-CODE + attr_writer :#{name} + def #{name} + @#{name}.respond_to?(:call) ? @#{name}[] : @#{name} + end + CODE + end + # Can this string be decrypted? Default is true. def can_decrypt? true diff --git a/lib/encrypted_strings/sha_cipher.rb b/lib/encrypted_strings/sha_cipher.rb index c300f55..df83d54 100644 --- a/lib/encrypted_strings/sha_cipher.rb +++ b/lib/encrypted_strings/sha_cipher.rb @@ -65,10 +65,10 @@ class << self @default_builder = lambda {|data, salt| "#{data}#{salt}"} # The algorithm to use for encryption/decryption - attr_accessor :algorithm + dynamic_accessor :algorithm # The salt value to use for encryption - attr_accessor :salt + dynamic_accessor :salt # The function to use to build the value that gets hashed attr_accessor :builder @@ -118,7 +118,7 @@ def encrypt(data) # * String # * Object that responds to :salt def salt_value(value) - if value.is_a?(Proc) + if value.respond_to?(:call) value.call elsif value.respond_to?(:salt) value.salt diff --git a/lib/encrypted_strings/symmetric_cipher.rb b/lib/encrypted_strings/symmetric_cipher.rb index 154e1f9..425cd09 100644 --- a/lib/encrypted_strings/symmetric_cipher.rb +++ b/lib/encrypted_strings/symmetric_cipher.rb @@ -51,11 +51,11 @@ class << self @default_algorithm = 'DES-EDE3-CBC' # The algorithm to use for encryption/decryption - attr_accessor :algorithm + dynamic_accessor :algorithm # The password that generates the key/initialization vector for the # algorithm - attr_accessor :password + dynamic_accessor :password # Creates a new cipher that uses a symmetric encryption strategy. # @@ -74,7 +74,7 @@ def initialize(options = {}) self.algorithm = options[:algorithm] self.password = options[:password] - raise NoPasswordError if password.nil? + raise NoPasswordError unless options[:password] super() end @@ -90,10 +90,11 @@ def encrypt(data) cipher = build_cipher(:encrypt) [cipher.update(data) + cipher.final].pack('m') end - + private def build_cipher(type) #:nodoc: cipher = OpenSSL::Cipher::Cipher.new(algorithm).send(type) + raise NoPasswordError if password.nil? cipher.pkcs5_keyivgen(password) cipher end diff --git a/nbproject/private/private.properties b/nbproject/private/private.properties new file mode 100644 index 0000000..cfa22f3 --- /dev/null +++ b/nbproject/private/private.properties @@ -0,0 +1,2 @@ +file.reference.encrypted_strings-lib=/srv/data/home/mklaus/Projects/encrypted_strings/lib +file.reference.encrypted_strings-test=/srv/data/home/mklaus/Projects/encrypted_strings/test diff --git a/nbproject/private/private.xml b/nbproject/private/private.xml new file mode 100644 index 0000000..c1f155a --- /dev/null +++ b/nbproject/private/private.xml @@ -0,0 +1,4 @@ + + + + diff --git a/nbproject/project.properties b/nbproject/project.properties new file mode 100644 index 0000000..303c8d5 --- /dev/null +++ b/nbproject/project.properties @@ -0,0 +1,5 @@ +main.file= +platform.active=Ruby_0 +source.encoding=UTF-8 +src.dir=${file.reference.encrypted_strings-lib} +test.src.dir=${file.reference.encrypted_strings-test} diff --git a/nbproject/project.xml b/nbproject/project.xml new file mode 100644 index 0000000..3df8d7f --- /dev/null +++ b/nbproject/project.xml @@ -0,0 +1,15 @@ + + + org.netbeans.modules.ruby.rubyproject + + + encrypted_strings + + + + + + + + + diff --git a/test/asymmetric_cipher_test.rb b/test/asymmetric_cipher_test.rb index 40c7ee1..5b99635 100644 --- a/test/asymmetric_cipher_test.rb +++ b/test/asymmetric_cipher_test.rb @@ -181,3 +181,172 @@ def test_should_be_able_to_decrypt assert_equal 'test', @asymmetric_cipher.decrypt("HbEh0Hwri26S7SWYqO26DBbzfhR1h/0pXYLjSKUpxF5DOaOCtD9oRN748+Na\nrfNaVN5Eg7RUhbRFZE+UnNHo6Q==\n") end end + +class AsymmetricCipherWithMemoryPrivateKeyTest < Test::Unit::TestCase + def setup + @key = OpenSSL::PKey::RSA.new(1024) + @asymmetric_cipher = EncryptedStrings::AsymmetricCipher.new(:private_key => @key) + end + + def test_should_not_be_public + assert !@asymmetric_cipher.public? + end + + def test_should_be_private + assert @asymmetric_cipher.private? + end + + def test_not_should_be_able_to_encrypt + assert_raise(EncryptedStrings::NoPublicKeyError) {@asymmetric_cipher.encrypt('test')} + end + + def test_should_be_able_to_decrypt + assert_equal 'test', @asymmetric_cipher.decrypt([@key.public_encrypt('test')].pack('m')) + end +end + +class AsymmetricCipherWithMemoryPublicKeyTest < Test::Unit::TestCase + def setup + @key = OpenSSL::PKey::RSA.new(1024) + @asymmetric_cipher = EncryptedStrings::AsymmetricCipher.new(:public_key => @key.public_key) + end + + def test_should_be_public + assert @asymmetric_cipher.public? + end + + def test_not_should_be_private + assert !@asymmetric_cipher.private? + end + + def test_should_be_able_to_encrypt + assert_equal 'test', @key.private_decrypt(@asymmetric_cipher.encrypt('test').unpack('m')[0]) + end + + def test_not_should_be_able_to_decrypt + assert_raise(EncryptedStrings::NoPrivateKeyError) {@asymmetric_cipher.decrypt('test')} + end +end + +class AsymmetricCipherWithExchangedRolesTest < Test::Unit::TestCase + def setup + @key = OpenSSL::PKey::RSA.new(1024) + @asymmetric_cipher = EncryptedStrings::AsymmetricCipher.new(:private_key => @key, :public_key => @key.public_key, :encrypt_with_private => true) + end + + def test_should_be_public + assert @asymmetric_cipher.public? + end + + def test_should_be_private + assert @asymmetric_cipher.private? + end + + def test_should_be_able_to_encrypt + assert_equal 'test', @key.public_decrypt(@asymmetric_cipher.encrypt('test').unpack('m')[0]) + end + + def test_should_be_able_to_decrypt + assert_equal 'test', @asymmetric_cipher.decrypt([@key.private_encrypt('test')].pack('m')) + end +end + +class AsymmetricCipherWithDerivedPublicKeyTest < Test::Unit::TestCase + def setup + file = File.dirname(__FILE__) + '/keys/private' + @key = OpenSSL::PKey::RSA.new(File.read(file)) + @asymmetric_cipher = EncryptedStrings::AsymmetricCipher.new(:private_key_file => file, :public_key => :derived) + end + + def test_should_be_public + assert @asymmetric_cipher.public? + end + + def test_should_be_private + assert @asymmetric_cipher.private? + end + + def test_should_be_able_to_encrypt + assert_equal 'test', @key.private_decrypt(@asymmetric_cipher.encrypt('test').unpack('m')[0]) + end + + def test_should_be_able_to_decrypt + assert_equal 'test', @asymmetric_cipher.decrypt([@key.public_encrypt('test')].pack('m')) + end +end + +class AsymmetricCipherWithProcAsPasswordTest < Test::Unit::TestCase + def setup + @pw1 = 'secret' + @pw2 = 'test' + @asymmetric_cipher = EncryptedStrings::AsymmetricCipher.new(:private_key_file => File.dirname(__FILE__) + '/keys/encrypted_private', :algorithm => 'DES-EDE3-CBC', :password => proc{@pw}) + end + + def test_should_have_the_right_password + [@pw1, @pw2].each do |pw| + assert pw, @asymmetric_cipher.password + end + end + + def test_should_be_private + @pw = @pw1 + assert @asymmetric_cipher.private? + end + + def test_should_be_able_to_decrypt + @pw = @pw1 + assert_equal 'test', @asymmetric_cipher.decrypt("HbEh0Hwri26S7SWYqO26DBbzfhR1h/0pXYLjSKUpxF5DOaOCtD9oRN748+Na\nrfNaVN5Eg7RUhbRFZE+UnNHo6Q==\n") + end + + def test_should_not_be_able_to_decrypt_on_wrong_password + @pw = @pw2 + assert_raise(OpenSSL::Cipher::CipherError) { @asymmetric_cipher.decrypt("HbEh0Hwri26S7SWYqO26DBbzfhR1h/0pXYLjSKUpxF5DOaOCtD9oRN748+Na\nrfNaVN5Eg7RUhbRFZE+UnNHo6Q==\n") } + end +end + +class AsymmetricCipherWithProcAsPublicKeyTest < Test::Unit::TestCase + def setup + @key = OpenSSL::PKey::RSA.new(1024) + @asymmetric_cipher = EncryptedStrings::AsymmetricCipher.new(:public_key => proc{@key.public_key}) + end + + def test_should_be_public + assert @asymmetric_cipher.public? + end + + def test_not_should_be_private + assert !@asymmetric_cipher.private? + end + + def test_should_be_able_to_encrypt + assert_equal 'test', @key.private_decrypt(@asymmetric_cipher.encrypt('test').unpack('m')[0]) + end + + def test_not_should_be_able_to_decrypt + assert_raise(EncryptedStrings::NoPrivateKeyError) {@asymmetric_cipher.decrypt('test')} + end +end + +class AsymmetricCipherWithProcAsPrivateKeyAndDerivedPublicKeyTest < Test::Unit::TestCase + def setup + file = File.dirname(__FILE__) + '/keys/private' + @key1 = OpenSSL::PKey::RSA.new(File.read(file)) + @key2 = OpenSSL::PKey::RSA.new(1024) + @asymmetric_cipher = EncryptedStrings::AsymmetricCipher.new(:private_key => proc{@key}, :public_key => :derived) + end + + def test_should_be_able_to_encrypt + [@key1, @key2].each do |k| + @key = k + assert_equal 'test', k.private_decrypt(@asymmetric_cipher.encrypt('test').unpack('m')[0]) + end + end + + def test_should_be_able_to_decrypt_with_key1 + [@key1, @key2].each do |k| + @key = k + assert_equal 'test', @asymmetric_cipher.decrypt([k.public_encrypt('test')].pack('m')) + end + end +end + diff --git a/test/symmetric_cipher_test.rb b/test/symmetric_cipher_test.rb index edd7ef0..2d5d4f2 100644 --- a/test/symmetric_cipher_test.rb +++ b/test/symmetric_cipher_test.rb @@ -97,3 +97,57 @@ def test_should_decrypt_using_custom_options assert_equal 'test', @symmetric_cipher.decrypt("QWz/eQ==\n") end end + +class SymmetricCipherWithProcsAsCustomDefaultsTest < Test::Unit::TestCase + def setup + @original_default_algorithm = EncryptedStrings::SymmetricCipher.default_algorithm + @original_default_password = EncryptedStrings::SymmetricCipher.default_password + + @al1 = 'DES-EDE3-CFB' + @al2 = nil + @pw1 = 'secret' + @pw2 = nil + + EncryptedStrings::SymmetricCipher.default_algorithm = proc{@al} + EncryptedStrings::SymmetricCipher.default_password = proc{@pw} + @symmetric_cipher = EncryptedStrings::SymmetricCipher.new + end + + def test_should_use_custom_default_algorithm + [@al1, @al2].each do |al| + @al = al + assert_equal al, @symmetric_cipher.algorithm + end + end + + def test_should_use_custom_default_password + [@pw1, @pw2].each do |pw| + @pw = pw + assert_equal pw, @symmetric_cipher.password + end + end + + def test_should_encrypt_using_custom_default_configuration + @al = @al1 + @pw = @pw1 + assert_equal "QWz/eQ==\n", @symmetric_cipher.encrypt('test') + end + + def test_should_not_encrypt_on_missing_password + @al = @al1 + @pw = @pw2 + assert_raise(EncryptedStrings::NoPasswordError) {@symmetric_cipher.encrypt('test')} + end + + def test_should_not_encrypt_on_missing_algorithm + @al = @al2 + @pw = @pw1 + assert_raise(TypeError) {@symmetric_cipher.encrypt('test')} + end + + def teardown + EncryptedStrings::SymmetricCipher.default_algorithm = @original_default_algorithm + EncryptedStrings::SymmetricCipher.default_password = @original_default_password + end +end +