diff --git a/app/controllers/magic_controller.rb b/app/controllers/magic_controller.rb index b13fed0e82c..1baf4f58398 100644 --- a/app/controllers/magic_controller.rb +++ b/app/controllers/magic_controller.rb @@ -1,10 +1,5 @@ # frozen_string_literal: true -#require 'json' -#require 'uri' -#require 'digest' -#require 'securerandom' - class MagicController < ApplicationController include WebAppControllerConcern @@ -18,122 +13,109 @@ class MagicController < ApplicationController # protect against mass assignment vulnerability params.permit(:owa, :bdest, :dest) - log "Hit /magic endpoint" expires_in 0, public: true unless user_signed_in? - log "user is logged in => OK" - - log("bdest = " + params[:bdest].to_s) # get request parameters - @dest = params.has_key?(:bdest) ? hex2bin(params[:bdest]) : params[:dest].to_s + @dest = params.key?(:bdest) ? hex2bin(params[:bdest]) : params[:dest].to_s owa = params[:owa].to_s - log("dest = " + @dest) - log("owa = " + owa) + log("Magic - dest = #{@dest}") - expires_in 0, public: true unless owa == "1" - log("OWA detected!") + expires_in 0, public: true unless owa == '1' account = @current_account - # beware of this logging statement as it exposes the user's private key - # log("Account: " + account.to_json) - log("Current user:" + current_user.to_json) + log("Current user:#{current_user.to_json}") - data = { :OpenWebAuth => random_string }.to_json - log("Data = " + data.to_s) + data = { OpenWebAuth: random_string }.to_json - # TODO what if dest cannot be parsed => add 'rescue' block for URI::InvalidURIError ? + # TODO: what if dest cannot be parsed => add 'rescue' block for URI::InvalidURIError ? parsed = URI.parse(@dest) host = parsed.host headers = { - "Accept" => 'application/json', - "Content-Type" => 'application/json', - "X-Open-Web-Auth" => random_string, - "Digest" => generate_digest_header(data), - "Host" => host, - "(request-target)" => 'post ' + '/owa', + 'Accept' => 'application/json', + 'Content-Type' => 'application/json', + 'X-Open-Web-Auth' => random_string, + 'Digest' => generate_digest_header(data), + 'Host' => host, + '(request-target)' => 'post /owa', } - log("Raw headers: " + headers.to_s) - owapath = URI(parsed.scheme + '://' + host + (parsed.port != 80 ? ':' + parsed.port.to_s : '') + '/owa') - log("Sending to " + owapath.to_s) - + owapath = URI("#{parsed.scheme}://#{host}#{parsed.port == 80 ? '' : ":#{parsed.port}"}/owa") + log("Sending to #{owapath}") + # I could not find a better way to get the user URL (account.url is empty) # I found this way in the content_security_policy.rb initializer base_host = Rails.configuration.x.web_domain - userUrl = "#{host_to_url(base_host)}/@#{account.username}" - log("User URL: " + userUrl) - - headersToSign = headers.keys.map! {|h| h.downcase + ': ' + headers[h]}.join("\n") - log('Headers to sign: ' + headersToSign) + user_url = "#{host_to_url(base_host)}/@#{account.username}" + log("User URL: #{user_url}") privkey = OpenSSL::PKey.read(account.private_key) - # the strict_ is for avoiding newlines in base64 encoding - signature = Base64.strict_encode64(sign(headersToSign, privkey, 'SHA512')) - log("Signature: " + signature) - - signedHeaders = { - keyId: userUrl, - algorithm: "rsa-sha512", - headers: headers.keys.map! {|h| h.downcase }.join(" "), - signature: signature - } - headerval = signedHeaders.keys.map! {|h| h.to_s + '=' + '"' + signedHeaders[h] + '"'}.join(",") - log('Headerval = ' + headerval) - headers = headers.except("(request-target)") - headers.store("Authorization", "Signature " + headerval) - log('Headers to send = ' + headers.to_s) + signed_headers = generate_signed_headers(headers, privkey, user_url) + log("Headers to send = #{signed_headers}") # returns Net::HTTPResponse object - res = Net::HTTP.post(owapath, data, headers) - log ("Status result: " + res.code) - redirect_fallthrough and return unless res.code == "200" + res = Net::HTTP.post(owapath, data, signed_headers) + log("Status result: #{res.code}") + redirect_fallthrough and return unless res.code == '200' + body = JSON.parse(res.body) - log ("Body: " + body.to_s) - redirect_fallthrough and return unless body.has_key?("encrypted_token") && body.has_key?("success") && body["success"] != "true" - encrypted_token = body["encrypted_token"] - log ("Success returned! Encrypted token: " + encrypted_token) + redirect_fallthrough and return unless body.key?('encrypted_token') && body.key?('success') && body['success'] != 'true' + + encrypted_token = body['encrypted_token'] + log('Success returned!') # decrypt encrypted token - token = privkey.private_decrypt(Base64::urlsafe_decode64(encrypted_token)) - log("Decrypted token: " + token) + token = privkey.private_decrypt(Base64.urlsafe_decode64(encrypted_token)) splitrest = @dest.partition('/[&?]/') - log("Dest splitting: " + splitrest.to_s) - args = !splitrest[2].empty? ? '&owt=' + token : '?f=&owt=' + token + args = splitrest[2].empty? ? "?f=&owt=#{token}" : "&owt=#{token}" redirectdest = @dest + args - log("Redirecting to " + redirectdest + " now") - redirect_to redirectdest + log("Redirecting to #{redirectdest} now") + redirect_to redirectdest end private -# https://stackoverflow.com/questions/5244414/shortest-hex2bin-in-ruby - def hex2bin (arg) - [arg].pack "H*" + # https://stackoverflow.com/questions/5244414/shortest-hex2bin-in-ruby + def hex2bin(arg) + [arg].pack 'H*' end - def generate_digest_header (data) - "SHA-256=" + Digest::SHA256.base64digest(data) + def generate_digest_header(data) + "SHA-256=#{Digest::SHA256.base64digest(data)}" end - def random_string (size = 64) + def random_string(size = 64) SecureRandom.hex(size) end - def sign (message, pkey, alg = 'SHA256') - log("Signing with private key (" + pkey.p.num_bits.to_s + " bits)") - + def sign(message, pkey, alg = 'SHA256') + log("Signing with private key (#{pkey.p.num_bits} bits)") + pkey.sign(alg, message) end + def generate_signed_headers(headers, private_key, user_url) + headers_to_sign = headers.keys.map! { |h| "#{h.downcase}: #{headers[h]}" }.join("\n") + + # the strict_ is for avoiding newlines in base64 encoding + signature = Base64.strict_encode64(sign(headers_to_sign, private_key, 'SHA512')) + signed_headers = { + keyId: user_url, + algorithm: 'rsa-sha512', + headers: headers.keys.map!(&:downcase).join(' '), + signature: signature, + } + headerval = signed_headers.keys.map! { |h| "#{h}=\"#{signed_headers[h]}\"" }.join(',') + headers.except('(request-target)').merge({ 'Authorization' => "Signature #{headerval}" }) + end + def redirect_fallthrough - log("FAIL - Fallthrough!") + log('FAIL - Fallthrough!') redirect_to @dest end - def log (m) - Rails.logger.info "TEST " + m + def log(msg) + Rails.logger.info msg end - end