secure_string 1.1.0 → 1.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -19,13 +19,20 @@ String version of the value.
19
19
  WARNING: it is important to note that the String method +length+ is not a good
20
20
  measure of a byte string's length, as depending on the encoding, it may count
21
21
  multibyte characters as a single element. To ensure that you get the byte
22
- length, use the standard string method +bytesize+.
22
+ length, use the standard string method +bytesize+. See the section on
23
+ String Encodings for more detail.
23
24
 
24
25
  = Installation & Configuration
25
26
 
26
27
  == Installation
27
28
 
28
- Install from gem, and add the following to your script:
29
+ SecureString is currently only supported in Ruby 1.9.x.
30
+
31
+ To install, first install the gem:
32
+
33
+ gem install secure_string
34
+
35
+ Then require the gem like so:
29
36
 
30
37
  require 'secure_string'
31
38
 
@@ -108,13 +115,14 @@ cryptographic hash sums for the data in the string. Note that since SecureStrin
108
115
  handles binary data well, the string value returns is NOT the hex string; to get
109
116
  the hex digest, simply call to_hex:
110
117
 
118
+ ss = SecureString.new("Hello World!")
111
119
  ss.to_md5.to_hex
112
120
  --> "ed076287532e86365e841e92bfc50d8c"
113
121
  ss.to_sha1.to_hex
114
122
  --> "2ef7bde608ce5404e97d5f042f95f89f1c232871"
115
123
  ss.to_sha256.to_hex
116
124
  --> "7f83b1657ff1fc53b92dc18148a1d65dfc2d4b1fa3d677284addd200126d9069"
117
- ss.to_digest(OpenSSL::Digest::SHA512).to_hex
125
+ ss.to_digest('SHA-512').to_hex
118
126
  --> "861844d6704e8573fec34d967e20bcfef3d424cf48be04e6dc08f2bd58c729743371015ead891cc3cf1c9d34b49264b510751b1ff9e537937bc46b5d6ff4ecc8"
119
127
 
120
128
  == RSA Methods Overview
@@ -167,6 +175,53 @@ cipher:
167
175
  # Now decrypt the message:
168
176
  decoded_text = cipher_text.from_aes(key, iv)
169
177
 
178
+ = String Encodings
179
+
180
+ == Overview
181
+
182
+ Starting in Ruby 1.9.x, String instances manage their own encodings. For example,
183
+ a string with Unicode characters are usually encoded as UTF-8, while a lot of
184
+ source is still written in US-ASCII.
185
+
186
+ Binary data is independent of the encoding that encodes it, however using the
187
+ binary data when it is assigned a multi-byte character encoding strategy can
188
+ lead to a few surprises.
189
+
190
+ For example, for a Unicode string, the +length+ method returns the number of
191
+ characters in the string, while +bytesize+ returns the number of bytes encoded
192
+ in the string:
193
+
194
+ s = "Resum\u00E9"
195
+ s.encoding --> UTF-8
196
+ s.length --> 6
197
+ s.bytesize --> 7
198
+
199
+ == SecureString Encoding
200
+
201
+ SecureString's basic design philosophy is to--as much as possible--only extend the
202
+ behavior of String, not replace it. <b>Therefore, SecureString does NOT affect the
203
+ encoding of the string it is using.</b>
204
+
205
+ This is normally not a problem--as long as you use +bytesize+ instead of +length+
206
+ to get the byte count of the data in the string.
207
+
208
+ In rare cases, you may want to change a String to the ASCII-8BIT binary
209
+ encoding without changing its data. To accomplish this, one should call
210
+ +force_encoding+ like so:
211
+
212
+ s = "Resum\u00E9"
213
+ s.force_encoding('BINARY')
214
+ s.encoding --> ASCII-8BIT
215
+ s.length --> 7
216
+ s.bytesize --> 7
217
+
218
+ Note that when you do this, equality tests can be broken like so:
219
+
220
+ s = "Resum\u00E9"
221
+ b = s.dup.force_encoding('BINARY')
222
+ s == b --> false
223
+ s.bytes.to_a == b.bytes.to_a --> true
224
+
170
225
  = Contact
171
226
 
172
227
  If you have any questions, comments, concerns, patches, or bugs, you can contact
@@ -179,7 +234,17 @@ or directly via e-mail at:
179
234
  mailto:jeff@paploo.net
180
235
 
181
236
  = Version History
182
- [1.1.0 - 2030-Nov-04] Extracted methods into a module that can be easily included
237
+ [1.1.1 - 2010-Nov-05] Backed down requirements to ruby 1.9.x; Bugfixes and minor changes.
238
+ * Tested in 1.9.1 and 1.9.2.
239
+ * Added some documentation and tests on String encodings.
240
+ * Added more spec tests.
241
+ * (FEATURE) Digest methods may be supplied via string now.
242
+ * (FEATURE) RSA encryption works with both public and private keys.
243
+ * (FEATURE) RSA keys encoded as SecureString can be asked if public or private.
244
+ * (CHANGE) BinaryStringDataMethods now is non-clobbering and independent.
245
+ * (CHANGE) An empty string's integer value is now zero.
246
+ * (FIX) Non-hex characters are ignored when accepting hex data input.
247
+ [1.1.0 - 2010-Nov-04] Extracted methods into a module that can be easily included
183
248
  on any String class.
184
249
  [1.0.0 - 2010-Nov-04] Added Tests, Examples, and Bugfixes
185
250
  * Added a full suite of spec tests.
@@ -193,13 +258,9 @@ mailto:jeff@paploo.net
193
258
 
194
259
  = TODO List
195
260
 
