opentoken 0.2.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+ source "http://rubygems.org"
2
+
3
+ gem "activesupport", "~> 3.0.3"
4
+ gem "i18n", ">= 0"
5
+
6
+ group :development do
7
+ gem "shoulda", ">= 0"
8
+ gem "timecop", '>=0.3.4'
9
+ gem "bundler", "~> 1.0.0"
10
+ gem "jeweler", "~> 1.5.2"
11
+ gem "rcov", ">= 0"
12
+ end
data/README.rdoc CHANGED
@@ -4,6 +4,14 @@ Parse encrypted opentoken properties
4
4
 
5
5
  see http://www.pingidentity.com/opentoken
6
6
 
7
+ == Usage
8
+
9
+ #configure decryption with shared key
10
+ OpenToken.password = 'shared_secret_to_decrypt'
11
+
12
+ #decrypt opentoken into hash of attributes
13
+ attributes = OpenToken.parse opentoken
14
+
7
15
  == Note on Patches/Pull Requests
8
16
 
9
17
  * Fork the project.
data/Rakefile CHANGED
@@ -1,23 +1,30 @@
1
1
  require 'rubygems'
2
+ require 'bundler'
3
+ begin
4
+ Bundler.setup(:default, :development)
5
+ rescue Bundler::BundlerError => e
6
+ $stderr.puts e.message
7
+ $stderr.puts "Run `bundle install` to install missing gems"
8
+ exit e.status_code
9
+ end
2
10
  require 'rake'
3
11
 
4
- begin
5
- require 'jeweler'
6
- Jeweler::Tasks.new do |gem|
7
- gem.name = "opentoken"
8
- gem.summary = %Q{ruby implementation of the opentoken specification}
9
- gem.description = %Q{parse opentoken properties passed for Single Signon requests}
10
- gem.email = "ryan@socialcast.com"
11
- gem.homepage = "http://github.com/wireframe/opentoken"
12
- gem.authors = ["Ryan Sonnek"]
13
- gem.add_development_dependency "shoulda", ">= 0"
14
- gem.add_development_dependency "timecop", ">=0.3.4"
15
- # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
16
- end
17
- Jeweler::RubygemsDotOrgTasks.new
18
- rescue LoadError
19
- puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
12
+ require 'jeweler'
13
+ Jeweler::Tasks.new do |gem|
14
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
15
+ gem.name = "opentoken"
16
+ gem.homepage = "http://github.com/wireframe/opentoken"
17
+ gem.license = "MIT"
18
+ gem.summary = %Q{ruby implementation of the opentoken specification}
19
+ gem.description = %Q{parse opentoken properties passed for Single Signon requests}
20
+ gem.email = "ryan@codecrate.com"
21
+ gem.authors = ["Ryan Sonnek"]
22
+ # Include your dependencies below. Runtime dependencies are required when using your gem,
23
+ # and development dependencies are only needed for development (ie running rake tasks, tests, etc)
24
+ # gem.add_runtime_dependency 'jabber4r', '> 0.1'
25
+ # gem.add_development_dependency 'rspec', '> 1.2.3'
20
26
  end
27
+ Jeweler::RubygemsDotOrgTasks.new
21
28
 
22
29
  require 'rake/testtask'
23
30
  Rake::TestTask.new(:test) do |test|
@@ -26,21 +33,13 @@ Rake::TestTask.new(:test) do |test|
26
33
  test.verbose = true
27
34
  end
28
35
 
29
- begin
30
- require 'rcov/rcovtask'
31
- Rcov::RcovTask.new do |test|
32
- test.libs << 'test'
33
- test.pattern = 'test/**/test_*.rb'
34
- test.verbose = true
35
- end
36
- rescue LoadError
37
- task :rcov do
38
- abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
39
- end
36
+ require 'rcov/rcovtask'
37
+ Rcov::RcovTask.new do |test|
38
+ test.libs << 'test'
39
+ test.pattern = 'test/**/test_*.rb'
40
+ test.verbose = true
40
41
  end
41
42
 
42
- task :test => :check_dependencies
43
-
44
43
  task :default => :test
45
44
 
46
45
  require 'rake/rdoctask'
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.2.1
1
+ 1.0.0
data/lib/opentoken.rb CHANGED
@@ -4,13 +4,13 @@ require 'digest/sha1'
4
4
  require 'zlib'
5
5
  require 'stringio'
6
6
  require 'cgi'
7
+ require File.join(File.dirname(__FILE__), 'opentoken', 'token')
7
8
  require File.join(File.dirname(__FILE__), 'opentoken', 'key_value_serializer')
8
9
  require File.join(File.dirname(__FILE__), 'opentoken', 'password_key_generator')
9
10
 
10
- class OpenToken
11
- class TokenExpiredError < StandardError; end
11
+ module OpenToken
12
+ class TokenInvalidError < StandardError; end
12
13
 
13
- DEBUG = false
14
14
  CIPHER_NULL = 0
15
15
  CIPHER_AES_256_CBC = 1
16
16
  CIPHER_AES_128_CBC = 2
@@ -37,123 +37,128 @@ class OpenToken
37
37
  }
38
38
  }
39
39
 
