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.
- data/README.rdoc +72 -11
- data/lib/secure_string.rb +17 -0
- data/lib/securize_string/binary_string_data_methods.rb +7 -18
- data/lib/securize_string/cipher_methods.rb +4 -4
- data/lib/securize_string/digest_finder.rb +56 -0
- data/lib/securize_string/digest_methods.rb +21 -7
- data/lib/securize_string/rsa_methods.rb +34 -12
- data/spec/base64_methods_spec.rb +1 -0
- data/spec/binary_string_data_methods_spec.rb +11 -3
- data/spec/digest_finder_spec.rb +48 -0
- data/spec/digest_methods_spec.rb +4 -6
- data/spec/example_spec.rb +75 -0
- data/spec/rsa_methods_spec.rb +48 -23
- data/spec/secure_string_spec.rb +70 -0
- metadata +8 -5
data/README.rdoc
CHANGED
@@ -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
|
-
|
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(
|
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.
|
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
|
-
*
|
197
|
-
|
198
|
-
|
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
|
data/lib/secure_string.rb
CHANGED
@@ -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
|
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
|
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
|
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
|
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
|
-
|
60
|
-
|
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
|
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
|
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
|
17
|
-
|
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(
|
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(
|
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
|
-
|
38
|
-
return to_digest(
|
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(
|
46
|
-
key = OpenSSL::PKey::RSA.new(
|
47
|
-
|
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(
|
52
|
-
key = OpenSSL::PKey::RSA.new(
|
53
|
-
|
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,
|
59
|
-
|
60
|
-
|
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
|
69
|
-
|
70
|
-
|
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
|
data/spec/base64_methods_spec.rb
CHANGED
@@ -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.
|
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.
|
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.
|
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
|
data/spec/digest_methods_spec.rb
CHANGED
@@ -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(
|
22
|
-
@message.to_digest(
|
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(
|
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(
|
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
|
data/spec/rsa_methods_spec.rb
CHANGED
@@ -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
|
-
|
71
|
-
|
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
|
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
|
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
|
-
|
108
|
-
|
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 =
|
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 =
|
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
|
-
|
120
|
-
|
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,
|
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
|
153
|
+
it 'should allow signing with other digests' do
|
129
154
|
encrypted_message = @message.to_rsa(@bob_pub_key)
|
130
|
-
|
131
|
-
[
|
132
|
-
next if
|
133
|
-
signature = encrypted_message.sign(@alice_pvt_key,
|
134
|
-
signature.should_not == encrypted_message.sign(@alice_pvt_key,
|
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
|
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
|
142
|
-
signature2 = encrypted_message.sign(@alice_pvt_key, digest_klass)
|
143
|
-
|
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)
|
data/spec/secure_string_spec.rb
CHANGED
@@ -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
|
-
-
|
9
|
-
version: 1.1.
|
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-
|
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
|
-
-
|
65
|
-
version: 1.9.
|
67
|
+
- 0
|
68
|
+
version: 1.9.0
|
66
69
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
67
70
|
none: false
|
68
71
|
requirements:
|