196
- * Add a +to_ss+ or +to_secure+ method to String for easy conversion.
197
- * to_digest should be able to take a string that is the algorithm name.
198
- * Explore how encodings affect the data. What about when finding length? What
199
- methods should be recommended to users for finding byte-length vs. char length.
200
- * RSA encoding/decoding should auto-detect if the given key is public or private,
201
- and use the correct encoding routine? What about key confusion?
202
- * RSA signature digests: accept Digest hashes, not just OpenSSL::Digest hashes.
261
+ * See what happens when including SecurizeString into an object that is not a String.
262
+ What are the expected root methods? +to_s+, <tt>self.new(string)</tt> are two
263
+ that I know of.
203
264
 
204
265
 
205
266
  = License
@@ -17,4 +17,21 @@ class SecureString < String
17
17
  self.replace( data_string )
18
18
  end
19
19
 
20
+ # Override the default to_i method to return the integer value of the data
21
+ # contained in the string, rather than the parsed value of the characters.
22
+ def to_i
23
+ return data_to_i
24
+ end
25
+
26
+ # Add a method to convert the internal binary data into a hex string.
27
+ def to_hex
28
+ return data_to_hex
29
+ end
30
+
31
+ # Override the default inspect to return the hexidecimal
32
+ # representation of the data contained in this string.
33
+ def inspect
34
+ return "<#{to_hex}>"
35
+ end
36
+
20
37
  end
@@ -1,8 +1,7 @@
1
1
  require 'base64'
2
2
 
3
3
  module SecurizeString
4
- # Adds the base methods necessary to make String or a String subclass handle
5
- # binary data better.
4
+ # Adds base methods that help in interpreting the binary data represented by the String value of an object.
6
5
  # See BinaryStringDataMethods::ClassMethods and BinaryStringDataMethods::InstanceMethods for more deatils.
7
6
  module BinaryStringDataMethods
8
7
 
@@ -11,8 +10,7 @@ module SecurizeString
11
10
  mod.send(:include, InstanceMethods)
12
11
  end
13
12
 
14
- # Adds basic binary data class methods to String or a String subclass, via
15
- # an include of SecurizeString::BinaryStringDataMethods
13
+ # Adds basic binary data class methods via an include of SecurizeString::BinaryStringDataMethods.
16
14
  module ClassMethods
17
15
 
18
16
  # Creates a data string from one many kinds of values:
@@ -23,7 +21,7 @@ module SecurizeString
23
21
  def parse_data(mode = :data, value)
24
22
  case mode
25
23
  when :hex
26
- hex_string = value.to_s
24
+ hex_string = value.to_s.delete('^0-9a-fA-F')
27
25
  data_string = [hex_string].pack('H' + hex_string.bytesize.to_s)
28
26
  when :data
29
27
  data_string = value.to_s
@@ -38,28 +36,19 @@ module SecurizeString
38
36
 
39
37
  end
40
38
 
41
- # Adds basic binary data instance methods to String or a String subclass, via
42
- # an include of SecurizeString::BinaryStringDataMethods.
39
+ # Adds basic binary data instance methods via an include of SecurizeString::BinaryStringDataMethods.
43
40
  module InstanceMethods
44
41
 
45
- # Override the default inspect to return the hexidecimal
46
- # representation of the data contained in this string.
47
- def inspect
48
- return "<#{to_hex}>"
49
- end
50
-
51
42
  # Returns the hexidecimal string representation of the data.
52
- def to_hex
43
+ def data_to_hex
53
44
  return (self.to_s.empty? ? '' : self.to_s.unpack('H' + (self.to_s.bytesize*2).to_s)[0])
54
45
  end
55
46
 
56
47
  # Returns the data converted from hexidecimal into an integer.
57
48
  # This is usually as a BigInt.
58
49
  #
59
- # WARNING: If the data string is empty, then this returns -1, as there is no
60
- # integer representation of the absence of data.
61
- def to_i
62
- return (self.to_s.empty? ? -1 : to_hex.hex)
50
+ def data_to_i
51
+ return (self.to_s.empty? ? 0 : self.data_to_hex.hex)
63
52
  end
64
53
 
65
54
  end
@@ -55,7 +55,7 @@ module SecurizeString
55
55
  cipher.encrypt # MUST set the mode BEFORE setting the key and iv!
56
56
  cipher.key = key
57
57
  cipher.iv = iv
58
- msg = cipher.update(self)
58
+ msg = cipher.update(self.to_s)
59
59
  msg << cipher.final
60
60
  return self.class.new(msg)
61
61
  end
@@ -67,7 +67,7 @@ module SecurizeString
67
67
  cipher.decrypt # MUST set the mode BEFORE setting the key and iv!
68
68
  cipher.key = key
69
69
  cipher.iv = iv
70
- msg = cipher.update(self)
70
+ msg = cipher.update(self.to_s)
71
71
  msg << cipher.final
72
72
  return self.class.new(msg)
73
73
  end
@@ -78,13 +78,13 @@ module SecurizeString
78
78
  # combination on two different messages as this weakens the security.
79
79
  def to_aes(key, iv)
80
80
  key_len = (key.bytesize * 8)
81
- return self.class.new( to_cipher("aes-#{key_len.to_i}-cbc", key, iv) )
81
+ return self.class.new( to_cipher("aes-#{key_len}-cbc", key, iv) )
82
82
  end
83
83
 
84
84
  # Given an AES key and init vector, AES-CBC decode the data.
85
85
  def from_aes(key, iv)
86
86
  key_len = (key.bytesize * 8)
87
- return self.class.new( from_cipher("aes-#{key_len.to_i}-cbc", key, iv) )
87
+ return self.class.new( from_cipher("aes-#{key_len}-cbc", key, iv) )
88
88
  end