40
- def initialize(token, options = {})
41
- #ruby 1.9 has Base64.urlsafe_decode64 which can be used instead of gsubbing '_' and '-'
42
- string = (token || '').gsub('*', '=').gsub('_', '/').gsub('-', '+')
43
- data = Base64.decode64(string)
44
- inspect_binary_string 'DATA', data
45
-
46
- #header: should be OTK
47
- header = data[0..2]
48
- raise "Invalid token header: #{header}" unless header == 'OTK'
49
-
50
- #version: should == 1
51
- version = data[3]
52
- raise "Unsupported token version: #{version}" unless version == 1
53
-
54
- #cipher suite identifier
55
- cipher_suite = data[4]
56
- cipher = CIPHERS[cipher_suite]
57
- raise "Unknown cipher suite: #{cipher_suite}" if cipher.nil?
58
-
59
- #SHA-1 HMAC
60
- payload_hmac = data[5..24]
61
- inspect_binary_string "PAYLOAD HMAC [5..24]", payload_hmac
62
-
63
- #Initialization Vector (iv)
64
- iv_length = data[25]
65
- iv_end = [26, 26 + iv_length - 1].max
66
- iv = data[26..iv_end]
67
- inspect_binary_string "IV [26..#{iv_end}]", iv
68
- raise "Cipher expects iv length of #{cipher[:iv_length]} and was: #{iv_length}" unless iv_length == cipher[:iv_length]
69
-
70
- #key (not currently used)
71
- key_length = data[iv_end + 1]
72
- key_end = iv_end + 1
73
- raise "Token key embedding is not currently supported" unless key_length == 0
74
-
75
- #payload
76
- payload_length = data[(key_end + 1)..(key_end + 2)].unpack('n').first
77
- payload_offset = key_end + 3
78
- encrypted_payload = data[payload_offset..(data.length - 1)]
79
- raise "Payload length is #{encrypted_payload.length} and was expected to be #{payload_length}" unless encrypted_payload.length == payload_length
80
- inspect_binary_string "ENCRYPTED PAYLOAD [#{payload_offset}..#{data.length - 1}]", encrypted_payload
81
-
82
- key = PasswordKeyGenerator.generate(options[:password], cipher)
83
- inspect_binary_string 'KEY', key
84
-
85
- compressed_payload = decrypt_payload(encrypted_payload, cipher, key, iv)
86
- inspect_binary_string 'COMPRESSED PAYLOAD', compressed_payload
87
-
88
- #decompress the payload
89
- #see http://stackoverflow.com/questions/1361892/how-to-decompress-gzip-data-in-ruby
90
- unparsed_payload = begin
91
- Zlib::Inflate.inflate(compressed_payload)
92
- rescue Zlib::BufError
93
- Zlib::Inflate.new(-Zlib::MAX_WBITS).inflate(compressed_payload[2, compressed_payload.size])
40
+ class << self
41
+ @@debug = nil
42
+ def debug=(flag)
43
+ @@debug = flag
94
44
  end
95
- puts 'EXPANDED PAYLOAD', unparsed_payload if DEBUG
96
-
97
- #validate payload hmac
98
- mac = "0x01".hex.chr
99
- mac += cipher_suite.chr
100
- mac += iv
101
- mac += key if key_length > 0 #key embedding is not currently supported
102
- mac += unparsed_payload
103
- hash = OpenSSL::HMAC.digest(PasswordKeyGenerator::SHA1_DIGEST, key, mac)
104
- if (hash <=> payload_hmac) != 0
105
- raise "HMAC for payload was #{hash} and expected to be #{payload_hmac}" unless payload_hmac == hash
45
+ def debug?
46
+ @@debug
47
+ end
48
+ @@password = nil
49
+ def password=(password)
50
+ @@password = password
51
+ end
52
+ def parse(opentoken = nil)
53
+ verify opentoken.present?, 'Unable to parse empty token'
54
+ data = decode(opentoken)
55
+ inspect_binary_string 'DATA', data
56
+
57
+ verify_header data
58
+ verify_version data
59
+
60
+ #cipher suite identifier
61
+ cipher_suite = data[4]
62
+ cipher = CIPHERS[cipher_suite]
63
+ verify !cipher.nil?, "Unknown cipher suite: #{cipher_suite}"
64
+
65
+ #SHA-1 HMAC
66
+ payload_hmac = data[5..24]
67
+ inspect_binary_string "PAYLOAD HMAC [5..24]", payload_hmac
68
+
69
+ #Initialization Vector (iv)
70
+ iv_length = data[25]
71
+ iv_end = [26, 26 + iv_length - 1].max
72
+ iv = data[26..iv_end]
73
+ inspect_binary_string "IV [26..#{iv_end}]", iv
74
+ verify iv_length == cipher[:iv_length], "Cipher expects iv length of #{cipher[:iv_length]} and was: #{iv_length}"
75
+
76
+ #key (not currently used)
77
+ key_length = data[iv_end + 1]
78
+ key_end = iv_end + 1
79
+ verify key_length == 0, "Token key embedding is not currently supported"
80
+
81
+ #payload
82
+ payload_length = data[(key_end + 1)..(key_end + 2)].unpack('n').first
83
+ payload_offset = key_end + 3
84
+ encrypted_payload = data[payload_offset..(data.length - 1)]
85
+ verify encrypted_payload.length == payload_length, "Payload length is #{encrypted_payload.length} and was expected to be #{payload_length}"
86
+ inspect_binary_string "ENCRYPTED PAYLOAD [#{payload_offset}..#{data.length - 1}]", encrypted_payload
87
+
88
+ key = OpenToken::PasswordKeyGenerator.generate(@@password, cipher)
89
+ inspect_binary_string 'KEY', key
90
+
91
+ compressed_payload = decrypt_payload(encrypted_payload, cipher, key, iv)
92
+ inspect_binary_string 'COMPRESSED PAYLOAD', compressed_payload
93
+
94
+ unparsed_payload = unzip_payload compressed_payload
95
+ puts 'EXPANDED PAYLOAD', unparsed_payload if debug?
96
+
97
+ #validate payload hmac
98
+ mac = []
99
+ mac << "0x01".hex.chr
100
+ mac << cipher_suite.chr
101
+ mac << iv
102
+ mac << key if key_length > 0 #key embedding is not currently supported
103
+ mac << unparsed_payload
104
+ hash = OpenSSL::HMAC.digest(OpenToken::PasswordKeyGenerator::SHA1_DIGEST, key, mac.join)
105
+ if (hash <=> payload_hmac) != 0
106
+ verify payload_hmac == hash, "HMAC for payload was #{hash} and expected to be #{payload_hmac}"
107
+ end
108
+
109
+ unescaped_payload = CGI::unescapeHTML(unparsed_payload)
110
+ puts 'UNESCAPED PAYLOAD', unescaped_payload if debug?
111
+ token = OpenToken::KeyValueSerializer.deserialize unescaped_payload
112
+ puts token.inspect if debug?
113
+ token.validate!
114
+ token
106
115
  end
