pluginaweek-encrypted_strings 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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