rack-protection 1.5.5 → 2.1.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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +13 -0
  3. data/License +4 -1
  4. data/README.md +41 -13
  5. data/Rakefile +29 -5
  6. data/lib/rack/protection.rb +41 -24
  7. data/lib/rack/protection/authenticity_token.rb +181 -9
  8. data/lib/rack/protection/base.rb +3 -22
  9. data/lib/rack/protection/content_security_policy.rb +79 -0
  10. data/lib/rack/protection/cookie_tossing.rb +75 -0
  11. data/lib/rack/protection/escaped_params.rb +2 -0
  12. data/lib/rack/protection/form_token.rb +1 -1
  13. data/lib/rack/protection/http_origin.rb +17 -2
  14. data/lib/rack/protection/json_csrf.rb +26 -4
  15. data/lib/rack/protection/path_traversal.rb +4 -12
  16. data/lib/rack/protection/referrer_policy.rb +25 -0
  17. data/lib/rack/protection/remote_token.rb +1 -1
  18. data/lib/rack/protection/session_hijacking.rb +1 -1
  19. data/lib/rack/protection/strict_transport.rb +39 -0
  20. data/lib/rack/protection/version.rb +1 -12
  21. data/lib/rack/protection/xss_header.rb +1 -1
  22. data/rack-protection.gemspec +26 -104
  23. metadata +21 -82
  24. data/spec/authenticity_token_spec.rb +0 -48
  25. data/spec/base_spec.rb +0 -40
  26. data/spec/escaped_params_spec.rb +0 -43
  27. data/spec/form_token_spec.rb +0 -33
  28. data/spec/frame_options_spec.rb +0 -39
  29. data/spec/http_origin_spec.rb +0 -38
  30. data/spec/ip_spoofing_spec.rb +0 -35
  31. data/spec/json_csrf_spec.rb +0 -58
  32. data/spec/path_traversal_spec.rb +0 -41
  33. data/spec/protection_spec.rb +0 -105
  34. data/spec/remote_referrer_spec.rb +0 -31
  35. data/spec/remote_token_spec.rb +0 -42
  36. data/spec/session_hijacking_spec.rb +0 -55
  37. data/spec/spec_helper.rb +0 -163
  38. data/spec/xss_header_spec.rb +0 -56
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2b7d78da301d9f7fc81ae73e46a389c2b8ce10ab8121f169fd760018ac506d47
4
- data.tar.gz: a91bd28f8624f325d6714262ac11d83e0a347405f14e600780cb1bfd846e5b34
3
+ metadata.gz: a3268bb2b60f8095b38658717f5e267da2e1dfbee57f487baf39a185d3cf9266
4
+ data.tar.gz: fc40122b95963a81333da038536782d85a9abdc92b823eb5d3044ef3c5c807c4
5
5
  SHA512:
6
- metadata.gz: 0c5de92c0283313c00d50c1f9a219c808ad587caabff81c4d1530abd8f0e7d9c0f3753ad9bab7c06a29ce97b2a717fddc04ced642adff058f3431419286e4da6
7
- data.tar.gz: d3bf5830bf30475871b73ba54ee38f962bd93c2e1f420b59b649d6dcb7f97d89f091d3add3f692ba847ffe5f5c6cade665d522c86220087d977fb3706e41bd58
6
+ metadata.gz: 7b381c903bb99d1e8cfcd00554642aafc2644f4432987be95e094a84b0f020648efb92b4a48e082cda5749045c44d14e5f08d97a1a733bf2b1e850eeaf69d67b
7
+ data.tar.gz: bfb60cf9484528f0096cd68a6da55b66fa3471407a120caff83ee961b54043f3e4aca45a219273e7e48cc85ce70742ee75dab750609239fb887dfc35dfbcae59
data/Gemfile ADDED
@@ -0,0 +1,13 @@
1
+ source "https://rubygems.org"
2
+ # encoding: utf-8
3
+
4
+ gem 'rake'
5
+
6
+ rack_version = ENV['rack'].to_s
7
+ rack_version = nil if rack_version.empty? or rack_version == 'stable'
8
+ rack_version = {:github => 'rack/rack'} if rack_version == 'master'
9
+ gem 'rack', rack_version
10
+
11
+ gem 'sinatra', path: '..'
12
+
13
+ gemspec
data/License CHANGED
@@ -1,4 +1,7 @@
1
- Copyright (c) 2011 Konstantin Haase
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2011-2017 Konstantin Haase
4
+ Copyright (c) 2015-2017 Zachary Scott
2
5
 
