diff --git a/lib/landline/util/cookie.rb b/lib/landline/util/cookie.rb index 7d9a3d5..70467b4 100644 --- a/lib/landline/util/cookie.rb +++ b/lib/landline/util/cookie.rb @@ -3,6 +3,7 @@ require_relative 'parseutils' require_relative 'errors' require 'date' +require 'openssl' HeaderRegexp = Landline::Util::HeaderRegexp ParserCommon = Landline::Util::ParserCommon @@ -29,15 +30,22 @@ module Landline raise Landline::ParsingError, "invalid cookie value: #{value}" end + # Make param keys strings + params.transform_keys!(&:to_s) + # Primary cookie parameters @key = key @value = value setup_params(params) + + # Cookie signing parameters + setup_hmac(params) end # Convert cookie to "Set-Cookie: " string representation. # @return [String] def to_s + sign(@hmac, algorithm: @algorithm, sep: @sep) if @hmac ParserCommon.make_value( "#{key.to_s.strip}=#{value.to_s.strip}", { @@ -58,6 +66,26 @@ module Landline "#{key.to_s.strip}=#{value.to_s.strip}" end + # Sign the cookie value with HMAC + # @param key [String] HMAC signing key + # @param algorithm [String] Hash algorithm to use + # @param sep [String] Hash separator + def sign(key, algorithm: "sha256", sep: "&") + @value += sep + ::OpenSSL::HMAC.base64digest(algorithm, key, @value) + end + + # Verify HMAC signature + # @param key [String] HMAC signing key + # @param algorithm [String] Hash algorithm + # @param sep [String] Hash separator + # @return [Boolean] whether value is signed and valid + def verify(key, algorithm: "sha256", sep: "&") + val, sig = @value.match(/\A(.*)#{sep}([A-Za-z0-9+\/=]+)\Z/).to_a[1..] + return false unless val and sig + + sig == ::OpenSSL::HMAC.base64digest(algorithm, key, val) + end + attr_accessor :key, :value attr_reader :domain, :path, :expires, :maxage, :samesite, :secure, :httponly @@ -66,7 +94,7 @@ module Landline # @return [Cookie] def self.from_setcookie_string(data) kvpair, params = parse_value(data, regexp: HeaderRegexp::COOKIE_PARAM) - key, value = kvpair.split("=").map(&:strip) + key, value = kvpair.match(/([^=]+)=?(.*)/).to_a[1..].map(&:strip) Cookie.new(key, value, params) end @@ -76,7 +104,8 @@ module Landline def self.from_cookie_string(data) hash = {} data.split(";").map do |cookiestr| - cookie = Cookie.new(*cookiestr.split("=").map(&:strip)) + key, value = cookiestr.match(/([^=]+)=?(.*)/).to_a[1..].map(&:strip) + cookie = Cookie.new(key, value) if hash[cookie.key] hash[cookie.key].append(cookie) else @@ -88,6 +117,12 @@ module Landline private + def setup_hmac(params) + @hmac = params['hmac'] + @algorithm = (params['algorithm'] or "sha256") + @sep = (params['sep'] or "&") + end + def setup_params(params) # Extended cookie params params.transform_keys!(&:downcase)