107
116
 
108
- unescaped_payload = CGI::unescapeHTML(unparsed_payload)
109
- puts 'UNESCAPED PAYLOAD', unescaped_payload if DEBUG
110
- @payload = KeyValueSerializer.deserialize unescaped_payload
111
- puts @payload.inspect if DEBUG
112
- raise TokenExpiredError.new("#{Time.now.utc} is not within token duration: #{self.start_at} - #{self.end_at}") if self.expired?
113
- end
114
-
115
- def [](key)
116
- @payload[key.to_s]
117
- end
118
- #verify that the current time is between the not-before and not-on-or-after values
119
- def expired?
120
- now = Time.now.utc
121
- now < start_at || now >= end_at
122
- end
123
- def start_at
124
- payload_date('not-before')
125
- end
126
- def end_at
127
- payload_date('not-on-or-after')
128
- end
129
- #"renew-until"=>"2010-03-05T07:19:15Z"
130
- def valid_until
131
- payload_date('renew-until')
132
- end
133
- def payload_date(key)
134
- Time.iso8601(self[key]).utc
135
- end
136
-
137
- private
138
- def decrypt_payload(encrypted_payload, cipher, key, iv)
139
- return encrypted_payload unless cipher[:algorithm]
117
+ private
118
+ def verify_header(data)
119
+ header = data[0..2]
120
+ verify header == 'OTK', "Invalid token header: #{header}"
121
+ end
122
+ def verify_version(data)
123
+ version = data[3]
124
+ verify version == 1, "Unsupported token version: #{version}"
125
+ end
126
+ #ruby 1.9 has Base64.urlsafe_decode64 which can be used instead of gsubbing '_' and '-'
127
+ def decode(token)
128
+ string = token.gsub('*', '=').gsub('_', '/').gsub('-', '+')
129
+ data = Base64.decode64(string)
130
+ end
131
+ def verify(assertion, message = 'Invalid Token')
132
+ raise OpenToken::TokenInvalidError.new(message) unless assertion
133
+ end
140
134
  #see http://snippets.dzone.com/posts/show/4975
141
135
  #see http://jdwyah.blogspot.com/2009/12/decrypting-ruby-aes-encryption.html
142
136
  #see http://snippets.dzone.com/posts/show/576
143
- crypt = OpenSSL::Cipher::Cipher.new(cipher[:algorithm])
144
- crypt.decrypt
145
- crypt.key = key
146
- crypt.iv = iv
147
- crypt.update(encrypted_payload) + crypt.final
148
- end
149
-
150
- def inspect_binary_string(header, string)
151
- return unless DEBUG
152
- puts "#{header}:"
153
- index = 0
154
- string.each_byte do |b|
155
- puts "#{index}: #{b} => #{b.chr}"
156
- index += 1
137
+ def decrypt_payload(encrypted_payload, cipher, key, iv)
138
+ return encrypted_payload unless cipher[:algorithm]
139
+ crypt = OpenSSL::Cipher::Cipher.new(cipher[:algorithm])
140
+ crypt.decrypt
141
+ crypt.key = key
142
+ crypt.iv = iv
143
+ crypt.update(encrypted_payload) + crypt.final
144
+ end
145
+ #decompress the payload
146
+ #see http://stackoverflow.com/questions/1361892/how-to-decompress-gzip-data-in-ruby
147
+ def unzip_payload(compressed_payload)
148
+ unparsed_payload = begin
149
+ Zlib::Inflate.inflate(compressed_payload)
150
+ rescue Zlib::BufError
151
+ Zlib::Inflate.new(-Zlib::MAX_WBITS).inflate(compressed_payload[2, compressed_payload.size])
152
+ end
153
+ end
154
+ def inspect_binary_string(header, string)
155
+ return unless debug?
156
+ puts "#{header}:"
157
+ index = 0
158
+ string.each_byte do |b|
159
+ puts "#{index}: #{b} => #{b.chr}"
160
+ index += 1
161
+ end
157
162
  end
