rack-protection 1.5.5 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
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