3
6
  Permission is hereby granted, free of charge, to any person obtaining
4
7
  a copy of this software and associated documentation files (the
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- You should use protection!
1
+ # Rack::Protection
2
2
 
3
3
  This gem protects against typical web attacks.
4
4
  Should work for all Rack apps, including Rails.
@@ -38,43 +38,55 @@ run MyApp
38
38
 
39
39
  Prevented by:
40
40
 
41
- * `Rack::Protection::AuthenticityToken` (not included by `use Rack::Protection`)
42
- * `Rack::Protection::FormToken` (not included by `use Rack::Protection`)
43
- * `Rack::Protection::JsonCsrf`
44
- * `Rack::Protection::RemoteReferrer` (not included by `use Rack::Protection`)
45
- * `Rack::Protection::RemoteToken`
46
- * `Rack::Protection::HttpOrigin`
41
+ * [`Rack::Protection::AuthenticityToken`][authenticity-token] (not included by `use Rack::Protection`)
42
+ * [`Rack::Protection::FormToken`][form-token] (not included by `use Rack::Protection`)
43
+ * [`Rack::Protection::JsonCsrf`][json-csrf]
44
+ * [`Rack::Protection::RemoteReferrer`][remote-referrer] (not included by `use Rack::Protection`)
45
+ * [`Rack::Protection::RemoteToken`][remote-token]
46
+ * [`Rack::Protection::HttpOrigin`][http-origin]
47
47
 
48
48
  ## Cross Site Scripting
49
49
 
50
50
  Prevented by:
51
51
 
52
- * `Rack::Protection::EscapedParams` (not included by `use Rack::Protection`)
53
- * `Rack::Protection::XSSHeader` (Internet Explorer only)
52
+ * [`Rack::Protection::EscapedParams`][escaped-params] (not included by `use Rack::Protection`)
53
+ * [`Rack::Protection::XSSHeader`][xss-header] (Internet Explorer and Chrome only)
54
+ * [`Rack::Protection::ContentSecurityPolicy`][content-security-policy]
54
55
 
55
56
  ## Clickjacking
56
57
 
57
58
  Prevented by:
58
59
 
59
- * `Rack::Protection::FrameOptions`
60
+ * [`Rack::Protection::FrameOptions`][frame-options]
60
61
 
61
62
  ## Directory Traversal
62
63
 
63
64
  Prevented by:
64
65
 
65
- * `Rack::Protection::PathTraversal`
66
+ * [`Rack::Protection::PathTraversal`][path-traversal]
66
67
 
67
68
  ## Session Hijacking
68
69
 
69
70
  Prevented by:
70
71
 
71
- * `Rack::Protection::SessionHijacking`
72
+ * [`Rack::Protection::SessionHijacking`][session-hijacking]
73
+
74
+ ## Cookie Tossing
75
+
76
+ Prevented by:
77
+ * [`Rack::Protection::CookieTossing`][cookie-tossing] (not included by `use Rack::Protection`)
72
78
 
73
79
  ## IP Spoofing
74
80
 
75
81
  Prevented by:
76
82
 
77
- * `Rack::Protection::IPSpoofing`
83
+ * [`Rack::Protection::IPSpoofing`][ip-spoofing]
84
+
85
+ ## Helps to protect against protocol downgrade attacks and cookie hijacking
86
+
87
+ Prevented by:
88
+
89
+ * [`Rack::Protection::StrictTransport`][strict-transport] (not included by `use Rack::Protection`)
78
90
 
79
91
  # Installation
80
92
 
@@ -88,3 +100,19 @@ use Rack::Protection, instrumenter: ActiveSupport::Notifications
88
100
  ```
89
101
 
90
102
  The instrumenter is passed a namespace (String) and environment (Hash). The namespace is 'rack.protection' and the attack type can be obtained from the environment key 'rack.protection.attack'.
103
+
104
+ [authenticity-token]: http://www.sinatrarb.com/protection/authenticity_token
105
+ [content-security-policy]: http://www.sinatrarb.com/protection/content_security_policy
106
+ [cookie-tossing]: http://www.sinatrarb.com/protection/cookie_tossing
107
+ [escaped-params]: http://www.sinatrarb.com/protection/escaped_params
108
+ [form-token]: http://www.sinatrarb.com/protection/form_token
109
+ [frame-options]: http://www.sinatrarb.com/protection/frame_options
110
+ [http-origin]: http://www.sinatrarb.com/protection/http_origin
111
+ [ip-spoofing]: http://www.sinatrarb.com/protection/ip_spoofing
112
+ [json-csrf]: http://www.sinatrarb.com/protection/json_csrf
113
+ [path-traversal]: http://www.sinatrarb.com/protection/path_traversal
114
+ [remote-referrer]: http://www.sinatrarb.com/protection/remote_referrer
115
+ [remote-token]: http://www.sinatrarb.com/protection/remote_token
116
+ [session-hijacking]: http://www.sinatrarb.com/protection/session_hijacking
117
+ [strict-transport]: http://www.sinatrarb.com/protection/strict_transport
118
+ [xss-header]: http://www.sinatrarb.com/protection/xss_header
data/Rakefile CHANGED
@@ -11,6 +11,33 @@ end
11
11
  desc "run specs"
12
12
  task(:spec) { ruby '-S rspec spec' }
13
13
 
14
+ namespace :doc do
15
+ task :readmes do
16
+ Dir.glob 'lib/rack/protection/*.rb' do |file|
17
+ excluded_files = %w[lib/rack/protection/base.rb lib/rack/protection/version.rb]
18
+ next if excluded_files.include?(file)
19
+ doc = File.read(file)[/^ module Protection(\n)+( #[^\n]*\n)*/m].scan(/^ *#(?!#) ?(.*)\n/).join("\n")
20
+ file = "doc/#{file[4..-4].tr("/_", "-")}.rdoc"
21
+ Dir.mkdir "doc" unless File.directory? "doc"
22
+ puts "writing #{file}"
23
+ File.open(file, "w") { |f| f << doc }
24
+ end
25
+ end
26
+
27
+ task :index do
28
+ doc = File.read("README.md")
29
+ file = "doc/rack-protection-readme.md"
30
+ Dir.mkdir "doc" unless File.directory? "doc"
31
+ puts "writing #{file}"
32
+ File.open(file, "w") { |f| f << doc }
33
+ end
34
+
35
+ task :all => [:readmes, :index]
36
+ end
37
+
38
+ desc "generate documentation"
39
+ task :doc => 'doc:all'
40
+
14
41
  desc "generate gemspec"
15
42
  task 'rack-protection.gemspec' do
16
43
  require 'rack/protection/version'
@@ -19,13 +46,10 @@ task 'rack-protection.gemspec' do
19
46
  # fetch data
20
47
  fields = {
21
48
  :authors => `git shortlog -sn`.force_encoding('utf-8').scan(/[^\d\s].*/),
22
- :email => `git shortlog -sne`.force_encoding('utf-8').scan(/[^<]+@[^>]+/),
23
- :files => `git ls-files`.force_encoding('utf-8').split("\n").reject { |f| f =~ /^(\.|Gemfile)/ }
49
+ :email => ["mail@zzak.io", "konstantin.haase@gmail.com"],
50
+ :files => %w(License README.md Rakefile Gemfile rack-protection.gemspec) + Dir['lib/**/*']
24
51
  }
25
52
 
26
- # double email :(
27
- fields[:email].delete("konstantin.haase@gmail.com")
28
-
29
53
  # insert data
30
54
  fields.each do |field, values|
31
55
  updated = " s.#{field} = ["
@@ -3,36 +3,53 @@ require 'rack'
3
3
 
4
4
  module Rack
5
5
  module Protection
6
- autoload :AuthenticityToken, 'rack/protection/authenticity_token'
7
- autoload :Base, 'rack/protection/base'
8
- autoload :EscapedParams, 'rack/protection/escaped_params'
9
- autoload :FormToken, 'rack/protection/form_token'
10
- autoload :FrameOptions, 'rack/protection/frame_options'
11
- autoload :HttpOrigin, 'rack/protection/http_origin'
12
- autoload :IPSpoofing, 'rack/protection/ip_spoofing'
13
- autoload :JsonCsrf, 'rack/protection/json_csrf'
14
- autoload :PathTraversal, 'rack/protection/path_traversal'
15
- autoload :RemoteReferrer, 'rack/protection/remote_referrer'
16
- autoload :RemoteToken, 'rack/protection/remote_token'
17
- autoload :SessionHijacking, 'rack/protection/session_hijacking'
18
- autoload :XSSHeader, 'rack/protection/xss_header'
6
+ autoload :AuthenticityToken, 'rack/protection/authenticity_token'
7
+ autoload :Base, 'rack/protection/base'
8
+ autoload :CookieTossing, 'rack/protection/cookie_tossing'
9
+ autoload :ContentSecurityPolicy, 'rack/protection/content_security_policy'
10
+ autoload :EscapedParams, 'rack/protection/escaped_params'
11
+ autoload :FormToken, 'rack/protection/form_token'
12
+ autoload :FrameOptions, 'rack/protection/frame_options'
13
+ autoload :HttpOrigin, 'rack/protection/http_origin'
14
+ autoload :IPSpoofing, 'rack/protection/ip_spoofing'
15
+ autoload :JsonCsrf, 'rack/protection/json_csrf'
16
+ autoload :PathTraversal, 'rack/protection/path_traversal'
17
+ autoload :ReferrerPolicy, 'rack/protection/referrer_policy'
18
+ autoload :RemoteReferrer, 'rack/protection/remote_referrer'
19
+ autoload :RemoteToken, 'rack/protection/remote_token'
20
+ autoload :SessionHijacking, 'rack/protection/session_hijacking'
21
+ autoload :StrictTransport, 'rack/protection/strict_transport'
22
+ autoload :XSSHeader, 'rack/protection/xss_header'
19
23
 
20
24
  def self.new(app, options = {})
21
25
  # does not include: RemoteReferrer, AuthenticityToken and FormToken
22
26
  except = Array options[:except]
23
27
  use_these = Array options[:use]
28
+
29
+ if options.fetch(:without_session, false)
30
+ except += [:session_hijacking, :remote_token]
31
+ end
32
+
24
33
  Rack::Builder.new do
25
- use ::Rack::Protection::RemoteReferrer, options if use_these.include? :remote_referrer
26
- use ::Rack::Protection::AuthenticityToken,options if use_these.include? :authenticity_token
27
- use ::Rack::Protection::FormToken, options if use_these.include? :form_token
28
- use ::Rack::Protection::FrameOptions, options unless except.include? :frame_options
29
- use ::Rack::Protection::HttpOrigin, options unless except.include? :http_origin
30
- use ::Rack::Protection::IPSpoofing, options unless except.include? :ip_spoofing
31
- use ::Rack::Protection::JsonCsrf, options unless except.include? :json_csrf
32
- use ::Rack::Protection::PathTraversal, options unless except.include? :path_traversal
33
- use ::Rack::Protection::RemoteToken, options unless except.include? :remote_token
34
- use ::Rack::Protection::SessionHijacking, options unless except.include? :session_hijacking
35
- use ::Rack::Protection::XSSHeader, options unless except.include? :xss_header
34
+ # Off by default, unless added
35
+ use ::Rack::Protection::AuthenticityToken, options if use_these.include? :authenticity_token
36
+ use ::Rack::Protection::ContentSecurityPolicy, options if use_these.include? :content_security_policy
37
+ use ::Rack::Protection::CookieTossing, options if use_these.include? :cookie_tossing
38
+ use ::Rack::Protection::EscapedParams, options if use_these.include? :escaped_params
39
+ use ::Rack::Protection::FormToken, options if use_these.include? :form_token
40
+ use ::Rack::Protection::ReferrerPolicy, options if use_these.include? :referrer_policy
41
+ use ::Rack::Protection::RemoteReferrer, options if use_these.include? :remote_referrer
42
+ use ::Rack::Protection::StrictTransport, options if use_these.include? :strict_transport
43
+
44
+ # On by default, unless skipped
45
+ use ::Rack::Protection::FrameOptions, options unless except.include? :frame_options
46
+ use ::Rack::Protection::HttpOrigin, options unless except.include? :http_origin
47
+ use ::Rack::Protection::IPSpoofing, options unless except.include? :ip_spoofing
48
+ use ::Rack::Protection::JsonCsrf, options unless except.include? :json_csrf
49
+ use ::Rack::Protection::PathTraversal, options unless except.include? :path_traversal
50
+ use ::Rack::Protection::RemoteToken, options unless except.include? :remote_token
51
+ use ::Rack::Protection::SessionHijacking, options unless except.include? :session_hijacking
52
+ use ::Rack::Protection::XSSHeader, options unless except.include? :xss_header
36
53
  run app
37
54
  end.to_app
38
55
  end
@@ -1,4 +1,6 @@
1
1
  require 'rack/protection'
2
+ require 'securerandom'
3
+ require 'base64'
2
4
 
3
5
  module Rack
4
6
  module Protection
@@ -7,24 +9,194 @@ module Rack
7
9
  # Supported browsers:: all
8
10
  # More infos:: http://en.wikipedia.org/wiki/Cross-site_request_forgery
9
11
  #
10
- # Only accepts unsafe HTTP requests if a given access token matches the token
11
- # included in the session.
12
+ # This middleware only accepts requests other than <tt>GET</tt>,
13
+ # <tt>HEAD</tt>, <tt>OPTIONS</tt>, <tt>TRACE</tt> if their given access
14
+ # token matches the token included in the session.
12
15
  #
13
- # Compatible with Rails and rack-csrf.
16
+ # It checks the <tt>X-CSRF-Token</tt> header and the <tt>POST</tt> form
17
+ # data.
14
18
  #
15
- # Options:
19
+ # Compatible with the {rack-csrf}[https://rubygems.org/gems/rack_csrf] gem.
16
20
  #
17
- # authenticity_param: Defines the param's name that should contain the token on a request.
21
+ # == Options
18
22
  #
23
+ # [<tt>:authenticity_param</tt>] the name of the param that should contain
24
+ # the token on a request. Default value:
25
+ # <tt>"authenticity_token"</tt>
26
+ #
27
+ # == Example: Forms application
28
+ #
29
+ # To show what the AuthenticityToken does, this section includes a sample
30
+ # program which shows two forms. One with, and one without a CSRF token
31
+ # The one without CSRF token field will get a 403 Forbidden response.
32
+ #
33
+ # Install the gem, then run the program:
34
+ #
35
+ # gem install 'rack-protection'
36
+ # ruby server.rb
37
+ #
38
+ # Here is <tt>server.rb</tt>:
39
+ #
40
+ # require 'rack/protection'
41
+ #
42
+ # app = Rack::Builder.app do
43
+ # use Rack::Session::Cookie, secret: 'secret'
44
+ # use Rack::Protection::AuthenticityToken
45
+ #
46
+ # run -> (env) do
47
+ # [200, {}, [
48
+ # <<~EOS
49
+ # <!DOCTYPE html>
50
+ # <html lang="en">
51
+ # <head>
52
+ # <meta charset="UTF-8" />
53
+ # <title>rack-protection minimal example</title>
54
+ # </head>
55
+ # <body>
56
+ # <h1>Without Authenticity Token</h1>
57
+ # <p>This takes you to <tt>Forbidden</tt></p>
58
+ # <form action="" method="post">
59
+ # <input type="text" name="foo" />
60
+ # <input type="submit" />
61
+ # </form>
62
+ #
63
+ # <h1>With Authenticity Token</h1>
64
+ # <p>This successfully takes you to back to this form.</p>
65
+ # <form action="" method="post">
66
+ # <input type="hidden" name="authenticity_token" value="#{Rack::Protection::AuthenticityToken.token(env['rack.session'])}" />
67
+ # <input type="text" name="foo" />
68
+ # <input type="submit" />
69
+ # </form>
70
+ # </body>
71
+ # </html>
72
+ # EOS
73
+ # ]]
74
+ # end
75
+ # end
76
+ #
77
+ # Rack::Handler::WEBrick.run app
78
+ #
79
+ # == Example: Customize which POST parameter holds the token
80
+ #
81
+ # To customize the authenticity parameter for form data, use the
82
+ # <tt>:authenticity_param</tt> option:
83
+ # use Rack::Protection::AuthenticityToken, authenticity_param: 'your_token_param_name'
19
84
  class AuthenticityToken < Base
20
- default_options :authenticity_param => 'authenticity_token'
85
+ TOKEN_LENGTH = 32
86
+
87
+ default_options :authenticity_param => 'authenticity_token',
88
+ :allow_if => nil
89
+
90
+ def self.token(session)
91
+ self.new(nil).mask_authenticity_token(session)
92
+ end
93
+
94
+ def self.random_token
95
+ SecureRandom.base64(TOKEN_LENGTH)
96
+ end
21
97
 
22
98
  def accepts?(env)
23
99
  session = session env
24
- token = session[:csrf] ||= session['_csrf_token'] || random_string
100
+ set_token(session)
101
+
25
102
  safe?(env) ||
26
- secure_compare(env['HTTP_X_CSRF_TOKEN'].to_s, token) ||
27
- secure_compare(Request.new(env).params[options[:authenticity_param]].to_s, token)
103
+ valid_token?(session, env['HTTP_X_CSRF_TOKEN']) ||
104
+ valid_token?(session, Request.new(env).params[options[:authenticity_param]]) ||
105
+ ( options[:allow_if] && options[:allow_if].call(env) )
106
+ end
107
+
108
+ def mask_authenticity_token(session)
109
+ token = set_token(session)
110
+ mask_token(token)
111
+ end
112
+
113
+ private
114
+
115
+ def set_token(session)
116
+ session[:csrf] ||= self.class.random_token
117
+ end
118
+
119
+ # Checks the client's masked token to see if it matches the
120
+ # session token.
121
+ def valid_token?(session, token)
122
+ return false if token.nil? || token.empty?
123
+
124
+ begin
125
+ token = decode_token(token)
126
+ rescue ArgumentError # encoded_masked_token is invalid Base64
127
+ return false
128
+ end
129
+
130
+ # See if it's actually a masked token or not. We should be able
131
+ # to handle any unmasked tokens that we've issued without error.
132
+
133
+ if unmasked_token?(token)
134
+ compare_with_real_token token, session
135
+
136
+ elsif masked_token?(token)
137
+ token = unmask_token(token)
138
+
139
+ compare_with_real_token token, session
140
+
141
+ else
142
+ false # Token is malformed
143
+ end
144
+ end
145
+
146
+ # Creates a masked version of the authenticity token that varies
147
+ # on each request. The masking is used to mitigate SSL attacks
148
+ # like BREACH.
149
+ def mask_token(token)
150
+ token = decode_token(token)
151
+ one_time_pad = SecureRandom.random_bytes(token.length)
152
+ encrypted_token = xor_byte_strings(one_time_pad, token)
153
+ masked_token = one_time_pad + encrypted_token
154
+ encode_token(masked_token)
155
+ end
156
+
157
+ # Essentially the inverse of +mask_token+.
158
+ def unmask_token(masked_token)
159
+ # Split the token into the one-time pad and the encrypted
160
+ # value and decrypt it
161
+ token_length = masked_token.length / 2
162
+ one_time_pad = masked_token[0...token_length]
163
+ encrypted_token = masked_token[token_length..-1]
164
+ xor_byte_strings(one_time_pad, encrypted_token)
165
+ end
166
+
167
+ def unmasked_token?(token)
168
+ token.length == TOKEN_LENGTH
169
+ end
170
+
171
+ def masked_token?(token)
172
+ token.length == TOKEN_LENGTH * 2
173
+ end
174
+
175
+ def compare_with_real_token(token, session)
176
+ secure_compare(token, real_token(session))
177
+ end
178
+
179
+ def real_token(session)
180
+ decode_token(session[:csrf])
181
+ end
182
+
183
+ def encode_token(token)
184
+ Base64.strict_encode64(token)
185
+ end
186
+
187
+ def decode_token(token)
188
+ Base64.strict_decode64(token)
189
+ end
190
+
191
+ def xor_byte_strings(s1, s2)
192
+ s2 = s2.dup
193
+ size = s1.bytesize
194
+ i = 0
195
+ while i < size
196
+ s2.setbyte(i, s1.getbyte(i) ^ s2.getbyte(i))
197
+ i += 1
198
+ end
199
+ s2
28
200
  end
29
201
  end
30
202
  end