diff --git a/config/sql/nonces.sql b/config/sql/nonces.sql new file mode 100644 index 000000000..5407dca52 --- /dev/null +++ b/config/sql/nonces.sql @@ -0,0 +1,13 @@ +-- Table: public.nonces + +-- DROP TABLE public.nonces; + +CREATE TABLE public.nonces +( + nonce text +) +WITH ( + OIDS=FALSE +); + +GRANT ALL ON TABLE public.nonces TO kemal; \ No newline at end of file diff --git a/setup.sh b/setup.sh index accae8dd0..7b708897e 100755 --- a/setup.sh +++ b/setup.sh @@ -7,3 +7,4 @@ psql invidious < config/sql/channels.sql psql invidious < config/sql/videos.sql psql invidious < config/sql/channel_videos.sql psql invidious < config/sql/users.sql +psql invidious < config/sql/nonces.sql diff --git a/src/invidious.cr b/src/invidious.cr index 245af3055..88663e3e1 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -142,7 +142,7 @@ before_all do |env| user = PG_DB.query_one?("SELECT * FROM users WHERE $1 = ANY(id)", sid, as: User) if user - challenge, token = create_response(user.email, "sign_out", HMAC_KEY, 1.week) + challenge, token = create_response(user.email, "sign_out", HMAC_KEY, PG_DB, 1.week) env.set "challenge", challenge env.set "token", token @@ -155,7 +155,7 @@ before_all do |env| client = make_client(YT_URL) user = get_user(sid, client, headers, PG_DB, false) - challenge, token = create_response(user.email, "sign_out", HMAC_KEY, 1.week) + challenge, token = create_response(user.email, "sign_out", HMAC_KEY, PG_DB, 1.week) env.set "challenge", challenge env.set "token", token @@ -624,7 +624,7 @@ get "/login" do |env| account_type ||= "invidious" if account_type == "invidious" - captcha = generate_captcha(HMAC_KEY) + captcha = generate_captcha(HMAC_KEY, PG_DB) end tfa = env.params.query["tfa"]? @@ -815,9 +815,26 @@ post "/login" do |env| next templated "error" end elsif account_type == "invidious" - challenge_response = env.params.body["challenge_response"]? + answer = env.params.body["answer"]? + + if !answer + error_message = "CAPTCHA is a required field" + next templated "error" + end + + answer = answer.lstrip('0') + answer = OpenSSL::HMAC.hexdigest(:sha256, HMAC_KEY, answer) + + challenge = env.params.body["challenge"]? token = env.params.body["token"]? + begin + validate_response(challenge, token, answer, "sign_in", HMAC_KEY, PG_DB) + rescue ex + error_message = ex.message + next templated "error" + end + action = env.params.body["action"]? action ||= "signin" @@ -831,18 +848,6 @@ post "/login" do |env| next templated "error" end - if !challenge_response || !token - error_message = "CAPTCHA is a required field" - next templated "error" - end - - challenge_response = challenge_response.lstrip('0') - if OpenSSL::HMAC.digest(:sha256, HMAC_KEY, challenge_response) == Base64.decode(token) - else - error_message = "Invalid CAPTCHA response" - next templated "error" - end - if action == "signin" user = PG_DB.query_one?("SELECT * FROM users WHERE LOWER(email) = LOWER($1) AND password IS NOT NULL", email, as: User) @@ -940,7 +945,7 @@ get "/signout" do |env| token = env.params.query["token"]? begin - validate_response(challenge, token, user.email, "sign_out", HMAC_KEY) + validate_response(challenge, token, user.email, "sign_out", HMAC_KEY, PG_DB) rescue ex error_message = ex.message next templated "error" @@ -1461,7 +1466,7 @@ get "/delete_account" do |env| if user user = user.as(User) - challenge, token = create_response(user.email, "delete_account", HMAC_KEY) + challenge, token = create_response(user.email, "delete_account", HMAC_KEY, PG_DB) templated "delete_account" else @@ -1480,7 +1485,7 @@ post "/delete_account" do |env| token = env.params.body["token"]? begin - validate_response(challenge, token, user.email, "delete_account", HMAC_KEY) + validate_response(challenge, token, user.email, "delete_account", HMAC_KEY, PG_DB) rescue ex error_message = ex.message next templated "error" @@ -1506,7 +1511,7 @@ get "/clear_watch_history" do |env| if user user = user.as(User) - challenge, token = create_response(user.email, "clear_watch_history", HMAC_KEY) + challenge, token = create_response(user.email, "clear_watch_history", HMAC_KEY, PG_DB) templated "clear_watch_history" else @@ -1525,7 +1530,7 @@ post "/clear_watch_history" do |env| token = env.params.body["token"]? begin - validate_response(challenge, token, user.email, "clear_watch_history", HMAC_KEY) + validate_response(challenge, token, user.email, "clear_watch_history", HMAC_KEY, PG_DB) rescue ex error_message = ex.message next templated "error" diff --git a/src/invidious/helpers/helpers.cr b/src/invidious/helpers/helpers.cr index aa9c04331..598adf809 100644 --- a/src/invidious/helpers/helpers.cr +++ b/src/invidious/helpers/helpers.cr @@ -130,55 +130,6 @@ def login_req(login_form, f_req) return HTTP::Params.encode(data) end -def generate_captcha(key) - minute = Random::Secure.rand(12) - minute_angle = minute * 30 - minute = minute * 5 - - hour = Random::Secure.rand(12) - hour_angle = hour * 30 + minute_angle.to_f / 12 - if hour == 0 - hour = 12 - end - - clock_svg = <<-END_SVG - - - - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 - - - - - - END_SVG - - challenge = "" - convert = Process.run(%(convert -density 1200 -resize 400x400 -background none svg:- png:-), shell: true, - input: IO::Memory.new(clock_svg), output: Process::Redirect::Pipe) do |proc| - challenge = proc.output.gets_to_end - challenge = Base64.strict_encode(challenge) - challenge = "data:image/png;base64,#{challenge}" - end - - answer = "#{hour}:#{minute.to_s.rjust(2, '0')}" - token = OpenSSL::HMAC.digest(:sha256, key, answer) - token = Base64.urlsafe_encode(token) - - return {challenge: challenge, token: token} -end - def html_to_content(description_html) if !description_html description = "" diff --git a/src/invidious/users.cr b/src/invidious/users.cr index 113fa1c23..d46029aa9 100644 --- a/src/invidious/users.cr +++ b/src/invidious/users.cr @@ -200,9 +200,10 @@ def create_user(sid, email, password) return user end -def create_response(user_id, operation, key, expire = 6.hours) +def create_response(user_id, operation, key, db, expire = 6.hours) expire = Time.now + expire - nonce = Random::Secure.hex(4) + nonce = Random::Secure.hex(16) + db.exec("INSERT INTO nonces VALUES ($1) ON CONFLICT DO NOTHING", nonce) challenge = "#{expire.to_unix}-#{nonce}-#{user_id}-#{operation}" token = OpenSSL::HMAC.digest(:sha256, key, challenge) @@ -213,7 +214,7 @@ def create_response(user_id, operation, key, expire = 6.hours) return challenge, token end -def validate_response(challenge, token, user_id, operation, key) +def validate_response(challenge, token, user_id, operation, key, db) if !challenge raise "Hidden field \"challenge\" is a required field" end @@ -235,6 +236,12 @@ def validate_response(challenge, token, user_id, operation, key) challenge = OpenSSL::HMAC.digest(:sha256, HMAC_KEY, challenge) challenge = Base64.urlsafe_encode(challenge) + if db.query_one?("SELECT EXISTS (SELECT true FROM nonces WHERE nonce = $1)", nonce, as: Bool) + db.exec("DELETE FROM nonces * WHERE nonce = $1", nonce) + else + raise "Invalid token" + end + if challenge != token raise "Invalid token" end @@ -251,3 +258,53 @@ def validate_response(challenge, token, user_id, operation, key) raise "Token is expired, please try again" end end + +def generate_captcha(key, db) + minute = Random::Secure.rand(12) + minute_angle = minute * 30 + minute = minute * 5 + + hour = Random::Secure.rand(12) + hour_angle = hour * 30 + minute_angle.to_f / 12 + if hour == 0 + hour = 12 + end + + clock_svg = <<-END_SVG + + + + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + + + + + + END_SVG + + image = "" + convert = Process.run(%(convert -density 1200 -resize 400x400 -background none svg:- png:-), shell: true, + input: IO::Memory.new(clock_svg), output: Process::Redirect::Pipe) do |proc| + image = proc.output.gets_to_end + image = Base64.strict_encode(image) + image = "data:image/png;base64,#{image}" + end + + answer = "#{hour}:#{minute.to_s.rjust(2, '0')}" + answer = OpenSSL::HMAC.hexdigest(:sha256, key, answer) + + challenge, token = create_response(answer, "sign_in", key, db) + + return {image: image, challenge: challenge, token: token} +end diff --git a/src/invidious/views/login.ecr b/src/invidious/views/login.ecr index dc88379f8..0243d900b 100644 --- a/src/invidious/views/login.ecr +++ b/src/invidious/views/login.ecr @@ -24,10 +24,11 @@ - + - - + + +