secure_string 0.9.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -9,6 +9,127 @@ functionality that can be used individually:
9
9
  * Secure string support: Easy methods for RSA encryption, AES encoding, and
10
10
  SHA/MD5 digest hashing, of the data in the strings.
11
11
 
12
+ One of the basic philosophies of SecureString is that it does not override--only
13
+ extends--the feature set of String. However there is one difference that
14
+ was added: +inspect+ is overridden to return the data as a hex-string, rather
15
+ than using the specified character encoding. This does not mean it's value has
16
+ in any way changed, just its presentation. Use +to_s+ to recover the standard
17
+ String version of the value.
18
+
19
+ WARNING: it is important to note that the String method +length+ is not a good
20
+ measure of a byte string's length, as depending on the encoding, it may count
21
+ multibyte characters as a single element. To ensure that you get the byte
22
+ length, use the standard string method +bytesize+.
23
+
24
+ = Examples
25
+
26
+ == Basic Usage
27
+
28
+ Creation of a SecureString from an normal String instance is easy:
29
+
30
+ ss = SecureString.new("Hello World!")
31
+ ss.to_s --> "Hello World!"
32
+ ss.inspect --> "<48656c6c6f20576f726c6421>"
33
+
34
+ Additionally, you can get at the byte data in various ways:
35
+
36
+ ss.to_hex --> "48656c6c6f20576f726c6421"
37
+ ss.to_i --> 22405534230753928650781647905
38
+ ss.to_base64 --> "SGVsbG8gV29ybGQh"
39
+
40
+ One can initialize a SecureString from any of these types like so:
41
+
42
+ ss1 = SecureString.new(:data, "Hello World!")
43
+ ss2 = SecureString.new(:hex, "48656c6c6f20576f726c6421")
44
+ ss3 = SecureString.new(:int, 22405534230753928650781647905)
45
+ ss4 = SecureString.new(:base64, "SGVsbG8gV29ybGQh")
46
+
47
+ ss1 == ss --> true
48
+ ss2 == ss --> true
49
+ ss3 == ss --> true
50
+ ss4 == ss --> true
51
+
52
+ All of these create equal-valued strings to <tt>"HelloWorld!"</tt>.
53
+
54
+ == Base64 Methods Overview
55
+
56
+ The SecureString::Base64Methods module adds +to_base64+, which we've seen:
57
+
58
+ SecureString.new("Hello World!").to_base64 --> "SGVsbG8gV29ybGQh"
59
+
60
+ It also adds +from_base64+, which can decode a Base64 encoded string. The
61
+ following example shows the various ways of decoding Bas64 data:
62
+
63
+ SecureString.new(:base64, "SGVsbG8gV29ybGQh") == "Hello World!" --> true
64
+ SecureString.new("SGVsbG8gV29ybGQh") == "Hello World!" --> false
65
+ SecureString.new("SGVsbG8gV29ybGQh").from_base64 == "Hello World!" --> true
66
+
67
+ == Digest Methods Overview
68
+
69
+ The SecureString::DigestMethods module adds convenience methods for calculating
70
+ cryptographic hash sums for the data in the string. Note that since SecureString
71
+ handles binary data well, the string value returns is NOT the hex string; to get
72
+ the hex digest, simply call to_hex:
73
+
74
+ ss.to_md5.to_hex
75
+ --> "ed076287532e86365e841e92bfc50d8c"
76
+ ss.to_sha1.to_hex
77
+ --> "2ef7bde608ce5404e97d5f042f95f89f1c232871"
78
+ ss.to_sha256.to_hex
79
+ --> "7f83b1657ff1fc53b92dc18148a1d65dfc2d4b1fa3d677284addd200126d9069"
80
+ ss.to_digest(OpenSSL::Digest::SHA512).to_hex
81
+ --> "861844d6704e8573fec34d967e20bcfef3d424cf48be04e6dc08f2bd58c729743371015ead891cc3cf1c9d34b49264b510751b1ff9e537937bc46b5d6ff4ecc8"
82
+
83
+ == RSA Methods Overview
84
+
85
+ The SecureString::RSAMethods module adds convenience methods for RSA key
86
+ generation, encryption, and signing, and verification.
87
+
88
+ The basic features of this module are illustrated on the following worked example:
89
+
90
+ First, alice and bob much each generate their public and private keys. For the
91
+ example, we do it like so:
92
+
93
+ alice_pvt_key, alice_pub_key = SecureString.rsa_keygen
94
+ bob_pvt_key, bob_pub_key = SecureString.rsa_keygen
95
+
96
+ Now, Alice creates a message and encrypts it for Bob and signs it.
97
+
98
+ message = SecureString.new("Hello World")
99
+ encrypted_message = message.to_rsa(bob_pub_key)
100
+ signature = encrypted_message.sign(alice_pvt_key)
101
+
102
+ Alice sends Bob the data in +encrypted_message+ and +signature+. Bob verifies
103
+ the message's signature, and then decrypts it:
104
+
105
+ is_verified = encrypted_message.verify?(alice_pub_key, signature)
106
+ if( is_verified )
107
+ decrypted_message = encrypted_message.from_rsa(bob_pvt_key).to_s
108
+ else
109
+ raise RuntimeError, "This is not from Alice!"
110
+ end
111
+
112
+ The value of Alice's original +message+ variable, and Bob's +decrypted_message+
113
+ should be identical.
114
+
115
+ == Cipher Methods Overview
116
+
117
+ The SecureString::CipherMethods module adds convenience methods for block cipher
118
+ encryption, particularly for the AES-256-CBC block cipher.
119
+
120
+ The following methods illustrate a sample session for the default AES-256-CBC
121
+ cipher:
122
+
123
+ # Generate a random key and initialization vector.
124
+ key, iv = SecureString.aes_keygen
125
+
126
+ # Now encrypt a message:
127
+ message = SecureString.new("Hello World!")
128
+ cipher_text = message.to_aes(key, iv)
129
+
130
+ # Now decrypt the message:
131
+ decoded_text = cipher_text.from_aes(key, iv)
132
+
12
133
  = Contact
