Make cookie expiry based on issued expiry date from challenge date instead of all expiring on bucket.

Fixes potential issue of challenges being incorrect if solved right on the bucket change
Allows to solve a challenge at any time (even in the background) and not see the challenge page twice in a small period
Allows for backend to make dynamic expiry of tokens e.g make tor tokens or based on IP reputation not last as long (not implemented atm)
Close #20
This commit is contained in:
Thomas Lynch
2023-02-11 20:55:30 +11:00
parent a303689641
commit 6e5cf2af31
3 changed files with 81 additions and 56 deletions

View File

@ -1,16 +1,12 @@
local _M = {}
local sha = require("sha")
local secret_bucket_duration = tonumber(os.getenv("BUCKET_DURATION"))
local bucket_duration = tonumber(os.getenv("BUCKET_DURATION"))
local challenge_includes_ip = os.getenv("CHALLENGE_INCLUDES_IP")
local tor_control_port_password = os.getenv("TOR_CONTROL_PORT_PASSWORD")
-- generate the challenge hash/user hash
function _M.generate_secret(context, salt, user_key, is_applet)
-- time bucket for expiry
local start_sec = core.now()['sec']
local bucket = start_sec - (start_sec % secret_bucket_duration)
function _M.generate_challenge(context, salt, user_key, is_applet)
-- optional IP to lock challenges/user_keys to IP (for clearnet or single-onion aka 99% of cases)
local ip = ""
@ -28,7 +24,11 @@ function _M.generate_secret(context, salt, user_key, is_applet)
user_agent = context.sf:req_fhdr('user-agent') or ""
end
return sha.sha3_256(salt .. bucket .. ip .. user_key .. user_agent)
local challenge_hash = sha.sha3_256(salt .. ip .. user_key .. user_agent)
local expiry = core.now()['sec'] + bucket_duration
return challenge_hash, expiry
end
@ -59,7 +59,7 @@ end
function _M.send_tor_control_port(circuit_identifier)
local tcp = core.tcp();
tcp:settimeout(1);
tcp:connect("127.0.0.1", 9051);
tcp:connect("127.0.0.1", 9051); --TODO: configurable host/port
-- not buffered, so we are better off sending it all at once
tcp:send('AUTHENTICATE "' .. tor_control_port_password .. '"\nCLOSECIRCUIT ' .. circuit_identifier ..'\n')
tcp:close()

View File

