pluginaweek-encrypted_strings 0.3.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,17 @@
1
+ module EncryptedStrings
2
+ # Represents the base class for all ciphers. By default, all ciphers are
3
+ # assumed to be able to decrypt strings. Note, however, that certain
4
+ # encryption algorithms do not allow decryption.
5
+ class Cipher
6
+ # Can this string be decrypted? Default is true.
7
+ def can_decrypt?
8
+ true
9
+ end
10
+
11
+ # Attempts to decrypt the given data using the current configuration. By
12
+ # default, decryption is not implemented.
13
+ def decrypt(data)
14
+ raise NotImplementedError, "Decryption is not supported using a(n) #{self.class.name}"
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,205 @@
1
+ module EncryptedStrings
2
+ module Extensions #:nodoc:
3
+ # Adds support for in-place encryption/decryption of strings
4
+ module String
5
+ def self.included(base) #:nodoc:
6
+ base.class_eval do
7
+ attr_accessor :cipher
8
+
9
+ alias_method :equals_without_encryption, :==
10
+ alias_method :==, :equals_with_encryption
11
+ end
12
+ end
13
+
14
+ # Encrypts the current string using the specified cipher. The default
15
+ # cipher is sha.
16
+ #
17
+ # Configuration options are cipher-specific. See each individual cipher
18
+ # class to find out the options available.
19
+ #
20
+ # == Example
21
+ #
22
+ # The following uses an SHA cipher to encrypt the string:
23
+ #
24
+ # password = 'shhhh'
25
+ # password.encrypt # => "66c85d26dadde7e1db27e15a0776c921e27143bd"
26
+ #
27
+ # == Custom encryption mode
28
+ #
29
+ # The following uses Symmetric cipher (with a default password) to
30
+ # encrypt the string:
31
+ #
32
+ # EncryptedStrings::SymmetricCipher.default_password = 'secret'
33
+ # password = 'shhhh'
34
+ # password.encrypt(:symmetric) # => "jDACXI5hMPI=\n"
35
+ #
36
+ # == Custom encryption options
37
+ #
38
+ # Some encryption modes also support additional configuration options
39
+ # that determine how to encrypt the string. For example, SHA supports
40
+ # a salt which seeds the algorithm:
41
+ #
42
+ # password = 'shhhh'
43
+ # password.encrypt(:sha, :salt => 'secret') # => "3b22cbe4acde873c3efc82681096f3ae69aff828"
44
+ def encrypt(*args)
45
+ cipher = cipher_from_args(*args)
46
+ encrypted_string = cipher.encrypt(self)
47
+ encrypted_string.cipher = cipher
48
+
49
+ encrypted_string
50
+ end
51
+
52
+ # Encrypts this string and replaces it with the encrypted value. This
53
+ # takes the same parameters as #encrypt, but returns the same string
54
+ # instead of a different one.
55
+ #
56
+ # == Example
57
+ #
58
+ # password = 'shhhh'
59
+ # password.encrypt!(:symmetric, :password => 'secret') # => "qSg8vOo6QfU=\n"
60
+ # password # => "qSg8vOo6QfU=\n"
61
+ def encrypt!(*args)
62
+ encrypted_string = encrypt(*args)
63
+ self.cipher = encrypted_string.cipher
64
+
65
+ replace(encrypted_string)
66
+ end
67
+
68
+ # Is this string encrypted? This will return true if the string is the
69
+ # result of a call to #encrypt or #encrypt!.
70
+ #
71
+ # == Example
72
+ #
73
+ # password = 'shhhh'
74
+ # password.encrypted? # => false
75
+ # password.encrypt! # => "66c85d26dadde7e1db27e15a0776c921e27143bd"
76
+ # password.encrypted? # => true
77
+ def encrypted?
78
+ !cipher.nil?
79
+ end
80
+
81
+ # Decrypts this string. If this is not a string that was previously
82
+ # encrypted, the cipher must be specified in the same way that it is
83
+ # when encrypting a string.
84
+ #
85
+ # == Example
86
+ #
87
+ # Without being previously encrypted:
88
+ #
89
+ # password = "qSg8vOo6QfU=\n"
90
+ # password.decrypt(:symmetric, :password => 'secret') # => "shhhh"
91
+ #
92
+ # After being previously encrypted:
93
+ #
94
+ # password = 'shhhh'
95
+ # password.encrypt!(:symmetric, :password => 'secret') # => "qSg8vOo6QfU=\n"
96
+ # password.decrypt # => "shhhh"
97
+ def decrypt(*args)
98
+ raise ArgumentError, 'Cipher cannot be inferred: must specify it as an argument' if args.empty? && !encrypted?
99
+
100
+ cipher = args.empty? && self.cipher || cipher_from_args(*args)
101
+ encrypted_string = cipher.decrypt(self)
102
+ encrypted_string.cipher = nil
103
+
104
+ encrypted_string
105
+ end
106
+
107
+ # Decrypts this string and replaces it with the decrypted value This
108
+ # takes the same parameters as #decrypt, but returns the same string
109
+ # instead of a different one.
110
+ #
111
+ # For example,
112
+ #
113
+ # password = "qSg8vOo6QfU=\n"
114
+ # password.decrypt!(:symmetric, :password => 'secret') # => "shhhh"
115
+ # password # => "shhhh"
116
+ def decrypt!(*args)
117
+ value = replace(decrypt(*args))
118
+ self.cipher = nil
119
+ value
120
+ end
121
+
122
+ # Can this string be decrypted? Strings can only be decrypted if they
123
+ # have previously been decrypted *and* the cipher supports decryption.
124
+ # To determine whether or not the cipher supports decryption, see the
125
+ # api for the cipher.
126
+ def can_decrypt?
127
+ encrypted? && cipher.can_decrypt?
128
+ end
129
+
130
+ # Tests whether the other object is equal to this one. Encrypted strings
131
+ # will be tested not only on their encrypted strings, but also by
132
+ # decrypting them and running tests against the decrypted value.
133
+ #
134
+ # == Equality with strings
135
+ #
136
+ # password = 'shhhh'
137
+ # password.encrypt! # => "66c85d26dadde7e1db27e15a0776c921e27143bd"
138
+ # password # => "66c85d26dadde7e1db27e15a0776c921e27143bd"
139
+ # password == "shhhh" # => true
140
+ #
141
+ # == Equality with encrypted strings
142
+ #
143
+ # password = 'shhhh'
144
+ # password.encrypt! # => "66c85d26dadde7e1db27e15a0776c921e27143bd"
145
+ # password # => "66c85d26dadde7e1db27e15a0776c921e27143bd"
146
+ # password == 'shhhh' # => true
147
+ #
148
+ # another_password = 'shhhh'
149
+ # another_password.encrypt! # => "66c85d26dadde7e1db27e15a0776c921e27143bd"
150
+ # password == another_password # => true
151
+ def equals_with_encryption(other)
152
+ if !(is_equal = equals_without_encryption(other)) && String === other
153
+ if encrypted?
154
+ if other.encrypted?
155
+ # We're both encrypted, so check if:
156
+ # (1) The other string is the encrypted value of this string
157
+ # (2) This string is the encrypted value of the other string
158
+ # (3) The other string is the encrypted value of this string, decrypted
159
+ # (4) This string is the encrypted value of the other string, decrypted
160
+ is_string_equal?(self, other) || is_string_equal?(other, self) || self.can_decrypt? && is_string_equal?(self.decrypt, other) || other.can_decrypt? && is_string_equal?(other.decrypt, self)
161
+ else
162
+ # Only we're encrypted
163
+ is_string_equal?(other, self)
164
+ end
165
+ else
166
+ if other.encrypted?
167
+ # Only the other string is encrypted
168
+ is_string_equal?(self, other)
169
+ else
170
+ # Neither are encrypted and equality test didn't work before, so
171
+ # they can't be equal
172
+ false
173
+ end
174
+ end
175
+ else
176
+ # The other value wasn't a string, so we can't check encryption equality
177
+ is_equal
178
+ end
179
+ end
180
+
181
+ private
182
+ def is_string_equal?(value, encrypted_value) #:nodoc:
183
+ # If the encrypted value can be decrypted, then test against the decrypted value
184
+ if encrypted_value.can_decrypt?
185
+ encrypted_value.decrypt.equals_without_encryption(value)
186
+ else
187
+ # Otherwise encrypt this value based on the cipher used on the encrypted value
188
+ # and test the equality of those strings
189
+ encrypted_value.equals_without_encryption(encrypted_value.cipher.encrypt(value))
190
+ end
191
+ end
192
+
193
+ # Builds the cipher to use from the given arguments
194
+ def cipher_from_args(*args) #:nodoc:
195
+ options = args.last.is_a?(Hash) ? args.pop : {}
196
+ name = (args.first || :sha).to_s.gsub(/(?:^|_)(.)/) {$1.upcase}
197
+ EncryptedStrings.const_get("#{name}Cipher").new(options)
198
+ end
199
+ end
200
+ end
201
+ end
202
+
203
+ ::String.class_eval do
204
+ include EncryptedStrings::Extensions::String
205
+ end
@@ -0,0 +1,67 @@
1
+ require 'digest/sha1'
2
+
3
+ module EncryptedStrings
4
+ # Encrypts a string using a Secure Hash Algorithm (SHA), specifically SHA-1.
5
+ #
6
+ # == Encrypting
7
+ #
8
+ # To encrypt a string using an SHA cipher, the salt used to seed the
9
+ # algorithm must be specified. You can define the default for this value
10
+ # like so:
11
+ #
12
+ # EncryptedStrings::ShaCipher.default_salt = 'secret'
13
+ #
14
+ # If these configuration options are not passed in to #encrypt, then the
15
+ # default values will be used. You can override the default values like so:
16
+ #
17
+ # password = 'shhhh'
18
+ # password.encrypt(:sha, :salt => 'secret') # => "ae645b35bb5dfea6c9133ac872e6adfa92a3c2bd"
19
+ #
20
+ # == Decrypting
21
+ #
22
+ # SHA-encrypted strings cannot be decrypted. The only way to determine
23
+ # whether an unencrypted value is equal to an SHA-encrypted string is to
24
+ # encrypt the value with the same salt. For example,
25
+ #
26
+ # password = 'shhhh'.encrypt(:sha, :salt => 'secret') # => "3b22cbe4acde873c3efc82681096f3ae69aff828"
27
+ # input = 'shhhh'.encrypt(:sha, :salt => 'secret') # => "3b22cbe4acde873c3efc82681096f3ae69aff828"
28
+ # password == input # => true
29
+ class ShaCipher < Cipher
30
+ class << self
31
+ # The default salt value to use during encryption
32
+ attr_accessor :default_salt
33
+ end
34
+
35
+ # Set defaults
36
+ @default_salt = 'salt'
37
+
38
+ # The salt value to use for encryption
39
+ attr_accessor :salt
40
+
41
+ # Creates a new cipher that uses an SHA encryption strategy.
42
+ #
43
+ # Configuration options:
44
+ # * <tt>:salt</tt> - Random bytes used as one of the inputs for generating
45
+ # the encrypted string
46
+ def initialize(options = {})
47
+ invalid_options = options.keys - [:salt]
48
+ raise ArgumentError, "Unknown key(s): #{invalid_options.join(", ")}" unless invalid_options.empty?
49
+
50
+ options = {:salt => ShaCipher.default_salt}.merge(options)
51
+
52
+ self.salt = options[:salt].to_s
53
+
54
+ super()
55
+ end
56
+
57
+ # Decryption is not supported
58
+ def can_decrypt?
59
+ false
60
+ end
61
+
62
+ # Returns the encrypted value of the data
63
+ def encrypt(data)
64
+ Digest::SHA1.hexdigest(data + salt)
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,101 @@
1
+ module EncryptedStrings
2
+ # Indicates no password was specified for the symmetric cipher
3
+ class NoPasswordError < StandardError
4
+ end
5
+
6
+ # Symmetric encryption uses a specific algorithm and password to encrypt
7
+ # the string. As long as the algorithm and password are known, the string
8
+ # can be decrypted.
9
+ #
10
+ # Source: http://support.microsoft.com/kb/246071
11
+ #
12
+ # == Encrypting
13
+ #
14
+ # To encrypt a string using a symmetric cipher, the algorithm and password
15
+ # must be specified. You can define the defaults for these values like so:
16
+ #
17
+ # EncryptedStrings::SymmetricCipher.default_algorithm = 'des-ecb'
18
+ # EncryptedStrings::SymmetricCipher.default_password = 'secret'
19
+ #
20
+ # If these configuration options are not passed in to #encrypt, then the
21
+ # default values will be used. You can override the default values like so:
22
+ #
23
+ # password = 'shhhh'
24
+ # password.encrypt(:symmetric, :algorithm => 'des-ecb', :password => 'secret') # => "S/sEkViX3v4=\n"
25
+ #
26
+ # An exception will be raised if no password is specified.
27
+ #
28
+ # == Decrypting
29
+ #
30
+ # To decrypt a string using an symmetric cipher, the algorithm and password
31
+ # must be specified. Defaults for these values can be defined as show above.
32
+ #
33
+ # If these configuration options are not passed in to #decrypt, then the
34
+ # default values will be used. You can override the default values like so:
35
+ #
36
+ # password = "S/sEkViX3v4=\n"
37
+ # password.decrypt(:symmetric, :algorithm => 'des-ecb', :password => 'secret') # => "shhhh"
38
+ #
39
+ # An exception will be raised if no password is specified.
40
+ class SymmetricCipher < Cipher
41
+ class << self
42
+ # The default algorithm to use for encryption. Default is DES-EDE3-CBC.
43
+ attr_accessor :default_algorithm
44
+
45
+ # The default password to use for generating the key and initialization
46
+ # vector. Default is nil.
47
+ attr_accessor :default_password
48
+ end
49
+
50
+ # Set default values
51
+ @default_algorithm = 'DES-EDE3-CBC'
52
+
53
+ # The algorithm to use for encryption/decryption
54
+ attr_accessor :algorithm
55
+
56
+ # The password that generates the key/initialization vector for the
57
+ # algorithm
58
+ attr_accessor :password
59
+
60
+ # Creates a new cipher that uses a symmetric encryption strategy.
61
+ #
62
+ # Configuration options:
63
+ # * <tt>:algorithm</tt> - The algorithm to use for generating the encrypted string
64
+ # * <tt>:password</tt> - The secret value to use for generating the
65
+ # key/initialization vector for the algorithm
66
+ def initialize(options = {})
67
+ invalid_options = options.keys - [:algorithm, :password]
68
+ raise ArgumentError, "Unknown key(s): #{invalid_options.join(", ")}" unless invalid_options.empty?
69
+
70
+ options = {
71
+ :algorithm => SymmetricCipher.default_algorithm,
72
+ :password => SymmetricCipher.default_password
73
+ }.merge(options)
74
+
75
+ self.algorithm = options[:algorithm]
76
+ self.password = options[:password]
77
+ raise NoPasswordError if password.nil?
78
+
79
+ super()
80
+ end
81
+
82
+ # Decrypts the current string using the current key and algorithm specified
83
+ def decrypt(data)
84
+ cipher = build_cipher(:decrypt)
85
+ cipher.update(data.unpack('m')[0]) + cipher.final
86
+ end
87
+
88
+ # Encrypts the current string using the current key and algorithm specified
89
+ def encrypt(data)
90
+ cipher = build_cipher(:encrypt)
91
+ [cipher.update(data) + cipher.final].pack('m')
92
+ end
93
+
94
+ private
95
+ def build_cipher(type) #:nodoc:
96
+ cipher = OpenSSL::Cipher::Cipher.new(algorithm).send(type)
97
+ cipher.pkcs5_keyivgen(password)
98
+ cipher
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,183 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ class NoPrivateKeyErrorTest < Test::Unit::TestCase
4
+ def test_should_exist
5
+ assert_not_nil EncryptedStrings::NoPrivateKeyError
6
+ end
7
+ end
8
+
9
+ class NoPublicKeyErrorTest < Test::Unit::TestCase
10
+ def test_should_exist
11
+ assert_not_nil EncryptedStrings::NoPublicKeyError
12
+ end
13
+ end
14
+
15
+ class AsymmetricCipherByDefaultTest < Test::Unit::TestCase
16
+ def setup
17
+ @asymmetric_cipher = EncryptedStrings::AsymmetricCipher.new(:public_key_file => File.dirname(__FILE__) + '/keys/public')
18
+ end
19
+
20
+ def test_should_raise_an_exception
21
+ assert_raise(ArgumentError) {EncryptedStrings::AsymmetricCipher.new}
22
+ end
23
+
24
+ def test_should_not_have_a_public_key_file
25
+ @asymmetric_cipher = EncryptedStrings::AsymmetricCipher.new(:private_key_file => File.dirname(__FILE__) + '/keys/private')
26
+ assert_nil @asymmetric_cipher.public_key_file
27
+ end
28
+
29
+ def test_should_not_have_a_private_key_file
30
+ assert_nil @asymmetric_cipher.private_key_file
31
+ end
32
+
33
+ def test_should_not_have_an_algorithm
34
+ assert_nil @asymmetric_cipher.algorithm
35
+ end
36
+
37
+ def test_should_not_have_a_password
38
+ assert_nil @asymmetric_cipher.password
39
+ end
40
+ end
41
+
42
+ class AsymmetricCipherWithCustomDefaultsTest < Test::Unit::TestCase
43
+ def setup
44
+ @original_default_public_key_file = EncryptedStrings::AsymmetricCipher.default_public_key_file
45
+ @original_default_private_key_file = EncryptedStrings::AsymmetricCipher.default_private_key_file
46
+
47
+ EncryptedStrings::AsymmetricCipher.default_public_key_file = File.dirname(__FILE__) + '/keys/public'
48
+ EncryptedStrings::AsymmetricCipher.default_private_key_file = File.dirname(__FILE__) + '/keys/private'
49
+
50
+ @asymmetric_cipher = EncryptedStrings::AsymmetricCipher.new
51
+ end
52
+
53
+ def test_should_use_default_public_key_file
54
+ assert_equal File.dirname(__FILE__) + '/keys/public', @asymmetric_cipher.public_key_file
55
+ end
56
+
57
+ def test_should_use_default_private_key_file
58
+ assert_equal File.dirname(__FILE__) + '/keys/private', @asymmetric_cipher.private_key_file
59
+ end
60
+
61
+ def test_should_not_have_an_algorithm
62
+ assert_nil @asymmetric_cipher.algorithm
63
+ end
64
+
65
+ def test_should_not_have_a_password
66
+ assert_nil @asymmetric_cipher.password
67
+ end
68
+
69
+ def teardown
70
+ EncryptedStrings::AsymmetricCipher.default_public_key_file = @original_default_public_key_file
71
+ EncryptedStrings::AsymmetricCipher.default_private_key_file = @original_default_private_key_file
72
+ end
73
+ end
74
+
75
+ class AsymmetricCipherWithInvalidOptionsTest < Test::Unit::TestCase
76
+ def test_should_throw_an_exception
77
+ assert_raise(ArgumentError) {EncryptedStrings::AsymmetricCipher.new(:invalid => true)}
78
+ end
79
+ end
80
+
81
+ class AsymmetricCipherTest < Test::Unit::TestCase
82
+ def setup
83
+ @asymmetric_cipher = EncryptedStrings::AsymmetricCipher.new(:public_key_file => File.dirname(__FILE__) + '/keys/public')
84
+ end
85
+
86
+ def test_should_be_able_to_decrypt
87
+ assert @asymmetric_cipher.can_decrypt?
88
+ end
89
+ end
90
+
91
+ class AsymmetricCipherWithoutPublicKeyTest < Test::Unit::TestCase
92
+ def setup
93
+ @asymmetric_cipher = EncryptedStrings::AsymmetricCipher.new(:public_key_file => nil, :private_key_file => File.dirname(__FILE__) + '/keys/private')
94
+ end
95
+
96
+ def test_should_not_be_public
97
+ assert !@asymmetric_cipher.public?
98
+ end
99
+
100
+ def test_should_not_be_able_to_encrypt
101
+ assert_raise(EncryptedStrings::NoPublicKeyError) {@asymmetric_cipher.encrypt('test')}
102
+ end
103
+ end
104
+
105
+ class AsymmetricCipherWithPublicKeyTest < Test::Unit::TestCase
106
+ def setup
107
+ @asymmetric_cipher = EncryptedStrings::AsymmetricCipher.new(:public_key_file => File.dirname(__FILE__) + '/keys/public')
108
+ end
109
+
110
+ def test_should_be_public
111
+ assert @asymmetric_cipher.public?
112
+ end
113
+
114
+ def test_should_not_be_private
115
+ assert !@asymmetric_cipher.private?
116
+ end
117
+
118
+ def test_should_be_able_to_encrypt
119
+ assert_equal 90, @asymmetric_cipher.encrypt('test').length
120
+ end
121
+
122
+ def test_should_not_be_able_to_decrypt
123
+ assert_raise(EncryptedStrings::NoPrivateKeyError) {@asymmetric_cipher.decrypt("HbEh0Hwri26S7SWYqO26DBbzfhR1h/0pXYLjSKUpxF5DOaOCtD9oRN748+Na\nrfNaVN5Eg7RUhbRFZE+UnNHo6Q==\n")}
124
+ end
125
+ end
126
+
127
+ class AsymmetricCipherWithoutPrivateKeyTest < Test::Unit::TestCase
128
+ def setup
129
+ @asymmetric_cipher = EncryptedStrings::AsymmetricCipher.new(:private_key_file => nil, :public_key_file => File.dirname(__FILE__) + '/keys/public')
130
+ end
131
+
132
+ def test_should_not_be_private
133
+ assert !@asymmetric_cipher.private?
134
+ end
135
+
136
+ def test_should_not_be_able_to_decrypt
137
+ assert_raise(EncryptedStrings::NoPrivateKeyError) {@asymmetric_cipher.decrypt("HbEh0Hwri26S7SWYqO26DBbzfhR1h/0pXYLjSKUpxF5DOaOCtD9oRN748+Na\nrfNaVN5Eg7RUhbRFZE+UnNHo6Q==\n")}
138
+ end
139
+ end
140
+
141
+ class AsymmetricCipherWithPrivateKeyTest < Test::Unit::TestCase
142
+ def setup
143
+ @asymmetric_cipher = EncryptedStrings::AsymmetricCipher.new(:private_key_file => File.dirname(__FILE__) + '/keys/private')
144
+ end
145
+
146
+ def test_should_not_be_public
147
+ assert !@asymmetric_cipher.public?
148
+ end
149
+
150
+ def test_should_be_private
151
+ assert @asymmetric_cipher.private?
152
+ end
153
+
154
+ def test_not_should_be_able_to_encrypt
155
+ assert_raise(EncryptedStrings::NoPublicKeyError) {@asymmetric_cipher.encrypt('test')}
156
+ end
157
+
158
+ def test_should_be_able_to_decrypt
159
+ assert_equal 'test', @asymmetric_cipher.decrypt("HbEh0Hwri26S7SWYqO26DBbzfhR1h/0pXYLjSKUpxF5DOaOCtD9oRN748+Na\nrfNaVN5Eg7RUhbRFZE+UnNHo6Q==\n")
160
+ end
161
+ end
162
+
163
+ class AsymmetricCipherWithEncryptedPrivateKeyTest < Test::Unit::TestCase
164
+ def setup
165
+ @asymmetric_cipher = EncryptedStrings::AsymmetricCipher.new(:private_key_file => File.dirname(__FILE__) + '/keys/encrypted_private', :algorithm => 'DES-EDE3-CBC', :password => 'secret')
166
+ end
167
+
168
+ def test_should_not_be_public
169
+ assert !@asymmetric_cipher.public?
170
+ end
171
+
172
+ def test_should_be_private
173
+ assert @asymmetric_cipher.private?
174
+ end
175
+
176
+ def test_should_not_be_able_to_encrypt
177
+ assert_raise(EncryptedStrings::NoPublicKeyError) {@asymmetric_cipher.encrypt('test')}
178
+ end
179
+
180
+ def test_should_be_able_to_decrypt
181
+ assert_equal 'test', @asymmetric_cipher.decrypt("HbEh0Hwri26S7SWYqO26DBbzfhR1h/0pXYLjSKUpxF5DOaOCtD9oRN748+Na\nrfNaVN5Eg7RUhbRFZE+UnNHo6Q==\n")
182
+ end
183
+ end