13
134
 
14
135
  If you have any questions, comments, concerns, patches, or bugs, you can contact
@@ -21,7 +142,13 @@ or directly via e-mail at:
21
142
  mailto:jeff@paploo.net
22
143
 
23
144
  = Version History
24
-
145
+ [1.0.0 - 2010-Nov-04] Added Tests, Examples, and Bugfixes
146
+ * Added a full suite of spec tests.
147
+ * (FEATURE) Can get a list of supported ciphers.
148
+ * (FEATURE) Auto-determine AES key length in +to_aes+ and +from_aes+.
149
+ * (CHANGE) RSA now defaults to 2048-bit keys instead of just 1024.
150
+ * (FIX) Init from integer works now.
151
+ * (FIX) RSA signatures can take digest classes, not just instances.
25
152
  [0.9.0 - 2010-Nov-03] Initial release.
26
153
  * Feature complete, but lacks spec tests and examples.
27
154
 
@@ -29,6 +156,15 @@ mailto:jeff@paploo.net
29
156
 
30
157
  * Add complete spec tests.
31
158
  * Add examples.
159
+ * Pull out all methods into modules so that it can be an extension of all Strings.
160
+ * Add a +to_ss+ or +to_secure+ method to String for easy conversion.
161
+ * to_digest should be able to take a string that is the algorithm name.
162
+ * Explore how encodings affect the data. What about when finding length? What
163
+ methods should be recommended to users for finding byte-length vs. char length.
164
+ * RSA encoding/decoding should auto-detect if the given key is public or private,
165
+ and use the correct encoding routine? What about key confusion?
166
+ * RSA signature digests: accept Digest hashes, not just OpenSSL::Digest hashes.
167
+
32
168
 
33
169
  = License
34
170
 
data/lib/secure_string.rb CHANGED
@@ -28,7 +28,7 @@ class SecureString < String
28
28
  when :data
29
29
  data = value.to_s
30
30
  when :int
31
- self.send(__method__, :hex, value.to_i.to_s(16))
31
+ data = self.send(__method__, :hex, value.to_i.to_s(16))
32
32
  when :base64
33
33
  data = Base64.decode64(value.to_s)
34
34
  end
@@ -1,12 +1,16 @@
1
1
  require 'base64'
2
2
 
3
3
  class SecureString < String
4
+ # Adds methods for Base64 conversion.
5
+ # See Base64Methods::InstanceMethods for more details.
4
6
  module Base64Methods
5
7
 
6
8
  def self.included(mod)
7
9
  mod.send(:include, InstanceMethods)
8
10
  end
9
11
 
12
+ # Adds instance methods for Base64 support via inclusion of
13
+ # SecureString::Base64Methods to a class.
10
14
  module InstanceMethods
11
15
 
12
16
  # Encodes to Base64. By default, the output is made URL safe, which means all
@@ -1,6 +1,8 @@
1
1
  require 'openssl'
2
2
 
3
3
  class SecureString < String
4
+ # Adds methods for OpenSSL::Cipher support including AES encryption.
5
+ # See CipherMethods::ClassMethods and CipherMethods::InstanceMethods for more details.
4
6
  module CipherMethods
5
7
 
6
8
  def self.included(mod)
@@ -8,12 +10,20 @@ class SecureString < String
8
10
  mod.send(:include, InstanceMethods)
9
11
  end
10
12
 
13
+ # Adds class methods for OpenSSL::Cipher support, including AES encryption,
14
+ # via inclusion of SecureString::CipherMethods into a class.
11
15
  module ClassMethods
12
16
 
17
+ # Returns a list of supported ciphers. These can be passed directly into
18
+ # the cipher methods.
19
+ def supported_ciphers
20
+ return OpenSSL::Cipher.ciphers
21
+ end
22
+
13
23
  # A convenience method for generating random cipher keys and initialization
14
24
  # vectors.
15
25
  def cipher_keygen(cipher_name)
16
- cipher = OpenSSL::Cipher::Cipher.new(cipher_name)
26
+ cipher = OpenSSL::Cipher.new(cipher_name)
17
27
  cipher.encrypt
18
28
  return [cipher.random_key, cipher.random_iv].map {|s| self.new(s)}
19
29
  end
@@ -26,6 +36,8 @@ class SecureString < String
26
36
 
27
37
  end
28
38
 
39
+ # Adds instance methods for OpenSSL::Cipher support, including AES encryption,
40
+ # via inclusion of SecureString::CipherMethods into a class.
29
41
  module InstanceMethods
30
42
 
31
43
  # Given an OpenSSL cipher name, a key, and initialization vector,