@ -116,9 +116,9 @@ function _M.view(applet)
-- get the user_key#challenge#sig
local user_key = sha.bin_to_hex(randbytes(16))
local challenge_hash = utils.generate_secret(applet, pow_cookie_secret, user_key, true)
local signature = sha.hmac(sha.sha3_256, hmac_cookie_secret, user_key .. challenge_hash)
local combined_challenge = user_key .. "#" .. challenge_hash .. "#" .. signature
local challenge_hash, expiry = utils.generate_challenge(applet, pow_cookie_secret, user_key, true)
local signature = sha.hmac(sha.sha3_256, hmac_cookie_secret, user_key .. challenge_hash .. expiry)
local combined_challenge = user_key .. "#" .. challenge_hash .. "#" .. expiry .. "#" .. signature
-- define body sections
local site_name_body = ""
@ -144,8 +144,9 @@ function _M.view(applet)
captcha_sitekey, captcha_script_src)
else
pow_body = templates.pow_section
noscript_extra_body = string.format(templates.noscript_extra, user_key, challenge_hash, signature,
math.ceil(pow_difficulty/8), argon_time, argon_kb)
noscript_extra_body = string.format(templates.noscript_extra, user_key,
challenge_hash, expiry, signature, math.ceil(pow_difficulty/8),
argon_time, argon_kb)
end
-- sub in the body sections
@ -171,23 +172,31 @@ function _M.view(applet)
-- handle setting the POW cookie
local user_pow_response = parsed_body["pow_response"]
local matched_expiry = 0 -- ensure captcha cookie expiry matches POW cookie
if user_pow_response then
-- split the response up (makes the nojs submission easier because it can be a single field)
local split_response = utils.split(user_pow_response, "#")
if #split_response == 4 then
if #split_response == 5 then
local given_user_key = split_response[1]
local given_challenge_hash = split_response[2]
local given_signature = split_response[3]
local given_answer = split_response[4]
local given_expiry = split_response[3]
local given_signature = split_response[4]
local given_answer = split_response[5]
-- expiry check
local number_expiry = tonumber(given_expiry, 10)
if number_expiry ~= nil and number_expiry > core.now()['sec'] then
-- regenerate the challenge and compare it
local generated_challenge_hash = utils.generate_secret(applet, pow_cookie_secret, given_user_key, true)
local generated_challenge_hash = utils.generate_challenge(applet, pow_cookie_secret, given_user_key, true)
if given_challenge_hash == generated_challenge_hash then
-- regenerate the signature and compare it
local generated_signature = sha.hmac(sha.sha3_256, hmac_cookie_secret, given_user_key .. given_challenge_hash)
local generated_signature = sha.hmac(sha.sha3_256, hmac_cookie_secret, given_user_key .. given_challenge_hash .. given_expiry)
if given_signature == generated_signature then
-- do the work with their given answer
@ -196,15 +205,16 @@ function _M.view(applet)
-- check the output is correct
local hash_output = utils.split(full_hash, '$')[6]:sub(0, 43) -- https://github.com/thibaultcha/lua-argon2/issues/37
local hex_hash_output = sha.bin_to_hex(sha.base64_to_bin(hash_output));
if utils.checkdiff(hex_hash_output, pow_difficulty) then
-- the answer was good, give them a cookie
local signature = sha.hmac(sha.sha3_256, hmac_cookie_secret, given_user_key .. given_challenge_hash .. given_answer)
local combined_cookie = given_user_key .. "#" .. given_challenge_hash .. "#" .. given_answer .. "#" .. signature
local signature = sha.hmac(sha.sha3_256, hmac_cookie_secret, given_user_key .. given_challenge_hash .. given_expiry .. given_answer)
local combined_cookie = given_user_key .. "#" .. given_challenge_hash .. "#" .. given_expiry .. "#" .. given_answer .. "#" .. signature
applet:add_header(
"set-cookie",
string.format(
"z_ddos_pow=%s; Expires=Thu, 31-Dec-37 23:55:55 GMT; Path=/; Domain=.%s; SameSite=Strict;%s",
"_basedflare_pow=%s; Expires=Thu, 31-Dec-37 23:55:55 GMT; Path=/; Domain=.%s; SameSite=Strict;%s",
combined_cookie,
applet.headers['host'][0],
secure_cookie_flag
@ -217,6 +227,7 @@ function _M.view(applet)
end
end
end
end
-- handle setting the captcha cookie
local user_captcha_response = parsed_body["h-captcha-response"] or parsed_body["g-recaptcha-response"]
@ -251,13 +262,13 @@ function _M.view(applet)
if api_response.success == true then
local user_key = sha.bin_to_hex(randbytes(16))
local user_hash = utils.generate_secret(applet, captcha_cookie_secret, user_key, true)
local signature = sha.hmac(sha.sha3_256, hmac_cookie_secret, user_key .. user_hash)
local combined_cookie = user_key .. "#" .. user_hash .. "#" .. signature
local user_hash = utils.generate_challenge(applet, captcha_cookie_secret, user_key, true)
local signature = sha.hmac(sha.sha3_256, hmac_cookie_secret, user_key .. user_hash .. matched_expiry)
local combined_cookie = user_key .. "#" .. user_hash .. "#" .. matched_expiry .. "#" .. signature
applet:add_header(
"set-cookie",
string.format(
"z_ddos_captcha=%s; Expires=Thu, 31-Dec-37 23:55:55 GMT; Path=/; Domain=.%s; SameSite=Strict;%s",
"_basedflare_captcha=%s; Expires=Thu, 31-Dec-37 23:55:55 GMT; Path=/; Domain=.%s; SameSite=Strict;%s",
combined_cookie,
applet.headers['host'][0],
secure_cookie_flag
@ -309,22 +320,29 @@ end
-- check if captcha cookie is valid, separate secret from POW
function _M.check_captcha_status(txn)
local parsed_request_cookies = cookie.get_cookie_table(txn.sf:hdr("Cookie"))
local received_captcha_cookie = parsed_request_cookies["z_ddos_captcha"] or ""
local received_captcha_cookie = parsed_request_cookies["_basedflare_captcha"] or ""
-- split the cookie up
local split_cookie = utils.split(received_captcha_cookie, "#")
if #split_cookie ~= 3 then
if #split_cookie ~= 4 then
return
end
local given_user_key = split_cookie[1]
local given_user_hash = split_cookie[2]
local given_signature = split_cookie[3]
local given_expiry = split_cookie[3]
local given_signature = split_cookie[4]
-- expiry check
local number_expiry = tonumber(given_expiry, 10)
if number_expiry == nil or number_expiry <= core.now()['sec'] then
return
end
-- regenerate the user hash and compare it
local generated_user_hash = utils.generate_secret(txn, captcha_cookie_secret, given_user_key, false)
local generated_user_hash = utils.generate_challenge(txn, captcha_cookie_secret, given_user_key, false)
if generated_user_hash ~= given_user_hash then
return
end
-- regenerate the signature and compare it
local generated_signature = sha.hmac(sha.sha3_256, hmac_cookie_secret, given_user_key .. given_user_hash)
local generated_signature = sha.hmac(sha.sha3_256, hmac_cookie_secret, given_user_key .. given_user_hash .. given_expiry)
if given_signature == generated_signature then
return txn:set_var("txn.captcha_passed", true)
end
@ -333,23 +351,30 @@ end
-- check if pow cookie is valid
function _M.check_pow_status(txn)
local parsed_request_cookies = cookie.get_cookie_table(txn.sf:hdr("Cookie"))
local received_pow_cookie = parsed_request_cookies["z_ddos_pow"] or ""
local received_pow_cookie = parsed_request_cookies["_basedflare_pow"] or ""
-- split the cookie up
local split_cookie = utils.split(received_pow_cookie, "#")
if #split_cookie ~= 4 then
if #split_cookie ~= 5 then
return
end
local given_user_key = split_cookie[1]
local given_challenge_hash = split_cookie[2]
local given_answer = split_cookie[3]
local given_signature = split_cookie[4]
local given_expiry = split_cookie[3]
local given_answer = split_cookie[4]
local given_signature = split_cookie[5]
-- expiry check
local number_expiry = tonumber(given_expiry, 10)
if number_expiry == nil or number_expiry <= core.now()['sec'] then
return
end
-- regenerate the challenge and compare it
local generated_challenge_hash = utils.generate_secret(txn, pow_cookie_secret, given_user_key, false)
local generated_challenge_hash = utils.generate_challenge(txn, pow_cookie_secret, given_user_key, false)
if given_challenge_hash ~= generated_challenge_hash then
return
end
-- regenerate the signature and compare it
local generated_signature = sha.hmac(sha.sha3_256, hmac_cookie_secret, given_user_key .. given_challenge_hash .. given_answer)
local generated_signature = sha.hmac(sha.sha3_256, hmac_cookie_secret, given_user_key .. given_challenge_hash .. given_expiry .. given_answer)
if given_signature == generated_signature then
return txn:set_var("txn.pow_passed", true)
end

View File

@ -57,7 +57,7 @@ _M.noscript_extra = [[
<li>
<p>Run this in a linux terminal (requires <code>argon2</code> package installed):</p>
<code style="word-break: break-all;">
echo "Q0g9IiQyIjtCPSQocHJpbnRmICcwJS4wcycgJChzZXEgMSAkNCkpO2VjaG8gIldvcmtpbmcuLi4iO0k9MDt3aGlsZSB0cnVlOyBkbyBIPSQoZWNobyAtbiAkQ0gkSSB8IGFyZ29uMiAkMSAtaWQgLXQgJDUgLWsgJDYgLXAgMSAtbCAzMiAtcik7RT0ke0g6MDokNH07W1sgJEUgPT0gJEIgXV0gJiYgZWNobyAiT3V0cHV0OiIgJiYgZWNobyAkMSMkMiMkMyMkSSAmJiBleGl0IDA7KChJKyspKTtkb25lOwo=" | base64 -d | bash -s %s %s %s %s %s %s
echo "Q0g9IiQyIjtCPSQocHJpbnRmIDAlLjBzICQoc2VxIDEgJDUpKTtlY2hvICJXb3JraW5nLi4uIjtJPTA7d2hpbGUgdHJ1ZTsgZG8gSD0kKGVjaG8gLW4gJENIJEkgfCBhcmdvbjIgJDEgLWlkIC10ICQ2IC1rICQ3IC1wIDEgLWwgMzIgLXIpO0U9JHtIOjA6JDV9O1tbICRFID09ICRCIF1dICYmIGVjaG8gIk91dHB1dDoiICYmIGVjaG8gJDEjJDIjJDMjJDQjJEkgJiYgZXhpdCAwOygoSSsrKSk7ZG9uZTsK" | base64 -d | bash -s %s %s %s %s %s %s %s
</code>
<li>Paste the script output into the box and submit:
<form method="post">