158
163
  end
159
164
  end
@@ -1,115 +1,117 @@
1
- class KeyValueSerializer
2
- LINE_START = 0
3
- EMPTY_SPACE = 1
4
- VALUE_START = 2
5
- LINE_END = 3
6
- IN_KEY = 4
7
- IN_VALUE = 5
8
- IN_QUOTED_VALUE = 6
1
+ module OpenToken
2
+ class KeyValueSerializer
3
+ LINE_START = 0
4
+ EMPTY_SPACE = 1
5
+ VALUE_START = 2
6
+ LINE_END = 3
7
+ IN_KEY = 4
8
+ IN_VALUE = 5
9
+ IN_QUOTED_VALUE = 6
9
10
 
10
- def self.unescape_value(value)
11
- value.gsub("\\\"", "\"").gsub("\\\'", "'")
12
- end
11
+ def self.unescape_value(value)
12
+ value.gsub("\\\"", "\"").gsub("\\\'", "'")
13
+ end
13
14
 
14
- def self.deserialize(string)
15
- result = {}
16
- state = LINE_START
17
- open_quote_char = 0.chr
18
- currkey = ""
19
- token = ""
20
- nextval = ""
15
+ def self.deserialize(string)
16
+ result = OpenToken::Token.new
17
+ state = LINE_START
18
+ open_quote_char = 0.chr
19
+ currkey = ""
20
+ token = ""
21
+ nextval = ""
21
22
 