@@ -60,13 +72,18 @@ class SecureString < String
60
72
  return self.class.new(msg)
61
73
  end
62
74
 
63
- # Given an AES key and initialization vector, AES encode the data.
64
- def to_aes(key, iv, key_len=256)
75
+ # Given an AES key and initialization vector, AES-CBC encode the data.
76
+ #
77
+ # Note that one normally never wants to use the same key and iv
78
+ # combination on two different messages as this weakens the security.
79
+ def to_aes(key, iv)
80
+ key_len = (key.bytesize * 8)
65
81
  return self.class.new( to_cipher("aes-#{key_len.to_i}-cbc", key, iv) )
66
82
  end
67
83
 
68
- # Given an AES key and init vector, AES decode the data.
69
- def from_aes(key, iv, key_len=256)
84
+ # Given an AES key and init vector, AES-CBC decode the data.
85
+ def from_aes(key, iv)
86
+ key_len = (key.bytesize * 8)
70
87
  return self.class.new( from_cipher("aes-#{key_len.to_i}-cbc", key, iv) )
71
88
  end
72
89
 
@@ -1,12 +1,16 @@
1
1
  require 'openssl'
2
2
 
3
3
  class SecureString < String
4
+ # Adds methods for OpenSSL::Digest support.
5
+ # See DigestMethods::ClassMethods and DigestMethods::InstanceMethods for more details.
4
6
  module DigestMethods
5
7
 
6
8
  def self.included(mod)
7
9
  mod.send(:include, InstanceMethods)
8
10
  end
9
11
 
12
+ # Adds instance methods for OpenSSL::Digest support via inclusion of
13
+ # SecureString::DigestMethods to a class.
10
14
  module InstanceMethods
11
15
 
12
16
  # Returns the digest of the byte string as a SecureString, using the passed OpenSSL object.
@@ -19,6 +23,11 @@ class SecureString < String
19
23
  return to_digest( OpenSSL::Digest::MD5.new )
20
24
  end
21
25
 
26
+ # Returns the SHA1 of the byte string as SecureString
27
+ def to_sha1
28
+ return to_digest( OpenSSL::Digest::SHA1.new )
29
+ end
30
+
22
31
  # Returns the SHA2 of the byte string as a SecureString.
23
32
  #
24
33
  # By default, this uses the 256 bit SHA2, but the optional arugment allows
@@ -1,6 +1,8 @@
1
1
  require 'openssl'
2
2
 
3
3
  class SecureString < String
4
+ # Adds methods for OpenSSL::PKey::RSA support.
5
+ # See RSAMethods::ClassMethods and RSAMethods::InstanceMethods for more details.
4
6
  module RSAMethods
5
7
 
6
8
  def self.included(mod)
@@ -8,15 +10,22 @@ class SecureString < String
8
10
  mod.send(:include, InstanceMethods)
9
11
  end
10
12
 
13
+ # Adds class methods for OpenSSL::PKey::RSA support via inclusion of
14
+ # SecureString::RSAMethods to a class.
11
15
  module ClassMethods
12
16
 
13
17
  # A convenience method for generating random public/private RSA key pairs.
14
- # Defaults to a key length of 1024.
18
+ # Defaults to a key length of 2048, as 1024 is starting to be phased out
19
+ # as the standard for secure communications.
15
20
  #
16
21
  # Returns the private key first, then the public key. Returns them in PEM file
17
22
  # format by default, as this is most useful for portability. DER format can
18
23
  # be explicitly specified with the second argument.
19
- def rsa_keygen(key_len=1024, format = :pem)
24
+ #
25
+ # For advanced usage of keys, instantiate an OpenSSL::PKey::RSA object
26
+ # passing the returned key as the argument to +new+. This will allow
27
+ # introspection of common parameters such as p, q, n, e, and d.
28
+ def rsa_keygen(key_len=2048, format = :pem)
20
29
  private_key_obj = OpenSSL::PKey::RSA.new(key_len.to_i)
21
30
  public_key_obj = private_key_obj.public_key
22
31
  formatting_method = (format == :der ? :to_der : :to_pem)
@@ -25,6 +34,8 @@ class SecureString < String
25
34
 
26
35
  end
27
36
 
37
+ # Adds instance methods for OpenSSL::PKey::RSA support via inclusion of
38
+ # SecureString::RSAMethods to a class.
28
39
  module InstanceMethods
29
40
 
30
41
  # Given an RSA public key, it RSA encrypts the data string.
@@ -46,6 +57,7 @@ class SecureString < String
46
57
  #
47
58
  # By default, signs using SHA256, but another digest object can be given.
48
59
  def sign(private_key, digest_obj=OpenSSL::Digest::SHA256.new)
60
+ digest_obj = (digest_obj.kind_of?(Class) ? digest_obj.new : digest_obj)
49
61
  key = OpenSSL::PKey::RSA.new(private_key)
50
62
  return self.class.new( key.sign(digest_obj, self) )
51
63
  end
@@ -55,6 +67,7 @@ class SecureString < String
55
67
  #
56
68
  # By default, verifies using SHA256, but another digest object can be given.
57
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)
58
71
  key = OpenSSL::PKey::RSA.new(public_key)
59
72
  return key.verify(digest_obj, signature.to_s, self)
60
73
  end