89
89
 
90
90
  end
@@ -0,0 +1,56 @@
1
+ require 'digest'
2
+ require 'openssl'
3
+
4
+ module SecurizeString
5
+ # A Helper class to look-up digests easily.
6
+ class DigestFinder
7
+ DIGESTS = {
8
+ 'DSS' => OpenSSL::Digest::DSS ,
9
+ 'DSS1' => OpenSSL::Digest::DSS1 ,
10
+ 'MD2' => OpenSSL::Digest::MD2 ,
11
+ 'MD4' => OpenSSL::Digest::MD4 ,
12
+ 'MD5' => OpenSSL::Digest::MD5 ,
13
+ 'SHA' => OpenSSL::Digest::SHA ,
14
+ 'SHA-0' => OpenSSL::Digest::SHA ,
15
+ 'SHA0' => OpenSSL::Digest::SHA ,
16
+ 'SHA-1' => OpenSSL::Digest::SHA1 ,
17
+ 'SHA1' => OpenSSL::Digest::SHA1 ,
18
+ 'SHA-224' => OpenSSL::Digest::SHA224 ,
19
+ 'SHA-256' => OpenSSL::Digest::SHA256 ,
20
+ 'SHA-384' => OpenSSL::Digest::SHA384 ,
21
+ 'SHA-512' => OpenSSL::Digest::SHA512 ,
22
+ 'SHA224' => OpenSSL::Digest::SHA224 ,
23
+ 'SHA256' => OpenSSL::Digest::SHA256 ,
24
+ 'SHA384' => OpenSSL::Digest::SHA384 ,
25
+ 'SHA512' => OpenSSL::Digest::SHA512 ,
26
+ 'RIPEMD-160' => OpenSSL::Digest::RIPEMD160,
27
+ 'RIPEMD160' => OpenSSL::Digest::RIPEMD160
28
+ }.freeze
29
+
30
+ # Returns the complete list of digest strings recognized by find.
31
+ def self.digests
32
+ @supported_digests = DIGESTS.keys.inject([]) {|a,v| a | [v.upcase, v.downcase]}.sort if @supported_digests.nil?
33
+ return @supported_digests
34
+ end
35
+
36
+ # Returns the OpenSSL::Digest class that goes with a given argument.
37
+ def self.find(obj)
38
+ if( obj.kind_of?(OpenSSL::Digest) )
39
+ klass = obj.class
40
+ elsif( obj.kind_of?(Class) && (obj < OpenSSL::Digest) )
41
+ klass = obj
42
+ elsif( obj.kind_of?(Class) && (obj < Digest::Base) )
43
+ klass = DIGESTS[obj.name.split('::').last.upcase]
44
+ elsif( obj.kind_of?(Digest::Base) )
45
+ klass = DIGESTS[obj.class.name.split('::').last.upcase]
46
+ else
47
+ klass = DIGESTS[obj.to_s.upcase]
48
+ end
49
+
50
+ raise ArgumentError, "Cannot convert to OpenSSL::Digest: #{obj.inspect}" if klass.nil?
51
+
52
+ return klass
53
+ end
54
+
55
+ end
56
+ end
@@ -1,31 +1,45 @@
1
1
  require 'openssl'
2
+ require_relative 'digest_finder'
2
3
 
3
4
  module SecurizeString
4
5
  # Adds methods for OpenSSL::Digest support.
5
6
  # See DigestMethods::ClassMethods and DigestMethods::InstanceMethods for more details.
6
7
  module DigestMethods
7
-
8
8
  def self.included(mod)
9
9
  mod.send(:include, InstanceMethods)
10
10
  end
11
11
 
12
+ # Adds instance methods for OpenSSL::Digest support via inclusion of
13
+ # SecurizeString::DigestMethods to a class.
14
+ module ClassMethods
15
+
16
+ # Returns a list of supported digests. These can be passed directly into
17
+ # the cipher methods.
18
+ def supported_digests
19
+ return DigestFinder.digests
20
+ end
21
+
22
+ end
23
+
12
24
  # Adds instance methods for OpenSSL::Digest support via inclusion of
13
25
  # SecurizeString::DigestMethods to a class.
14
26
  module InstanceMethods
15
27
 
16
- # Returns the digest of the byte string as a SecureString, using the passed OpenSSL object.
17
- def to_digest(digest_obj)
28
+ # Returns the digest of the byte string as a SecureString using the passed
29
+ # digest from the list of digests in +supported_digests+.
30
+ def to_digest(digest)
31
+ digest_obj = DigestFinder.find(digest).new
18
32
  return self.class.new( digest_obj.digest(self) )
19
33
  end
20
34
 
21
35
  # Returns the MD5 of the byte string as a SecureString.
22
36
  def to_md5
23
- return to_digest( OpenSSL::Digest::MD5.new )
37
+ return to_digest('MD5')
24
38
  end
25
39
 
26
40
  # Returns the SHA1 of the byte string as SecureString
27
41
  def to_sha1
28
- return to_digest( OpenSSL::Digest::SHA1.new )
42
+ return to_digest('SHA-1')
29
43
  end
30
44
 
31
45
  # Returns the SHA2 of the byte string as a SecureString.
@@ -34,8 +48,8 @@ module SecurizeString
34
48
  # specification of which bit length to use.
35
49
  def to_sha2(length=256)
36
50
  if [224,256,384,512].include?(length)
37
- digest_klass = OpenSSL::Digest.const_get("SHA#{length}", false)
38
- return to_digest( digest_klass )
51
+ digest = "SHA-#{length}"
52
+ return to_digest( digest )
39
53
  else