22
- string.split(//).each do |c|
23
- nextval = c
23
+ string.split(//).each do |c|
24
+ nextval = c
24
25
 
25
- case c
26
- when "\t"
27
- if state == IN_KEY
28
- # key ends
29
- currkey = token
30
- token = ""
31
- state = EMPTY_SPACE
32
- elsif state == IN_VALUE
33
- # non-quoted value ends
34
- result[currkey] = self.deserialize(token)
35
- token = ""
36
- state = LINE_END
37
- elsif state == IN_QUOTED_VALUE
38
- token += c
39
- end
40
- when " "
41
- if state == IN_KEY
42
- # key ends
43
- currkey = token
44
- token = ""
45
- state = EMPTY_SPACE
46
- elsif state == IN_VALUE
47
- # non-quoted value ends
48
- result[currkey] = self.deserialize(token)
49
- token = ""
50
- state = LINE_END
51
- elsif state == IN_QUOTED_VALUE
52
- token += c
53
- end
54
- when "\n"
55
- # newline
56
- if (state == IN_VALUE) || (state == VALUE_START)
57
- result[currkey] = self.unescape_value(token)
58
- token = ""
59
- state = LINE_START
60
- elsif state == LINE_END
61
- token = ""
62
- state = LINE_START
63
- elsif state == IN_QUOTED_VALUE
64
- token += c
65
- end
66
- when "="
67
- if state == IN_KEY
68
- currkey = token
69
- token = ""
70
- state = VALUE_START
71
- elsif (state == IN_QUOTED_VALUE) || (state == IN_VALUE)
72
- token += c
73
- end
74
- when "\""
75
- if state == IN_QUOTED_VALUE
76
- if (c == open_quote_char) && (token[token.size-1] != "\\"[0])
77
- result[currkey] = self.unescape_value(token)
26
+ case c
27
+ when "\t"
28
+ if state == IN_KEY
29
+ # key ends
30
+ currkey = token
31
+ token = ""
32
+ state = EMPTY_SPACE
33
+ elsif state == IN_VALUE
34
+ # non-quoted value ends
35
+ result[currkey] = self.deserialize(token)
78
36
  token = ""
79
37
  state = LINE_END
80
- else
38
+ elsif state == IN_QUOTED_VALUE
81
39
  token += c
82
40
  end
83
- elsif state == VALUE_START
84
- state = IN_QUOTED_VALUE
85
- open_quote_char = c
86
- end
87
- when "'"
88
- if state == IN_QUOTED_VALUE
89
- if (c == open_quote_char) && (token[token.size-1] != "\\"[0])
90
- result[currkey] = self.unescape_value(token)
41
+ when " "
42
+ if state == IN_KEY
43
+ # key ends
44
+ currkey = token
45
+ token = ""
46
+ state = EMPTY_SPACE
47
+ elsif state == IN_VALUE
48
+ # non-quoted value ends
49
+ result[currkey] = self.deserialize(token)
91
50
  token = ""
92
51
  state = LINE_END
93
- else
52
+ elsif state == IN_QUOTED_VALUE
94
53
  token += c
95
54
  end
96
- else state == VALUE_START
97
- state = IN_QUOTED_VALUE
98
- open_quote_char = c
99
- end
100
- else
101
- if state == LINE_START
102
- state = IN_KEY
103
- elsif state == VALUE_START
104
- state = IN_VALUE
55
+ when "\n"
56
+ # newline
57
+ if (state == IN_VALUE) || (state == VALUE_START)
58
+ result[currkey] = self.unescape_value(token)
59
+ token = ""
60
+ state = LINE_START
61
+ elsif state == LINE_END
62
+ token = ""
63
+ state = LINE_START
64
+ elsif state == IN_QUOTED_VALUE
65
+ token += c
66
+ end
67
+ when "="
68
+ if state == IN_KEY
69
+ currkey = token
70
+ token = ""
71
+ state = VALUE_START
72
+ elsif (state == IN_QUOTED_VALUE) || (state == IN_VALUE)
73
+ token += c
74
+ end
75
+ when "\""
76
+ if state == IN_QUOTED_VALUE
77
+ if (c == open_quote_char) && (token[token.size-1] != "\\"[0])
78
+ result[currkey] = self.unescape_value(token)
79
+ token = ""
80
+ state = LINE_END
81
+ else
82
+ token += c
83
+ end
84
+ elsif state == VALUE_START
85
+ state = IN_QUOTED_VALUE
86
+ open_quote_char = c
87
+ end
88
+ when "'"
89
+ if state == IN_QUOTED_VALUE
90
+ if (c == open_quote_char) && (token[token.size-1] != "\\"[0])
91
+ result[currkey] = self.unescape_value(token)
92
+ token = ""
93
+ state = LINE_END
94
+ else
95
+ token += c
96
+ end
97
+ else state == VALUE_START
98
+ state = IN_QUOTED_VALUE
99
+ open_quote_char = c
100
+ end
101
+ else
102
+ if state == LINE_START
103
+ state = IN_KEY
104
+ elsif state == VALUE_START
105
+ state = IN_VALUE
106
+ end
107
+ token += c
105
108
  end
106
- token += c
107
- end
108
109
 
109
- if (state == IN_QUOTED_VALUE) || (state == IN_VALUE)
110
- result[currkey] = unescape_value(token)
110
+ if (state == IN_QUOTED_VALUE) || (state == IN_VALUE)
111
+ result[currkey] = unescape_value(token)
112
+ end
111
113
  end
114
+ result
112
115
  end
113
- result
114
116
  end
115
- end
117
+ end
@@ -1,55 +1,57 @@
1
- class PasswordKeyGenerator
2
- SHA1_DIGEST = OpenSSL::Digest::Digest.new('sha1')
1
+ module OpenToken
2
+ class PasswordKeyGenerator
3
+ SHA1_DIGEST = OpenSSL::Digest::Digest.new('sha1')
3
4
 
4
- def self.generate(password, cipher_suite)
5
- salt = 0.chr * 8
6
- self.generate_impl(password, cipher_suite, salt, 1000)
7
- end
5
+ def self.generate(password, cipher_suite)
6
+ salt = 0.chr * 8
7
+ self.generate_impl(password, cipher_suite, salt, 1000)
8
+ end
8
9
 
9
- def self.generate_block(password, salt, count, index)
10
- mac = salt
11
- mac += [index].pack("N")
10
+ def self.generate_block(password, salt, count, index)
11
+ mac = salt
12
+ mac += [index].pack("N")
12
13
 
13
- result = OpenSSL::HMAC.digest(SHA1_DIGEST, password, mac)
14
- cur = result
14
+ result = OpenSSL::HMAC.digest(SHA1_DIGEST, password, mac)
15
+ cur = result
15
16
 
16
- i_count = 1
17
- while i_count < count
18
- i_count +=1
17
+ i_count = 1
18
+ while i_count < count
19
+ i_count +=1
19
20
 
20
- cur = OpenSSL::HMAC.digest(SHA1_DIGEST, password, cur)
21
+ cur = OpenSSL::HMAC.digest(SHA1_DIGEST, password, cur)
21
22
 
22
- 20.times do |i|
23
- result[i] = result[i] ^ cur[i]
23
+ 20.times do |i|
24
+ result[i] = result[i] ^ cur[i]
25
+ end
24
26
  end
25
- end
26
27
 
27
- return result
28
- end
28
+ return result
29
+ end
29
30
 
30
- def self.generate_impl(password, cipher, salt, iterations)
31
- return unless cipher[:algorithm]
31
+ def self.generate_impl(password, cipher, salt, iterations)
32
+ return unless cipher[:algorithm]
32
33
 
33
- key_size = cipher[:key_length] / 8
34
- numblocks = key_size / 20
35
- numblocks += 1 if (key_size % 20) > 0
34
+ key_size = cipher[:key_length] / 8
35
+ numblocks = key_size / 20
36
+ numblocks += 1 if (key_size % 20) > 0
36
37
 
37
- # Generate the appropriate number of blocks and write their output to
38
- # the key bytes; note that it's important to start from 1 (vs. 0) as the
39
- # initial block number affects the hash. It's not clear that this fact
40
- # is stated explicitly anywhere, but without this approach, the generated
41
- # keys will not match up with test cases defined in RFC 3962.
42
- key_buffer_index = 0
43
- key = ""
38
+ # Generate the appropriate number of blocks and write their output to
39
+ # the key bytes; note that it's important to start from 1 (vs. 0) as the
40
+ # initial block number affects the hash. It's not clear that this fact
41
+ # is stated explicitly anywhere, but without this approach, the generated
42
+ # keys will not match up with test cases defined in RFC 3962.
43
+ key_buffer_index = 0
44
+ key = ""
44
45
 
45
- numblocks.times do |i|
46
- i+=1 # Previously zero based, needs to be 1 based
47
- block = self.generate_block(password, salt, iterations, i)
48
- len = [20, (key_size - key_buffer_index)].min
49
- key += block[0, len]
50
- key_buffer_index += len
51
- end
46
+ numblocks.times do |i|
47
+ i+=1 # Previously zero based, needs to be 1 based
48
+ block = self.generate_block(password, salt, iterations, i)
49
+ len = [20, (key_size - key_buffer_index)].min
50
+ key += block[0, len]
51
+ key_buffer_index += len
52
+ end
52
53
 
53
- return key
54
+ return key
55
+ end
54
56
  end
55
- end
57
+ end
@@ -0,0 +1,34 @@
1
+ require 'time'
2
+ require 'active_support/hash_with_indifferent_access'
3
+ require 'active_support/core_ext/time/calculations'
4
+
5
+ module OpenToken
6
+ class TokenExpiredError < StandardError; end
7
+
8
+ class Token < ActiveSupport::HashWithIndifferentAccess
9
+ def validate!
10
+ raise OpenToken::TokenExpiredError.new("#{Time.now.utc} is not within token duration: #{self.start_at} - #{self.end_at}") if self.expired?
11
+ end
12
+ #verify that the current time is between the not-before and not-on-or-after values
13
+ def valid?
14
+ start_at.past? && end_at.future?
15
+ end
16
+ def expired?
17
+ !valid?
18
+ end
19
+ def start_at
20
+ payload_date('not-before')
21
+ end
22
+ def end_at
23
+ payload_date('not-on-or-after')
24
+ end
25
+ def valid_until
26
+ payload_date('renew-until')
27
+ end
28
+
29
+ private
30
+ def payload_date(key)
31
+ Time.iso8601(self[key])
32
+ end
33
+ end
34
+ end
data/opentoken.gemspec CHANGED
@@ -5,19 +5,20 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{opentoken}
8
- s.version = "0.2.1"
8
+ s.version = "1.0.0"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Ryan Sonnek"]
12
- s.date = %q{2011-01-13}
12
+ s.date = %q{2011-01-18}
13
13
  s.description = %q{parse opentoken properties passed for Single Signon requests}
14
- s.email = %q{ryan@socialcast.com}
14
+ s.email = %q{ryan@codecrate.com}
15
15
  s.extra_rdoc_files = [
16
16
  "LICENSE",
17
17
  "README.rdoc"
18
18
  ]
19
19
  s.files = [
20
20
  ".document",
21
+ "Gemfile",
21
22
  "LICENSE",
22
23
  "README.rdoc",
23
24
  "Rakefile",
@@ -25,11 +26,13 @@ Gem::Specification.new do |s|
25
26
  "lib/opentoken.rb",
26
27
  "lib/opentoken/key_value_serializer.rb",
27
28
  "lib/opentoken/password_key_generator.rb",
29
+ "lib/opentoken/token.rb",
28
30
  "opentoken.gemspec",
29
31
  "test/helper.rb",
30
32
  "test/test_opentoken.rb"
31
33
  ]
32
34
  s.homepage = %q{http://github.com/wireframe/opentoken}
35
+ s.licenses = ["MIT"]
33
36
  s.require_paths = ["lib"]
34
37
  s.rubygems_version = %q{1.4.2}
35
38
  s.summary = %q{ruby implementation of the opentoken specification}
@@ -42,15 +45,30 @@ Gem::Specification.new do |s|
42
45
  s.specification_version = 3
43
46
 
44
47
  if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
48
+ s.add_runtime_dependency(%q<activesupport>, ["~> 3.0.3"])
49
+ s.add_runtime_dependency(%q<i18n>, [">= 0"])
45
50
  s.add_development_dependency(%q<shoulda>, [">= 0"])
46
51
  s.add_development_dependency(%q<timecop>, [">= 0.3.4"])
52
+ s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
53
+ s.add_development_dependency(%q<jeweler>, ["~> 1.5.2"])
54
+ s.add_development_dependency(%q<rcov>, [">= 0"])
47
55
  else
56
+ s.add_dependency(%q<activesupport>, ["~> 3.0.3"])
57
+ s.add_dependency(%q<i18n>, [">= 0"])
48
58
  s.add_dependency(%q<shoulda>, [">= 0"])
49
59
  s.add_dependency(%q<timecop>, [">= 0.3.4"])
60
+ s.add_dependency(%q<bundler>, ["~> 1.0.0"])
61
+ s.add_dependency(%q<jeweler>, ["~> 1.5.2"])
62
+ s.add_dependency(%q<rcov>, [">= 0"])
50
63
  end
51
64
  else
65
+ s.add_dependency(%q<activesupport>, ["~> 3.0.3"])
66
+ s.add_dependency(%q<i18n>, [">= 0"])
52
67
  s.add_dependency(%q<shoulda>, [">= 0"])
53
68
  s.add_dependency(%q<timecop>, [">= 0.3.4"])
69
+ s.add_dependency(%q<bundler>, ["~> 1.0.0"])
70
+ s.add_dependency(%q<jeweler>, ["~> 1.5.2"])
71
+ s.add_dependency(%q<rcov>, [">= 0"])
54
72
  end
55
73
  end
56
74
 
data/test/helper.rb CHANGED
@@ -1,8 +1,15 @@
1
1
  require 'rubygems'
2
+ require 'bundler'
3
+ begin
4
+ Bundler.setup(:default, :development)
5
+ rescue Bundler::BundlerError => e
6
+ $stderr.puts e.message
7
+ $stderr.puts "Run `bundle install` to install missing gems"
8
+ exit e.status_code
9
+ end
2
10
  require 'test/unit'
3
11
  require 'shoulda'
4
12
  require 'timecop'
5
- require 'activesupport'
6
13
 
7
14
  $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
8
15
  $LOAD_PATH.unshift(File.dirname(__FILE__))
@@ -8,18 +8,22 @@ class TestOpentoken < Test::Unit::TestCase
8
8
  setup do
9
9
  @opentoken = "T1RLAQJ0Ca97sl6MLJAZDa_hdFzMlicMQBDjqUzrXl0EOXKmpj5oo7L5AACgaWoW8fZizrsLbtxb_F00aTdFmhw8flGy4iGqPWPtqYpdIzQZzg5WvrvYH8Rnq7ckJpYk2YPZw6yNyA4ohG-BgFdTHc0U7CwZTFmodg1MuO0cTh7T98s2RXiTcaZa21MNO0yuXKm2Q10cbrWhnB5yHJUhSHx6JLxlgMTZ0oE0DoUOB6JmoLMYHcyL9hKRiPTh62ky_QmXRaifDNOdl4sH2w**"
10
10
  @password = 'Test123'
11
+ OpenToken.password = @password
11
12
  end
12
13
  context "parsing token between expiration dates" do
13
14
  setup do
14
15
  Timecop.travel(Time.iso8601('2010-03-04T19:20:10Z')) do
15
16
  assert_nothing_raised do
16
- @token = OpenToken.new @opentoken, :password => @password
17
+ @token = OpenToken.parse @opentoken
17
18
  end
18
19
  end
19
20
  end
20
21
  should "decrypt subject from token payload" do
21
22
  assert_equal 'john@example.com', @token[:subject]
22
23
  end
24
+ should "decrypt subject using string or symbol" do
25
+ assert_equal 'john@example.com', @token['subject']
26
+ end
23
27
  should "parse 'renew-until' date" do
24
28
  assert_equal Time.iso8601('2010-03-05T07:19:15Z'), @token.valid_until
25
29
  end
@@ -29,7 +33,7 @@ class TestOpentoken < Test::Unit::TestCase
29
33
  should "raise TokenExpiredError" do
30
34
  Timecop.travel(Time.iso8601('2010-03-04T19:19:10Z')) do
31
35
  assert_raises OpenToken::TokenExpiredError do
32
- @token = OpenToken.new @opentoken, :password => @password
36
+ @token = OpenToken.parse @opentoken
33
37
  end
34
38
  end
35
39
  end
@@ -39,7 +43,7 @@ class TestOpentoken < Test::Unit::TestCase
39
43
  should "raise TokenExpiredError" do
40
44
  Timecop.travel(Time.iso8601('2010-03-04T19:24:15Z')) do
41
45
  assert_raises OpenToken::TokenExpiredError do
42
- @token = OpenToken.new @opentoken, :password => @password
46
+ @token = OpenToken.parse @opentoken
43
47
  end
44
48
  end
45
49
  end
@@ -49,12 +53,18 @@ class TestOpentoken < Test::Unit::TestCase
49
53
  setup do
50
54
  Timecop.travel(Time.iso8601('2011-01-13T11:08:01Z')) do
51
55
  @opentoken = "T1RLAQLIjiqgexqi1PQcEKCetvGoSYR2jhDFSIfE5ctlSBxEnq3S1ydjAADQUNRIKJx6_14aE3MQZnDABupGJrKNfoJHFS5VOnKexjMtboeOgst31Hf-D9CZBrpB7Jv0KBwnQ7DN3HizecPT76oX3UGtq_Vi5j5bKYCeObYm9W6h7NY-VzcZY5TTqIuulc2Jit381usAWZ2Sv1c_CWwhrH4hw-x7vUQMSjErvXK1qvsrFCpfNr7XlArx0HjI6kT5XEaHgQNdC0zrLw9cZ4rewoEisR3H5oM7B6gMaP82wTSFVBXvpn5r0KT-Iuc3JuG2en1zVh3GNf110oQCKQ**"
52
- @token = OpenToken.new @opentoken, :password => @password
56
+ @token = OpenToken.parse @opentoken
53
57
  end
54
58
  end
55
59
  should 'preserve apostrophe in attribute payload' do
56
60
  assert_equal "D'angelo", @token[:last_name]
57
61
  end
58
62
  end
63
+
64
+ should 'raise invalid token error parsing nil token' do
65
+ assert_raises OpenToken::TokenInvalidError do
66
+ OpenToken.parse nil
67
+ end
68
+ end
59
69
  end
60
70
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: opentoken
3
3
  version: !ruby/object:Gem::Version
4
- hash: 21
4
+ hash: 23
5
5
  prerelease:
6
6
  segments:
7
- - 0
8
- - 2
9
7
  - 1
10
- version: 0.2.1
8
+ - 0
9
+ - 0
10
+ version: 1.0.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - Ryan Sonnek
@@ -15,13 +15,28 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2011-01-13 00:00:00 -06:00
18
+ date: 2011-01-18 00:00:00 -06:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency
22
- name: shoulda
22
+ type: :runtime
23
+ version_requirements: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ~>
27
+ - !ruby/object:Gem::Version
28
+ hash: 1
29
+ segments:
30
+ - 3
31
+ - 0
32
+ - 3
33
+ version: 3.0.3
34
+ requirement: *id001
23
35
  prerelease: false
24
- requirement: &id001 !ruby/object:Gem::Requirement
36
+ name: activesupport
37
+ - !ruby/object:Gem::Dependency
38
+ type: :runtime
39
+ version_requirements: &id002 !ruby/object:Gem::Requirement
25
40
  none: false
26
41
  requirements:
27
42
  - - ">="
@@ -30,12 +45,26 @@ dependencies:
30
45
  segments:
31
46
  - 0
32
47
  version: "0"
33
- type: :development
34
- version_requirements: *id001
48
+ requirement: *id002
49
+ prerelease: false
50
+ name: i18n
35
51
  - !ruby/object:Gem::Dependency
36
- name: timecop
52
+ type: :development
53
+ version_requirements: &id003 !ruby/object:Gem::Requirement
54
+ none: false
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ hash: 3
59
+ segments:
60
+ - 0
61
+ version: "0"
62
+ requirement: *id003
37
63
  prerelease: false
38
- requirement: &id002 !ruby/object:Gem::Requirement
64
+ name: shoulda
65
+ - !ruby/object:Gem::Dependency
66
+ type: :development
67
+ version_requirements: &id004 !ruby/object:Gem::Requirement
39
68
  none: false
40
69
  requirements:
41
70
  - - ">="
@@ -46,10 +75,57 @@ dependencies:
46
75
  - 3
47
76
  - 4
48
77
  version: 0.3.4
78
+ requirement: *id004
79
+ prerelease: false
80
+ name: timecop
81
+ - !ruby/object:Gem::Dependency
82
+ type: :development
83
+ version_requirements: &id005 !ruby/object:Gem::Requirement
84
+ none: false
85
+ requirements:
86
+ - - ~>
87
+ - !ruby/object:Gem::Version
88
+ hash: 23
89
+ segments:
90
+ - 1
91
+ - 0
92
+ - 0
93
+ version: 1.0.0
94
+ requirement: *id005
95
+ prerelease: false
96
+ name: bundler
97
+ - !ruby/object:Gem::Dependency
98
+ type: :development
99
+ version_requirements: &id006 !ruby/object:Gem::Requirement
100
+ none: false
101
+ requirements:
102
+ - - ~>
103
+ - !ruby/object:Gem::Version
104
+ hash: 7
105
+ segments:
106
+ - 1
107
+ - 5
108
+ - 2
109
+ version: 1.5.2
110
+ requirement: *id006
111
+ prerelease: false
112
+ name: jeweler
113
+ - !ruby/object:Gem::Dependency
49
114
  type: :development
50
- version_requirements: *id002
115
+ version_requirements: &id007 !ruby/object:Gem::Requirement
116
+ none: false
117
+ requirements:
118
+ - - ">="
119
+ - !ruby/object:Gem::Version
120
+ hash: 3
121
+ segments:
122
+ - 0
123
+ version: "0"
124
+ requirement: *id007
125
+ prerelease: false
126
+ name: rcov
51
127
  description: parse opentoken properties passed for Single Signon requests
52
- email: ryan@socialcast.com
128
+ email: ryan@codecrate.com
53
129
  executables: []
54
130
 
55
131
  extensions: []
@@ -59,6 +135,7 @@ extra_rdoc_files:
59
135
  - README.rdoc
60
136
  files:
61
137
  - .document
138
+ - Gemfile
62
139
  - LICENSE
63
140
  - README.rdoc
64
141
  - Rakefile
@@ -66,13 +143,14 @@ files:
66
143
  - lib/opentoken.rb
67
144
  - lib/opentoken/key_value_serializer.rb
68
145
  - lib/opentoken/password_key_generator.rb
146
+ - lib/opentoken/token.rb
69
147
  - opentoken.gemspec
70
148
  - test/helper.rb
71
149
  - test/test_opentoken.rb
72
150
  has_rdoc: true
73
151
  homepage: http://github.com/wireframe/opentoken
74
- licenses: []
75
-
152
+ licenses:
153
+ - MIT
76
154
  post_install_message:
77
155
  rdoc_options: []
78
156