@@ -0,0 +1,46 @@
1
+ require File.join(File.dirname(__FILE__), 'spec_helper')
2
+
3
+ describe "SecureString" do
4
+
5
+ describe "Base64 Methods" do
6
+
7
+ before(:all) do
8
+ @messages = MESSAGES
9
+ end
10
+
11
+ it 'should convert self to Base64; not URL safe' do
12
+ @messages.each do |message|
13
+ ss = SecureString.new(message[:string])
14
+ # First make sure that line feeds are being put in somewhere.
15
+ ss.to_base64(false).should include("\n")
16
+ # Now, compare the data itself (no line-feeds).
17
+ ss.to_base64(false).delete("\n").should == message[:base64].delete("\n")
18
+ end
19
+ end
20
+
21
+ it 'should convert self to Base64; URL safe' do
22
+ @messages.each do |message|
23
+ ss = SecureString.new(message[:string])
24
+ # First make sure that there are no line feeds.
25
+ ss.to_base64(true).should_not include("\n")
26
+ # Now compare the result with the line-feed less expected value.
27
+ ss.to_base64(true).should == message[:base64].delete("\n")
28
+ end
29
+ end
30
+
31
+ it 'should default to URL safe' do
32
+ @messages.each do |message|
33
+ ss = SecureString.new(message[:string])
34
+ ss.to_base64.should == ss.to_base64(true)
35
+ end
36
+ end
37
+
38
+ it 'should convert self from Base64' do
39
+ @messages.each do |message|
40
+ ss = SecureString.new(message[:base64])
41
+ ss.from_base64.should == message[:string]
42
+ end
43
+ end
44
+ end
45
+
46
+ end
@@ -0,0 +1,128 @@
1
+ describe "SecureString" do
2
+
3
+ describe "Cipher Methods" do
4
+
5
+ describe "Keygen" do
6
+
7
+ it 'should provide a keygen helper' do
8
+ SecureString.should respond_to(:cipher_keygen)
9
+ end
10
+
11
+ it 'should provide generated keys as a SecureString class' do
12
+ key, iv = SecureString.cipher_keygen('DES')
13
+
14
+ key.should_not be_nil
15
+ iv.should_not be_nil
16
+
17
+ key.should be_kind_of(SecureString)
18
+ iv.should be_kind_of(SecureString)
19
+ end
20
+
21
+ it 'should provide an AES keygen helper' do
22
+ SecureString.should respond_to(:aes_keygen)
23
+ end
24
+
25
+ it 'should provide generated AES keysas a SecureString class' do
26
+ key, iv = SecureString.aes_keygen
27
+
28
+ key.should_not be_nil
29
+ iv.should_not be_nil
30
+
31
+ key.should be_kind_of(SecureString)
32
+ iv.should be_kind_of(SecureString)
33
+ end
34
+
35
+ it 'should provide AES keys of various bit lengths' do
36
+ [128,192,256].each do |bits|
37
+ key, iv = SecureString.aes_keygen(bits)
38
+ (key.bytesize * 8).should == bits
39
+ end
40
+ end
41
+
42
+ it 'should raise an exception in an invalid AES bit length' do
43
+ lambda {SecureString.aes_keygen(1234)}.should raise_error
44
+ end
45
+
46
+ it 'should default to a 256 bit AES key' do
47
+ key, iv = SecureString.aes_keygen
48
+ (key.bytesize * 8).should == 256
49
+ end
50
+
51
+ it 'should provide a list of supported ciphers' do
52
+ SecureString.should respond_to(:supported_ciphers)
53
+ SecureString.supported_ciphers.should be_kind_of(Array)
54
+ end
55
+
56
+ end
57
+
58
+ describe "Cipher" do
59
+
60
+ before(:all) do
61
+ @cipher = "DES"
62
+ @key = SecureString.new(:hex, "4f42383e091ffc44")
63
+ @iv = SecureString.new(:hex, "0b9299d6c2cb5003")
64
+ @message = SecureString.new("We the People of the United States, in Order to form a more perfect Union, establish Justice, insure domestic Tranquility, provide for the common defence, promote the general Welfare, and secure the Blessings of Liberty to ourselves and our Posterity, do ordain and establish this Constitution for the United States of America.")
65
+ @encoded_message = SecureString.new(:hex, "cfe8245b2c1f3f789b8ab78930c9582d1fead6792d8fe7efd418ba06d7da4e96f8525e8b437cf29af71ec66801c2031292fc17d88f5aaa9c776b3ca048169b48394e05d5ae6cbba5c78461a25bc3d5abc646f5f760e3a159b8448d79eed80a209473ca67536ebf417a24f05cf029e9e3ca5b1fb22e4e03427705d79b622d720c7d64cf3621319581a1a89b4cbb630611eea29cbd2c48caef0cf774ea0218b16d600cb4c025dcae177b702040bd7c62569bbda33f8e775dbadba4154074f482385c56449882efa31b908dfe5be17d8c0d220fd99414a78f6ce3cfee007fbd5dfc7fd50c343e6118d1a9174bb75db7c5adbaa558010f56571d087982ae791960c31041bae08adfa4d1a88f457897e38bd56a7e92234d21c8742fa577878b2d65500877d2f910d1fdbb5460afa73642778de8bd4442981baa93a481482f1cfb90fa85025ddf8588ecc7")
66
+ end
67
+
68
+ it 'should encode to a SecureString' do
69
+ @message.to_cipher(@cipher, @key, @iv).should be_kind_of(SecureString)
70
+ end
71
+
72
+ it 'should encode with a given cipher and key' do
73
+ encoded_message = @message.to_cipher(@cipher, @key, @iv)
74
+ encoded_message.should == @encoded_message
75
+ end
76
+
77
+ it 'should decode to a secure string' do
78
+ @encoded_message.from_cipher(@cipher, @key, @iv).should be_kind_of(SecureString)
79
+ end
80
+
81
+ it 'should decode with a given cipher and key' do
82
+ decoded_message = @encoded_message.from_cipher(@cipher, @key, @iv)
83
+ decoded_message.should == @message
84
+ end
85
+
86
+ end
87
+
88
+ describe "AES" do
89
+
90
+ before(:all) do
91
+ @key = SecureString.new(:hex, "83f8577c1bc406e85ceeebc166c9fd4d087670de792b0d957c58f1beae6fb514")
92
+ @iv = SecureString.new(:hex, "778c1086b5daf809e7abadde6995219d")
93
+ @message = SecureString.new("We the People of the United States, in Order to form a more perfect Union, establish Justice, insure domestic Tranquility, provide for the common defence, promote the general Welfare, and secure the Blessings of Liberty to ourselves and our Posterity, do ordain and establish this Constitution for the United States of America.")
94
+ @encoded_message = SecureString.new(:hex, "1fee4d01d33daf50915dc4b7aaf8b536f3b19ee9798b21200d475925460fd3aabc581d89e560b7b826e6017e02911687425d9c4a781d8e03a9d75dab5a85191f86b11fb74063133c3952201a8f2089afb0e298e29fe8becbe93ce110073b9abb968a268857aaabd94caa760fa402ce803f8643ad8870e77714b093e9ea09a4ad9d7e056836939f614a61f6b3e09057bc05f13432aa53cfc7de59d41a121f9fcb7c51825da2615c48debff6ed0fbaa0c85594aef54e11b73b8766f6e56d2cf7488909c14272e846cccca0008599bae334c5d404c122d286dcf04eec7b7711978686a66182f53e569297b91c25100ecfdad1f02de444c4de8f9d04067e885a2a17cad707b51ea8c2e8a15051138de617f8864cca8a4d201246a97b95cee5f78f742aace79629e03498f63b6385cff64d53a0425f7f52c6a8ba65771e043590b61191804fe91760e617412d842831928e57")
95
+ end
96
+
97
+ it 'should encode to a SecureString' do
98
+ @message.to_aes(@key, @iv).should be_kind_of(SecureString)
99
+ end
100
+
101
+ it 'should encode with a key' do
102
+ encoded_message = @message.to_aes(@key, @iv)
103
+ encoded_message.should == @encoded_message
104
+ end
105
+
106
+ it 'should decode to a secure string' do
107
+ @encoded_message.from_aes(@key, @iv).should be_kind_of(SecureString)
108
+ end
109
+
110
+ it 'should decode with a key' do
111
+ decoded_message = @encoded_message.from_aes(@key, @iv)
112
+ decoded_message.should == @message
113
+ end
114
+
115
+ it 'should encode and decode from a non 256 bit key' do
116
+ key, iv = SecureString.aes_keygen(128)
117
+ encoded_message = @message.to_aes(key, iv)
118
+ decoded_message = encoded_message.from_aes(key, iv)
119
+
120
+ encoded_message.should_not == decoded_message
121
+ decoded_message.should == @message
122
+ end
123
+
124
+ end
125
+
126
+ end
127
+
128
+ end
@@ -0,0 +1,121 @@
1
+ # Make sure to test that the cipher methods are from openssl and not jsut digest?
2
+
3
+ require File.join(File.dirname(__FILE__), 'spec_helper')
4
+
5
+ describe "SecureString" do
6
+
7
+ describe "Digest Methods" do
8
+
9
+ describe "to_digest" do
10
+
11
+ before(:all) do
12
+ @openssl_digest_class_sample = [OpenSSL::Digest::MD5, OpenSSL::Digest::SHA1, OpenSSL::Digest::SHA256, OpenSSL::Digest::SHA512]
13
+ @digest_class_sample = [Digest::MD5, Digest::SHA1, Digest::SHA256, Digest::SHA512]
14
+
15
+ @message = SecureString.new("Hello World!")
16
+ @message_md5_hex = "ed076287532e86365e841e92bfc50d8c"
17
+ @message_sha512_hex = "861844d6704e8573fec34d967e20bcfef3d424cf48be04e6dc08f2bd58c729743371015ead891cc3cf1c9d34b49264b510751b1ff9e537937bc46b5d6ff4ecc8"
18
+ end
19
+
20
+ 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)
23
+ end
24
+
25
+ it 'should contain the raw value, not the hex value' do
26
+ md5 = @message.to_digest(OpenSSL::Digest::MD5)
27
+ md5.should_not == @message_md5_hex
28
+ md5.to_hex.should == @message_md5_hex
29
+
30
+ sha512 = @message.to_digest(OpenSSL::Digest::SHA512)
31
+ sha512.should_not == @message_sha512_hex
32
+ sha512.to_hex.should == @message_sha512_hex
33
+ end
34
+
35
+ it 'should take an OpenSSL::Digest class' do
36
+ @openssl_digest_class_sample.each do |klass|
37
+ @message.to_digest(klass).to_hex.should == klass.hexdigest(@message)
38
+ end
39
+ end
40
+
41
+ it 'should take an OpenSSL::Digest instance' do
42
+ @openssl_digest_class_sample.each do |klass|
43
+ @message.to_digest(klass.new).to_hex.should == klass.hexdigest(@message)
44
+ end
45
+ end
46
+
47
+ it 'should take a Digest class' do
48
+ @digest_class_sample.each do |klass|
49
+ @message.to_digest(klass).to_hex.should == klass.hexdigest(@message)
50
+ end
51
+ end
52
+
53
+ it 'should take a Digest instance' do
54
+ @digest_class_sample.each do |klass|
55
+ @message.to_digest(klass.new).to_hex.should == klass.hexdigest(@message)
56
+ end
57
+ end
58
+
59
+ end
60
+
61
+
62
+ describe "Convenience Methods" do
63
+
64
+ before(:all) do
65
+ @message = SecureString.new("Hello World!")
66
+
67
+ @hex_digests = {
68
+ :md5 => "ed076287532e86365e841e92bfc50d8c",
69
+ :sha1 => "2ef7bde608ce5404e97d5f042f95f89f1c232871",
70
+ :sha256 => "7f83b1657ff1fc53b92dc18148a1d65dfc2d4b1fa3d677284addd200126d9069",
71
+ :sha512 => "861844d6704e8573fec34d967e20bcfef3d424cf48be04e6dc08f2bd58c729743371015ead891cc3cf1c9d34b49264b510751b1ff9e537937bc46b5d6ff4ecc8"
72
+ }
73
+ end
74
+
75
+ it 'should MD5' do
76
+ @message.should respond_to(:to_md5)
77
+ digest = @message.to_md5
78
+ digest.should be_kind_of(SecureString)
79
+ digest.to_hex.should == @hex_digests[:md5]
80
+ end
81
+
82
+ it 'should SHA1' do
83
+ @message.should respond_to(:to_sha1)
84
+ digest = @message.to_sha1
85
+ digest.should be_kind_of(SecureString)
86
+ digest.to_hex.should == @hex_digests[:sha1]
87
+ end
88
+
89
+ it 'should SHA2-256' do
90
+ @message.should respond_to(:to_sha256)
91
+ digest = @message.to_sha256
92
+ digest.should be_kind_of(SecureString)
93
+ digest.to_hex.should == @hex_digests[:sha256]
94
+ end
95
+
96
+ it 'should SHA2=512' do
97
+ @message.should respond_to(:to_sha512)
98
+ digest = @message.to_sha512
99
+ digest.should be_kind_of(SecureString)
100
+ digest.to_hex.should == @hex_digests[:sha512]
101
+ end
102
+
103
+ it 'should SHA2' do
104
+ @message.should respond_to(:to_sha2)
105
+ digest = @message.to_sha2
106
+ digest.should be_kind_of(SecureString)
107
+ digest.should == @message.to_sha2(256)
108
+
109
+ digest = @message.to_sha2(256)
110
+ digest.should_not == @hex_digests[:sha256]
111
+ digest.to_hex.should == @hex_digests[:sha256]
112
+
113
+ digest = @message.to_sha2(512)
114
+ digest.should_not == @hex_digests[:sha512]
115
+ digest.to_hex.should == @hex_digests[:sha512]
116
+ end
117
+
118
+ end
119
+ end
120
+
121
+ end
@@ -0,0 +1,166 @@
1
+ describe "SecureString" do
2
+
3
+ describe "RSA Methods" do
4
+
5
+ describe "Keygen" do
6
+
7
+ it 'should generate a public/private key pair' do
8
+ SecureString.should respond_to(:rsa_keygen)
9
+ end
10
+
11
+ it 'should generate key pairs of varying length' do
12
+ [256, 512, 1024, 2048].each do |bits|
13
+ pvt_key, pub_key = SecureString.rsa_keygen(bits)
14
+ [pvt_key, pub_key].each do |key|
15
+ key_obj = OpenSSL::PKey::RSA.new(key)
16
+ # The bit length of the key is the bit length of the modulus, n.
17
+ # It turns out that an OpenSSL::PKey::RSA object returns an
18
+ # OpenSSL::BN object when you get the modulus, which has a nice
19
+ # little method for getting the number of bytes it is long.
20
+ key_length_in_bits = key_obj.n.num_bytes * 8
21
+ key_length_in_bits.should == bits
22
+ end
23
+ end
24
+ end
25
+
26
+ it 'should default to a 2048 bit key' do
27
+ pvt_key, pub_key = SecureString.rsa_keygen
28
+ [pvt_key, pub_key].each do |key|
29
+ key_obj = OpenSSL::PKey::RSA.new(key)
30
+ # The bit length of the key is the bit length of the modulus, n.
31
+ # It turns out that an OpenSSL::PKey::RSA object returns an
32
+ # OpenSSL::BN object when you get the modulus, which has a nice
33
+ # little method for getting the number of bytes it is long.
34
+ key_length_in_bits = key_obj.n.num_bytes * 8
35
+ key_length_in_bits.should == 2048
36
+ end
37
+ end
38
+
39
+ it 'should return key pairs as SecureString instances' do
40
+ pvt_key, pub_key = SecureString.rsa_keygen
41
+ [pvt_key, pub_key].each do |key|
42
+ key.should be_kind_of(SecureString)
43
+ end
44
+ end
45
+
46
+ it 'should support both pem and der formats' do
47
+ [:pem, :der].each do |format|
48
+ pvt_key, pub_key = SecureString.rsa_keygen(512, format)
49
+ [pvt_key, pub_key].each do |key|
50
+ key_obj = OpenSSL::PKey::RSA.new(key)
51
+ key.should == key_obj.send("to_#{format}".to_sym)
52
+ end
53
+ end
54
+ end
55
+
56
+ it 'should default to pem format' do
57
+ pvt_key, pub_key = SecureString.rsa_keygen
58
+ [pvt_key, pub_key].each do |key|
59
+ key_obj = OpenSSL::PKey::RSA.new(key)
60
+ key.should == key_obj.to_pem
61
+ end
62
+ end
63
+
64
+ end
65
+
66
+ describe "Encryption" do
67
+
68
+ before(:all) do
69
+ @key_length = 128
70
+ @pvt_key = SecureString.new(:hex, "2d2d2d2d2d424547494e205253412050524956415445204b45592d2d2d2d2d0a4d47454341514143455143364c704764426d334a78495048513945345537443141674d424141454345474c62517a6e724a6652525762433365474b3561656b430a4351446d633569312f5669666277494a414d37536b4534554b7750624167682b7831316430564274395149494a4648372f346f784a364d4343474e646e3933330a4a43774d0a2d2d2d2d2d454e44205253412050524956415445204b45592d2d2d2d2d0a")
71
+ @pub_key = SecureString.new(:hex, "2d2d2d2d2d424547494e20525341205055424c4943204b45592d2d2d2d2d0a4d426743455143364c704764426d334a78495048513945345537443141674d424141453d0a2d2d2d2d2d454e4420525341205055424c4943204b45592d2d2d2d2d0a")
72
+ @message = SecureString.new("Hello")
73
+ end
74
+
75
+ it 'should encrypt and decrypt to a SecureString' do
76
+ encrypted_message = @message.to_rsa(@pub_key)
77
+ encrypted_message.should be_kind_of(SecureString)
78
+ decrypted_message = encrypted_message.from_rsa(@pvt_key)
79
+ decrypted_message.should be_kind_of(SecureString)
80
+ end
81
+
82
+ # We cannot independently test encryption because it changes everytime
83
+ # thanks to padding generation.
84
+ it 'should encrypt and decrypy a message' do
85
+ encrypted_message = @message.to_rsa(@pub_key)
86
+ (encrypted_message.bytesize * 8).should == @key_length
87
+
88
+ decrypted_message = encrypted_message.from_rsa(@pvt_key)
89
+ decrypted_message.should == @message
90
+ end
91
+
92
+ end
93
+
94
+
95
+ describe "Verification" do
96
+
97
+ before(:all) do
98
+ @key_length = 1024
99
+ @alice_pvt_key, @alice_pub_key = SecureString.rsa_keygen(@key_length)
100
+ @bob_pvt_key, @bob_pub_key = SecureString.rsa_keygen(@key_length)
101
+
102
+ @message = SecureString.new("Hello")
103
+ end
104
+
105
+ it 'should sign and verify a message using a private key' do
106
+ # 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)
109
+
110
+ # Verify it came from Alice.
111
+ is_verified = @encrypted_message.verify?(@alice_pub_key, @signature)
112
+ is_verified.should be_true
113
+
114
+ # Verify it did not come from Bob.
115
+ is_verified = @encrypted_message.verify?(@bob_pub_key, @signature)
116
+ is_verified.should be_false
117
+
118
+ # Bob should now decrypt it
119
+ @decrypted_message = @encrypted_message.from_rsa(@bob_pvt_key)
120
+ @decrypted_message.should == @message
121
+ end
122
+
123
+ it 'should default to signing with SHA-256' do
124
+ 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)
126
+ end
127
+
128
+ it 'should allow signing with other digest' do
129
+ 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)
135
+ end
136
+ end
137
+
138
+ it 'should allow passing the digest method as an instance or class' do
139
+ encrypted_message = @message.to_rsa(@bob_pub_key)
140
+ [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
144
+ end
145
+ end
146
+
147
+ it 'should work with Digest scoped digest classes' do
148
+ pending "TODO: postponing feature"
149
+ encrypted_message = @message.to_rsa(@bob_pub_key)
150
+ signature1 = encrypted_message.sign(@alice_pvt_key, Digest::SHA256)
151
+ signature2 = encrypted_message.sign(@alice_pvt_key, OpenSSL::Digest::SHA256)
152
+ signature1.should == signature2
153
+ end
154
+
155
+ it 'should work with Digest scoped digest instances' do
156
+ pending "TODO: postponing feature"
157
+ encrypted_message = @message.to_rsa(@bob_pub_key)
158
+ signature1 = encrypted_message.sign(@alice_pvt_key, Digest::SHA256.new)
159
+ signature2 = encrypted_message.sign(@alice_pvt_key, OpenSSL::Digest::SHA256.new)
160
+ signature1.should == signature2
161
+ end
162
+
163
+ end
164
+
165
+ end
166
+ end
@@ -0,0 +1,90 @@
1
+ require File.join(File.dirname(__FILE__), 'spec_helper')
2
+
3
+ describe "SecureString" do
4
+
5
+ before(:all) do
6
+ @messages = MESSAGES
7
+ end
8
+
9
+ it 'should be a subclass of String' do
10
+ (SecureString < String).should be_true
11
+ end
12
+
13
+ it 'should initialize like a string by default.' do
14
+ @messages.each do |message|
15
+ s = String.new(message[:string])
16
+ ss = SecureString.new(message[:string])
17
+ ss.should == s
18
+ end
19
+ end
20
+
21
+ it 'should initialize from hex' do
22
+ @messages.each do |message|
23
+ ss = SecureString.new(:hex, message[:hex])
24
+ ss.should == message[:string]
25
+
26
+ ss = SecureString.new(:hex, message[:hex].upcase)
27
+ ss.should == message[:string]
28
+ end
29
+ end
30
+
31
+ it 'should initialize from data' do
32
+ @messages.each do |message|
33
+ ss = SecureString.new(:data, message[:string])
34
+ ss.should == message[:string]
35
+ end
36
+ end
37
+
38
+ it 'should initialize from int' do
39
+ @messages.each do |message|
40
+ ss = SecureString.new(:int, message[:int])
41
+ ss.should == message[:string]
42
+ end
43
+ end
44
+
45
+ it 'should initialize from Base64' do
46
+ @messages.each do |message|
47
+ ss = SecureString.new(:base64, message[:base64])
48
+ ss.should == message[:string]
49
+ end
50
+
51
+ # We also want to make sure non newline terminated, and multi-line messages
52
+ # work right. The sample data should contain at least one with multiple
53
+ # linefeeds, and one with no linefeeds.
54
+ newline_count = @messages.map {|message| message[:base64].delete("^\n").length}
55
+ newline_count.should include(0)
56
+ newline_count.select {|nl_count| nl_count > 1}.should_not be_empty
57
+ end
58
+
59
+ it 'should be able to convert to a hex string' do
60
+ @messages.each do |message|
61
+ ss = SecureString.new(message[:string])
62
+ ss.to_hex.should == message[:hex]
63
+ end
64
+ end
65
+
66
+ it 'should be able to convert to an int value' do
67
+ @messages.each do |message|
68
+ ss = SecureString.new(message[:string])
69
+ ss.to_i.should == message[:int]
70
+ end
71
+ end
72
+
73
+ it 'should output like a string for to_s' do
74
+ @messages.each do |message|
75
+ s = String.new(message[:string])
76
+ ss = SecureString.new(message[:string])
77
+ ss.to_s.should == s.to_s
78
+ end
79
+ end
80
+
81
+ it 'should output the hex value with inspect' do
82
+ @messages.each do |message|
83
+ s = String.new(message[:string])
84
+ ss = SecureString.new(message[:string])
85
+ ss.inspect.should include(ss.to_hex)
86
+ ss.inspect.should_not include(s.to_s)
87
+ end
88
+ end
89
+
90
+ end
data/spec/spec_helper.rb CHANGED
@@ -1,4 +1,20 @@
1
1
  # Add the lib dir to the load path.
2
2
  $LOAD_PATH.unshift File.join(File.dirname(__FILE__), '..', 'lib')
3
3
  # Require the main require file.
4
- require File.basename(File.expand_path(File.join(File.dirname(__FILE__),'..')))
4
+ require File.basename(File.expand_path(File.join(File.dirname(__FILE__),'..')))
5
+
6
+ MESSAGES = [
7
+ {
8
+ :string => "Hello",
9
+ :hex => "48656c6c6f",
10
+ :int => 310939249775,
11
+ :base64 => "SGVsbG8="
12
+ },
13
+
14
+ {
15
+ :string => "This is a test of the emergency broadcast system; this is only a test.",
16
+ :hex => "5468697320697320612074657374206f662074686520656d657267656e63792062726f6164636173742073797374656d3b2074686973206973206f6e6c79206120746573742e",
17
+ :int => 1244344095146357680190496293767338268850834164562379171846588371816488740307922111470765515885864931093899586331709567338989540039042962957732585272476408412061178229806,
18
+ :base64 => "VGhpcyBpcyBhIHRlc3Qgb2YgdGhlIGVtZXJnZW5jeSBicm9hZGNhc3Qgc3lz\ndGVtOyB0aGlzIGlzIG9ubHkgYSB0ZXN0Lg==\n"
19
+ }
20
+ ].freeze
metadata CHANGED
@@ -3,10 +3,10 @@ name: secure_string
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease: false
5
5
  segments:
6
+ - 1
6
7
  - 0
7
- - 9
8
8
  - 0
9
- version: 0.9.0
9
+ version: 1.0.0
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-03 00:00:00 -07:00
17
+ date: 2010-11-04 00:00:00 -07:00
18
18
  default_executable:
19
19
  dependencies: []
20
20
 
@@ -35,6 +35,11 @@ files:
35
35
  - lib/secure_string/digest_methods.rb
36
36
  - lib/secure_string/rsa_methods.rb
37
37
  - lib/secure_string.rb
38
+ - spec/base64_methods_spec.rb
39
+ - spec/cipher_methods_spec.rb
40
+ - spec/digest_methods_spec.rb
41
+ - spec/rsa_methods_spec.rb
42
+ - spec/secure_string_spec.rb
38
43
  - spec/spec_helper.rb
39
44
  has_rdoc: true
40
45
  homepage: http://www.github.com/paploo/secure_string