40
54
  raise ArgumentError, "Invalid SHA2 length: #{length}"
41
55
  end
@@ -42,22 +42,25 @@ module SecurizeString
42
42
  #
43
43
  # Note that the key must be 11 bytes longer than the data string or it doesn't
44
44
  # work.
45
- def to_rsa(public_key)
46
- key = OpenSSL::PKey::RSA.new(public_key)
47
- return self.class.new( key.public_encrypt(self) )
45
+ def to_rsa(key)
46
+ key = OpenSSL::PKey::RSA.new(key)
47
+ cipher_text = key.private? ? key.private_encrypt(self.to_s) : key.public_encrypt(self.to_s)
48
+ return self.class.new(cipher_text)
48
49
  end
49
50
 
50
51
  # Given an RSA private key, it decrypts the data string back into the original text.
51
- def from_rsa(private_key)
52
- key = OpenSSL::PKey::RSA.new(private_key)
53
- return self.class.new( key.private_decrypt(self) )
52
+ def from_rsa(key)
53
+ key = OpenSSL::PKey::RSA.new(key)
54
+ plain_text = key.private? ? key.private_decrypt(self.to_s) : key.public_decrypt(self.to_s)
55
+ return self.class.new(plain_text)
54
56
  end
55
57
 
56
58
  # Signs the given message using hte given private key.
57
59
  #
58
- # By default, signs using SHA256, but another digest object can be given.
59
- def sign(private_key, digest_obj=OpenSSL::Digest::SHA256.new)
60
- digest_obj = (digest_obj.kind_of?(Class) ? digest_obj.new : digest_obj)
60
+ # By default, verifies using SHA256, but another digest method can be given
61
+ # using the list of DigestFinder supported digests.
62
+ def sign(private_key, digest_method='SHA-256')
63
+ digest_obj = DigestFinder.find(digest_method).new
61
64
  key = OpenSSL::PKey::RSA.new(private_key)
62
65
  return self.class.new( key.sign(digest_obj, self) )
63
66
  end
@@ -65,13 +68,32 @@ module SecurizeString
65
68
  # Verifies the given signature matches the messages digest, using the
66
69
  # signer's public key.
67
70
  #
68
- # By default, verifies using SHA256, but another digest object can be given.
69
- def verify?(public_key, signature, digest_obj=OpenSSL::Digest::SHA256.new)
70
- digest_obj = (digest_obj.kind_of?(Class) ? digest_obj.new : digest_obj)
71
+ # By default, verifies using SHA256, but another digest method can be given
72
+ # using the list of DigestFinder supported digests.
73
+ def verify?(public_key, signature, digest_method='SHA-256')
74
+ digest_obj = DigestFinder.find(digest_method).new
71
75
  key = OpenSSL::PKey::RSA.new(public_key)
72
76
  return key.verify(digest_obj, signature.to_s, self)
73
77
  end
74
78
 
79
+ # Interpret the conetents of the string as an RSA key, and determine if it is public.
80
+ #
81
+ # Even though private keys contain all the information necessary to reconstitute
82
+ # a public key, this method returns false. This is in contrast to the
83
+ # behavior of OpenSSL::PKey::RSA, which return true for both public and
84
+ # private checks with a private key (since it reconstituted the public
85
+ # key and it is available for use).
86
+ def public_rsa_key?
87
+ # There is an interesting bug I came across, where +public?+ can be true on a private key!
88
+ return !private_rsa_key?
89
+ end
90
+
91
+ # Interpret the conents of the string as an RSA key, and determine if it is private.
92
+ def private_rsa_key?
93
+ key = OpenSSL::PKey::RSA.new(self.to_s)
94
+ return key.private?
95
+ end
96
+
75
97
  end
76
98
 
77
99
  end
@@ -41,6 +41,7 @@ describe "SecureString" do
41
41
  ss.from_base64.should == message[:string]
42
42
  end
43
43
  end
44
+
44
45
  end
45
46
 
46
47
  end
@@ -11,14 +11,14 @@ describe "SecurityString" do
11
11
  it 'should be able to convert to a hex string' do
12
12
  @messages.each do |message|
13
13
  ss = SecureString.new(message[:string])
14
- ss.to_hex.should == message[:hex]
14
+ ss.data_to_hex.should == message[:hex]
15
15
  end
16
16
  end
17
17
 
18
18
  it 'should be able to convert to an int value' do
19
19
  @messages.each do |message|
20
20
  ss = SecureString.new(message[:string])
21
- ss.to_i.should == message[:int]
21
+ ss.data_to_i.should == message[:int]
22
22
  end
23
23
  end
24
24
 
@@ -34,11 +34,19 @@ describe "SecurityString" do
34
34
  @messages.each do |message|
35
35
  s = String.new(message[:string])
36
36
  ss = SecureString.new(message[:string])
37
- ss.inspect.should include(ss.to_hex)
37
+ ss.inspect.should include(ss.data_to_hex)
38
38
  ss.inspect.should_not include(s.to_s)
39
39
  end
40
40
  end
41
41
 
42
+ it 'should return appropriate values on an empty string' do
43
+ ss = SecureString.new('')
44
+
45
+ ss.data_to_hex.should == ''
46
+ ss.data_to_i.should be_kind_of(Integer)
47
+ ss.data_to_i.should == 0
48
+ end
49
+
42
50
  end
43
51
 
44
52
  end
