diff --git a/config/sql/nonces.sql b/config/sql/nonces.sql
new file mode 100644
index 00000000..5407dca5
--- /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 accae8dd..7b708897 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 245af305..88663e3e 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 aa9c0433..598adf80 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
-
- 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 113fa1c2..d46029aa 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
+
+ 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 dc88379f..0243d900 100644
--- a/src/invidious/views/login.ecr
+++ b/src/invidious/views/login.ecr
@@ -24,10 +24,11 @@
-
+
-
-
+
+
+