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
+