@@ -0,0 +1,48 @@
1
+ require File.join(File.dirname(__FILE__), 'spec_helper')
2
+
3
+ describe "Digest Finder" do
4
+
5
+ it 'should give a list of digests' do
6
+ digests = SecurizeString::DigestFinder.digests
7
+ digests.should be_kind_of(Array)
8
+ digests.should_not be_empty
9
+ end
10
+
11
+ it 'should list both upcase and downcase versions of each digest, just like OpenSSL::Cipher::ciphers' do
12
+ digests = SecurizeString::DigestFinder.digests
13
+ lower = digests.select {|d| d == d.downcase}
14
+ upper = digests.select {|d| d == d.upcase}
15
+
16
+ lower.length == upper.length
17
+ (lower.length * 2) == digests.length
18
+ end
19
+
20
+ it 'should find an OpenSSL::Digest instance for a string' do
21
+ SecurizeString::DigestFinder.find('SHA-256').should == OpenSSL::Digest::SHA256
22
+ SecurizeString::DigestFinder.find('sha-256').should == OpenSSL::Digest::SHA256
23
+ SecurizeString::DigestFinder.find('sha256').should == OpenSSL::Digest::SHA256
24
+ SecurizeString::DigestFinder.find('SHA256').should == OpenSSL::Digest::SHA256
25
+
26
+ lambda{ SecurizeString::DigestFinder.find('foobar') }.should raise_error(ArgumentError)
27
+ end
28
+
29
+ it 'should pass through a valid OpenSSL::Digest instance' do
30
+ SecurizeString::DigestFinder.find(OpenSSL::Digest::SHA256).should == OpenSSL::Digest::SHA256
31
+ end
32
+
33
+ it 'should instantiate an OpenSSL::Digest class' do
34
+ digest_obj = OpenSSL::Digest::SHA256.new
35
+ SecurizeString::DigestFinder.find(digest_obj).should == OpenSSL::Digest::SHA256
36
+ end
37
+
38
+ it 'should instantiate a Digest class' do
39
+ SecurizeString::DigestFinder.find(Digest::SHA256).should == OpenSSL::Digest::SHA256
40
+ end
41
+
42
+ it 'sould convert a Digest instance' do
43
+ digest_obj = Digest::SHA256.new
44
+
45
+ SecurizeString::DigestFinder.find(digest_obj).should == OpenSSL::Digest::SHA256
46
+ end
47
+
48
+ end
@@ -1,5 +1,3 @@
1
- # Make sure to test that the cipher methods are from openssl and not jsut digest?
2
-
3
1
  require File.join(File.dirname(__FILE__), 'spec_helper')
4
2
 
5
3
  describe "SecureString" do
@@ -18,16 +16,16 @@ describe "SecureString" do
18
16
  end
19
17
 
20
18
  it 'should encode as a SecureString' do
21
- @message.to_digest(OpenSSL::Digest::MD5).should be_kind_of(SecureString)
22
- @message.to_digest(OpenSSL::Digest::SHA512).should be_kind_of(SecureString)
19
+ @message.to_digest('MD5').should be_kind_of(SecureString)
20
+ @message.to_digest('SHA-512').should be_kind_of(SecureString)
23
21
  end
24
22
 
25
23
  it 'should contain the raw value, not the hex value' do
26
- md5 = @message.to_digest(OpenSSL::Digest::MD5)
24
+ md5 = @message.to_digest('MD5')
27
25
  md5.should_not == @message_md5_hex
28
26
  md5.to_hex.should == @message_md5_hex
29
27
 
30
- sha512 = @message.to_digest(OpenSSL::Digest::SHA512)
28
+ sha512 = @message.to_digest('SHA-512')
31
29
  sha512.should_not == @message_sha512_hex
32
30
  sha512.to_hex.should == @message_sha512_hex
33
31
  end
@@ -0,0 +1,75 @@
1
+
2
+
3
+ describe "Examples" do
4
+
5
+ it 'should perform the basic usage example' do
6
+
7
+ ss = SecureString.new("Hello World!")
8
+ ss.to_s.should == "Hello World!"
9
+ ss.inspect.should == "<48656c6c6f20576f726c6421>"
10
+
11
+ ss.to_hex.should == "48656c6c6f20576f726c6421"
12
+ ss.to_i.should == 22405534230753928650781647905
13
+ ss.to_base64.should == "SGVsbG8gV29ybGQh"
14
+
15
+ ss1 = SecureString.new(:data, "Hello World!")
16
+ ss2 = SecureString.new(:hex, "48656c6c6f20576f726c6421")
17
+ ss3 = SecureString.new(:int, 22405534230753928650781647905)
18
+ ss4 = SecureString.new(:base64, "SGVsbG8gV29ybGQh")
19
+
20
+ ss1.should == ss
21
+ ss2.should == ss
22
+ ss3.should == ss
23
+ ss4.should == ss
24
+ end
25
+
26
+ it 'should perform the base64 example' do
27
+ SecureString.new("Hello World!").to_base64.should == "SGVsbG8gV29ybGQh"
28
+
29
+ (SecureString.new(:base64, "SGVsbG8gV29ybGQh") == "Hello World!" ).should be_true
30
+ (SecureString.new("SGVsbG8gV29ybGQh") == "Hello World!" ).should be_false
31
+ (SecureString.new("SGVsbG8gV29ybGQh").from_base64 == "Hello World!").should be_true
32
+ end
33
+
34
+ it 'should perform digest example' do
35
+ ss = SecureString.new("Hello World!")
36
+ ss.to_md5.to_hex.should == "ed076287532e86365e841e92bfc50d8c"
37
+ ss.to_sha1.to_hex.should == "2ef7bde608ce5404e97d5f042f95f89f1c232871"
38
+ ss.to_sha256.to_hex.should == "7f83b1657ff1fc53b92dc18148a1d65dfc2d4b1fa3d677284addd200126d9069"
39
+ ss.to_digest(OpenSSL::Digest::SHA512).to_hex.should == "861844d6704e8573fec34d967e20bcfef3d424cf48be04e6dc08f2bd58c729743371015ead891cc3cf1c9d34b49264b510751b1ff9e537937bc46b5d6ff4ecc8"
40
+ end
41
+
42
+ it 'should perform the cipher example' do
43
+ # Generate a random key and initialization vector.
44
+ key, iv = SecureString.aes_keygen
45
+
46
+ # Now encrypt a message:
47
+ message = SecureString.new("Hello World!")
48
+ cipher_text = message.to_aes(key, iv)
49
+
50
+ # Now decrypt the message:
51
+ decoded_text = cipher_text.from_aes(key, iv)
52
+
53
+ decoded_text.should == message
54
+ cipher_text.should_not == message
55
+ end
56
+
57
+ it 'should perform the char encoding example' do
58
+ s = "Resum\u00E9"
59
+ s.encoding.should == Encoding.find("UTF-8")
60
+ s.length.should == 6
61
+ s.bytesize.should == 7
62
+
63
+ s = "Resum\u00E9"
64
+ s.force_encoding('BINARY')
65
+ s.encoding.should == Encoding.find("ASCII-8BIT")
66
+ s.length.should == 7
67
+ s.bytesize.should == 7
68
+
69
+ s = "Resum\u00E9"
70
+ b = s.dup.force_encoding('BINARY')
71
+ (s == b).should be_false
72
+ (s.bytes.to_a == b.bytes.to_a).should be_true
73
+ end
74
+
75
+ end
@@ -61,14 +61,29 @@ describe "SecureString" do
61
61
  end
