opentoken-newrelic-rails23 1.2.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gitignore +22 -0
- data/CONTRIBUTORS.txt +7 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +33 -0
- data/Rakefile +11 -0
- data/lib/opentoken-newrelic-rails23.rb +182 -0
- data/lib/opentoken-newrelic-rails23/cipher.rb +69 -0
- data/lib/opentoken-newrelic-rails23/key_value_serializer.rb +142 -0
- data/lib/opentoken-newrelic-rails23/password_key_generator.rb +65 -0
- data/lib/opentoken-newrelic-rails23/token.rb +33 -0
- data/lib/opentoken-newrelic-rails23/version.rb +3 -0
- data/opentoken-newrelic-rails23.gemspec +28 -0
- data/test/helper.rb +19 -0
- data/test/test_opentoken.rb +105 -0
- metadata +159 -0
data/.document
ADDED
data/.gitignore
ADDED
data/CONTRIBUTORS.txt
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
The MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2011 Socialcast, Inc
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
22
|
+
|
data/README.md
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# opentoken
|
2
|
+
|
3
|
+
Parse encrypted opentoken properties
|
4
|
+
|
5
|
+
see http://www.pingidentity.com/opentoken
|
6
|
+
|
7
|
+
## Usage
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
# configure decryption with shared key
|
11
|
+
OpenToken.password = 'shared_secret_to_decrypt'
|
12
|
+
|
13
|
+
# decrypt opentoken into hash of attributes
|
14
|
+
attributes = OpenToken.decode 'opentoken-hashed-string'
|
15
|
+
|
16
|
+
# encrypt opentoken from hash of attributes
|
17
|
+
attributes = { 'subject' => 'foo', 'bar' => 'bak' }
|
18
|
+
token = OpenToken.encode attributes, OpenToken::Cipher::AES_128_CBC
|
19
|
+
```
|
20
|
+
|
21
|
+
## Contributing
|
22
|
+
|
23
|
+
* Fork the project
|
24
|
+
* Fix the issue
|
25
|
+
* Add tests
|
26
|
+
* Send me a pull request. Bonus points for topic branches.
|
27
|
+
|
28
|
+
see CONTRIBUTORS.txt for complete list of contributors.
|
29
|
+
|
30
|
+
## Copyright
|
31
|
+
|
32
|
+
Copyright (c) 2011 Socialcast Inc.
|
33
|
+
See LICENSE.txt for details.
|
data/Rakefile
ADDED
@@ -0,0 +1,182 @@
|
|
1
|
+
require 'base64'
|
2
|
+
require 'openssl'
|
3
|
+
require 'digest/sha1'
|
4
|
+
require 'zlib'
|
5
|
+
require 'stringio'
|
6
|
+
require 'cgi'
|
7
|
+
require 'time'
|
8
|
+
require File.join(File.dirname(__FILE__), 'opentoken-newrelic-rails23', 'token')
|
9
|
+
require File.join(File.dirname(__FILE__), 'opentoken-newrelic-rails23', 'key_value_serializer')
|
10
|
+
require File.join(File.dirname(__FILE__), 'opentoken-newrelic-rails23', 'password_key_generator')
|
11
|
+
require File.join(File.dirname(__FILE__), 'opentoken-newrelic-rails23', 'cipher')
|
12
|
+
|
13
|
+
module OpenToken
|
14
|
+
class TokenInvalidError < StandardError; end
|
15
|
+
|
16
|
+
class << self
|
17
|
+
attr_accessor :debug
|
18
|
+
def debug?
|
19
|
+
!!debug
|
20
|
+
end
|
21
|
+
|
22
|
+
attr_accessor :password
|
23
|
+
attr_accessor :token_lifetime
|
24
|
+
attr_accessor :renew_until_lifetime
|
25
|
+
|
26
|
+
def encode(attributes, cipher)
|
27
|
+
attributes['not-before'] = Time.now.utc.iso8601.to_s
|
28
|
+
attributes['not-on-or-after'] = Time.at(Time.now.to_i + token_lifetime).utc.iso8601.to_s
|
29
|
+
attributes['renew-until'] = Time.at(Time.now.to_i + renew_until_lifetime).utc.iso8601.to_s
|
30
|
+
|
31
|
+
serialized = OpenToken::KeyValueSerializer.serialize(attributes)
|
32
|
+
compressed = zip_payload serialized
|
33
|
+
|
34
|
+
key = cipher.generate_key
|
35
|
+
iv = cipher.generate_iv
|
36
|
+
encrypted = cipher.encrypt_payload compressed, key, iv
|
37
|
+
|
38
|
+
mac = []
|
39
|
+
mac << "0x01".hex.chr # OTK version
|
40
|
+
mac << cipher.suite.chr
|
41
|
+
mac << iv
|
42
|
+
mac << force_encoding(serialized, 'BINARY')
|
43
|
+
hash = OpenSSL::HMAC.digest(OpenToken::PasswordKeyGenerator::SHA1_DIGEST, key, mac.join)
|
44
|
+
|
45
|
+
token_string = ""
|
46
|
+
token_string = "OTK" + 1.chr + cipher.suite.chr
|
47
|
+
token_string += hash
|
48
|
+
token_string += cipher.iv_length.chr
|
49
|
+
token_string += iv
|
50
|
+
token_string += 0.chr # key info length
|
51
|
+
token_string += ((encrypted.length >> 8) &0xFF ).chr
|
52
|
+
token_string += (encrypted.length & 0xFF).chr
|
53
|
+
token_string += encrypted
|
54
|
+
inspect_binary_string "Unencoded", token_string
|
55
|
+
encoded = urlsafe_encode64 token_string
|
56
|
+
inspect_binary_string "Encoded", encoded
|
57
|
+
encoded
|
58
|
+
end
|
59
|
+
def decode(opentoken = nil)
|
60
|
+
verify opentoken.present?, 'Unable to parse empty token'
|
61
|
+
data = urlsafe_decode64(opentoken)
|
62
|
+
inspect_binary_string 'DATA', data
|
63
|
+
|
64
|
+
verify_header data
|
65
|
+
verify_version data
|
66
|
+
|
67
|
+
#cipher suite identifier
|
68
|
+
cipher_suite = char_value_of data[4]
|
69
|
+
cipher = OpenToken::Cipher.for_suite cipher_suite
|
70
|
+
|
71
|
+
#SHA-1 HMAC
|
72
|
+
payload_hmac = data[5..24]
|
73
|
+
inspect_binary_string "PAYLOAD HMAC [5..24]", payload_hmac
|
74
|
+
|
75
|
+
#Initialization Vector (iv)
|
76
|
+
iv_length = char_value_of data[25]
|
77
|
+
iv_end = char_value_of [26, 26 + iv_length - 1].max
|
78
|
+
iv = data[26..iv_end]
|
79
|
+
inspect_binary_string "IV [26..#{iv_end}]", iv
|
80
|
+
verify iv_length == cipher.iv_length, "Cipher expects iv length of #{cipher.iv_length} and was: #{iv_length}"
|
81
|
+
|
82
|
+
#key (not currently used)
|
83
|
+
key_length = char_value_of data[iv_end + 1]
|
84
|
+
key_end = iv_end + 1
|
85
|
+
verify key_length == 0, "Token key embedding is not currently supported. Key length is: #{key_length}"
|
86
|
+
|
87
|
+
#payload
|
88
|
+
payload_length = data[(key_end + 1)..(key_end + 2)].unpack('n').first
|
89
|
+
payload_offset = key_end + 3
|
90
|
+
encrypted_payload = data[payload_offset..(data.length - 1)]
|
91
|
+
verify encrypted_payload.length == payload_length, "Payload length is #{encrypted_payload.length} and was expected to be #{payload_length}"
|
92
|
+
inspect_binary_string "ENCRYPTED PAYLOAD [#{payload_offset}..#{data.length - 1}]", encrypted_payload
|
93
|
+
|
94
|
+
key = cipher.generate_key
|
95
|
+
inspect_binary_string 'KEY', key
|
96
|
+
|
97
|
+
compressed_payload = cipher.decrypt_payload encrypted_payload, key, iv
|
98
|
+
inspect_binary_string 'COMPRESSED PAYLOAD', compressed_payload
|
99
|
+
|
100
|
+
unparsed_payload = unzip_payload compressed_payload
|
101
|
+
puts 'EXPANDED PAYLOAD', unparsed_payload if debug?
|
102
|
+
|
103
|
+
#validate payload hmac
|
104
|
+
mac = []
|
105
|
+
mac << "0x01".hex.chr
|
106
|
+
mac << cipher_suite.chr
|
107
|
+
mac << iv
|
108
|
+
mac << key if key_length > 0 #key embedding is not currently supported
|
109
|
+
mac << unparsed_payload
|
110
|
+
hash = OpenSSL::HMAC.digest(OpenToken::PasswordKeyGenerator::SHA1_DIGEST, key, mac.join)
|
111
|
+
if (hash <=> payload_hmac) != 0
|
112
|
+
verify payload_hmac == hash, "HMAC for payload was #{hash} and expected to be #{payload_hmac}"
|
113
|
+
end
|
114
|
+
|
115
|
+
unescaped_payload = CGI::unescapeHTML(unparsed_payload)
|
116
|
+
puts 'UNESCAPED PAYLOAD', unescaped_payload if debug?
|
117
|
+
token = OpenToken::KeyValueSerializer.deserialize force_encoding(unescaped_payload, 'UTF-8')
|
118
|
+
puts token.inspect if debug?
|
119
|
+
token.validate!
|
120
|
+
token
|
121
|
+
end
|
122
|
+
|
123
|
+
private
|
124
|
+
def char_value_of(character)
|
125
|
+
if RUBY_VERSION < "1.9"
|
126
|
+
return character
|
127
|
+
else
|
128
|
+
return character.chr.ord
|
129
|
+
end
|
130
|
+
end
|
131
|
+
def verify_header(data)
|
132
|
+
header = data[0..2]
|
133
|
+
verify header == 'OTK', "Invalid token header: #{header}"
|
134
|
+
end
|
135
|
+
def verify_version(data)
|
136
|
+
version = char_value_of data[3]
|
137
|
+
verify version == 1, "Unsupported token version: '#{version}'"
|
138
|
+
end
|
139
|
+
#ruby 1.9 has Base64.urlsafe_decode64 which can be used instead of gsubbing '_' and '-'
|
140
|
+
def urlsafe_decode64(token)
|
141
|
+
string = token.gsub('*', '=').gsub('_', '/').gsub('-', '+')
|
142
|
+
data = Base64.decode64(string)
|
143
|
+
end
|
144
|
+
def urlsafe_encode64(token)
|
145
|
+
string = Base64.encode64(token);
|
146
|
+
string = string.gsub('=', '*').gsub('/', '_').gsub('+', '-').gsub(10.chr, '').gsub(11.chr, '')
|
147
|
+
string
|
148
|
+
end
|
149
|
+
def verify(assertion, message = 'Invalid Token')
|
150
|
+
raise OpenToken::TokenInvalidError.new(message) unless assertion
|
151
|
+
end
|
152
|
+
#decompress the payload
|
153
|
+
#see http://stackoverflow.com/questions/1361892/how-to-decompress-gzip-data-in-ruby
|
154
|
+
def unzip_payload(compressed_payload)
|
155
|
+
unparsed_payload = begin
|
156
|
+
Zlib::Inflate.inflate(compressed_payload)
|
157
|
+
rescue Zlib::BufError
|
158
|
+
Zlib::Inflate.new(-Zlib::MAX_WBITS).inflate(compressed_payload[2, compressed_payload.size])
|
159
|
+
end
|
160
|
+
end
|
161
|
+
def zip_payload(uncompressed)
|
162
|
+
compressed = Zlib::Deflate.deflate(uncompressed, 9)
|
163
|
+
compressed
|
164
|
+
end
|
165
|
+
def inspect_binary_string(header, string)
|
166
|
+
return unless debug?
|
167
|
+
puts "#{header}:"
|
168
|
+
index = 0
|
169
|
+
string.each_byte do |b|
|
170
|
+
puts "#{index}: #{b} => #{b.chr}"
|
171
|
+
index += 1
|
172
|
+
end
|
173
|
+
end
|
174
|
+
def force_encoding(string, encoding)
|
175
|
+
string.respond_to?(:force_encoding) ? string.force_encoding(encoding) : string
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
# intialize defaults
|
181
|
+
OpenToken.token_lifetime = 300
|
182
|
+
OpenToken.renew_until_lifetime = 43200
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
|
3
|
+
module OpenToken
|
4
|
+
class Cipher
|
5
|
+
class InvalidCipherError < StandardError; end
|
6
|
+
|
7
|
+
attr_reader :algorithm
|
8
|
+
attr_reader :iv_length
|
9
|
+
attr_reader :key_length
|
10
|
+
attr_reader :suite
|
11
|
+
|
12
|
+
def initialize(attrs = {})
|
13
|
+
@suite = attrs[:suite]
|
14
|
+
@iv_length = attrs[:iv_length]
|
15
|
+
@key_length = attrs[:key_length]
|
16
|
+
@algorithm = attrs[:algorithm]
|
17
|
+
end
|
18
|
+
def self.for_suite(cipher_suite)
|
19
|
+
cipher = REGISTERED_CIPHERS.detect {|c| c.suite == cipher_suite }
|
20
|
+
raise InvalidCipherError.new("Unknown cipher suite: #{cipher_suite}") unless cipher
|
21
|
+
cipher
|
22
|
+
end
|
23
|
+
|
24
|
+
def generate_key
|
25
|
+
OpenToken::PasswordKeyGenerator.generate OpenToken.password, self
|
26
|
+
end
|
27
|
+
def generate_iv
|
28
|
+
OpenSSL::Random.random_bytes(iv_length)
|
29
|
+
end
|
30
|
+
|
31
|
+
#see http://snippets.dzone.com/posts/show/4975
|
32
|
+
#see http://jdwyah.blogspot.com/2009/12/decrypting-ruby-aes-encryption.html
|
33
|
+
#see http://snippets.dzone.com/posts/show/576
|
34
|
+
def decrypt_payload(encrypted_payload, key, iv)
|
35
|
+
return encrypted_payload unless algorithm
|
36
|
+
c = crypt :decrypt, key, iv
|
37
|
+
c.update(encrypted_payload) + c.final
|
38
|
+
end
|
39
|
+
def encrypt_payload(payload, key, iv)
|
40
|
+
c = crypt :encrypt, key, iv
|
41
|
+
padding = if payload.length % iv_length == 0
|
42
|
+
iv_length
|
43
|
+
else
|
44
|
+
iv_length - (payload.length % iv_length)
|
45
|
+
end
|
46
|
+
c.update(payload + (padding.chr * padding))
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
def crypt(operation, key, iv)
|
51
|
+
crypt = OpenSSL::Cipher::Cipher.new(algorithm)
|
52
|
+
crypt.send operation
|
53
|
+
crypt.key = key
|
54
|
+
crypt.iv = iv
|
55
|
+
crypt
|
56
|
+
end
|
57
|
+
|
58
|
+
NULL = Cipher.new(:suite => 0, :iv_length => 0)
|
59
|
+
AES_256_CBC = Cipher.new(:suite => 1, :iv_length => 32, :key_length => 256, :algorithm => 'aes-256-cbc')
|
60
|
+
AES_128_CBC = Cipher.new(:suite => 2, :iv_length => 16, :key_length => 128, :algorithm => 'aes-128-cbc')
|
61
|
+
DES3_168_CBC = Cipher.new(:suite => 3, :iv_length => 8, :key_length => 168, :algorithm => 'des-cbc')
|
62
|
+
|
63
|
+
REGISTERED_CIPHERS = []
|
64
|
+
REGISTERED_CIPHERS << NULL
|
65
|
+
REGISTERED_CIPHERS << AES_256_CBC
|
66
|
+
REGISTERED_CIPHERS << AES_128_CBC
|
67
|
+
REGISTERED_CIPHERS << DES3_168_CBC
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,142 @@
|
|
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
|
10
|
+
|
11
|
+
class << self
|
12
|
+
def serialize(hashmap)
|
13
|
+
result = String.new
|
14
|
+
count = 0;
|
15
|
+
hashmap.each_pair do |key,value|
|
16
|
+
if (count != 0)
|
17
|
+
result = result + "\n"
|
18
|
+
end
|
19
|
+
count +=1
|
20
|
+
result += key + "="
|
21
|
+
result += escape_value(value)
|
22
|
+
end
|
23
|
+
result
|
24
|
+
end
|
25
|
+
def deserialize(string)
|
26
|
+
result = OpenToken::Token.new
|
27
|
+
state = LINE_START
|
28
|
+
open_quote_char = 0.chr
|
29
|
+
currkey = ""
|
30
|
+
token = ""
|
31
|
+
nextval = ""
|
32
|
+
|
33
|
+
string.split(//).each do |c|
|
34
|
+
nextval = c
|
35
|
+
|
36
|
+
case c
|
37
|
+
when "\t"
|
38
|
+
if state == IN_KEY
|
39
|
+
# key ends
|
40
|
+
currkey = token
|
41
|
+
token = ""
|
42
|
+
state = EMPTY_SPACE
|
43
|
+
elsif state == IN_VALUE
|
44
|
+
# non-quoted value ends
|
45
|
+
result[currkey] = self.deserialize(token)
|
46
|
+
token = ""
|
47
|
+
state = LINE_END
|
48
|
+
elsif state == IN_QUOTED_VALUE
|
49
|
+
token += c
|
50
|
+
end
|
51
|
+
when " "
|
52
|
+
if state == IN_KEY
|
53
|
+
# key ends
|
54
|
+
currkey = token
|
55
|
+
token = ""
|
56
|
+
state = EMPTY_SPACE
|
57
|
+
elsif state == IN_VALUE
|
58
|
+
# non-quoted value ends
|
59
|
+
result[currkey] = self.deserialize(token)
|
60
|
+
token = ""
|
61
|
+
state = LINE_END
|
62
|
+
elsif state == IN_QUOTED_VALUE
|
63
|
+
token += c
|
64
|
+
end
|
65
|
+
when "\n"
|
66
|
+
# newline
|
67
|
+
if (state == IN_VALUE) || (state == VALUE_START)
|
68
|
+
result[currkey] = unescape_value(token)
|
69
|
+
token = ""
|
70
|
+
state = LINE_START
|
71
|
+
elsif state == LINE_END
|
72
|
+
token = ""
|
73
|
+
state = LINE_START
|
74
|
+
elsif state == IN_QUOTED_VALUE
|
75
|
+
token += c
|
76
|
+
end
|
77
|
+
when "="
|
78
|
+
if state == IN_KEY
|
79
|
+
currkey = token
|
80
|
+
token = ""
|
81
|
+
state = VALUE_START
|
82
|
+
elsif (state == IN_QUOTED_VALUE) || (state == IN_VALUE)
|
83
|
+
token += c
|
84
|
+
end
|
85
|
+
when "\""
|
86
|
+
if state == IN_QUOTED_VALUE
|
87
|
+
if (c == open_quote_char) && (token[token.size-1] != "\\"[0])
|
88
|
+
result[currkey] = unescape_value(token)
|
89
|
+
token = ""
|
90
|
+
state = LINE_END
|
91
|
+
else
|
92
|
+
token += c
|
93
|
+
end
|
94
|
+
elsif state == VALUE_START
|
95
|
+
state = IN_QUOTED_VALUE
|
96
|
+
open_quote_char = c
|
97
|
+
end
|
98
|
+
when "'"
|
99
|
+
if state == IN_QUOTED_VALUE
|
100
|
+
if (c == open_quote_char) && (token[token.size-1] != "\\"[0])
|
101
|
+
result[currkey] = unescape_value(token)
|
102
|
+
token = ""
|
103
|
+
state = LINE_END
|
104
|
+
else
|
105
|
+
token += c
|
106
|
+
end
|
107
|
+
else state == VALUE_START
|
108
|
+
state = IN_QUOTED_VALUE
|
109
|
+
open_quote_char = c
|
110
|
+
end
|
111
|
+
else
|
112
|
+
if state == LINE_START
|
113
|
+
state = IN_KEY
|
114
|
+
elsif state == VALUE_START
|
115
|
+
state = IN_VALUE
|
116
|
+
end
|
117
|
+
token += c
|
118
|
+
end
|
119
|
+
|
120
|
+
if (state == IN_QUOTED_VALUE) || (state == IN_VALUE)
|
121
|
+
result[currkey] = unescape_value(token)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
result
|
125
|
+
end
|
126
|
+
private
|
127
|
+
def unescape_value(value)
|
128
|
+
value.gsub("\\\"", "\"").gsub("\\\'", "'")
|
129
|
+
end
|
130
|
+
def escape_value(value)
|
131
|
+
value.each_byte do |b|
|
132
|
+
c = b.chr
|
133
|
+
if c == "\n" or c == "\t" or c == " " or c == "'" or c == "\""
|
134
|
+
value = "'" + value.gsub("'", "\'").gsub("\"", "\\\"") + "'"
|
135
|
+
break
|
136
|
+
end
|
137
|
+
end
|
138
|
+
value
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module OpenToken
|
2
|
+
class PasswordKeyGenerator
|
3
|
+
SHA1_DIGEST = OpenSSL::Digest::Digest.new('sha1')
|
4
|
+
|
5
|
+
class << self
|
6
|
+
def generate(password, cipher)
|
7
|
+
salt = 0.chr * 8
|
8
|
+
generate_impl(password, cipher, salt, 1000)
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
def generate_block(password, salt, count, index)
|
13
|
+
mac = salt
|
14
|
+
mac += [index].pack("N")
|
15
|
+
|
16
|
+
result = OpenSSL::HMAC.digest(SHA1_DIGEST, password, mac)
|
17
|
+
cur = result
|
18
|
+
|
19
|
+
i_count = 1
|
20
|
+
while i_count < count
|
21
|
+
i_count +=1
|
22
|
+
|
23
|
+
cur = OpenSSL::HMAC.digest(SHA1_DIGEST, password, cur)
|
24
|
+
|
25
|
+
20.times do |i|
|
26
|
+
if RUBY_VERSION < "1.9"
|
27
|
+
result[i] = result[i] ^ cur[i]
|
28
|
+
else
|
29
|
+
result[i] = (result[i].chr.ord ^ cur[i].chr.ord).chr
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
return result
|
35
|
+
end
|
36
|
+
|
37
|
+
def generate_impl(password, cipher, salt, iterations)
|
38
|
+
return unless cipher.algorithm
|
39
|
+
|
40
|
+
key_size = cipher.key_length / 8
|
41
|
+
numblocks = key_size / 20
|
42
|
+
numblocks += 1 if (key_size % 20) > 0
|
43
|
+
|
44
|
+
# Generate the appropriate number of blocks and write their output to
|
45
|
+
# the key bytes; note that it's important to start from 1 (vs. 0) as the
|
46
|
+
# initial block number affects the hash. It's not clear that this fact
|
47
|
+
# is stated explicitly anywhere, but without this approach, the generated
|
48
|
+
# keys will not match up with test cases defined in RFC 3962.
|
49
|
+
key_buffer_index = 0
|
50
|
+
key = ""
|
51
|
+
|
52
|
+
numblocks.times do |i|
|
53
|
+
i+=1 # Previously zero based, needs to be 1 based
|
54
|
+
block = generate_block(password, salt, iterations, i)
|
55
|
+
len = [20, (key_size - key_buffer_index)].min
|
56
|
+
key += block[0, len]
|
57
|
+
key_buffer_index += len
|
58
|
+
end
|
59
|
+
|
60
|
+
return key
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'time'
|
2
|
+
require 'active_support'
|
3
|
+
|
4
|
+
module OpenToken
|
5
|
+
class TokenExpiredError < StandardError; end
|
6
|
+
|
7
|
+
class Token < HashWithIndifferentAccess
|
8
|
+
def validate!
|
9
|
+
raise OpenToken::TokenExpiredError.new("#{Time.now.utc} is not within token duration: #{self.start_at} - #{self.end_at}") if self.expired?
|
10
|
+
end
|
11
|
+
#verify that the current time is between the not-before and not-on-or-after values
|
12
|
+
def valid?
|
13
|
+
start_at.past? && end_at.future?
|
14
|
+
end
|
15
|
+
def expired?
|
16
|
+
!valid?
|
17
|
+
end
|
18
|
+
def start_at
|
19
|
+
payload_date('not-before')
|
20
|
+
end
|
21
|
+
def end_at
|
22
|
+
payload_date('not-on-or-after')
|
23
|
+
end
|
24
|
+
def valid_until
|
25
|
+
payload_date('renew-until')
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
def payload_date(key)
|
30
|
+
Time.iso8601(self[key])
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "opentoken-newrelic-rails23/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "opentoken-newrelic-rails23"
|
7
|
+
s.version = OpenToken::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ["Ryan Sonnek"]
|
10
|
+
s.email = ["ryan@socialcast.com"]
|
11
|
+
s.homepage = "http://github.com/newrelic/opentoken-newrelic-rails23"
|
12
|
+
s.summary = %q{ruby implementation of the opentoken specification, forked for Rails 2.3 compatibility. Use the non-forked version for Rails 3+}
|
13
|
+
s.description = %q{parse opentoken properties passed for Single Signon requests}
|
14
|
+
|
15
|
+
s.rubyforge_project = "opentoken-newrelic-rails23"
|
16
|
+
|
17
|
+
# If you are on a newer version of Rails, you should be using the opentoken gem, not our fork.
|
18
|
+
s.add_runtime_dependency(%q<activesupport>, ["~> 2.3.14"])
|
19
|
+
s.add_runtime_dependency(%q<i18n>, [">= 0"])
|
20
|
+
s.add_development_dependency(%q<shoulda>, ["2.11.3"])
|
21
|
+
s.add_development_dependency(%q<timecop>, ["0.3.5"])
|
22
|
+
s.add_development_dependency(%q<rake>, ["0.9.2"])
|
23
|
+
|
24
|
+
s.files = `git ls-files`.split("\n")
|
25
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
26
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
27
|
+
s.require_paths = ["lib"]
|
28
|
+
end
|
data/test/helper.rb
ADDED
@@ -0,0 +1,19 @@
|
|
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
|
10
|
+
require 'test/unit'
|
11
|
+
require 'shoulda'
|
12
|
+
require 'timecop'
|
13
|
+
|
14
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
15
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
16
|
+
require 'opentoken-newrelic-rails23'
|
17
|
+
|
18
|
+
class Test::Unit::TestCase
|
19
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class TestOpentoken < Test::Unit::TestCase
|
4
|
+
# OpenToken.debug = true
|
5
|
+
|
6
|
+
#"renew-until"=>"2010-03-05T07:19:15Z"
|
7
|
+
#"not-before"=>"2010-03-04T19:19:15Z"
|
8
|
+
#"not-on-or-after"=>"2010-03-04T19:24:15Z"
|
9
|
+
context "aes-128-cbc token with subject attribute" do
|
10
|
+
setup do
|
11
|
+
@opentoken = "T1RLAQJ0Ca97sl6MLJAZDa_hdFzMlicMQBDjqUzrXl0EOXKmpj5oo7L5AACgaWoW8fZizrsLbtxb_F00aTdFmhw8flGy4iGqPWPtqYpdIzQZzg5WvrvYH8Rnq7ckJpYk2YPZw6yNyA4ohG-BgFdTHc0U7CwZTFmodg1MuO0cTh7T98s2RXiTcaZa21MNO0yuXKm2Q10cbrWhnB5yHJUhSHx6JLxlgMTZ0oE0DoUOB6JmoLMYHcyL9hKRiPTh62ky_QmXRaifDNOdl4sH2w**"
|
12
|
+
@password = 'Test123'
|
13
|
+
OpenToken.password = @password
|
14
|
+
end
|
15
|
+
context "decoding token between expiration dates" do
|
16
|
+
setup do
|
17
|
+
Timecop.travel(Time.iso8601('2010-03-04T19:20:10Z')) do
|
18
|
+
assert_nothing_raised do
|
19
|
+
@token = OpenToken.decode @opentoken
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
should "decrypt subject from token payload" do
|
24
|
+
assert_equal 'john@example.com', @token[:subject]
|
25
|
+
end
|
26
|
+
should "decrypt subject using string or symbol" do
|
27
|
+
assert_equal 'john@example.com', @token['subject']
|
28
|
+
end
|
29
|
+
should "parse 'renew-until' date" do
|
30
|
+
assert_equal Time.iso8601('2010-03-05T07:19:15Z'), @token.valid_until
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
context "decoding token when current time is before expiration date" do
|
35
|
+
should "raise TokenExpiredError" do
|
36
|
+
Timecop.travel(Time.iso8601('2010-03-04T19:19:10Z')) do
|
37
|
+
assert_raises OpenToken::TokenExpiredError do
|
38
|
+
@token = OpenToken.decode @opentoken
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
context "decoding token when current time is equal to expiration date" do
|
45
|
+
should "raise TokenExpiredError" do
|
46
|
+
Timecop.travel(Time.iso8601('2010-03-04T19:24:15Z')) do
|
47
|
+
assert_raises OpenToken::TokenExpiredError do
|
48
|
+
@token = OpenToken.decode @opentoken
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
context "decoding token with attribute value containing apostrophe" do
|
55
|
+
setup do
|
56
|
+
Timecop.travel(Time.iso8601('2011-01-13T11:08:01Z')) do
|
57
|
+
@opentoken = "T1RLAQLIjiqgexqi1PQcEKCetvGoSYR2jhDFSIfE5ctlSBxEnq3S1ydjAADQUNRIKJx6_14aE3MQZnDABupGJrKNfoJHFS5VOnKexjMtboeOgst31Hf-D9CZBrpB7Jv0KBwnQ7DN3HizecPT76oX3UGtq_Vi5j5bKYCeObYm9W6h7NY-VzcZY5TTqIuulc2Jit381usAWZ2Sv1c_CWwhrH4hw-x7vUQMSjErvXK1qvsrFCpfNr7XlArx0HjI6kT5XEaHgQNdC0zrLw9cZ4rewoEisR3H5oM7B6gMaP82wTSFVBXvpn5r0KT-Iuc3JuG2en1zVh3GNf110oQCKQ**"
|
58
|
+
@token = OpenToken.decode @opentoken
|
59
|
+
end
|
60
|
+
end
|
61
|
+
should 'preserve apostrophe in attribute payload' do
|
62
|
+
assert_equal "D'angelo", @token[:last_name]
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
should 'raise invalid token error parsing nil token' do
|
67
|
+
assert_raises OpenToken::TokenInvalidError do
|
68
|
+
OpenToken.decode nil
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
context "encoding token" do
|
74
|
+
setup do
|
75
|
+
OpenToken.password = "Password1"
|
76
|
+
end
|
77
|
+
context "with aes-128-cbc and subject attribute" do
|
78
|
+
setup do
|
79
|
+
@attributesIn = { "subject" => "john", "email" => "john@example.com"}
|
80
|
+
@token = OpenToken.encode @attributesIn, OpenToken::Cipher::AES_128_CBC
|
81
|
+
end
|
82
|
+
should "be decodable" do
|
83
|
+
@attributesOut = OpenToken.decode @token
|
84
|
+
assert_equal @attributesIn, @attributesOut
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
context "encoding token with utf-8 values" do
|
90
|
+
setup do
|
91
|
+
OpenToken.password = "Password1"
|
92
|
+
end
|
93
|
+
context "with aes-128-cbc and subject attribute" do
|
94
|
+
setup do
|
95
|
+
@subject = OpenToken.send(:force_encoding, "Andr\xC3\xA9", 'UTF-8')
|
96
|
+
@attributesIn = { "subject" => @subject, "email" => "john@example.com"}
|
97
|
+
@token = OpenToken.encode @attributesIn, OpenToken::Cipher::AES_128_CBC
|
98
|
+
end
|
99
|
+
should "be decodable" do
|
100
|
+
@attributesOut = OpenToken.decode @token
|
101
|
+
assert_equal @attributesIn, @attributesOut
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
metadata
ADDED
@@ -0,0 +1,159 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: opentoken-newrelic-rails23
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 27
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 1
|
8
|
+
- 2
|
9
|
+
- 2
|
10
|
+
version: 1.2.2
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Ryan Sonnek
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2012-05-09 00:00:00 Z
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: activesupport
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ~>
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
hash: 31
|
29
|
+
segments:
|
30
|
+
- 2
|
31
|
+
- 3
|
32
|
+
- 14
|
33
|
+
version: 2.3.14
|
34
|
+
type: :runtime
|
35
|
+
version_requirements: *id001
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: i18n
|
38
|
+
prerelease: false
|
39
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
40
|
+
none: false
|
41
|
+
requirements:
|
42
|
+
- - ">="
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
hash: 3
|
45
|
+
segments:
|
46
|
+
- 0
|
47
|
+
version: "0"
|
48
|
+
type: :runtime
|
49
|
+
version_requirements: *id002
|
50
|
+
- !ruby/object:Gem::Dependency
|
51
|
+
name: shoulda
|
52
|
+
prerelease: false
|
53
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
54
|
+
none: false
|
55
|
+
requirements:
|
56
|
+
- - "="
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
hash: 37
|
59
|
+
segments:
|
60
|
+
- 2
|
61
|
+
- 11
|
62
|
+
- 3
|
63
|
+
version: 2.11.3
|
64
|
+
type: :development
|
65
|
+
version_requirements: *id003
|
66
|
+
- !ruby/object:Gem::Dependency
|
67
|
+
name: timecop
|
68
|
+
prerelease: false
|
69
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
70
|
+
none: false
|
71
|
+
requirements:
|
72
|
+
- - "="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
hash: 25
|
75
|
+
segments:
|
76
|
+
- 0
|
77
|
+
- 3
|
78
|
+
- 5
|
79
|
+
version: 0.3.5
|
80
|
+
type: :development
|
81
|
+
version_requirements: *id004
|
82
|
+
- !ruby/object:Gem::Dependency
|
83
|
+
name: rake
|
84
|
+
prerelease: false
|
85
|
+
requirement: &id005 !ruby/object:Gem::Requirement
|
86
|
+
none: false
|
87
|
+
requirements:
|
88
|
+
- - "="
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
hash: 63
|
91
|
+
segments:
|
92
|
+
- 0
|
93
|
+
- 9
|
94
|
+
- 2
|
95
|
+
version: 0.9.2
|
96
|
+
type: :development
|
97
|
+
version_requirements: *id005
|
98
|
+
description: parse opentoken properties passed for Single Signon requests
|
99
|
+
email:
|
100
|
+
- ryan@socialcast.com
|
101
|
+
executables: []
|
102
|
+
|
103
|
+
extensions: []
|
104
|
+
|
105
|
+
extra_rdoc_files: []
|
106
|
+
|
107
|
+
files:
|
108
|
+
- .document
|
109
|
+
- .gitignore
|
110
|
+
- CONTRIBUTORS.txt
|
111
|
+
- Gemfile
|
112
|
+
- LICENSE.txt
|
113
|
+
- README.md
|
114
|
+
- Rakefile
|
115
|
+
- lib/opentoken-newrelic-rails23.rb
|
116
|
+
- lib/opentoken-newrelic-rails23/cipher.rb
|
117
|
+
- lib/opentoken-newrelic-rails23/key_value_serializer.rb
|
118
|
+
- lib/opentoken-newrelic-rails23/password_key_generator.rb
|
119
|
+
- lib/opentoken-newrelic-rails23/token.rb
|
120
|
+
- lib/opentoken-newrelic-rails23/version.rb
|
121
|
+
- opentoken-newrelic-rails23.gemspec
|
122
|
+
- test/helper.rb
|
123
|
+
- test/test_opentoken.rb
|
124
|
+
homepage: http://github.com/newrelic/opentoken-newrelic-rails23
|
125
|
+
licenses: []
|
126
|
+
|
127
|
+
post_install_message:
|
128
|
+
rdoc_options: []
|
129
|
+
|
130
|
+
require_paths:
|
131
|
+
- lib
|
132
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
133
|
+
none: false
|
134
|
+
requirements:
|
135
|
+
- - ">="
|
136
|
+
- !ruby/object:Gem::Version
|
137
|
+
hash: 3
|
138
|
+
segments:
|
139
|
+
- 0
|
140
|
+
version: "0"
|
141
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
142
|
+
none: false
|
143
|
+
requirements:
|
144
|
+
- - ">="
|
145
|
+
- !ruby/object:Gem::Version
|
146
|
+
hash: 3
|
147
|
+
segments:
|
148
|
+
- 0
|
149
|
+
version: "0"
|
150
|
+
requirements: []
|
151
|
+
|
152
|
+
rubyforge_project: opentoken-newrelic-rails23
|
153
|
+
rubygems_version: 1.8.10
|
154
|
+
signing_key:
|
155
|
+
specification_version: 3
|
156
|
+
summary: ruby implementation of the opentoken specification, forked for Rails 2.3 compatibility. Use the non-forked version for Rails 3+
|
157
|
+
test_files:
|
158
|
+
- test/helper.rb
|
159
|
+
- test/test_opentoken.rb
|