From 3304f4f2c29bb58c595d48608013a31feb1475b6 Mon Sep 17 00:00:00 2001 From: Jeremy Evans Date: Tue, 19 Sep 2023 08:55:24 -0700 Subject: [PATCH] Remove dependency on base64 library from sessions and route_csrf plugin, as it will not be part of the standard library in Ruby 3.4+ Add internal _base64 plugin used by route_csrf and sessions. --- CHANGELOG | 4 ++++ lib/roda/plugins/_base64.rb | 40 ++++++++++++++++++++++++++++++++++ lib/roda/plugins/route_csrf.rb | 11 ++++++---- lib/roda/plugins/sessions.rb | 9 +++++--- spec/plugin/sessions_spec.rb | 15 ++++++++----- 5 files changed, 66 insertions(+), 13 deletions(-) create mode 100644 lib/roda/plugins/_base64.rb diff --git a/CHANGELOG b/CHANGELOG index 8e4f7d00..1eb83175 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ += master + +* Remove dependency on base64 library from sessions and route_csrf plugin, as it will not be part of the standard library in Ruby 3.4+ (jeremyevans) + = 3.72.0 (2023-09-12) * Add invalid_request_body plugin for custom handling of invalid request bodies (jeremyevans) diff --git a/lib/roda/plugins/_base64.rb b/lib/roda/plugins/_base64.rb new file mode 100644 index 00000000..b7a9b05c --- /dev/null +++ b/lib/roda/plugins/_base64.rb @@ -0,0 +1,40 @@ +# frozen-string-literal: true + +# +class Roda + module RodaPlugins + module Base64_ + class << self + if RUBY_VERSION >= '2.4' + def decode64(str) + str.unpack1("m0") + end + # :nocov: + else + def decode64(str) + str.unpack("m0")[0] + end + # :nocov: + end + + def urlsafe_encode64(bin) + str = [bin].pack("m0") + str.tr!("+/", "-_") + str + end + + def urlsafe_decode64(str) + if str.length % 4 == 0 + str = str.tr("-_", "+/") + else + str = str.ljust((str.length + 3) & ~3, "=") + str.tr!("-_", "+/") + end + decode64(str) + end + end + end + + register_plugin(:_base64, Base64_) + end +end diff --git a/lib/roda/plugins/route_csrf.rb b/lib/roda/plugins/route_csrf.rb index deee9bcd..9188b721 100644 --- a/lib/roda/plugins/route_csrf.rb +++ b/lib/roda/plugins/route_csrf.rb @@ -1,6 +1,5 @@ # frozen-string-literal: true -require 'base64' require 'openssl' require 'securerandom' require 'uri' @@ -163,6 +162,10 @@ module RouteCsrf # a valid CSRF token was not provided. class InvalidToken < RodaError; end + def self.load_dependencies(app, opts=OPTS) + app.plugin :_base64 + end + def self.configure(app, opts=OPTS, &block) options = app.opts[:route_csrf] = (app.opts[:route_csrf] || DEFAULTS).merge(opts) if block || opts[:csrf_failure].is_a?(Proc) @@ -260,7 +263,7 @@ def csrf_tag(*args) def csrf_token(path=nil, method=('POST' if path)) token = SecureRandom.random_bytes(31) token << csrf_hmac(token, method, path) - Base64.strict_encode64(token) + [token].pack("m0") end # Whether request-specific CSRF tokens should be used by default. @@ -314,7 +317,7 @@ def csrf_invalid_message(opts) end begin - submitted_hmac = Base64.strict_decode64(encoded_token) + submitted_hmac = Base64_.decode64(encoded_token) rescue ArgumentError return "encoded token is not valid base64" end @@ -354,7 +357,7 @@ def csrf_hmac(random_data, method, path) # JSON is used for session serialization). def csrf_secret key = session[csrf_options[:key]] ||= SecureRandom.base64(32) - Base64.strict_decode64(key) + Base64_.decode64(key) end end end diff --git a/lib/roda/plugins/sessions.rb b/lib/roda/plugins/sessions.rb index 7d5984c3..7ec901ea 100644 --- a/lib/roda/plugins/sessions.rb +++ b/lib/roda/plugins/sessions.rb @@ -10,7 +10,6 @@ # :nocov: end -require 'base64' require 'json' require 'securerandom' require 'zlib' @@ -171,6 +170,10 @@ def self.split_secret(name, secret) [cipher_secret.freeze, hmac_secret.freeze] end + def self.load_dependencies(app, opts=OPTS) + app.plugin :_base64 + end + # Configure the plugin, see Sessions for details on options. def self.configure(app, opts=OPTS) opts = (app.opts[:sessions] || DEFAULT_OPTIONS).merge(opts) @@ -344,7 +347,7 @@ def _deserialize_session(data) opts = roda_class.opts[:sessions] begin - data = Base64.urlsafe_decode64(data) + data = Base64_.urlsafe_decode64(data) rescue ArgumentError return _session_serialization_error("Unable to decode session: invalid base64") end @@ -493,7 +496,7 @@ def _serialize_session(session) data << encrypted_data data << OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, opts[:hmac_secret], data+opts[:key]) - data = Base64.urlsafe_encode64(data) + data = Base64_.urlsafe_encode64(data) if data.bytesize >= 4096 raise CookieTooLarge, "attempted to create cookie larger than 4096 bytes" diff --git a/spec/plugin/sessions_spec.rb b/spec/plugin/sessions_spec.rb index d3b3907e..3d048fdf 100644 --- a/spec/plugin/sessions_spec.rb +++ b/spec/plugin/sessions_spec.rb @@ -1,4 +1,7 @@ require_relative "../spec_helper" +require_relative "../../lib/roda/plugins/_base64" + +base64 = Roda::RodaPlugins::Base64_ if RUBY_VERSION >= '2' [true, false].each do |per_cookie_cipher_secret| @@ -292,7 +295,7 @@ def errors end it "raises CookieTooLarge if cookie is too large" do - proc{req('/s/foo/'+Base64.urlsafe_encode64(SecureRandom.random_bytes(8192)))}.must_raise Roda::RodaPlugins::Sessions::CookieTooLarge + proc{req('/s/foo/'+base64.urlsafe_encode64(SecureRandom.random_bytes(8192)))}.must_raise Roda::RodaPlugins::Sessions::CookieTooLarge end it "ignores session cookies if session exceeds max time since create" do @@ -348,23 +351,23 @@ def errors body('/g/foo').must_equal '' errors.must_equal ["Unable to decode session: invalid base64"] - @cookie = k+Base64.urlsafe_encode64('') + @cookie = k+base64.urlsafe_encode64('') body('/g/foo').must_equal '' errors.must_equal ["Unable to decode session: no data"] - @cookie = k+Base64.urlsafe_encode64("\0" * 60) + @cookie = k+base64.urlsafe_encode64("\0" * 60) body('/g/foo').must_equal '' errors.must_equal ["Unable to decode session: data too short"] - @cookie = k+Base64.urlsafe_encode64("\1" * 92) + @cookie = k+base64.urlsafe_encode64("\1" * 92) body('/g/foo').must_equal '' errors.must_equal ["Unable to decode session: data too short"] - @cookie = k+Base64.urlsafe_encode64('1'*75) + @cookie = k+base64.urlsafe_encode64('1'*75) body('/g/foo').must_equal '' errors.must_equal ["Unable to decode session: version marker unsupported"] - @cookie = k+Base64.urlsafe_encode64("\0"*75) + @cookie = k+base64.urlsafe_encode64("\0"*75) body('/g/foo').must_equal '' errors.must_equal ["Not decoding session: HMAC invalid"] end