62
62
  end
63
63
 
64
+ it 'should be able to determine if the key is public or private' do
65
+ pvt_key, pub_key = SecureString.rsa_keygen
66
+
67
+ pvt_key.public_rsa_key?.should be_false
68
+ pvt_key.private_rsa_key?.should be_true
69
+
70
+ pub_key.public_rsa_key?.should be_true
71
+ pub_key.private_rsa_key?.should be_false
72
+ end
73
+
64
74
  end
65
75
 
66
76
  describe "Encryption" do
67
77
 
68
78
  before(:all) do
69
79
  @key_length = 128
70
- @pvt_key = SecureString.new(:hex, "2d2d2d2d2d424547494e205253412050524956415445204b45592d2d2d2d2d0a4d47454341514143455143364c704764426d334a78495048513945345537443141674d424141454345474c62517a6e724a6652525762433365474b3561656b430a4351446d633569312f5669666277494a414d37536b4534554b7750624167682b7831316430564274395149494a4648372f346f784a364d4343474e646e3933330a4a43774d0a2d2d2d2d2d454e44205253412050524956415445204b45592d2d2d2d2d0a")
71
- @pub_key = SecureString.new(:hex, "2d2d2d2d2d424547494e20525341205055424c4943204b45592d2d2d2d2d0a4d426743455143364c704764426d334a78495048513945345537443141674d424141453d0a2d2d2d2d2d454e4420525341205055424c4943204b45592d2d2d2d2d0a")
80
+
81
+ #@pvt_key = SecureString.new(:hex, "2d2d2d2d2d424547494e205253412050524956415445204b45592d2d2d2d2d0a4d47454341514143455143364c704764426d334a78495048513945345537443141674d424141454345474c62517a6e724a6652525762433365474b3561656b430a4351446d633569312f5669666277494a414d37536b4534554b7750624167682b7831316430564274395149494a4648372f346f784a364d4343474e646e3933330a4a43774d0a2d2d2d2d2d454e44205253412050524956415445204b45592d2d2d2d2d0a")
82
+ @pvt_key = SecureString.new("-----BEGIN RSA PRIVATE KEY-----\nMGECAQACEQC6LpGdBm3JxIPHQ9E4U7D1AgMBAAECEGLbQznrJfRRWbC3eGK5aekC\nCQDmc5i1/VifbwIJAM7SkE4UKwPbAgh+x11d0VBt9QIIJFH7/4oxJ6MCCGNdn933\nJCwM\n-----END RSA PRIVATE KEY-----")
83
+
84
+ #@pub_key = SecureString.new(:hex, "2d2d2d2d2d424547494e20525341205055424c4943204b45592d2d2d2d2d0a4d426743455143364c704764426d334a78495048513945345537443141674d424141453d0a2d2d2d2d2d454e4420525341205055424c4943204b45592d2d2d2d2d0a")
85
+ @pub_key = SecureString.new("-----BEGIN RSA PUBLIC KEY-----\nMBgCEQC6LpGdBm3JxIPHQ9E4U7D1AgMBAAE=\n-----END RSA PUBLIC KEY-----")
86
+
72
87
  @message = SecureString.new("Hello")
73
88
  end
74
89
 
@@ -81,7 +96,7 @@ describe "SecureString" do
81
96
 
82
97
  # We cannot independently test encryption because it changes everytime
83
98
  # thanks to padding generation.
84
- it 'should encrypt and decrypy a message' do
99
+ it 'should encrypt and decrypt a message' do
85
100
  encrypted_message = @message.to_rsa(@pub_key)
86
101
  (encrypted_message.bytesize * 8).should == @key_length
87
102
 
@@ -89,6 +104,14 @@ describe "SecureString" do
89
104
  decrypted_message.should == @message
90
105
  end
91
106
 
