Remember nonce to prevent replay attacks
This commit is contained in:
parent
c7f0a6f2e1
commit
d185ba84bf
13
config/sql/nonces.sql
Normal file
13
config/sql/nonces.sql
Normal file
|
@ -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;
|
1
setup.sh
1
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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
<svg viewBox="0 0 100 100" width="200px">
|
||||
<circle cx="50" cy="50" r="45" fill="#eee" stroke="black" stroke-width="2"></circle>
|
||||
|
||||
<text x="69" y="20.091" text-anchor="middle" fill="black" font-family="Arial" font-size="10px"> 1</text>
|
||||
<text x="82.909" y="34" text-anchor="middle" fill="black" font-family="Arial" font-size="10px"> 2</text>
|
||||
<text x="88" y="53" text-anchor="middle" fill="black" font-family="Arial" font-size="10px"> 3</text>
|
||||
<text x="82.909" y="72" text-anchor="middle" fill="black" font-family="Arial" font-size="10px"> 4</text>
|
||||
<text x="69" y="85.909" text-anchor="middle" fill="black" font-family="Arial" font-size="10px"> 5</text>
|
||||
<text x="50" y="91" text-anchor="middle" fill="black" font-family="Arial" font-size="10px"> 6</text>
|
||||
<text x="31" y="85.909" text-anchor="middle" fill="black" font-family="Arial" font-size="10px"> 7</text>
|
||||
<text x="17.091" y="72" text-anchor="middle" fill="black" font-family="Arial" font-size="10px"> 8</text>
|
||||
<text x="12" y="53" text-anchor="middle" fill="black" font-family="Arial" font-size="10px"> 9</text>
|
||||
<text x="17.091" y="34" text-anchor="middle" fill="black" font-family="Arial" font-size="10px">10</text>
|
||||
<text x="31" y="20.091" text-anchor="middle" fill="black" font-family="Arial" font-size="10px">11</text>
|
||||
<text x="50" y="15" text-anchor="middle" fill="black" font-family="Arial" font-size="10px">12</text>
|
||||
|
||||
<circle cx="50" cy="50" r="3" fill="black"></circle>
|
||||
<line id="minute" transform="rotate(#{minute_angle}, 50, 50)" x1="50" y1="50" x2="50" y2="16" fill="black" stroke="black" stroke-width="2"></line>
|
||||
<line id="hour" transform="rotate(#{hour_angle}, 50, 50)" x1="50" y1="50" x2="50" y2="24" fill="black" stroke="black" stroke-width="2"></line>
|
||||
</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 = ""
|
||||
|
|
|
@ -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
|
||||
<svg viewBox="0 0 100 100" width="200px">
|
||||
<circle cx="50" cy="50" r="45" fill="#eee" stroke="black" stroke-width="2"></circle>
|
||||
|
||||
<text x="69" y="20.091" text-anchor="middle" fill="black" font-family="Arial" font-size="10px"> 1</text>
|
||||
<text x="82.909" y="34" text-anchor="middle" fill="black" font-family="Arial" font-size="10px"> 2</text>
|
||||
<text x="88" y="53" text-anchor="middle" fill="black" font-family="Arial" font-size="10px"> 3</text>
|
||||
<text x="82.909" y="72" text-anchor="middle" fill="black" font-family="Arial" font-size="10px"> 4</text>
|
||||
<text x="69" y="85.909" text-anchor="middle" fill="black" font-family="Arial" font-size="10px"> 5</text>
|
||||
<text x="50" y="91" text-anchor="middle" fill="black" font-family="Arial" font-size="10px"> 6</text>
|
||||
<text x="31" y="85.909" text-anchor="middle" fill="black" font-family="Arial" font-size="10px"> 7</text>
|
||||
<text x="17.091" y="72" text-anchor="middle" fill="black" font-family="Arial" font-size="10px"> 8</text>
|
||||
<text x="12" y="53" text-anchor="middle" fill="black" font-family="Arial" font-size="10px"> 9</text>
|
||||
<text x="17.091" y="34" text-anchor="middle" fill="black" font-family="Arial" font-size="10px">10</text>
|
||||
<text x="31" y="20.091" text-anchor="middle" fill="black" font-family="Arial" font-size="10px">11</text>
|
||||
<text x="50" y="15" text-anchor="middle" fill="black" font-family="Arial" font-size="10px">12</text>
|
||||
|
||||
<circle cx="50" cy="50" r="3" fill="black"></circle>
|
||||
<line id="minute" transform="rotate(#{minute_angle}, 50, 50)" x1="50" y1="50" x2="50" y2="16" fill="black" stroke="black" stroke-width="2"></line>
|
||||
<line id="hour" transform="rotate(#{hour_angle}, 50, 50)" x1="50" y1="50" x2="50" y2="24" fill="black" stroke="black" stroke-width="2"></line>
|
||||
</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
|
||||
|
|
|
@ -24,10 +24,11 @@
|
|||
<label for="password">Password:</label>
|
||||
<input required class="pure-input-1" name="password" type="password" placeholder="Password">
|
||||
|
||||
<img style="width:100%" src='<%= captcha.not_nil![:challenge] %>'/>
|
||||
<img style="width:100%" src='<%= captcha.not_nil![:image] %>'/>
|
||||
<input type="hidden" name="token" value="<%= captcha.not_nil![:token] %>">
|
||||
<label for="challenge_response">Time (h:mm):</label>
|
||||
<input required type="text" name="challenge_response" type="text>" placeholder="hh:mm">
|
||||
<input type="hidden" name="challenge" value="<%= captcha.not_nil![:challenge] %>">
|
||||
<label for="answer">Time (h:mm):</label>
|
||||
<input required type="text" name="answer" type="text>" placeholder="hh:mm">
|
||||
|
||||
<button type="submit" name="action" value="signin" class="pure-button pure-button-primary">Sign In</button>
|
||||
<button type="submit" name="action" value="register" class="pure-button pure-button-primary">Register</button>
|
||||
|
|
Loading…
Reference in a new issue