Skip to content

Commit

Permalink
Add parse_uri() support for Steam TOTP (#153)
Browse files Browse the repository at this point in the history
`otpauth://` URIs with the parameter `encoder=steam` are parsed as Steam TOTP.
  • Loading branch information
einfachIrgendwer0815 authored Jul 27, 2023
1 parent 55f69ba commit 6c42125
Show file tree
Hide file tree
Showing 3 changed files with 44 additions and 4 deletions.
18 changes: 15 additions & 3 deletions src/pyotp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ def parse_uri(uri: str) -> OTP:
# Secret (to be filled in later)
secret = None

# Encoder (to be filled in later)
encoder = None

# Digits (to be filled in later)
digits = None

# Data we'll parse to the correct constructor
otp_data: Dict[str, Any] = {}

Expand Down Expand Up @@ -74,22 +80,28 @@ def parse_uri(uri: str) -> OTP:
otp_data["digest"] = hashlib.sha512
else:
raise ValueError("Invalid value for algorithm, must be SHA1, SHA256 or SHA512")
elif key == "encoder":
encoder = value
elif key == "digits":
digits = int(value)
if digits not in [6, 7, 8]:
raise ValueError("Digits may only be 6, 7, or 8")
otp_data["digits"] = digits
elif key == "period":
otp_data["interval"] = int(value)
elif key == "counter":
otp_data["initial_count"] = int(value)
elif key != "image":
raise ValueError("{} is not a valid parameter".format(key))


if encoder != "steam":
if digits is not None and digits not in [6, 7, 8]:
raise ValueError("Digits may only be 6, 7, or 8")

if not secret:
raise ValueError("No secret found in URI")

# Create objects
if encoder == "steam":
return contrib.Steam(secret, **otp_data)
if parsed_uri.netloc == "totp":
return TOTP(secret, **otp_data)
elif parsed_uri.netloc == "hotp":
Expand Down
9 changes: 8 additions & 1 deletion src/pyotp/contrib/steam.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,14 @@ class Steam(TOTP):
Steam's custom TOTP. Subclass of `pyotp.totp.TOTP`.
"""

def __init__(self, s: str, name: Optional[str] = None, issuer: Optional[str] = None, interval: int = 30) -> None:
def __init__(
self,
s: str,
name: Optional[str] = None,
issuer: Optional[str] = None,
interval: int = 30,
digits: int = 5
) -> None:
"""
:param s: secret in base32 format
:param interval: the time interval in seconds for OTP. This defaults to 30.
Expand Down
21 changes: 21 additions & 0 deletions test.py
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,13 @@ def test_invalids(self):
with self.assertRaises(ValueError) as cm:
pyotp.parse_uri("otpauth://totp?algorithm=aes")
self.assertEqual("Invalid value for algorithm, must be SHA1, SHA256 or SHA512", str(cm.exception))

def test_parse_steam(self):
otp = pyotp.parse_uri("otpauth://totp/Steam:?secret=SOME_SECRET&encoder=steam")
self.assertEqual(type(otp), pyotp.contrib.Steam)

otp = pyotp.parse_uri("otpauth://totp/Steam:?secret=SOME_SECRET")
self.assertNotEqual(type(otp), pyotp.contrib.Steam)

@unittest.skipIf(sys.version_info < (3, 6), "Skipping test that requires deterministic dict key enumeration")
def test_algorithms(self):
Expand Down Expand Up @@ -420,6 +427,20 @@ def test_algorithms(self):
otp = pyotp.parse_uri(otp.provisioning_uri(name="n", issuer_name="i", image="https://test.net/test.png"))
self.assertEqual(hashlib.sha512, otp.digest)

otp = pyotp.parse_uri("otpauth://totp/Steam:?secret=FMXNK4QEGKVPULRTADY6JIDK5VHUBGZW&encoder=steam")
self.assertEqual(type(otp), pyotp.contrib.Steam)
self.assertEqual(otp.at(0), "C5V56")
self.assertEqual(otp.at(30), "QJY8Y")
self.assertEqual(otp.at(60), "R3WQY")
self.assertEqual(otp.at(90), "JG3T3")

# period and digits should be ignored
otp = pyotp.parse_uri("otpauth://totp/Steam:?secret=FMXNK4QEGKVPULRTADY6JIDK5VHUBGZW&period=15&digits=7&encoder=steam")
self.assertEqual(type(otp), pyotp.contrib.Steam)
self.assertEqual(otp.at(0), "C5V56")
self.assertEqual(otp.at(30), "QJY8Y")
self.assertEqual(otp.at(60), "R3WQY")
self.assertEqual(otp.at(90), "JG3T3")

class Timecop(object):
"""
Expand Down

0 comments on commit 6c42125

Please sign in to comment.