diff options
| author | Simeon Simeonov | 2022-03-21 13:25:26 +0100 |
|---|---|---|
| committer | Simeon Simeonov | 2022-03-21 13:25:26 +0100 |
| commit | 12c5e38691b24d12a8fc9605e3b93fac86d461ae (patch) | |
| tree | e27a66b696bf42b61fe2c27e864e9a73c9b57d4a | |
| parent | 9179abf6c4b3e3eb2c991cbbd74738e160b910cc (diff) | |
Reformat the code with annotations and new code style
| -rw-r--r-- | README.md | 28 | ||||
| -rw-r--r-- | otp2289/__init__.py | 51 | ||||
| -rw-r--r-- | otp2289/__main__.py | 155 | ||||
| -rw-r--r-- | otp2289/generator.py | 142 | ||||
| -rw-r--r-- | otp2289/server.py | 120 |
5 files changed, 296 insertions, 200 deletions
| @@ -1,6 +1,6 @@ | |||
| 1 | # pyotp2289 | 1 | # pyotp2289 |
| 2 | 2 | ||
| 3 | pyotp2289 is a pure Python 3 implementation of "A One-Time Password System" - | 3 | *pyotp2289* is a pure Python 3 implementation of "A One-Time Password System" - |
| 4 | RFC-2289. | 4 | RFC-2289. |
| 5 | 5 | ||
| 6 | It requires no additional libraries. | 6 | It requires no additional libraries. |
| @@ -30,7 +30,7 @@ I hope that somebody will find it useful. | |||
| 30 | 30 | ||
| 31 | ### FreeBSD | 31 | ### FreeBSD |
| 32 | 32 | ||
| 33 | `pyotp2289` is included in the official ports-tree. | 33 | *pyotp2289* is included in the official ports-tree. |
| 34 | 34 | ||
| 35 | ```bash | 35 | ```bash |
| 36 | cd /usr/ports/security/py-pyotp2289 | 36 | cd /usr/ports/security/py-pyotp2289 |
| @@ -41,21 +41,13 @@ I hope that somebody will find it useful. | |||
| 41 | ### Gentoo | 41 | ### Gentoo |
| 42 | 42 | ||
| 43 | ```bash | 43 | ```bash |
| 44 | layman -a sgs | 44 | # add sgs' custom repository using app-eselect/eselect-repository |
| 45 | emerge dev-python/pyotp2289 | 45 | eselect repository add sgs |
| 46 | ``` | ||
| 47 | |||
| 48 | |||
| 49 | ### Fedora ( >= 31) | ||
| 50 | 46 | ||
| 51 | Set up a custom repo as described: https://pkg.pichove.org/Fedora/README.txt | 47 | # ... or using app-portage/layman (obsolete) |
| 52 | 48 | layman -a sgs | |
| 53 | Key fingerprint: A664 5797 661E 2F47 3DD3 FF06 BCE7 0555 C3BB 08F7 | ||
| 54 | |||
| 55 | Install the package: | ||
| 56 | 49 | ||
| 57 | ```bash | 50 | emerge dev-python/pyotp2289 |
| 58 | sudo dnf install python3-pyotp2289 | ||
| 59 | ``` | 51 | ``` |
| 60 | 52 | ||
| 61 | 53 | ||
| @@ -189,7 +181,7 @@ a complete reference. | |||
| 189 | 181 | ||
| 190 | If you don't care about developing applications in Python and only care about | 182 | If you don't care about developing applications in Python and only care about |
| 191 | generating one-time passwords (tokens / hex digests) and authenticating with | 183 | generating one-time passwords (tokens / hex digests) and authenticating with |
| 192 | existing solutions (f.i. FreeBSD servers), pyotp2289 comes with a simple CLI: | 184 | existing solutions (f.i. FreeBSD servers), *pyotp2289* comes with a simple CLI: |
| 193 | 185 | ||
| 194 | ```bash | 186 | ```bash |
| 195 | python -m otp2289 --generate-otp-response -f token -i 498 -s TesT | 187 | python -m otp2289 --generate-otp-response -f token -i 498 -s TesT |
| @@ -207,7 +199,7 @@ starting from (and including) 498. | |||
| 207 | 199 | ||
| 208 | ## Support and contributing | 200 | ## Support and contributing |
| 209 | 201 | ||
| 210 | pyotp2289 is hosted on GitHub: https://github.com/blackm0re/pyotp2289 | 202 | *pyotp2289* is hosted on GitHub: https://github.com/blackm0re/pyotp2289 |
| 211 | 203 | ||
| 212 | 204 | ||
| 213 | ## Author | 205 | ## Author |
| @@ -217,7 +209,7 @@ Simeon Simeonov - sgs @ LiberaChat | |||
| 217 | 209 | ||
| 218 | ## [License](https://github.com/blackm0re/pyotp2289/blob/master/LICENSE) | 210 | ## [License](https://github.com/blackm0re/pyotp2289/blob/master/LICENSE) |
| 219 | 211 | ||
| 220 | Copyright (c) 2020, Simeon Simeonov | 212 | Copyright (c) 2020-2022 Simeon Simeonov |
| 221 | All rights reserved. | 213 | All rights reserved. |
| 222 | 214 | ||
| 223 | [Licensed](https://github.com/blackm0re/pyotp2289/blob/master/LICENSE) under the BSD 2-clause. | 215 | [Licensed](https://github.com/blackm0re/pyotp2289/blob/master/LICENSE) under the BSD 2-clause. |
diff --git a/otp2289/__init__.py b/otp2289/__init__.py index 36d1659..608595b 100644 --- a/otp2289/__init__.py +++ b/otp2289/__init__.py | |||
| @@ -1,7 +1,7 @@ | |||
| 1 | # -*- coding: utf-8 -*- | 1 | # -*- coding: utf-8 -*- |
| 2 | # SPDX-License-Identifier: BSD-2-Clause-FreeBSD | 2 | # SPDX-License-Identifier: BSD-2-Clause-FreeBSD |
| 3 | # | 3 | # |
| 4 | # Copyright (c) 2020, Simeon Simeonov | 4 | # Copyright (c) 2020-2022 Simeon Simeonov |
| 5 | # All rights reserved. | 5 | # All rights reserved. |
| 6 | # | 6 | # |
| 7 | # Redistribution and use in source and binary forms, with or without | 7 | # Redistribution and use in source and binary forms, with or without |
| @@ -24,20 +24,23 @@ | |||
| 24 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF | 24 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF |
| 25 | # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | 25 | # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| 26 | """A pure Python implementation of RFC-2289""" | 26 | """A pure Python implementation of RFC-2289""" |
| 27 | from .generator import (OTP_ALGO_MD5, | 27 | from .generator import ( |
| 28 | OTP_ALGO_SHA1, | 28 | OTP_ALGO_MD5, |
| 29 | OTPChallengeException, | 29 | OTP_ALGO_SHA1, |
| 30 | OTPGenerator, | 30 | OTPChallengeException, |
| 31 | OTPGeneratorException) | 31 | OTPGenerator, |
| 32 | from .server import (OTPInvalidResponse, | 32 | OTPGeneratorException, |
| 33 | OTPState, | 33 | ) |
| 34 | OTPStateException, | 34 | from .server import ( |
| 35 | OTPStore, | 35 | OTPInvalidResponse, |
| 36 | OTPStoreException) | 36 | OTPState, |
| 37 | 37 | OTPStateException, | |
| 38 | OTPStore, | ||
| 39 | OTPStoreException, | ||
| 40 | ) | ||
| 38 | 41 | ||
| 39 | __author__ = 'Simeon Simeonov' | 42 | __author__ = 'Simeon Simeonov' |
| 40 | __version__ = '1.0.0' | 43 | __version__ = '1.1.0-beta1' |
| 41 | __license__ = 'BSD 2-Clause' | 44 | __license__ = 'BSD 2-Clause' |
| 42 | 45 | ||
| 43 | 46 | ||
| @@ -51,13 +54,15 @@ def int_or_str(value): | |||
| 51 | 54 | ||
| 52 | VERSION = tuple(map(int_or_str, __version__.split('.'))) | 55 | VERSION = tuple(map(int_or_str, __version__.split('.'))) |
| 53 | 56 | ||
| 54 | __all__ = ['OTP_ALGO_MD5', | 57 | __all__ = [ |
| 55 | 'OTP_ALGO_SHA1', | 58 | 'OTP_ALGO_MD5', |
| 56 | 'OTPChallengeException', | 59 | 'OTP_ALGO_SHA1', |
| 57 | 'OTPGenerator', | 60 | 'OTPChallengeException', |
| 58 | 'OTPGeneratorException', | 61 | 'OTPGenerator', |
| 59 | 'OTPInvalidResponse', | 62 | 'OTPGeneratorException', |
| 60 | 'OTPState', | 63 | 'OTPInvalidResponse', |
| 61 | 'OTPStateException', | 64 | 'OTPState', |
| 62 | 'OTPStore', | 65 | 'OTPStateException', |
| 63 | 'OTPStoreException'] | 66 | 'OTPStore', |
| 67 | 'OTPStoreException', | ||
| 68 | ] | ||
diff --git a/otp2289/__main__.py b/otp2289/__main__.py index 7bb0673..471ae24 100644 --- a/otp2289/__main__.py +++ b/otp2289/__main__.py | |||
| @@ -1,7 +1,7 @@ | |||
| 1 | # -*- coding: utf-8 -*- | 1 | # -*- coding: utf-8 -*- |
| 2 | # SPDX-License-Identifier: BSD-2-Clause-FreeBSD | 2 | # SPDX-License-Identifier: BSD-2-Clause-FreeBSD |
| 3 | # | 3 | # |
| 4 | # Copyright (c) 2020, Simeon Simeonov | 4 | # Copyright (c) 2020-2022 Simeon Simeonov |
| 5 | # All rights reserved. | 5 | # All rights reserved. |
| 6 | # | 6 | # |
| 7 | # Redistribution and use in source and binary forms, with or without | 7 | # Redistribution and use in source and binary forms, with or without |
| @@ -35,6 +35,7 @@ python -m otp2289 --generate-otp-response -s TesT -i 499 -f token | |||
| 35 | import argparse | 35 | import argparse |
| 36 | import errno | 36 | import errno |
| 37 | import getpass | 37 | import getpass |
| 38 | import io | ||
| 38 | import os | 39 | import os |
| 39 | import secrets | 40 | import secrets |
| 40 | import string | 41 | import string |
| @@ -48,7 +49,7 @@ def eprint(*arg, **kwargs): | |||
| 48 | print(*arg, file=sys.stderr, flush=True, **kwargs) | 49 | print(*arg, file=sys.stderr, flush=True, **kwargs) |
| 49 | 50 | ||
| 50 | 51 | ||
| 51 | def generate_otp_response(args): | 52 | def generate_otp_response(args: argparse.Namespace) -> str: |
| 52 | """ | 53 | """ |
| 53 | Generates a response based on the parameters sent from the parser | 54 | Generates a response based on the parameters sent from the parser |
| 54 | 55 | ||
| @@ -65,24 +66,29 @@ def generate_otp_response(args): | |||
| 65 | generator = otp2289.generator.OTPGenerator( | 66 | generator = otp2289.generator.OTPGenerator( |
| 66 | args.password.encode(), | 67 | args.password.encode(), |
| 67 | args.seed, | 68 | args.seed, |
| 68 | args.hash_algo) | 69 | args.hash_algo, |
| 70 | ) | ||
| 69 | if args.challenge_string: | 71 | if args.challenge_string: |
| 70 | if args.output_format == 'token': | 72 | if args.output_format == 'token': |
| 71 | return generator.generate_otp_words_from_challenge( | 73 | return generator.generate_otp_words_from_challenge( |
| 72 | args.challenge_string) | 74 | args.challenge_string |
| 75 | ) | ||
| 73 | return generator.generate_otp_hexdigest_from_challenge( | 76 | return generator.generate_otp_hexdigest_from_challenge( |
| 74 | args.challenge_string) | 77 | args.challenge_string |
| 78 | ) | ||
| 75 | # regular parameters | 79 | # regular parameters |
| 76 | header = '' | 80 | header = '' |
| 77 | if not args.quiet: | 81 | if not args.quiet: |
| 78 | header = (f'Seed: {args.seed}, Step: {args.step}, ' | 82 | header = ( |
| 79 | f'Hash: {args.hash_algo}{os.linesep}') | 83 | f'Seed: {args.seed}, Step: {args.step}, ' |
| 84 | f'Hash: {args.hash_algo}{os.linesep}' | ||
| 85 | ) | ||
| 80 | if args.output_format == 'token': | 86 | if args.output_format == 'token': |
| 81 | return header + generator.generate_otp_words(args.step) | 87 | return header + generator.generate_otp_words(args.step) |
| 82 | return header + generator.generate_otp_hexdigest(args.step) | 88 | return header + generator.generate_otp_hexdigest(args.step) |
| 83 | 89 | ||
| 84 | 90 | ||
| 85 | def generate_otp_range(args): | 91 | def generate_otp_range(args: argparse.Namespace) -> str: |
| 86 | """ | 92 | """ |
| 87 | Generates range of responses based on the parameters sent from the parser | 93 | Generates range of responses based on the parameters sent from the parser |
| 88 | 94 | ||
| @@ -99,7 +105,8 @@ def generate_otp_range(args): | |||
| 99 | generator = otp2289.generator.OTPGenerator( | 105 | generator = otp2289.generator.OTPGenerator( |
| 100 | args.password.encode(), | 106 | args.password.encode(), |
| 101 | args.seed, | 107 | args.seed, |
| 102 | args.hash_algo) | 108 | args.hash_algo, |
| 109 | ) | ||
| 103 | if args.output_format == 'token': | 110 | if args.output_format == 'token': |
| 104 | method = generator.generate_otp_words | 111 | method = generator.generate_otp_words |
| 105 | else: | 112 | else: |
| @@ -112,25 +119,35 @@ def generate_otp_range(args): | |||
| 112 | # any need for quiet? | 119 | # any need for quiet? |
| 113 | header = '' | 120 | header = '' |
| 114 | if not args.quiet: | 121 | if not args.quiet: |
| 115 | header = (f'Seed: {args.seed}, Step: {args.step}, ' | 122 | header = ( |
| 116 | f'Hash: {args.hash_algo}, Range: {args.range}{os.linesep}') | 123 | f'Seed: {args.seed}, Step: {args.step}, ' |
| 124 | f'Hash: {args.hash_algo}, Range: {args.range}{os.linesep}' | ||
| 125 | ) | ||
| 117 | return header + os.linesep.join( | 126 | return header + os.linesep.join( |
| 118 | [f'{step}: ' + method(step) for step in range( | 127 | [ |
| 119 | args.step, args.step - args.range, -1)]) | 128 | f'{step}: ' + method(step) |
| 129 | for step in range( | ||
| 130 | args.step, | ||
| 131 | args.step - args.range, | ||
| 132 | -1, | ||
| 133 | ) | ||
| 134 | ] | ||
| 135 | ) | ||
| 120 | 136 | ||
| 121 | 137 | ||
| 122 | def get_rnd_seed(): | 138 | def get_rnd_seed() -> str: |
| 123 | """ | 139 | """ |
| 124 | Returns a random seed in the format: | 140 | Returns a random seed in the format: |
| 125 | 141 | ||
| 126 | 2 random letters (capitalize()) + 5 random digits | 142 | 2 random letters (capitalize()) + 5 random digits |
| 127 | """ | 143 | """ |
| 128 | rnd = secrets.SystemRandom() | 144 | rnd = secrets.SystemRandom() |
| 129 | return (''.join(rnd.choices(string.ascii_lowercase, k=2)).capitalize() + | 145 | return ''.join( |
| 130 | ''.join(rnd.choices(string.digits, k=5))) | 146 | rnd.choices(string.ascii_lowercase, k=2) |
| 147 | ).capitalize() + ''.join(rnd.choices(string.digits, k=5)) | ||
| 131 | 148 | ||
| 132 | 149 | ||
| 133 | def initiate_new_sequence(args): | 150 | def initiate_new_sequence(args: argparse.Namespace) -> str: |
| 134 | """ | 151 | """ |
| 135 | Generates a new sequence based on the parameters sent from the parser. | 152 | Generates a new sequence based on the parameters sent from the parser. |
| 136 | 153 | ||
| @@ -148,15 +165,19 @@ def initiate_new_sequence(args): | |||
| 148 | args.seed = get_rnd_seed() | 165 | args.seed = get_rnd_seed() |
| 149 | header = '' | 166 | header = '' |
| 150 | if not args.quiet: | 167 | if not args.quiet: |
| 151 | header = (f'Seed: {args.seed}, Step: {args.step}, ' | 168 | header = ( |
| 152 | f'Hash: {args.hash_algo}{os.linesep}') | 169 | f'Seed: {args.seed}, Step: {args.step}, ' |
| 170 | f'Hash: {args.hash_algo}{os.linesep}' | ||
| 171 | ) | ||
| 153 | generator = otp2289.generator.OTPGenerator( | 172 | generator = otp2289.generator.OTPGenerator( |
| 154 | args.password.encode(), | 173 | args.password.encode(), |
| 155 | args.seed, | 174 | args.seed, |
| 156 | args.hash_algo) | 175 | args.hash_algo, |
| 176 | ) | ||
| 157 | if args.challenge_string: | 177 | if args.challenge_string: |
| 158 | return header + generator.generate_otp_hexdigest_from_challenge( | 178 | return header + generator.generate_otp_hexdigest_from_challenge( |
| 159 | args.challenge_string) | 179 | args.challenge_string |
| 180 | ) | ||
| 160 | return header + generator.generate_otp_hexdigest(args.step) | 181 | return header + generator.generate_otp_hexdigest(args.step) |
| 161 | 182 | ||
| 162 | 183 | ||
| @@ -164,92 +185,122 @@ def main(args=None): | |||
| 164 | """the main entry point""" | 185 | """the main entry point""" |
| 165 | parser = argparse.ArgumentParser( | 186 | parser = argparse.ArgumentParser( |
| 166 | prog=__package__, | 187 | prog=__package__, |
| 167 | epilog=(f'%(prog)s {otp2289.__version__} by Simeon Simeonov ' | 188 | epilog=( |
| 168 | '(sgs @ LiberaChat)'), | 189 | f'%(prog)s {otp2289.__version__} by Simeon Simeonov ' |
| 169 | description='The following options are available') | 190 | '(sgs @ LiberaChat)' |
| 191 | ), | ||
| 192 | description='The following options are available', | ||
| 193 | ) | ||
| 170 | group = parser.add_mutually_exclusive_group(required=True) | 194 | group = parser.add_mutually_exclusive_group(required=True) |
| 171 | group.add_argument( | 195 | group.add_argument( |
| 172 | '--generate-otp-range', | 196 | '--generate-otp-range', |
| 173 | action='store_true', | 197 | action='store_true', |
| 174 | dest='generate_otp_range', | 198 | dest='generate_otp_range', |
| 175 | default=False, | 199 | default=False, |
| 176 | help='Generates a range of OTP responses') | 200 | help='Generates a range of OTP responses', |
| 201 | ) | ||
| 177 | group.add_argument( | 202 | group.add_argument( |
| 178 | '--generate-otp-response', | 203 | '--generate-otp-response', |
| 179 | action='store_true', | 204 | action='store_true', |
| 180 | dest='generate_otp_response', | 205 | dest='generate_otp_response', |
| 181 | default=False, | 206 | default=False, |
| 182 | help='Generates a new OTP response') | 207 | help='Generates a new OTP response', |
| 208 | ) | ||
| 183 | group.add_argument( | 209 | group.add_argument( |
| 184 | '--initiate-new-sequence', | 210 | '--initiate-new-sequence', |
| 185 | action='store_true', | 211 | action='store_true', |
| 186 | dest='initiate_new_sequence', | 212 | dest='initiate_new_sequence', |
| 187 | default=False, | 213 | default=False, |
| 188 | help=('Initiates a new OTP sequence. Essentially the same as ' | 214 | help=( |
| 189 | '--generate-otp-response only it prompts twice for password ' | 215 | 'Initiates a new OTP sequence. Essentially the same as ' |
| 190 | 'and always outputs hex (ignores -f).')) | 216 | '--generate-otp-response only it prompts twice for password ' |
| 217 | 'and always outputs hex (ignores -f).' | ||
| 218 | ), | ||
| 219 | ) | ||
| 191 | parser.add_argument( | 220 | parser.add_argument( |
| 192 | '-a', '--hash-algorithm', | 221 | '-a', |
| 222 | '--hash-algorithm', | ||
| 193 | metavar='<md5 | sha1>', | 223 | metavar='<md5 | sha1>', |
| 194 | type=str, | 224 | type=str, |
| 195 | dest='hash_algo', | 225 | dest='hash_algo', |
| 196 | default='md5', | 226 | default='md5', |
| 197 | help='The hash algorithm to use. Possible values: md5 (default), sha1') | 227 | help='The hash algorithm to use. Possible values: md5 (default), sha1', |
| 228 | ) | ||
| 198 | parser.add_argument( | 229 | parser.add_argument( |
| 199 | '-c', '--challenge-string', | 230 | '-c', |
| 231 | '--challenge-string', | ||
| 200 | metavar='<challenge string>', | 232 | metavar='<challenge string>', |
| 201 | type=str, | 233 | type=str, |
| 202 | dest='challenge_string', | 234 | dest='challenge_string', |
| 203 | default='', | 235 | default='', |
| 204 | help='Use challenge string when generating response') | 236 | help='Use challenge string when generating response', |
| 237 | ) | ||
| 205 | parser.add_argument( | 238 | parser.add_argument( |
| 206 | '-f', '--output-format', | 239 | '-f', |
| 240 | '--output-format', | ||
| 207 | metavar='<hex | token>', | 241 | metavar='<hex | token>', |
| 208 | type=str, | 242 | type=str, |
| 209 | dest='output_format', | 243 | dest='output_format', |
| 210 | default='hex', | 244 | default='hex', |
| 211 | help='The output format to use. Possible values: hex (default), token') | 245 | help='The output format to use. Possible values: hex (default), token', |
| 246 | ) | ||
| 212 | parser.add_argument( | 247 | parser.add_argument( |
| 213 | '-i', '--step', | 248 | '-i', |
| 249 | '--step', | ||
| 214 | metavar='<step>', | 250 | metavar='<step>', |
| 215 | type=int, | 251 | type=int, |
| 216 | dest='step', | 252 | dest='step', |
| 217 | default=500, | 253 | default=500, |
| 218 | help='The step. Default for initiating a new sequence is: 500') | 254 | help='The step. Default for initiating a new sequence is: 500', |
| 255 | ) | ||
| 219 | parser.add_argument( | 256 | parser.add_argument( |
| 220 | '-p', '--password', | 257 | '-p', |
| 258 | '--password', | ||
| 221 | metavar='<PASSWORD[FILE]>', | 259 | metavar='<PASSWORD[FILE]>', |
| 222 | type=str, | 260 | type=str, |
| 223 | dest='password', | 261 | dest='password', |
| 224 | default='', | 262 | default='', |
| 225 | help=('The password or path to password file ' | 263 | help=( |
| 226 | '(default & recommended: prompt for passwd)')) | 264 | 'The password or path to password file ' |
| 265 | '(default & recommended: prompt for passwd)' | ||
| 266 | ), | ||
| 267 | ) | ||
| 227 | parser.add_argument( | 268 | parser.add_argument( |
| 228 | '-q', '--quiet', | 269 | '-q', |
| 270 | '--quiet', | ||
| 229 | action='store_true', | 271 | action='store_true', |
| 230 | dest='quiet', | 272 | dest='quiet', |
| 231 | default=False, | 273 | default=False, |
| 232 | help='Dot not show headers. Only hex / tokens') | 274 | help='Dot not show headers. Only hex / tokens', |
| 275 | ) | ||
| 233 | parser.add_argument( | 276 | parser.add_argument( |
| 234 | '-r', '--range', | 277 | '-r', |
| 278 | '--range', | ||
| 235 | metavar='<range>', | 279 | metavar='<range>', |
| 236 | type=int, | 280 | type=int, |
| 237 | dest='range', | 281 | dest='range', |
| 238 | default=1, | 282 | default=1, |
| 239 | help='Amount of consecutive OTP hex/tokens to generate. default: 1') | 283 | help='Amount of consecutive OTP hex/tokens to generate. default: 1', |
| 284 | ) | ||
| 240 | parser.add_argument( | 285 | parser.add_argument( |
| 241 | '-s', '--seed', | 286 | '-s', |
| 287 | '--seed', | ||
| 242 | metavar='[seed]', | 288 | metavar='[seed]', |
| 243 | type=str, | 289 | type=str, |
| 244 | dest='seed', | 290 | dest='seed', |
| 245 | default='', | 291 | default='', |
| 246 | help=('The seed to use (1 to 16 alphanumeric characters) ' | 292 | help=( |
| 247 | '(default & recommended: random seed)')) | 293 | 'The seed to use (1 to 16 alphanumeric characters) ' |
| 294 | '(default & recommended: random seed)' | ||
| 295 | ), | ||
| 296 | ) | ||
| 248 | parser.add_argument( | 297 | parser.add_argument( |
| 249 | '-v', '--version', | 298 | '-v', |
| 299 | '--version', | ||
| 250 | action='version', | 300 | action='version', |
| 251 | version=f'%(prog)s {otp2289.__version__}', | 301 | version=f'%(prog)s {otp2289.__version__}', |
| 252 | help='display program-version and exit') | 302 | help='display program-version and exit', |
| 303 | ) | ||
| 253 | args = parser.parse_args(args) | 304 | args = parser.parse_args(args) |
| 254 | # handle the password before everything else | 305 | # handle the password before everything else |
| 255 | if not args.password: | 306 | if not args.password: |
| @@ -257,8 +308,8 @@ def main(args=None): | |||
| 257 | while True: | 308 | while True: |
| 258 | args.password = getpass.getpass() | 309 | args.password = getpass.getpass() |
| 259 | if ( | 310 | if ( |
| 260 | not args.initiate_new_sequence or | 311 | not args.initiate_new_sequence |
| 261 | args.password == getpass.getpass('Repeat password: ') | 312 | or args.password == getpass.getpass('Repeat password: ') |
| 262 | ): | 313 | ): |
| 263 | break | 314 | break |
| 264 | eprint('The passwords do not match') | 315 | eprint('The passwords do not match') |
| @@ -267,7 +318,7 @@ def main(args=None): | |||
| 267 | sys.exit(errno.EACCES) | 318 | sys.exit(errno.EACCES) |
| 268 | elif os.path.isfile(args.password): | 319 | elif os.path.isfile(args.password): |
| 269 | try: | 320 | try: |
| 270 | with open(args.password, 'r') as fp: | 321 | with io.open(args.password, 'r', encoding='utf-8') as fp: |
| 271 | args.password = fp.readline().strip() | 322 | args.password = fp.readline().strip() |
| 272 | except Exception as exp: | 323 | except Exception as exp: |
| 273 | eprint(f'Unable to open password file: {exp}') | 324 | eprint(f'Unable to open password file: {exp}') |
diff --git a/otp2289/generator.py b/otp2289/generator.py index 5ea738b..7b86e1b 100644 --- a/otp2289/generator.py +++ b/otp2289/generator.py | |||
| @@ -1,7 +1,7 @@ | |||
| 1 | # -*- coding: utf-8 -*- | 1 | # -*- coding: utf-8 -*- |
| 2 | # SPDX-License-Identifier: BSD-2-Clause-FreeBSD | 2 | # SPDX-License-Identifier: BSD-2-Clause-FreeBSD |
| 3 | # | 3 | # |
| 4 | # Copyright (c) 2020, Simeon Simeonov | 4 | # Copyright (c) 2020-2022 Simeon Simeonov |
| 5 | # All rights reserved. | 5 | # All rights reserved. |
| 6 | # | 6 | # |
| 7 | # Redistribution and use in source and binary forms, with or without | 7 | # Redistribution and use in source and binary forms, with or without |
| @@ -28,7 +28,6 @@ import binascii | |||
| 28 | import hashlib | 28 | import hashlib |
| 29 | import string | 29 | import string |
| 30 | 30 | ||
| 31 | |||
| 32 | OTP_ALGO_MD5 = 1 | 31 | OTP_ALGO_MD5 = 1 |
| 33 | OTP_ALGO_SHA1 = 2 | 32 | OTP_ALGO_SHA1 = 2 |
| 34 | 33 | ||
| @@ -306,7 +305,12 @@ class OTPChallengeException(Exception): | |||
| 306 | class OTPGenerator: | 305 | class OTPGenerator: |
| 307 | """OTPGenerator class""" | 306 | """OTPGenerator class""" |
| 308 | 307 | ||
| 309 | def __init__(self, password, seed='', hash_algo=OTP_ALGO_MD5): | 308 | def __init__( |
| 309 | self, | ||
| 310 | password: bytes, | ||
| 311 | seed: str = '', | ||
| 312 | hash_algo=OTP_ALGO_MD5, | ||
| 313 | ): | ||
| 310 | """ | 314 | """ |
| 311 | Constructs an OTPGenerator object with a given password and seed. | 315 | Constructs an OTPGenerator object with a given password and seed. |
| 312 | 316 | ||
| @@ -330,16 +334,19 @@ class OTPGenerator: | |||
| 330 | raise OTPGeneratorException('Password must be a byte-string') | 334 | raise OTPGeneratorException('Password must be a byte-string') |
| 331 | if len(password) < 10: | 335 | if len(password) < 10: |
| 332 | raise OTPGeneratorException( | 336 | raise OTPGeneratorException( |
| 333 | 'Password must be longer than 10 bytes') | 337 | 'Password must be longer than 10 bytes' |
| 338 | ) | ||
| 334 | self._password = password | 339 | self._password = password |
| 335 | 340 | ||
| 336 | def __repr__(self): | 341 | def __repr__(self): |
| 337 | """repr implementation""" | 342 | """repr implementation""" |
| 338 | return (f'{self.__class__} at {id(self)} (seed={self._seed}, ' | 343 | return ( |
| 339 | f'hash_algo={self._hash_algo})') | 344 | f'{self.__class__} at {id(self)} (seed={self._seed}, ' |
| 345 | f'hash_algo={self._hash_algo})' | ||
| 346 | ) | ||
| 340 | 347 | ||
| 341 | @staticmethod | 348 | @staticmethod |
| 342 | def bit_pair_sum(bit_stream): | 349 | def bit_pair_sum(bit_stream: str) -> int: |
| 343 | """ | 350 | """ |
| 344 | Split bit_stream in bit-pairs and sum them all together. | 351 | Split bit_stream in bit-pairs and sum them all together. |
| 345 | 352 | ||
| @@ -359,7 +366,7 @@ class OTPGenerator: | |||
| 359 | return value | 366 | return value |
| 360 | 367 | ||
| 361 | @staticmethod | 368 | @staticmethod |
| 362 | def bytes_to_tokens(hash_bytes): | 369 | def bytes_to_tokens(hash_bytes: bytes) -> str: |
| 363 | """ | 370 | """ |
| 364 | Returns a 6 words token from bytes as specified by RFC-2289. | 371 | Returns a 6 words token from bytes as specified by RFC-2289. |
| 365 | 372 | ||
| @@ -369,8 +376,7 @@ class OTPGenerator: | |||
| 369 | :return: 6 words tokens | 376 | :return: 6 words tokens |
| 370 | :rtype: str | 377 | :rtype: str |
| 371 | """ | 378 | """ |
| 372 | bit_stream = ''.join( | 379 | bit_stream = ''.join([f'{byte:0>8b}' for byte in hash_bytes]) |
| 373 | ['{0:0>8b}'.format(byte) for byte in hash_bytes]) | ||
| 374 | bit_pair_sum = OTPGenerator.bit_pair_sum(bit_stream) | 380 | bit_pair_sum = OTPGenerator.bit_pair_sum(bit_stream) |
| 375 | tokens = [] | 381 | tokens = [] |
| 376 | tokens.append(RFC1760_TOKENS[int(bit_stream[:11], 2)]) | 382 | tokens.append(RFC1760_TOKENS[int(bit_stream[:11], 2)]) |
| @@ -379,12 +385,17 @@ class OTPGenerator: | |||
| 379 | tokens.append(RFC1760_TOKENS[int(bit_stream[33:44], 2)]) | 385 | tokens.append(RFC1760_TOKENS[int(bit_stream[33:44], 2)]) |
| 380 | tokens.append(RFC1760_TOKENS[int(bit_stream[44:55], 2)]) | 386 | tokens.append(RFC1760_TOKENS[int(bit_stream[44:55], 2)]) |
| 381 | tokens.append( | 387 | tokens.append( |
| 382 | RFC1760_TOKENS[int( | 388 | RFC1760_TOKENS[ |
| 383 | bit_stream[55:64] + '{0:0>8b}'.format(bit_pair_sum)[-2:], 2)]) | 389 | int( |
| 390 | bit_stream[55:64] + f'{bit_pair_sum:0>8b}'[-2:], | ||
| 391 | 2, | ||
| 392 | ) | ||
| 393 | ] | ||
| 394 | ) | ||
| 384 | return ' '.join(tokens) | 395 | return ' '.join(tokens) |
| 385 | 396 | ||
| 386 | @staticmethod | 397 | @staticmethod |
| 387 | def get_tokens_from_challenge(challenge): | 398 | def get_tokens_from_challenge(challenge: str) -> tuple: |
| 388 | """ | 399 | """ |
| 389 | Returns tokens (seed, hash_algo and step) from a challenge string. | 400 | Returns tokens (seed, hash_algo and step) from a challenge string. |
| 390 | 401 | ||
| @@ -410,7 +421,7 @@ class OTPGenerator: | |||
| 410 | raise OTPChallengeException('Invalid challenge') from None | 421 | raise OTPChallengeException('Invalid challenge') from None |
| 411 | 422 | ||
| 412 | @staticmethod | 423 | @staticmethod |
| 413 | def sha1_digest_folding(sha1_digest): | 424 | def sha1_digest_folding(sha1_digest: bytes) -> bytes: |
| 414 | """ | 425 | """ |
| 415 | Implementation of the 160bit -> 64bit folding algorithm | 426 | Implementation of the 160bit -> 64bit folding algorithm |
| 416 | for sha1 digest. | 427 | for sha1 digest. |
| @@ -425,14 +436,17 @@ class OTPGenerator: | |||
| 425 | raise OTPGeneratorException('sha1_digest must be of type bytes') | 436 | raise OTPGeneratorException('sha1_digest must be of type bytes') |
| 426 | if len(sha1_digest) != 20: | 437 | if len(sha1_digest) != 20: |
| 427 | raise OTPGeneratorException( | 438 | raise OTPGeneratorException( |
| 428 | 'sha1_digest must be 160 bits (20 bytes) long') | 439 | 'sha1_digest must be 160 bits (20 bytes) long' |
| 440 | ) | ||
| 429 | digested = list(5 * b'i') # 5 bytes (40 bits) | 441 | digested = list(5 * b'i') # 5 bytes (40 bits) |
| 430 | result = list(8 * b'x') # 8 bytes (64 bits) | 442 | result = list(8 * b'x') # 8 bytes (64 bits) |
| 431 | for i in range(5): | 443 | for i in range(5): |
| 432 | digested[i] = (((sha1_digest[i * 4 + 0] & 0xff) << 24) | | 444 | digested[i] = ( |
| 433 | ((sha1_digest[i * 4 + 1] & 0xff) << 16) | | 445 | ((sha1_digest[i * 4 + 0] & 0xFF) << 24) |
| 434 | ((sha1_digest[i * 4 + 2] & 0xff) << 8) | | 446 | | ((sha1_digest[i * 4 + 1] & 0xFF) << 16) |
| 435 | (sha1_digest[i * 4 + 3] & 0xff)) | 447 | | ((sha1_digest[i * 4 + 2] & 0xFF) << 8) |
| 448 | | (sha1_digest[i * 4 + 3] & 0xFF) | ||
| 449 | ) | ||
| 436 | # sha.digest[0] ^= sha.digest[2]; | 450 | # sha.digest[0] ^= sha.digest[2]; |
| 437 | # sha.digest[1] ^= sha.digest[3]; | 451 | # sha.digest[1] ^= sha.digest[3]; |
| 438 | # sha.digest[0] ^= sha.digest[4]; | 452 | # sha.digest[0] ^= sha.digest[4]; |
| @@ -446,18 +460,18 @@ class OTPGenerator: | |||
| 446 | # result[j+3] = (unsigned char)((sha.digest[i] >> 24) & 0xff); | 460 | # result[j+3] = (unsigned char)((sha.digest[i] >> 24) & 0xff); |
| 447 | # } | 461 | # } |
| 448 | # just hardcoding the two iterations for better efficiency | 462 | # just hardcoding the two iterations for better efficiency |
| 449 | result[0] = digested[0] & 0xff | 463 | result[0] = digested[0] & 0xFF |
| 450 | result[1] = (digested[0] >> 8) & 0xff | 464 | result[1] = (digested[0] >> 8) & 0xFF |
| 451 | result[2] = (digested[0] >> 16) & 0xff | 465 | result[2] = (digested[0] >> 16) & 0xFF |
| 452 | result[3] = (digested[0] >> 24) & 0xff | 466 | result[3] = (digested[0] >> 24) & 0xFF |
| 453 | result[4] = digested[1] & 0xff | 467 | result[4] = digested[1] & 0xFF |
| 454 | result[5] = (digested[1] >> 8) & 0xff | 468 | result[5] = (digested[1] >> 8) & 0xFF |
| 455 | result[6] = (digested[1] >> 16) & 0xff | 469 | result[6] = (digested[1] >> 16) & 0xFF |
| 456 | result[7] = (digested[1] >> 24) & 0xff | 470 | result[7] = (digested[1] >> 24) & 0xFF |
| 457 | return bytes(result) | 471 | return bytes(result) |
| 458 | 472 | ||
| 459 | @staticmethod | 473 | @staticmethod |
| 460 | def strxor(byte_str1, byte_str2): | 474 | def strxor(byte_str1: bytes, byte_str2: bytes) -> bytes: |
| 461 | """ | 475 | """ |
| 462 | Implementation of strxor similar to the one provided by pycrypto. | 476 | Implementation of strxor similar to the one provided by pycrypto. |
| 463 | 477 | ||
| @@ -472,17 +486,17 @@ class OTPGenerator: | |||
| 472 | """ | 486 | """ |
| 473 | if not (isinstance(byte_str1, bytes) and isinstance(byte_str2, bytes)): | 487 | if not (isinstance(byte_str1, bytes) and isinstance(byte_str2, bytes)): |
| 474 | raise OTPGeneratorException( | 488 | raise OTPGeneratorException( |
| 475 | 'byte_str1 and byte_str2 must be of type bytes') | 489 | 'byte_str1 and byte_str2 must be of type bytes' |
| 490 | ) | ||
| 476 | length = len(byte_str1) | 491 | length = len(byte_str1) |
| 477 | if length != len(byte_str2) or length < 1: | 492 | if length != len(byte_str2) or length < 1: |
| 478 | raise OTPGeneratorException( | 493 | raise OTPGeneratorException( |
| 479 | 'byte_str1 and byte_str2 must be of the same length > 0') | 494 | 'byte_str1 and byte_str2 must be of the same length > 0' |
| 480 | return bytes( | 495 | ) |
| 481 | [byte_str1[i] ^ byte_str2[i] for i in range(length)] | 496 | return bytes([byte_str1[i] ^ byte_str2[i] for i in range(length)]) |
| 482 | ) | ||
| 483 | 497 | ||
| 484 | @staticmethod | 498 | @staticmethod |
| 485 | def tokens_to_bytes(tokens_str): | 499 | def tokens_to_bytes(tokens_str: str) -> bytes: |
| 486 | """ | 500 | """ |
| 487 | Returns bytes from a 6 words token as specified by RFC-2289. | 501 | Returns bytes from a 6 words token as specified by RFC-2289. |
| 488 | 502 | ||
| @@ -499,14 +513,17 @@ class OTPGenerator: | |||
| 499 | tokens = tokens_str.split() | 513 | tokens = tokens_str.split() |
| 500 | if len(tokens) != 6: | 514 | if len(tokens) != 6: |
| 501 | raise OTPGeneratorException( | 515 | raise OTPGeneratorException( |
| 502 | 'Tokens-string does not contain 6 tokens') | 516 | 'Tokens-string does not contain 6 tokens' |
| 517 | ) | ||
| 503 | token_ints = [] | 518 | token_ints = [] |
| 504 | try: | 519 | try: |
| 505 | token_ints = [RFC1760_TOKENS.index(token.upper()) for token in | 520 | token_ints = [ |
| 506 | tokens] | 521 | RFC1760_TOKENS.index(token.upper()) for token in tokens |
| 522 | ] | ||
| 507 | except ValueError: | 523 | except ValueError: |
| 508 | raise OTPGeneratorException( | 524 | raise OTPGeneratorException( |
| 509 | 'One or more words not present in RFC1760') from None | 525 | 'One or more words not present in RFC1760' |
| 526 | ) from None | ||
| 510 | # now we build a string of bits | 527 | # now we build a string of bits |
| 511 | bit_stream = format(token_ints[0], '011b') | 528 | bit_stream = format(token_ints[0], '011b') |
| 512 | bit_stream += format(token_ints[1], '011b') | 529 | bit_stream += format(token_ints[1], '011b') |
| @@ -519,14 +536,14 @@ class OTPGenerator: | |||
| 519 | # OTP servers MUST verify this checksum explicitly as part of the | 536 | # OTP servers MUST verify this checksum explicitly as part of the |
| 520 | # operation of decoding this representation of the one-time password. | 537 | # operation of decoding this representation of the one-time password. |
| 521 | if ( | 538 | if ( |
| 522 | '{0:0>8b}'.format(OTPGenerator.bit_pair_sum( | 539 | f'{OTPGenerator.bit_pair_sum(bit_stream[:64]):0>8b}'[-2:] |
| 523 | bit_stream[:64]))[-2:] != bit_stream[-2:] | 540 | != bit_stream[-2:] |
| 524 | ): | 541 | ): |
| 525 | raise OTPGeneratorException('Invalid bit checksum') | 542 | raise OTPGeneratorException('Invalid bit checksum') |
| 526 | return int(bit_stream[:64], 2).to_bytes(8, 'big') | 543 | return int(bit_stream[:64], 2).to_bytes(8, 'big') |
| 527 | 544 | ||
| 528 | @staticmethod | 545 | @staticmethod |
| 529 | def validate_hash_algo(hash_algo): | 546 | def validate_hash_algo(hash_algo) -> str: |
| 530 | """ | 547 | """ |
| 531 | Validates the provided hash-algorithm. | 548 | Validates the provided hash-algorithm. |
| 532 | 549 | ||
| @@ -541,19 +558,20 @@ class OTPGenerator: | |||
| 541 | if isinstance(hash_algo, int): | 558 | if isinstance(hash_algo, int): |
| 542 | if hash_algo not in _ALGO_DICT: | 559 | if hash_algo not in _ALGO_DICT: |
| 543 | raise OTPGeneratorException( | 560 | raise OTPGeneratorException( |
| 544 | 'hash_algo is not among the known algorithms') | 561 | 'hash_algo is not among the known algorithms' |
| 562 | ) | ||
| 545 | hash_algo = _ALGO_DICT.get(hash_algo) | 563 | hash_algo = _ALGO_DICT.get(hash_algo) |
| 546 | if not isinstance(hash_algo, str): | 564 | if not isinstance(hash_algo, str): |
| 547 | raise OTPGeneratorException( | 565 | raise OTPGeneratorException('hash_algo must be an int or a str') |
| 548 | 'hash_algo must be an int or a str') | ||
| 549 | if hash_algo not in hashlib.algorithms_available: | 566 | if hash_algo not in hashlib.algorithms_available: |
| 550 | raise OTPGeneratorException( | 567 | raise OTPGeneratorException( |
| 551 | f'{hash_algo} is not supported by this version of the ' | 568 | f'{hash_algo} is not supported by this version of the ' |
| 552 | 'hashlib module') | 569 | 'hashlib module' |
| 570 | ) | ||
| 553 | return hash_algo | 571 | return hash_algo |
| 554 | 572 | ||
| 555 | @staticmethod | 573 | @staticmethod |
| 556 | def validate_seed(seed): | 574 | def validate_seed(seed: str) -> str: |
| 557 | """ | 575 | """ |
| 558 | Validates the provided seed as defined by RFC-2289. | 576 | Validates the provided seed as defined by RFC-2289. |
| 559 | 577 | ||
| @@ -569,15 +587,17 @@ class OTPGenerator: | |||
| 569 | raise OTPGeneratorException('Seed must be a string') | 587 | raise OTPGeneratorException('Seed must be a string') |
| 570 | if not seed or len(seed) > 16: | 588 | if not seed or len(seed) > 16: |
| 571 | raise OTPGeneratorException( | 589 | raise OTPGeneratorException( |
| 572 | 'The seed MUST be of 1 to 16 characters in length') | 590 | 'The seed MUST be of 1 to 16 characters in length' |
| 591 | ) | ||
| 573 | for char in seed: | 592 | for char in seed: |
| 574 | if char not in string.ascii_letters + string.digits: | 593 | if char not in string.ascii_letters + string.digits: |
| 575 | raise OTPGeneratorException( | 594 | raise OTPGeneratorException( |
| 576 | 'The seed MUST consist of purely alphanumeric characters') | 595 | 'The seed MUST consist of purely alphanumeric characters' |
| 596 | ) | ||
| 577 | return seed | 597 | return seed |
| 578 | 598 | ||
| 579 | @staticmethod | 599 | @staticmethod |
| 580 | def validate_step(step): | 600 | def validate_step(step: int) -> int: |
| 581 | """ | 601 | """ |
| 582 | Validates the provided step as defined by RFC-2289. | 602 | Validates the provided step as defined by RFC-2289. |
| 583 | 603 | ||
| @@ -595,7 +615,7 @@ class OTPGenerator: | |||
| 595 | raise OTPGeneratorException('Step value MUST be >= 0') | 615 | raise OTPGeneratorException('Step value MUST be >= 0') |
| 596 | return step | 616 | return step |
| 597 | 617 | ||
| 598 | def generate_otp_hexdigest(self, step): | 618 | def generate_otp_hexdigest(self, step: int) -> str: |
| 599 | """ | 619 | """ |
| 600 | Generates the OTP hexdigest for the given step. | 620 | Generates the OTP hexdigest for the given step. |
| 601 | 621 | ||
| @@ -607,7 +627,7 @@ class OTPGenerator: | |||
| 607 | """ | 627 | """ |
| 608 | return '0x' + binascii.hexlify(self._generate_otp_bytes(step)).decode() | 628 | return '0x' + binascii.hexlify(self._generate_otp_bytes(step)).decode() |
| 609 | 629 | ||
| 610 | def generate_otp_hexdigest_from_challenge(self, challenge): | 630 | def generate_otp_hexdigest_from_challenge(self, challenge: str) -> str: |
| 611 | """ | 631 | """ |
| 612 | Same as generate_otp_hexdigest, but it generates hex. from a challenge. | 632 | Same as generate_otp_hexdigest, but it generates hex. from a challenge. |
| 613 | 633 | ||
| @@ -628,7 +648,7 @@ class OTPGenerator: | |||
| 628 | self._hash_algo = self.validate_hash_algo(hash_algo) | 648 | self._hash_algo = self.validate_hash_algo(hash_algo) |
| 629 | return self.generate_otp_hexdigest(step) | 649 | return self.generate_otp_hexdigest(step) |
| 630 | 650 | ||
| 631 | def generate_otp_words(self, step): | 651 | def generate_otp_words(self, step: int) -> str: |
| 632 | """ | 652 | """ |
| 633 | Generates the OTP six words token for the given step. | 653 | Generates the OTP six words token for the given step. |
| 634 | 654 | ||
| @@ -640,7 +660,7 @@ class OTPGenerator: | |||
| 640 | """ | 660 | """ |
| 641 | return self.bytes_to_tokens(self._generate_otp_bytes(step)) | 661 | return self.bytes_to_tokens(self._generate_otp_bytes(step)) |
| 642 | 662 | ||
| 643 | def generate_otp_words_from_challenge(self, challenge): | 663 | def generate_otp_words_from_challenge(self, challenge: str) -> str: |
| 644 | """ | 664 | """ |
| 645 | Same as generate_otp_words, but it generates words from a challenge. | 665 | Same as generate_otp_words, but it generates words from a challenge. |
| 646 | 666 | ||
| @@ -661,7 +681,7 @@ class OTPGenerator: | |||
| 661 | self._hash_algo = self.validate_hash_algo(hash_algo) | 681 | self._hash_algo = self.validate_hash_algo(hash_algo) |
| 662 | return self.generate_otp_words(step) | 682 | return self.generate_otp_words(step) |
| 663 | 683 | ||
| 664 | def hexdigest_range(self, start=499, stop=0): | 684 | def hexdigest_range(self, start: int = 499, stop: int = 0): |
| 665 | """ | 685 | """ |
| 666 | Returns an iterator that providing hexdigests corresponding to steps | 686 | Returns an iterator that providing hexdigests corresponding to steps |
| 667 | from `start` to and including `stop`. | 687 | from `start` to and including `stop`. |
| @@ -679,11 +699,12 @@ class OTPGenerator: | |||
| 679 | raise OTPGeneratorException('Step value MUST be an int') | 699 | raise OTPGeneratorException('Step value MUST be an int') |
| 680 | if start < stop: | 700 | if start < stop: |
| 681 | raise OTPGeneratorException( | 701 | raise OTPGeneratorException( |
| 682 | 'Start value can not be lower than stop') | 702 | 'Start value can not be lower than stop' |
| 703 | ) | ||
| 683 | for step in range(start, stop - 1, -1): | 704 | for step in range(start, stop - 1, -1): |
| 684 | yield self.generate_otp_hexdigest(step) | 705 | yield self.generate_otp_hexdigest(step) |
| 685 | 706 | ||
| 686 | def words_range(self, start=499, stop=0): | 707 | def words_range(self, start: int = 499, stop: int = 0): |
| 687 | """ | 708 | """ |
| 688 | Returns an iterator that providing the words corresponding to steps | 709 | Returns an iterator that providing the words corresponding to steps |
| 689 | from `start` to and including `stop`. | 710 | from `start` to and including `stop`. |
| @@ -701,11 +722,12 @@ class OTPGenerator: | |||
| 701 | raise OTPGeneratorException('Step value MUST be an int') | 722 | raise OTPGeneratorException('Step value MUST be an int') |
| 702 | if start < stop: | 723 | if start < stop: |
| 703 | raise OTPGeneratorException( | 724 | raise OTPGeneratorException( |
| 704 | 'Start value can not be lower than stop') | 725 | 'Start value can not be lower than stop' |
| 726 | ) | ||
| 705 | for step in range(start, stop - 1, -1): | 727 | for step in range(start, stop - 1, -1): |
| 706 | yield self.generate_otp_words(step) | 728 | yield self.generate_otp_words(step) |
| 707 | 729 | ||
| 708 | def _generate_otp_bytes(self, step): | 730 | def _generate_otp_bytes(self, step: int) -> bytes: |
| 709 | """ | 731 | """ |
| 710 | Generates the OTP bytes for the given step. | 732 | Generates the OTP bytes for the given step. |
| 711 | 733 | ||
| @@ -733,6 +755,6 @@ class OTPGenerator: | |||
| 733 | digest = self.sha1_digest_folding(large_digest) | 755 | digest = self.sha1_digest_folding(large_digest) |
| 734 | else: | 756 | else: |
| 735 | raise OTPGeneratorException( | 757 | raise OTPGeneratorException( |
| 736 | '{hash_algo} is not supported by this module'.format( | 758 | f'{self._hash_algo} is not supported by this module' |
| 737 | hash_algo=self._hash_algo)) | 759 | ) |
| 738 | return digest | 760 | return digest |
diff --git a/otp2289/server.py b/otp2289/server.py index d0c3e01..7f742cd 100644 --- a/otp2289/server.py +++ b/otp2289/server.py | |||
| @@ -1,7 +1,7 @@ | |||
| 1 | # -*- coding: utf-8 -*- | 1 | # -*- coding: utf-8 -*- |
| 2 | # SPDX-License-Identifier: BSD-2-Clause-FreeBSD | 2 | # SPDX-License-Identifier: BSD-2-Clause-FreeBSD |
| 3 | # | 3 | # |
| 4 | # Copyright (c) 2020, Simeon Simeonov | 4 | # Copyright (c) 2020-2022 Simeon Simeonov |
| 5 | # All rights reserved. | 5 | # All rights reserved. |
| 6 | # | 6 | # |
| 7 | # Redistribution and use in source and binary forms, with or without | 7 | # Redistribution and use in source and binary forms, with or without |
| @@ -27,9 +27,7 @@ | |||
| 27 | import binascii | 27 | import binascii |
| 28 | import hashlib | 28 | import hashlib |
| 29 | 29 | ||
| 30 | from .generator import (OTP_ALGO_MD5, | 30 | from .generator import OTP_ALGO_MD5, OTPGenerator, OTPGeneratorException |
| 31 | OTPGenerator, | ||
| 32 | OTPGeneratorException) | ||
| 33 | 31 | ||
| 34 | 32 | ||
| 35 | class OTPStateException(Exception): | 33 | class OTPStateException(Exception): |
| @@ -53,7 +51,13 @@ class OTPState: | |||
| 53 | - validate the corresponding generated response from the generator | 51 | - validate the corresponding generated response from the generator |
| 54 | """ | 52 | """ |
| 55 | 53 | ||
| 56 | def __init__(self, ot_hex, current_step, seed, hash_algo=OTP_ALGO_MD5): | 54 | def __init__( |
| 55 | self, | ||
| 56 | ot_hex: str, | ||
| 57 | current_step: int, | ||
| 58 | seed: str, | ||
| 59 | hash_algo=OTP_ALGO_MD5, | ||
| 60 | ): | ||
| 57 | """ | 61 | """ |
| 58 | Constructs an OTPState object with the given arguments. | 62 | Constructs an OTPState object with the given arguments. |
| 59 | 63 | ||
| @@ -87,45 +91,47 @@ class OTPState: | |||
| 87 | 91 | ||
| 88 | def __repr__(self): | 92 | def __repr__(self): |
| 89 | """repr implementation""" | 93 | """repr implementation""" |
| 90 | return (f'{self.__class__} at {id(self)} ' | 94 | return ( |
| 91 | f'(ot_hex={self._current_digest}, current_step={self._step}, ' | 95 | f'{self.__class__} at {id(self)} ' |
| 92 | f'seed={self._seed}, ' | 96 | f'(ot_hex={self._current_digest}, current_step={self._step}, ' |
| 93 | f'hash_algo={self._hash_algo})') | 97 | f'seed={self._seed}, ' |
| 98 | f'hash_algo={self._hash_algo})' | ||
| 99 | ) | ||
| 94 | 100 | ||
| 95 | @property | 101 | @property |
| 96 | def challenge_string(self): | 102 | def challenge_string(self) -> str: |
| 97 | """challenge_string-property""" | 103 | """challenge_string-property""" |
| 98 | # RFC-2289: "...the entire challenge string MUST be | 104 | # RFC-2289: "...the entire challenge string MUST be |
| 99 | # terminated with either a space or a new line." | 105 | # terminated with either a space or a new line." |
| 100 | return f'otp-{self._hash_algo} {self._step} {self._seed} ' | 106 | return f'otp-{self._hash_algo} {self._step} {self._seed} ' |
| 101 | 107 | ||
| 102 | @property | 108 | @property |
| 103 | def current_digest(self): | 109 | def current_digest(self) -> bytes: |
| 104 | """current_digest-property""" | 110 | """current_digest-property""" |
| 105 | return self._current_digest | 111 | return self._current_digest |
| 106 | 112 | ||
| 107 | @property | 113 | @property |
| 108 | def hash_algo(self): | 114 | def hash_algo(self) -> str: |
| 109 | """hash_algo-property""" | 115 | """hash_algo-property""" |
| 110 | return self._hash_algo | 116 | return self._hash_algo |
| 111 | 117 | ||
| 112 | @property | 118 | @property |
| 113 | def seed(self): | 119 | def seed(self) -> str: |
| 114 | """seed-property""" | 120 | """seed-property""" |
| 115 | return self._seed | 121 | return self._seed |
| 116 | 122 | ||
| 117 | @property | 123 | @property |
| 118 | def step(self): | 124 | def step(self) -> int: |
| 119 | """step-property""" | 125 | """step-property""" |
| 120 | return self._step | 126 | return self._step |
| 121 | 127 | ||
| 122 | @property | 128 | @property |
| 123 | def validated(self): | 129 | def validated(self) -> bool: |
| 124 | """validated-property""" | 130 | """validated-property""" |
| 125 | return bool(self._new_digest_hex) | 131 | return bool(self._new_digest_hex) |
| 126 | 132 | ||
| 127 | @classmethod | 133 | @classmethod |
| 128 | def from_dict(cls, dict_obj): | 134 | def from_dict(cls, dict_obj: dict): |
| 129 | """ | 135 | """ |
| 130 | Returns an OTPState object from the dict-object | 136 | Returns an OTPState object from the dict-object |
| 131 | 137 | ||
| @@ -138,7 +144,7 @@ class OTPState: | |||
| 138 | return cls(**dict_obj) | 144 | return cls(**dict_obj) |
| 139 | 145 | ||
| 140 | @staticmethod | 146 | @staticmethod |
| 141 | def response_to_bytes(response): | 147 | def response_to_bytes(response: str) -> bytes: |
| 142 | """ | 148 | """ |
| 143 | A wrapper that handles/validates the response as specified by RFC-2289. | 149 | A wrapper that handles/validates the response as specified by RFC-2289. |
| 144 | 150 | ||
| @@ -166,10 +172,11 @@ class OTPState: | |||
| 166 | return OTPState.validate_hex(response) | 172 | return OTPState.validate_hex(response) |
| 167 | except OTPStateException: | 173 | except OTPStateException: |
| 168 | raise OTPInvalidResponse( | 174 | raise OTPInvalidResponse( |
| 169 | 'The response is neither a valid token or hex') from None | 175 | 'The response is neither a valid token or hex' |
| 176 | ) from None | ||
| 170 | 177 | ||
| 171 | @staticmethod | 178 | @staticmethod |
| 172 | def validate_hex(ot_hex): | 179 | def validate_hex(ot_hex: str) -> bytes: |
| 173 | """ | 180 | """ |
| 174 | Validates the provided hexidigest. | 181 | Validates the provided hexidigest. |
| 175 | 182 | ||
| @@ -187,8 +194,10 @@ class OTPState: | |||
| 187 | ot_hex = ot_hex[2:] | 194 | ot_hex = ot_hex[2:] |
| 188 | ot_hex = ot_hex.strip().lower() | 195 | ot_hex = ot_hex.strip().lower() |
| 189 | if len(ot_hex) != 16: | 196 | if len(ot_hex) != 16: |
| 190 | raise OTPStateException('The length of the hex should be 16 ' | 197 | raise OTPStateException( |
| 191 | '(representing 64 bits digest)') | 198 | 'The length of the hex should be 16 ' |
| 199 | '(representing 64 bits digest)' | ||
| 200 | ) | ||
| 192 | try: | 201 | try: |
| 193 | return binascii.unhexlify(ot_hex) | 202 | return binascii.unhexlify(ot_hex) |
| 194 | except binascii.Error: | 203 | except binascii.Error: |
| @@ -206,12 +215,18 @@ class OTPState: | |||
| 206 | """ | 215 | """ |
| 207 | if self._new_digest_hex is None: | 216 | if self._new_digest_hex is None: |
| 208 | return None | 217 | return None |
| 209 | return OTPState(self._new_digest_hex, | 218 | return OTPState( |
| 210 | self._step - 1, | 219 | self._new_digest_hex, |
| 211 | self._seed, | 220 | self._step - 1, |
| 212 | self._hash_algo) | 221 | self._seed, |
| 222 | self._hash_algo, | ||
| 223 | ) | ||
| 213 | 224 | ||
| 214 | def response_validates(self, response, store_valid_response=True): | 225 | def response_validates( |
| 226 | self, | ||
| 227 | response: str, | ||
| 228 | store_valid_response: str = True, | ||
| 229 | ) -> bool: | ||
| 215 | """ | 230 | """ |
| 216 | Validates the incoming response as specified by RFC-2289. | 231 | Validates the incoming response as specified by RFC-2289. |
| 217 | 232 | ||
| @@ -233,32 +248,35 @@ class OTPState: | |||
| 233 | if self._hash_algo == 'md5': | 248 | if self._hash_algo == 'md5': |
| 234 | digest = hashlib.md5(response_bytes).digest() | 249 | digest = hashlib.md5(response_bytes).digest() |
| 235 | if ( | 250 | if ( |
| 236 | self._current_digest is None or | 251 | self._current_digest is None |
| 237 | OTPGenerator.strxor(digest[0:8], digest[8:]) == | 252 | or OTPGenerator.strxor(digest[0:8], digest[8:]) |
| 238 | self._current_digest | 253 | == self._current_digest |
| 239 | ): | 254 | ): |
| 240 | if store_valid_response: | 255 | if store_valid_response: |
| 241 | self._new_digest_hex = binascii.hexlify( | 256 | self._new_digest_hex = binascii.hexlify( |
| 242 | response_bytes).decode() | 257 | response_bytes |
| 258 | ).decode() | ||
| 243 | return True | 259 | return True |
| 244 | return False | 260 | return False |
| 245 | if self._hash_algo == 'sha1': | 261 | if self._hash_algo == 'sha1': |
| 246 | digest = hashlib.sha1(response_bytes).digest() | 262 | digest = hashlib.sha1(response_bytes).digest() |
| 247 | if ( | 263 | if ( |
| 248 | self._current_digest is None or | 264 | self._current_digest is None |
| 249 | OTPGenerator.sha1_digest_folding( | 265 | or OTPGenerator.sha1_digest_folding( |
| 250 | hashlib.sha1( | 266 | hashlib.sha1(response_bytes).digest() |
| 251 | response_bytes).digest()) == self._current_digest | 267 | ) |
| 268 | == self._current_digest | ||
| 252 | ): | 269 | ): |
| 253 | if store_valid_response: | 270 | if store_valid_response: |
| 254 | self._new_digest_hex = binascii.hexlify( | 271 | self._new_digest_hex = binascii.hexlify( |
| 255 | response_bytes).decode() | 272 | response_bytes |
| 273 | ).decode() | ||
| 256 | return True | 274 | return True |
| 257 | return False | 275 | return False |
| 258 | # this should not happen since the hash_algo is validated by the caller | 276 | # this should not happen since the hash_algo is validated by the caller |
| 259 | raise OTPInvalidResponse(f'Ivalid hash_algo: {self._hash_algo}') | 277 | raise OTPInvalidResponse(f'Ivalid hash_algo: {self._hash_algo}') |
| 260 | 278 | ||
| 261 | def to_dict(self): | 279 | def to_dict(self) -> dict: |
| 262 | """ | 280 | """ |
| 263 | Returns a dict representation of the object. | 281 | Returns a dict representation of the object. |
| 264 | 282 | ||
| @@ -270,10 +288,12 @@ class OTPState: | |||
| 270 | ot_hex = self._current_digest | 288 | ot_hex = self._current_digest |
| 271 | if ot_hex is not None: | 289 | if ot_hex is not None: |
| 272 | ot_hex = binascii.hexlify(self._current_digest).decode() | 290 | ot_hex = binascii.hexlify(self._current_digest).decode() |
| 273 | return {'ot_hex': ot_hex, | 291 | return { |
| 274 | 'current_step': self._step, | 292 | 'ot_hex': ot_hex, |
| 275 | 'seed': self._seed, | 293 | 'current_step': self._step, |
| 276 | 'hash_algo': self._hash_algo} | 294 | 'seed': self._seed, |
| 295 | 'hash_algo': self._hash_algo, | ||
| 296 | } | ||
| 277 | 297 | ||
| 278 | 298 | ||
| 279 | class OTPStore: | 299 | class OTPStore: |
| @@ -285,6 +305,7 @@ class OTPStore: | |||
| 285 | 305 | ||
| 286 | The class could serve as a base class when implementing store backends. | 306 | The class could serve as a base class when implementing store backends. |
| 287 | """ | 307 | """ |
| 308 | |||
| 288 | def __init__(self, data=None): | 309 | def __init__(self, data=None): |
| 289 | """ | 310 | """ |
| 290 | Constructs an OTPStore object from data | 311 | Constructs an OTPStore object from data |
| @@ -310,7 +331,7 @@ class OTPStore: | |||
| 310 | return len(self._data) | 331 | return len(self._data) |
| 311 | 332 | ||
| 312 | @property | 333 | @property |
| 313 | def data(self): | 334 | def data(self) -> dict: |
| 314 | """ | 335 | """ |
| 315 | data-property | 336 | data-property |
| 316 | 337 | ||
| @@ -320,7 +341,7 @@ class OTPStore: | |||
| 320 | return self._data | 341 | return self._data |
| 321 | 342 | ||
| 322 | @property | 343 | @property |
| 323 | def states(self): | 344 | def states(self) -> dict: |
| 324 | """ | 345 | """ |
| 325 | states-property | 346 | states-property |
| 326 | 347 | ||
| @@ -329,7 +350,7 @@ class OTPStore: | |||
| 329 | """ | 350 | """ |
| 330 | return self._states | 351 | return self._states |
| 331 | 352 | ||
| 332 | def add_state(self, key, state): | 353 | def add_state(self, key: str, state: OTPState): |
| 333 | """ | 354 | """ |
| 334 | Adds an OTPState object with a given key. | 355 | Adds an OTPState object with a given key. |
| 335 | 356 | ||
| @@ -356,7 +377,7 @@ class OTPStore: | |||
| 356 | """A wrapper for dict.items""" | 377 | """A wrapper for dict.items""" |
| 357 | return self._data.items() | 378 | return self._data.items() |
| 358 | 379 | ||
| 359 | def pop_state(self, key): | 380 | def pop_state(self, key: str) -> OTPState: |
| 360 | """ | 381 | """ |
| 361 | Removes specified key and returns the corresponding OTPState-object. | 382 | Removes specified key and returns the corresponding OTPState-object. |
| 362 | 383 | ||
| @@ -376,7 +397,12 @@ class OTPStore: | |||
| 376 | self._states.pop(state) | 397 | self._states.pop(state) |
| 377 | return state | 398 | return state |
| 378 | 399 | ||
| 379 | def response_validates(self, key, response, store_valid_response=True): | 400 | def response_validates( |
| 401 | self, | ||
| 402 | key: str, | ||
| 403 | response: str, | ||
| 404 | store_valid_response: bool = True, | ||
| 405 | ) -> bool: | ||
| 380 | """ | 406 | """ |
| 381 | A method that wraps around OTPState.response_validates and | 407 | A method that wraps around OTPState.response_validates and |
| 382 | OTPState.get_next_state. | 408 | OTPState.get_next_state. |
| @@ -411,7 +437,7 @@ class OTPStore: | |||
| 411 | self._states.pop(state) | 437 | self._states.pop(state) |
| 412 | return rvalue | 438 | return rvalue |
| 413 | 439 | ||
| 414 | def to_dict(self): | 440 | def to_dict(self) -> dict: |
| 415 | """ | 441 | """ |
| 416 | Returns a dict representation of the object. | 442 | Returns a dict representation of the object. |
| 417 | 443 | ||
| @@ -422,7 +448,7 @@ class OTPStore: | |||
| 422 | """ | 448 | """ |
| 423 | return {key: state.to_dict() for key, state in self._data.items()} | 449 | return {key: state.to_dict() for key, state in self._data.items()} |
| 424 | 450 | ||
| 425 | def _add_data(self, dict_obj): | 451 | def _add_data(self, dict_obj: dict) -> dict: |
| 426 | """ | 452 | """ |
| 427 | Adds data from a dict object (dict_obj). | 453 | Adds data from a dict object (dict_obj). |
| 428 | 454 | ||
