1# -*- coding: utf-8 -*- 2# SPDX-License-Identifier: BSD-2-Clause-FreeBSD 3# 4# Copyright (c) 2020, Simeon Simeonov 5# All rights reserved. 6# 7# Redistribution and use in source and binary forms, with or without 8# modification, are permitted provided that the following conditions 9# are met: 10# 1. Redistributions of source code must retain the above copyright 11# notice, this list of conditions and the following disclaimer. 12# 2. Redistributions in binary form must reproduce the above copyright notice, 13# this list of conditions and the following disclaimer in the documentation 14# and/or other materials provided with the distribution. 15# 16# THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR 17# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 18# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 19# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, 20# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 21# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 22# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 23# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 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. 26""" 27CLI entry point for the otp2289 package 28 29Examples: 30python -m otp2289 --initiate-new-sequence -s TesT 31 32python -m otp2289 --generate-otp-response -c "otp-md5 499 TesT " -f token 33python -m otp2289 --generate-otp-response -s TesT -i 499 -f token 34""" 35import argparse 36import errno 37import getpass 38import os 39import secrets 40import string 41import sys 42 43import otp2289 44 45 46def eprint(*arg, **kwargs): 47 """stdderr print wrapper""" 48 print(*arg, file=sys.stderr, flush=True, **kwargs) 49 50 51def generate_otp_response(args): 52 """ 53 Generates a response based on the parameters sent from the parser 54 55 :param args: The arguments assigned from argparse 56 :type args: argparse.Namespace 57 58 :raises otp2289.OTPChallengeException: If the challenge is invalid 59 60 :raises otp2289.OTPGeneratorException: If generator parameters are wrong 61 62 :return: The response string 63 :rtype: str 64 """ 65 generator = otp2289.generator.OTPGenerator( 66 args.password.encode(), 67 args.seed, 68 args.hash_algo) 69 if args.challenge_string: 70 if args.output_format == 'token': 71 return generator.generate_otp_words_from_challenge( 72 args.challenge_string) 73 return generator.generate_otp_hexdigest_from_challenge( 74 args.challenge_string) 75 # regular parameters 76 header = '' 77 if not args.quiet: 78 header = (f'Seed: {args.seed}, Step: {args.step}, ' 79 f'Hash: {args.hash_algo}{os.linesep}') 80 if args.output_format == 'token': 81 return header + generator.generate_otp_words(args.step) 82 return header + generator.generate_otp_hexdigest(args.step) 83 84 85def generate_otp_range(args): 86 """ 87 Generates range of responses based on the parameters sent from the parser 88 89 :param args: The arguments assigned from argparse 90 :type args: argparse.Namespace 91 92 :raises otp2289.OTPChallengeException: If the challenge is invalid 93 94 :raises otp2289.OTPGeneratorException: If generator parameters are wrong 95 96 :return: The responses string 97 :rtype: str 98 """ 99 generator = otp2289.generator.OTPGenerator( 100 args.password.encode(), 101 args.seed, 102 args.hash_algo) 103 if args.output_format == 'token': 104 method = generator.generate_otp_words 105 else: 106 method = generator.generate_otp_hexdigest 107 # handle most cases explicitly 108 if args.range == 1: 109 return f'{args.step}: ' + method(args.step) 110 if args.range > args.step + 1: 111 args.range = args.step + 1 112 # any need for quiet? 113 header = '' 114 if not args.quiet: 115 header = (f'Seed: {args.seed}, Step: {args.step}, ' 116 f'Hash: {args.hash_algo}, Range: {args.range}{os.linesep}') 117 return header + os.linesep.join( 118 [f'{step}: ' + method(step) for step in range( 119 args.step, args.step - args.range, -1)]) 120 121 122def get_rnd_seed(): 123 """ 124 Returns a random seed in the format: 125 126 2 random letters (capitalize()) + 5 random digits 127 """ 128 rnd = secrets.SystemRandom() 129 return (''.join(rnd.choices(string.ascii_lowercase, k=2)).capitalize() + 130 ''.join(rnd.choices(string.digits, k=5))) 131 132 133def initiate_new_sequence(args): 134 """ 135 Generates a new sequence based on the parameters sent from the parser. 136 137 :param args: The arguments assigned from argparse 138 :type args: argparse.Namespace 139 140 :raises otp2289.OTPChallengeException: If the challenge is invalid 141 142 :raises otp2289.OTPGeneratorException: If generator parameters are wrong 143 144 :return: The response string 145 :rtype: str 146 """ 147 if not args.seed: 148 args.seed = get_rnd_seed() 149 header = '' 150 if not args.quiet: 151 header = (f'Seed: {args.seed}, Step: {args.step}, ' 152 f'Hash: {args.hash_algo}{os.linesep}') 153 generator = otp2289.generator.OTPGenerator( 154 args.password.encode(), 155 args.seed, 156 args.hash_algo) 157 if args.challenge_string: 158 return header + generator.generate_otp_hexdigest_from_challenge( 159 args.challenge_string) 160 return header + generator.generate_otp_hexdigest(args.step) 161 162 163def main(args=None): 164 """the main entry point""" 165 parser = argparse.ArgumentParser( 166 prog=__package__, 167 epilog=(f'%(prog)s {otp2289.__version__} by Simeon Simeonov ' 168 '(sgs @ Freenode)'), 169 description='The following options are available') 170 group = parser.add_mutually_exclusive_group(required=True) 171 group.add_argument( 172 '--generate-otp-range', 173 action='store_true', 174 dest='generate_otp_range', 175 default=False, 176 help='Generates a range of OTP responses') 177 group.add_argument( 178 '--generate-otp-response', 179 action='store_true', 180 dest='generate_otp_response', 181 default=False, 182 help='Generates a new OTP response') 183 group.add_argument( 184 '--initiate-new-sequence', 185 action='store_true', 186 dest='initiate_new_sequence', 187 default=False, 188 help=('Initiates a new OTP sequence. Essentially the same as ' 189 '--generate-otp-response only it prompts twice for password ' 190 'and always outputs hex (ignores -f).')) 191 parser.add_argument( 192 '-a', '--hash-algorithm', 193 metavar='<md5 | sha1>', 194 type=str, 195 dest='hash_algo', 196 default='md5', 197 help='The hash algorithm to use. Possible values: md5 (default), sha1') 198 parser.add_argument( 199 '-c', '--challenge-string', 200 metavar='<challenge string>', 201 type=str, 202 dest='challenge_string', 203 default='', 204 help='Use challenge string when generating response') 205 parser.add_argument( 206 '-f', '--output-format', 207 metavar='<hex | token>', 208 type=str, 209 dest='output_format', 210 default='hex', 211 help='The output format to use. Possible values: hex (default), token') 212 parser.add_argument( 213 '-i', '--step', 214 metavar='<step>', 215 type=int, 216 dest='step', 217 default=500, 218 help='The step. Default for initiating a new sequence is: 500') 219 parser.add_argument( 220 '-p', '--password', 221 metavar='<PASSWORD[FILE]>', 222 type=str, 223 dest='password', 224 default='', 225 help=('The password or path to password file ' 226 '(default & recommended: prompt for passwd)')) 227 parser.add_argument( 228 '-q', '--quiet', 229 action='store_true', 230 dest='quiet', 231 default=False, 232 help='Dot not show headers. Only hex / tokens') 233 parser.add_argument( 234 '-r', '--range', 235 metavar='<range>', 236 type=int, 237 dest='range', 238 default=1, 239 help='Amount of consecutive OTP hex/tokens to generate. default: 1') 240 parser.add_argument( 241 '-s', '--seed', 242 metavar='[seed]', 243 type=str, 244 dest='seed', 245 default='', 246 help=('The seed to use (1 to 16 alphanumeric characters) ' 247 '(default & recommended: random seed)')) 248 parser.add_argument( 249 '-v', '--version', 250 action='version', 251 version=f'%(prog)s {otp2289.__version__}', 252 help='display program-version and exit') 253 args = parser.parse_args(args) 254 # handle the password before everything else 255 if not args.password: 256 try: 257 while True: 258 args.password = getpass.getpass() 259 if ( 260 not args.initiate_new_sequence or 261 args.password == getpass.getpass('Repeat password: ') 262 ): 263 break 264 eprint('The passwords do not match') 265 except KeyboardInterrupt: 266 eprint(os.linesep + 'Prompt terminated') 267 sys.exit(errno.EACCES) 268 elif os.path.isfile(args.password): 269 try: 270 with open(args.password, 'r') as fp: 271 args.password = fp.readline().strip() 272 except Exception as exp: 273 eprint(f'Unable to open password file: {exp}') 274 sys.exit(1) 275 try: 276 if args.initiate_new_sequence: 277 print(initiate_new_sequence(args)) 278 if args.generate_otp_range: 279 print(generate_otp_range(args)) 280 if args.generate_otp_response: 281 print(generate_otp_response(args)) 282 sys.exit(0) 283 except otp2289.generator.OTPGeneratorException as exp: 284 eprint(f'GeneratorException: {exp}') 285 except otp2289.generator.OTPChallengeException as exp: 286 eprint(f'ChallengeException: {exp}') 287 except Exception as exp: 288 eprint(f'Unknown error: {exp}') 289 sys.exit(1) 290 291 292if __name__ == '__main__': 293 main() 294