ronin-web-session_cookie 0.1.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,169 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # Copyright (c) 2023-2024 Hal Brodigan (postmodern.mod3@gmail.com)
4
+ #
5
+ # ronin-web-session_cookie is free software: you can redistribute it and/or modify
6
+ # it under the terms of the GNU Lesser General Public License as published
7
+ # by the Free Software Foundation, either version 3 of the License, or
8
+ # (at your option) any later version.
9
+ #
10
+ # ronin-web-session_cookie is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU Lesser General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU Lesser General Public License
16
+ # along with ronin-web-session_cookie. If not, see <https://www.gnu.org/licenses/>.
17
+ #
18
+
19
+ require 'ronin/web/session_cookie/cookie'
20
+ require 'ronin/support/encoding/base64'
21
+ require 'ronin/support/encoding/base62'
22
+
23
+ require 'python/pickle'
24
+
25
+ module Ronin
26
+ module Web
27
+ module SessionCookie
28
+ #
29
+ # Represents a Django signed session cookie (JSON or Pickle serialized).
30
+ #
31
+ # ## Examples
32
+ #
33
+ # Parse a Django JSON session cookie:
34
+ #
35
+ # Ronin::Web::SessionCookie.parse('sessionid=eyJmb28iOiJiYXIifQ:1pQcTx:UufiSnuPIjNs7zOAJS0UpqnyvRt7KET7BVes0I8LYbA')
36
+ # # =>
37
+ # # #<Ronin::Web::SessionCookie::Django:0x00007f29bb9c6b70
38
+ # # @hmac=
39
+ # # "R\xE7\xE2J{\x8F\"3l\xEF3\x80%-\x14\xA6\xA9\xF2\xBD\e{(D\xFB\x05W\xAC\xD0\x8F\va\xB0",
40
+ # # @params={"foo"=>"bar"},
41
+ # # @salt=1676070425>
42
+ #
43
+ # Parse a Django Pickled session cookie:
44
+ #
45
+ # Ronin::Web::SessionCookie.parse('sessionid=gAWVEAAAAAAAAAB9lIwDZm9vlIwDYmFylHMu:1pQcay:RjaK8DKN4xXQ_APIXXWEyFS08Q-PGo6UlRBFpedFk9M')
46
+ # # =>
47
+ # # #<Ronin::Web::SessionCookie::Django:0x00007f29b7aa6dc8
48
+ # # @hmac=
49
+ # # "F6\x8A\xF02\x8D\xE3\x15\xD0\xFC\x03\xC8]u\x84\xC8T\xB4\xF1\x0F\x8F\x1A\x8E\x94\x95\x10E\xA5\xE7E\x93\xD3",
50
+ # # @params={"foo"=>"bar"},
51
+ # # @salt=1676070860>
52
+ #
53
+ # @see https://docs.djangoproject.com/en/4.1/topics/http/sessions/#using-cookie-based-sessions
54
+ #
55
+ class Django < Cookie
56
+
57
+ # The salt used to sign the cookie.
58
+ #
59
+ # @return [Integer]
60
+ #
61
+ # @api public
62
+ attr_reader :salt
63
+
64
+ # The SHA256 HMAC of the Base64 encoded serialized {#params}.
65
+ #
66
+ # @return [String]
67
+ #
68
+ # @api public
69
+ attr_reader :hmac
70
+
71
+ #
72
+ # Initializes the Django cookie.
73
+ #
74
+ # @param [Hash{String => Object}] params
75
+ # The deserialized params of the session cookie.
76
+ #
77
+ # @param [Integer] salt
78
+ # The Base62 decoded timestamp that is used to salt the HMAC.
79
+ #
80
+ # @param [Integer] hmac
81
+ # The SHA256 HMAC of the Base64 encoded serialized {#params}.
82
+ #
83
+ # @api private
84
+ #
85
+ def initialize(params,salt,hmac)
86
+ super(params)
87
+
88
+ @salt = salt
89
+ @hmac = hmac
90
+ end
91
+
92
+ # Regular expression to match Django session cookies.
93
+ REGEXP = /\A(?:sessionid=)?#{URL_SAFE_BASE64_REGEXP}:#{URL_SAFE_BASE64_REGEXP}:#{URL_SAFE_BASE64_REGEXP}\z/
94
+
95
+ #
96
+ # Identifies if the cookie is a Django session cookie.
97
+ #
98
+ # @param [String] string
99
+ # The raw session cookie value.
100
+ #
101
+ # @return [Boolean]
102
+ # Indicates whether the session cookie value is a Django session
103
+ # cookie.
104
+ #
105
+ # @api public
106
+ #
107
+ def self.identify?(string)
108
+ string =~ REGEXP
109
+ end
110
+
111
+ #
112
+ # Parses a Django session cookie.
113
+ #
114
+ # @param [String] string
115
+ # The raw session cookie string to parse.
116
+ #
117
+ # @return [Django]
118
+ # The parsed and deserialized session cookie
119
+ #
120
+ # @api public
121
+ #
122
+ def self.parse(string)
123
+ # remove any 'sessionid' prefix.
124
+ string = string.sub(/\Asessionid=/,'')
125
+
126
+ # split the cookie
127
+ params, salt, hmac = string.split(':',3)
128
+
129
+ params = Support::Encoding::Base64.decode(params, mode: :url_safe)
130
+ params = if params.start_with?('{') && params.end_with?('}')
131
+ # JSON serialized cookie
132
+ JSON.parse(params)
133
+ else
134
+ # unpickle the Python Pickle serialized session cookie
135
+ Python::Pickle.load(params)
136
+ end
137
+
138
+ salt = Support::Encoding::Base62.decode(salt)
139
+ hmac = Support::Encoding::Base64.decode(hmac, mode: :url_safe)
140
+
141
+ return new(params,salt,hmac)
142
+ end
143
+
144
+ #
145
+ # Extracts the Django session cookie from the HTTP response.
146
+ #
147
+ # @param [Net::HTTPResponse] response
148
+ # The HTTP response object.
149
+ #
150
+ # @return [Django, nil]
151
+ # The parsed Django session cookie, or `nil` if there was no
152
+ # `Set-Cookie` header containing a Django session cookie.
153
+ #
154
+ # @api public
155
+ #
156
+ def self.extract(response)
157
+ if (set_cookie = response['Set-Cookie'])
158
+ cookie = set_cookie.split(';',2).first
159
+
160
+ if identify?(cookie)
161
+ return parse(cookie)
162
+ end
163
+ end
164
+ end
165
+
166
+ end
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,151 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # Copyright (c) 2023-2024 Hal Brodigan (postmodern.mod3@gmail.com)
4
+ #
5
+ # ronin-web-session_cookie is free software: you can redistribute it and/or modify
6
+ # it under the terms of the GNU Lesser General Public License as published
7
+ # by the Free Software Foundation, either version 3 of the License, or
8
+ # (at your option) any later version.
9
+ #
10
+ # ronin-web-session_cookie is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU Lesser General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU Lesser General Public License
16
+ # along with ronin-web-session_cookie. If not, see <https://www.gnu.org/licenses/>.
17
+ #
18
+
19
+ require 'ronin/web/session_cookie/cookie'
20
+
21
+ require 'base64'
22
+ require 'json'
23
+
24
+ module Ronin
25
+ module Web
26
+ module SessionCookie
27
+ #
28
+ # Represents a [JSON Web Token (JWT)][JWT].
29
+ #
30
+ # [JWT]: https://jwt.io
31
+ #
32
+ # ## Examples
33
+ #
34
+ # Ronin::Web::SessionCookie.parse('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c')
35
+ # # =>
36
+ # # #<Ronin::Web::SessionCookie::JWT:0x00007f18d5a45e58
37
+ # # @header={"alg"=>"HS256", "typ"=>"JWT"},
38
+ # # @hmac=
39
+ # # ":\x93\x92K\x0E\xDE\xE3\xCEK8\xFEO\xAF4\x9C\xC4v\xFBI\x1E\xAC\x00\xE3\x11rG\xC5\xC2.+\xA7\xBA",
40
+ # # @params={"id"=>123456789, "name"=>"Joseph"}>
41
+ #
42
+ # @see https://jwt.io/
43
+ #
44
+ class JWT < Cookie
45
+
46
+ # The parsed JWT header information.
47
+ #
48
+ # @return [Hash{String => Object}]
49
+ #
50
+ # @api public
51
+ attr_reader :header
52
+
53
+ # The SHA256 HMAC of the encoded {#header} + `.` + the encoded
54
+ # {#payload}.
55
+ #
56
+ # @return [String]
57
+ #
58
+ # @api public
59
+ attr_reader :hmac
60
+
61
+ alias payload params
62
+
63
+ #
64
+ # Initializes the parsed JWT session cookie.
65
+ #
66
+ # @param [Hash{String => Object}] header
67
+ # The parsed header information.
68
+ #
69
+ # @param [Hash{String => Object}] payload
70
+ # The parsed JWT payload.
71
+ #
72
+ # @param [String] hmac
73
+ # The SHA256 HMAC of the encoded header + `.` + the encoded payload.
74
+ #
75
+ # @api private
76
+ #
77
+ def initialize(header,payload,hmac)
78
+ @header = header
79
+
80
+ super(payload)
81
+
82
+ @hmac = hmac
83
+ end
84
+
85
+ # Regular expression to match JWT session cookies.
86
+ REGEXP = /\A(Bearer )?#{URL_SAFE_BASE64_REGEXP}\.#{URL_SAFE_BASE64_REGEXP}\.#{URL_SAFE_BASE64_REGEXP}\z/
87
+
88
+ #
89
+ # Identifies whether the string is a JWT session cookie.
90
+ #
91
+ # @param [String] string
92
+ # The raw session cookie value to identify.
93
+ #
94
+ # @return [Boolean]
95
+ # Indicates whether the session cookie value is a JWT session cookie.
96
+ #
97
+ # @api public
98
+ #
99
+ def self.identify?(string)
100
+ string =~ REGEXP
101
+ end
102
+
103
+ #
104
+ # Parses a JWT session cookie.
105
+ #
106
+ # @param [String] string
107
+ # The raw session cookie string to parse.
108
+ #
109
+ # @return [JWT]
110
+ # The parsed and deserialized session cookie
111
+ #
112
+ # @api public
113
+ #
114
+ def self.parse(string)
115
+ # remove any 'Bearer ' prefix.
116
+ string = string.sub(/\ABearer /,'')
117
+
118
+ # split the string
119
+ header, payload, hmac = string.split('.',3)
120
+
121
+ header = JSON.parse(Base64.decode64(header))
122
+ payload = JSON.parse(Base64.decode64(payload))
123
+ hmac = Base64.decode64(hmac)
124
+
125
+ return new(header,payload,hmac)
126
+ end
127
+
128
+ #
129
+ # Extracts the JWT session cookie from the HTTP response.
130
+ #
131
+ # @param [Net::HTTPResponse] response
132
+ # The HTTP response object.
133
+ #
134
+ # @return [JWT, nil]
135
+ # The parsed JWT session cookie, or `nil` if there was no
136
+ # `Authorization` header containing a JWT session cookie.
137
+ #
138
+ # @api public
139
+ #
140
+ def self.extract(response)
141
+ if (authorization = response['Authorization'])
142
+ if (match = authorization.match(REGEXP))
143
+ return parse(match[0])
144
+ end
145
+ end
146
+ end
147
+
148
+ end
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # Copyright (c) 2023-2024 Hal Brodigan (postmodern.mod3@gmail.com)
4
+ #
5
+ # ronin-web-session_cookie is free software: you can redistribute it and/or modify
6
+ # it under the terms of the GNU Lesser General Public License as published
7
+ # by the Free Software Foundation, either version 3 of the License, or
8
+ # (at your option) any later version.
9
+ #
10
+ # ronin-web-session_cookie is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU Lesser General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU Lesser General Public License
16
+ # along with ronin-web-session_cookie. If not, see <https://www.gnu.org/licenses/>.
17
+ #
18
+
19
+ require 'ronin/web/session_cookie/cookie'
20
+
21
+ require 'base64'
22
+ require 'delegate'
23
+ require 'rack/session/cookie'
24
+
25
+ module Ronin
26
+ module Web
27
+ module SessionCookie
28
+ #
29
+ # Represents a [Rack][rack-session] session cookie.
30
+ #
31
+ # [rack-session]: https://github.com/rack/rack-session
32
+ #
33
+ # ## Examples
34
+ #
35
+ # Ronin::Web::SessionCookie.parse('rack.session=BAh7CEkiD3Nlc3Npb25faWQGOgZFVG86HVJhY2s6OlNlc3Npb246OlNlc3Npb25JZAY6D0BwdWJsaWNfaWRJIkUyYWJkZTdkM2I0YTMxNDE5OThiYmMyYTE0YjFmMTZlNTNlMWMzYWJlYzhiYzc4ZjVhMGFlMGUwODJmMjJlZGIxBjsARkkiCWNzcmYGOwBGSSIxNHY1TmRCMGRVaklXdjhzR3J1b2ZhM2xwNHQyVGp5ZHptckQycjJRWXpIZz0GOwBGSSINdHJhY2tpbmcGOwBGewZJIhRIVFRQX1VTRVJfQUdFTlQGOwBUSSItOTkxNzUyMWYzN2M4ODJkNDIyMzhmYmI5Yzg4MzFmMWVmNTAwNGQyYwY7AEY%3D--02184e43850f38a46c8f22ffb49f7f22be58e272')
36
+ # # =>
37
+ # # #<Ronin::Web::SessionCookie::Rack:0x00007ff67455ee30
38
+ # # @params=
39
+ # # {"session_id"=>"2abde7d3b4a3141998bbc2a14b1f16e53e1c3abec8bc78f5a0ae0e082f22edb1",
40
+ # # "csrf"=>"4v5NdB0dUjIWv8sGruofa3lp4t2TjydzmrD2r2QYzHg=",
41
+ # # "tracking"=>{"HTTP_USER_AGENT"=>"9917521f37c882d42238fbb9c8831f1ef5004d2c"}}>
42
+ #
43
+ # @see https://github.com/rack/rack-session
44
+ #
45
+ class Rack < Cookie
46
+
47
+ # The HMAC for the deserialized and Base64 encoded session cookie.
48
+ #
49
+ # @return [String]
50
+ attr_reader :hmac
51
+
52
+ #
53
+ # Initializes the parsed Rack session cookie.
54
+ #
55
+ # @param [Hash{String => Object}] params
56
+ # The parsed params for the session cookie.
57
+ #
58
+ # @param [String] hmac
59
+ # The HMAC for the serialized and Base64 encoded session cookie.
60
+ #
61
+ # @api private
62
+ #
63
+ def initialize(params,hmac)
64
+ super(params)
65
+
66
+ @hmac = hmac
67
+ end
68
+
69
+ # Regular expression to match Rack session cookies.
70
+ REGEXP = /\A(rack\.session=)?(?:#{STRICT_BASE64_REGEXP}|#{URI_ENCODED_BASE64_REGEXP})--[0-9a-f]{40}\z/
71
+
72
+ #
73
+ # Identifies if the cookie is a Rack session cookie.
74
+ #
75
+ # @param [String] string
76
+ # The raw session cookie value to identify.
77
+ #
78
+ # @return [Boolean]
79
+ # Indicates whether the session cookie is a Rack session cookie.
80
+ #
81
+ # @api public
82
+ #
83
+ def self.identify?(string)
84
+ string =~ REGEXP
85
+ end
86
+
87
+ #
88
+ # Parses a Django session cookie.
89
+ #
90
+ # @param [String] string
91
+ # The raw session cookie string to parse.
92
+ #
93
+ # @return [Rack]
94
+ # The parsed and deserialized session cookie
95
+ #
96
+ # @api public
97
+ #
98
+ def self.parse(string)
99
+ # remove any 'rack.session' prefix.
100
+ string = string.sub(/\Arack\.session=/,'')
101
+
102
+ payload, hmac = string.split('--',2)
103
+
104
+ return new(Marshal.load(Base64.decode64(payload)),hmac)
105
+ end
106
+
107
+ #
108
+ # Extracts the Rack session cookie from the HTTP response.
109
+ #
110
+ # @param [Net::HTTPResponse] response
111
+ # The HTTP response object.
112
+ #
113
+ # @return [Rack, nil]
114
+ # The parsed Rack session cookie, or `nil` if there was no
115
+ # `Set-Cookie` header containing a Rack session cookie.
116
+ #
117
+ # @api public
118
+ #
119
+ def self.extract(response)
120
+ if (set_cookie = response['Set-Cookie'])
121
+ cookie = set_cookie.split(';',2).first
122
+
123
+ if identify?(cookie)
124
+ return parse(cookie)
125
+ end
126
+ end
127
+ end
128
+
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # Copyright (c) 2023-2024 Hal Brodigan (postmodern.mod3@gmail.com)
4
+ #
5
+ # ronin-web-session_cookie is free software: you can redistribute it and/or modify
6
+ # it under the terms of the GNU Lesser General Public License as published
7
+ # by the Free Software Foundation, either version 3 of the License, or
8
+ # (at your option) any later version.
9
+ #
10
+ # ronin-web-session_cookie is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU Lesser General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU Lesser General Public License
16
+ # along with ronin-web-session_cookie. If not, see <https://www.gnu.org/licenses/>.
17
+ #
18
+
19
+ module Ronin
20
+ module Web
21
+ module SessionCookie
22
+ # ronin-web-session_cookie version
23
+ VERSION = '0.1.0.rc1'
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # Copyright (c) 2023-2024 Hal Brodigan (postmodern.mod3@gmail.com)
4
+ #
5
+ # ronin-web-session_cookie is free software: you can redistribute it and/or modify
6
+ # it under the terms of the GNU Lesser General Public License as published
7
+ # by the Free Software Foundation, either version 3 of the License, or
8
+ # (at your option) any later version.
9
+ #
10
+ # ronin-web-session_cookie is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU Lesser General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU Lesser General Public License
16
+ # along with ronin-web-session_cookie. If not, see <https://www.gnu.org/licenses/>.
17
+ #
18
+
19
+ require 'ronin/web/session_cookie/rack'
20
+ require 'ronin/web/session_cookie/django'
21
+ require 'ronin/web/session_cookie/jwt'
22
+
23
+ module Ronin
24
+ module Web
25
+ #
26
+ # Namespace for `ronin-web-session_cookie`.
27
+ #
28
+ module SessionCookie
29
+ # All session cookie classes.
30
+ #
31
+ # @api private
32
+ CLASSES = [
33
+ Rack,
34
+ JWT,
35
+ Django
36
+ ]
37
+
38
+ #
39
+ # Parses the session cookie.
40
+ #
41
+ # @param [String] string
42
+ # The raw session cookie to parse.
43
+ #
44
+ # @return [Rack, Django, JWT, nil]
45
+ # The parsed and deserialized session cookie data.
46
+ # Returns `nil` if the session cookie did not match any of the supported
47
+ # formats.
48
+ #
49
+ # @api public
50
+ #
51
+ def self.parse(string)
52
+ CLASSES.each do |klass|
53
+ if klass.identify?(string)
54
+ return klass.parse(string)
55
+ end
56
+ end
57
+
58
+ return nil
59
+ end
60
+
61
+ #
62
+ # Extracts and parses the session cookie from the HTTP response.
63
+ #
64
+ # @param [Net::HTTPResponse] response
65
+ # The HTTP response object.
66
+ #
67
+ # @return [Rack, Django, JWT, nil]
68
+ # The parsed session cookie or `nil` if no session cookie could be
69
+ # detected.
70
+ #
71
+ # @api public
72
+ #
73
+ def self.extract(response)
74
+ CLASSES.each do |klass|
75
+ if (session_cookie = klass.extract(response))
76
+ return session_cookie
77
+ end
78
+ end
79
+
80
+ return nil
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+
5
+ Gem::Specification.new do |gem|
6
+ gemspec = YAML.load_file('gemspec.yml')
7
+
8
+ gem.name = gemspec.fetch('name')
9
+ gem.version = gemspec.fetch('version') do
10
+ lib_dir = File.join(File.dirname(__FILE__),'lib')
11
+ $LOAD_PATH << lib_dir unless $LOAD_PATH.include?(lib_dir)
12
+
13
+ require 'ronin/web/session_cookie/version'
14
+ Ronin::Web::SessionCookie::VERSION
15
+ end
16
+
17
+ gem.summary = gemspec['summary']
18
+ gem.description = gemspec['description']
19
+ gem.licenses = Array(gemspec['license'])
20
+ gem.authors = Array(gemspec['authors'])
21
+ gem.email = gemspec['email']
22
+ gem.homepage = gemspec['homepage']
23
+ gem.metadata = gemspec['metadata'] if gemspec['metadata']
24
+
25
+ glob = ->(patterns) { gem.files & Dir[*patterns] }
26
+
27
+ gem.files = `git ls-files`.split($/)
28
+ gem.files = glob[gemspec['files']] if gemspec['files']
29
+ gem.files += Array(gemspec['generated_files'])
30
+ # exclude test files from the packages gem
31
+ gem.files -= glob[gemspec['test_files'] || 'spec/{**/}*']
32
+
33
+ gem.executables = gemspec.fetch('executables') do
34
+ glob['bin/*'].map { |path| File.basename(path) }
35
+ end
36
+
37
+ gem.extensions = glob[gemspec['extensions'] || 'ext/**/extconf.rb']
38
+ gem.extra_rdoc_files = glob[gemspec['extra_doc_files'] || '*.{txt,md}']
39
+
40
+ gem.require_paths = Array(gemspec.fetch('require_paths') {
41
+ %w[ext lib].select { |dir| File.directory?(dir) }
42
+ })
43
+
44
+ gem.requirements = gemspec['requirements']
45
+ gem.required_ruby_version = gemspec['required_ruby_version']
46
+ gem.required_rubygems_version = gemspec['required_rubygems_version']
47
+ gem.post_install_message = gemspec['post_install_message']
48
+
49
+ split = ->(string) { string.split(/,\s*/) }
50
+
51
+ if gemspec['dependencies']
52
+ gemspec['dependencies'].each do |name,versions|
53
+ gem.add_dependency(name,split[versions])
54
+ end
55
+ end
56
+
57
+ if gemspec['development_dependencies']
58
+ gemspec['development_dependencies'].each do |name,versions|
59
+ gem.add_development_dependency(name,split[versions])
60
+ end
61
+ end
62
+ end