yubicootp: Add signature support
Works both ways! Optional too!
This commit is contained in:
parent
490916a1ca
commit
0487031974
2 changed files with 35 additions and 6 deletions
|
@ -3,6 +3,8 @@ import re
|
||||||
import config
|
import config
|
||||||
import secrets
|
import secrets
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import base64
|
||||||
|
import hmac
|
||||||
|
|
||||||
|
|
||||||
class YubicoOTP(Cog):
|
class YubicoOTP(Cog):
|
||||||
|
@ -53,10 +55,31 @@ class YubicoOTP(Cog):
|
||||||
|
|
||||||
return int("".join(hexconv), 16)
|
return int("".join(hexconv), 16)
|
||||||
|
|
||||||
|
def calc_signature(self, text):
|
||||||
|
key = base64.b64decode(config.yubico_otp_secret)
|
||||||
|
signature_bytes = hmac.digest(key, text.encode(), "SHA1")
|
||||||
|
return base64.b64encode(signature_bytes).decode()
|
||||||
|
|
||||||
|
def validate_response_signature(self, response_dict):
|
||||||
|
yubico_signature = response_dict["h"]
|
||||||
|
to_sign = ""
|
||||||
|
for key in sorted(response_dict.keys()):
|
||||||
|
if key == "h":
|
||||||
|
continue
|
||||||
|
to_sign += f"{key}={response_dict[key]}&"
|
||||||
|
our_signature = self.calc_signature(to_sign.strip("&"))
|
||||||
|
return our_signature == yubico_signature
|
||||||
|
|
||||||
async def validate_yubico_otp(self, otp):
|
async def validate_yubico_otp(self, otp):
|
||||||
nonce = secrets.token_hex(15) # Random number in the valid range
|
nonce = secrets.token_hex(15) # Random number in the valid range
|
||||||
|
params = f"id={config.yubico_otp_client_id}&nonce={nonce}&otp={otp}"
|
||||||
|
|
||||||
|
# If secret is supplied, sign our request
|
||||||
|
if config.yubico_otp_secret:
|
||||||
|
params += "&h=" + self.calc_signature(params)
|
||||||
|
|
||||||
for api_server in self.api_servers:
|
for api_server in self.api_servers:
|
||||||
url = f"{api_server}/wsapi/2.0/verify?id={config.yubico_otp_client_id}&otp={otp}&nonce={nonce}"
|
url = f"{api_server}/wsapi/2.0/verify?{params}"
|
||||||
try:
|
try:
|
||||||
resp = await self.bot.aiosession.get(url)
|
resp = await self.bot.aiosession.get(url)
|
||||||
assert resp.status == 200
|
assert resp.status == 200
|
||||||
|
@ -71,7 +94,13 @@ class YubicoOTP(Cog):
|
||||||
datafields = resptext.strip().split("\r\n")
|
datafields = resptext.strip().split("\r\n")
|
||||||
datafields = {line.split("=")[0]: line.split("=")[1] for line in datafields}
|
datafields = {line.split("=")[0]: line.split("=")[1] for line in datafields}
|
||||||
|
|
||||||
datafields["nonce"] != nonce
|
# Verify nonce
|
||||||
|
assert datafields["nonce"] == nonce
|
||||||
|
|
||||||
|
# Verify signature if secret is present
|
||||||
|
if config.yubico_otp_secret:
|
||||||
|
assert self.validate_response_signature(datafields)
|
||||||
|
|
||||||
# If we got a success, then return True
|
# If we got a success, then return True
|
||||||
if datafields["status"] == "OK":
|
if datafields["status"] == "OK":
|
||||||
return True
|
return True
|
||||||
|
|
|
@ -326,9 +326,9 @@ pingmods_role = 360138431524765707
|
||||||
modtoggle_role = 360138431524765707
|
modtoggle_role = 360138431524765707
|
||||||
|
|
||||||
# == Only if you want to use cogs.yubicootp ==
|
# == Only if you want to use cogs.yubicootp ==
|
||||||
# Client ID from https://upgrade.yubico.com/getapikey/
|
# Optiona: Get your own from https://upgrade.yubico.com/getapikey/
|
||||||
yubico_otp_client_id = 1
|
yubico_otp_client_id = 1
|
||||||
|
# Note: You can keep client ID on 1, it will function.
|
||||||
yubico_otp_secret = ""
|
yubico_otp_secret = ""
|
||||||
# Note: YOU CAN KEEP THIS ON 1, IT WILL STILL FUNCTION.
|
# Optional: If you provide a secret, requests will be signed
|
||||||
# Note: Secret is not currently used, but it's recommended for you to add
|
# and responses will be verified.
|
||||||
# if you use your own client ID so if I ever implement it, your bot won't break.
|
|
||||||
|
|
Loading…
Reference in a new issue