Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Modify OTP generation to run in constant time #148

Merged
merged 1 commit into from Dec 14, 2022
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
9 changes: 4 additions & 5 deletions src/pyotp/otp.py
Expand Up @@ -18,6 +18,8 @@ def __init__(
issuer: Optional[str] = None,
) -> None:
self.digits = digits
if digits > 10:
raise ValueError("digits must be no greater than 10")
Comment on lines +21 to +22

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This introduces a breaking limitation. It is needed because of the hardcoded 10_000_000_000 below. See there for a suggestion of how to deal with this.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The limitation already existed. It comes from the fact that the HMAC is only 32 bits, so if you try to extract more than 10 digits you'll only get extra zeroes.

The proper limit is actually 9 digits, but the Steam subclass needs 10, so I allowed 10.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The HMAC is as long as the hash digest (160 bits for SHA1) but @Changaco is correct that the output of the DT transform as specified in the HOTP RFC is shorter - it actually has 31 bits of information (the high order bit is masked out). That's still over 9 decimal digits worth of information, so 10 digits (not 9) is correct.

self.digest = digest
self.secret = s
self.name = name or "Secret"
Expand All @@ -39,11 +41,8 @@ def generate_otp(self, input: int) -> str:
| (hmac_hash[offset + 2] & 0xFF) << 8
| (hmac_hash[offset + 3] & 0xFF)
)
str_code = str(code % 10**self.digits)
while len(str_code) < self.digits:
str_code = "0" + str_code

return str_code
str_code = str(10_000_000_000 + (code % 10**self.digits))
return str_code[-self.digits:]
Comment on lines +44 to +45

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dec_power = 10**self.digits
return str(dec_power + code % dec_power)[1:]


def byte_secret(self) -> bytes:
secret = self.secret
Expand Down