107
+ it 'should encrypt and decrypt a message swapping pub and pvt keys' do
108
+ encrypted_message = @message.to_rsa(@pvt_key)
109
+ (encrypted_message.bytesize * 8).should == @key_length
110
+
111
+ decrypted_message = encrypted_message.from_rsa(@pub_key)
112
+ decrypted_message.should == @message
113
+ end
114
+
92
115
  end
93
116
 
94
117
 
@@ -102,50 +125,53 @@ describe "SecureString" do
102
125
  @message = SecureString.new("Hello")
103
126
  end
104
127
 
105
- it 'should sign and verify a message using a private key' do
128
+ it 'should do a standard encrypt/sign then verify/decrypt cycle' do
106
129
  # Alice encrypts a message for Bob and signs is.
107
- @encrypted_message = @message.to_rsa(@bob_pub_key)
108
- @signature = @encrypted_message.sign(@alice_pvt_key)
130
+ encrypted_message = @message.to_rsa(@bob_pub_key)
131
+ signature = encrypted_message.sign(@alice_pvt_key)
132
+ encrypted_message.should_not be_empty
133
+ signature.should_not be_empty
109
134
 
110
135
  # Verify it came from Alice.
111
- is_verified = @encrypted_message.verify?(@alice_pub_key, @signature)
136
+ is_verified = encrypted_message.verify?(@alice_pub_key, signature)
112
137
  is_verified.should be_true
113
138
 
114
139
  # Verify it did not come from Bob.
115
- is_verified = @encrypted_message.verify?(@bob_pub_key, @signature)
140
+ is_verified = encrypted_message.verify?(@bob_pub_key, signature)
116
141
  is_verified.should be_false
117
142
 
118
143
  # Bob should now decrypt it
119
- @decrypted_message = @encrypted_message.from_rsa(@bob_pvt_key)
120
- @decrypted_message.should == @message
144
+ decrypted_message = encrypted_message.from_rsa(@bob_pvt_key)
145
+ decrypted_message.should == @message
121
146
  end
122
147
 
123
148
  it 'should default to signing with SHA-256' do
124
149
  encrypted_message = @message.to_rsa(@bob_pub_key)
125
- encrypted_message.sign(@alice_pvt_key).should == encrypted_message.sign(@alice_pvt_key, OpenSSL::Digest::SHA256.new)
150
+ encrypted_message.sign(@alice_pvt_key).should == encrypted_message.sign(@alice_pvt_key, 'SHA-256')
126
151
  end
127
152
 
128
- it 'should allow signing with other digest' do
153
+ it 'should allow signing with other digests' do
129
154
  encrypted_message = @message.to_rsa(@bob_pub_key)
130
- comparison_digest_klass = OpenSSL::Digest::SHA256
131
- [OpenSSL::Digest::SHA512, OpenSSL::Digest::MD5, OpenSSL::Digest::SHA1].each do |digest_klass|
132
- next if digest_klass == comparison_digest_klass
133
- signature = encrypted_message.sign(@alice_pvt_key, digest_klass.new)
134
- signature.should_not == encrypted_message.sign(@alice_pvt_key, comparison_digest_klass.new)
155
+ comparison_digest_method = 'SHA-256'
156
+ ['SHA-512', 'MD5', 'SHA-1'].each do |digest_method|
157
+ next if digest_method == comparison_digest_method
158
+ signature = encrypted_message.sign(@alice_pvt_key, digest_method)
159
+ signature.should_not == encrypted_message.sign(@alice_pvt_key, comparison_digest_method)
135
160
  end
136
161
  end
137
162
 
138
- it 'should allow passing the digest method as an instance or class' do
163
+ it 'should allow passing the digest method as an instance, class, or string' do
139
164
  encrypted_message = @message.to_rsa(@bob_pub_key)
140
165
  [OpenSSL::Digest::SHA512, OpenSSL::Digest::SHA256, OpenSSL::Digest::MD5, OpenSSL::Digest::SHA1].each do |digest_klass|
141
- signature1 = encrypted_message.sign(@alice_pvt_key, digest_klass.new)
142
- signature2 = encrypted_message.sign(@alice_pvt_key, digest_klass)
143
- signature1.should == signature2
166
+ signature1 = encrypted_message.sign(@alice_pvt_key, digest_klass)
167
+ signature2 = encrypted_message.sign(@alice_pvt_key, digest_klass.new)
168
+ signature3 = encrypted_message.sign(@alice_pvt_key, digest_klass.name.split('::').last)
169
+ signature2.should == signature1
170
+ signature3.should == signature1
144
171
  end
145
172
  end
146
173
 
147
174
  it 'should work with Digest scoped digest classes' do
148
- pending "TODO: postponing feature"
149
175
  encrypted_message = @message.to_rsa(@bob_pub_key)
150
176
  signature1 = encrypted_message.sign(@alice_pvt_key, Digest::SHA256)
151
177
  signature2 = encrypted_message.sign(@alice_pvt_key, OpenSSL::Digest::SHA256)
@@ -153,7 +179,6 @@ describe "SecureString" do
153
179
  end
154
180
 
155
181
  it 'should work with Digest scoped digest instances' do
156
- pending "TODO: postponing feature"
157
182
  encrypted_message = @message.to_rsa(@bob_pub_key)
158
183
  signature1 = encrypted_message.sign(@alice_pvt_key, Digest::SHA256.new)
159
184
  signature2 = encrypted_message.sign(@alice_pvt_key, OpenSSL::Digest::SHA256.new)
@@ -1,3 +1,5 @@
1
+ #encoding: UTF-8
2
+
1
3
  require File.join(File.dirname(__FILE__), 'spec_helper')
2
4
 
3
5
  describe "SecureString" do
