diff --git a/cogs/yubicootp.py b/cogs/yubicootp.py index 6af9ee8..71ee31e 100644 --- a/cogs/yubicootp.py +++ b/cogs/yubicootp.py @@ -3,6 +3,8 @@ import re import config import secrets import asyncio +import base64 +import hmac class YubicoOTP(Cog): @@ -53,10 +55,31 @@ class YubicoOTP(Cog): 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): 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: - 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: resp = await self.bot.aiosession.get(url) assert resp.status == 200 @@ -71,7 +94,13 @@ class YubicoOTP(Cog): datafields = resptext.strip().split("\r\n") 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 datafields["status"] == "OK": return True diff --git a/config_template.py b/config_template.py index af11fee..acb8028 100644 --- a/config_template.py +++ b/config_template.py @@ -326,9 +326,9 @@ pingmods_role = 360138431524765707 modtoggle_role = 360138431524765707 # == 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 +# Note: You can keep client ID on 1, it will function. yubico_otp_secret = "" -# Note: YOU CAN KEEP THIS ON 1, IT WILL STILL FUNCTION. -# Note: Secret is not currently used, but it's recommended for you to add -# if you use your own client ID so if I ever implement it, your bot won't break. +# Optional: If you provide a secret, requests will be signed +# and responses will be verified.