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 +12 -0
- data/README.rdoc +8 -0
- data/Rakefile +28 -29
- data/VERSION +1 -1
- data/lib/opentoken.rb +119 -114
- data/lib/opentoken/key_value_serializer.rb +100 -98
- data/lib/opentoken/password_key_generator.rb +43 -41
- data/lib/opentoken/token.rb +34 -0
- data/opentoken.gemspec +21 -3
- data/test/helper.rb +8 -1
- data/test/test_opentoken.rb +14 -4
- metadata +93 -15
data/Gemfile
ADDED
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
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
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
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
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.
|
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
|
-
|
11
|
-
class
|
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
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
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
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
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
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
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
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
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
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
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
|
-
|
11
|
-
|
12
|
-
|
11
|
+
def self.unescape_value(value)
|
12
|
+
value.gsub("\\\"", "\"").gsub("\\\'", "'")
|
13
|
+
end
|
13
14
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
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
|
-
|
23
|
-
|
23
|
+
string.split(//).each do |c|
|
24
|
+
nextval = c
|
24
25
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
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
|
-
|
38
|
+
elsif state == IN_QUOTED_VALUE
|
81
39
|
token += c
|
82
40
|
end
|
83
|
-
|
84
|
-
state
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
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
|
-
|
52
|
+
elsif state == IN_QUOTED_VALUE
|
94
53
|
token += c
|
95
54
|
end
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
state
|
103
|
-
|
104
|
-
|
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
|
-
|
110
|
-
|
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
|
-
|
2
|
-
|
1
|
+
module OpenToken
|
2
|
+
class PasswordKeyGenerator
|
3
|
+
SHA1_DIGEST = OpenSSL::Digest::Digest.new('sha1')
|
3
4
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
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
|
-
|
10
|
-
|
11
|
-
|
10
|
+
def self.generate_block(password, salt, count, index)
|
11
|
+
mac = salt
|
12
|
+
mac += [index].pack("N")
|
12
13
|
|
13
|
-
|
14
|
-
|
14
|
+
result = OpenSSL::HMAC.digest(SHA1_DIGEST, password, mac)
|
15
|
+
cur = result
|
15
16
|
|
16
|
-
|
17
|
-
|
18
|
-
|
17
|
+
i_count = 1
|
18
|
+
while i_count < count
|
19
|
+
i_count +=1
|
19
20
|
|
20
|
-
|
21
|
+
cur = OpenSSL::HMAC.digest(SHA1_DIGEST, password, cur)
|
21
22
|
|
22
|
-
|
23
|
-
|
23
|
+
20.times do |i|
|
24
|
+
result[i] = result[i] ^ cur[i]
|
25
|
+
end
|
24
26
|
end
|
25
|
-
end
|
26
27
|
|
27
|
-
|
28
|
-
|
28
|
+
return result
|
29
|
+
end
|
29
30
|
|
30
|
-
|
31
|
-
|
31
|
+
def self.generate_impl(password, cipher, salt, iterations)
|
32
|
+
return unless cipher[:algorithm]
|
32
33
|
|
33
|
-
|
34
|
-
|
35
|
-
|
34
|
+
key_size = cipher[:key_length] / 8
|
35
|
+
numblocks = key_size / 20
|
36
|
+
numblocks += 1 if (key_size % 20) > 0
|
36
37
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
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
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
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
|
-
|
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.
|
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-
|
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@
|
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__))
|
data/test/test_opentoken.rb
CHANGED
@@ -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.
|
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.
|
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.
|
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.
|
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:
|
4
|
+
hash: 23
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
|
-
- 0
|
8
|
-
- 2
|
9
7
|
- 1
|
10
|
-
|
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-
|
18
|
+
date: 2011-01-18 00:00:00 -06:00
|
19
19
|
default_executable:
|
20
20
|
dependencies:
|
21
21
|
- !ruby/object:Gem::Dependency
|
22
|
-
|
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
|
-
|
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
|
-
|
34
|
-
|
48
|
+
requirement: *id002
|
49
|
+
prerelease: false
|
50
|
+
name: i18n
|
35
51
|
- !ruby/object:Gem::Dependency
|
36
|
-
|
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
|
-
|
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:
|
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@
|
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
|
|