@@ -56,4 +58,72 @@ describe "SecureString" do
56
58
  newline_count.select {|nl_count| nl_count > 1}.should_not be_empty
57
59
  end
58
60
 
61
+ it 'should implement to_hex' do
62
+ SecureString.instance_methods.should include(:to_hex)
63
+
64
+ @messages.each do |message|
65
+ ss = SecureString.new(message[:string])
66
+ ss.to_hex.should == ss.data_to_hex
67
+ end
68
+ end
69
+
70
+ it 'should implement to_i properly' do
71
+ SecureString.instance_methods.should include(:to_i)
72
+
73
+ @messages.each do |message|
74
+ ss = SecureString.new(message[:string])
75
+ ss.to_i.should == ss.data_to_i
76
+ end
77
+ end
78
+
79
+ it 'should parse hex data with spaces' do
80
+
81
+ data = <<-DATA
82
+ a766a602 b65cffe7 73bcf258 26b322b3 d01b1a97 2684ef53 3e3b4b7f 53fe3762
83
+ 24c08e47 e959b2bc 3b519880 b9286568 247d110f 70f5c5e2 b4590ca3 f55f52fe
84
+ effd4c8f e68de835 329e603c c51e7f02 545410d1 671d108d f5a4000d cf20a439
85
+ 4949d72c d14fbb03 45cf3a29 5dcda89f 998f8755 2c9a58b1 bdc38483 5e477185
86
+ f96e68be bb0025d2 d2b69edf 21724198 f688b41d eb9b4913 fbe696b5 457ab399
87
+ 21e1d759 1f89de84 57e8613c 6c9e3b24 2879d4d8 783b2d9c a9935ea5 26a729c0
88
+ 6edfc501 37e69330 be976012 cc5dfe1c 14c4c68b d1db3ecb 24438a59 a09b5db4
89
+ 35563e0d 8bdf572f 77b53065 cef31f32 dc9dbaa0 4146261e 9994bd5c d0758e3d"
90
+ DATA
91
+
92
+ ss = SecureString.new(:hex, data)
93
+
94
+ # This was taken from a publically published SHA-0 data collision document,
95
+ # so the best way to know that the data is good is to SHA-0 it and see if
96
+ # we get back the value! (http://fr.wikipedia.org/wiki/SHA-0)
97
+ OpenSSL::Digest::SHA.hexdigest(ss).should == "c9f160777d4086fe8095fba58b7e20c228a4006b"
98
+ end
99
+
100
+ describe 'Encodings' do
101
+
102
+ before(:each) do
103
+ @unicode_string = "A resumé for the moose; Eine Zusammenfassung für die Elche; Резюме для лосей; アメリカヘラジカのための概要; Μια περίληψη για τις άλκες; 麋的一份簡歷; Un résumé pour les orignaux."
104
+ end
105
+
106
+ it 'should NOT change the encoding of a string' do
107
+ @unicode_string.encoding.should == Encoding.find("UTF-8")
108
+ ss = SecureString.new(@unicode_string)
109
+ ss.encoding.should == @unicode_string.encoding
110
+ end
111
+
112
+ it 'should NOT change the length to the byte count for UTF-8 encoded strings.' do
113
+ @unicode_string.encoding.should == Encoding.find("UTF-8")
114
+ ss = SecureString.new(@unicode_string)
115
+ ss.length.should < ss.bytesize
116
+ end
117
+
118
+ it 'should allow forced transcoding to binary' do
119
+ ss = SecureString.new(@unicode_string)
120
+
121
+ ss.encoding.should == Encoding.find("UTF-8")
122
+ ss.force_encoding("Binary")
123
+ ss.encoding.should == Encoding.find("ASCII-8BIT")
124
+ @unicode_string.encoding.should == Encoding.find("UTF-8")
125
+ end
126
+
127
+ end
128
+
59
129
  end
metadata CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
5
5
  segments:
6
6
  - 1
7
7
  - 1
8
- - 0
9
- version: 1.1.0
8
+ - 1
9
+ version: 1.1.1
10
10
  platform: ruby
11
11
  authors:
12
12
  - Jeff Reinecke
@@ -14,7 +14,7 @@ autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
16
 
17
- date: 2010-11-04 00:00:00 -07:00
17
+ date: 2010-11-05 00:00:00 -07:00
18
18
  default_executable:
19
19
  dependencies: []
20
20
 
@@ -34,13 +34,16 @@ files:
34
34
  - lib/securize_string/base64_methods.rb
35
35
  - lib/securize_string/binary_string_data_methods.rb
36
36
  - lib/securize_string/cipher_methods.rb
37
+ - lib/securize_string/digest_finder.rb
37
38
  - lib/securize_string/digest_methods.rb
38
39
  - lib/securize_string/rsa_methods.rb
39
40
  - lib/securize_string.rb
40
41
  - spec/base64_methods_spec.rb
41
42
  - spec/binary_string_data_methods_spec.rb
42
43
  - spec/cipher_methods_spec.rb
44
+ - spec/digest_finder_spec.rb
43
45
  - spec/digest_methods_spec.rb
46
+ - spec/example_spec.rb
44
47
  - spec/rsa_methods_spec.rb
45
48
  - spec/secure_string_spec.rb
46
49
  - spec/spec_helper.rb
@@ -61,8 +64,8 @@ required_ruby_version: !ruby/object:Gem::Requirement
61
64
  segments:
62
65
  - 1
63
66
  - 9
64
- - 2
65
- version: 1.9.2
67
+ - 0
68
+ version: 1.9.0
66
69
  required_rubygems_version: !ruby/object:Gem::Requirement
67
70
  none: false
68
71
  requirements: