Source code for otp2289.__main__

# -*- coding: utf-8 -*-
# SPDX-License-Identifier: BSD-2-Clause-FreeBSD
#
# Copyright (c) 2020-2023 Simeon Simeonov
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright notice,
#    this list of conditions and the following disclaimer in the documentation
#    and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""
CLI entry point for the otp2289 package

Examples:
python -m otp2289 --initiate-new-sequence -s TesT

python -m otp2289 --generate-otp-response -c "otp-md5 499 TesT " -f token
python -m otp2289 --generate-otp-response -s TesT -i 499 -f token
"""
import argparse
import errno
import getpass
import io
import os
import secrets
import string
import sys

import otp2289


[docs]def eprint(*arg, **kwargs): """stdderr print wrapper""" print(*arg, file=sys.stderr, flush=True, **kwargs)
[docs]def generate_otp_response(args: argparse.Namespace) -> str: """ Generates a response based on the parameters sent from the parser :param args: The arguments assigned from argparse :type args: argparse.Namespace :raises otp2289.OTPChallengeException: If the challenge is invalid :raises otp2289.OTPGeneratorException: If generator parameters are wrong :return: The response string :rtype: str """ generator = otp2289.generator.OTPGenerator( args.password.encode(), args.seed, args.hash_algo, ) if args.challenge_string: if args.output_format == 'token': return generator.generate_otp_words_from_challenge( args.challenge_string ) return generator.generate_otp_hexdigest_from_challenge( args.challenge_string ) # regular parameters header = '' if not args.quiet: header = ( f'Seed: {args.seed}, Step: {args.step}, ' f'Hash: {args.hash_algo}{os.linesep}' ) if args.output_format == 'token': return header + generator.generate_otp_words(args.step) return header + generator.generate_otp_hexdigest(args.step)
[docs]def generate_otp_range(args: argparse.Namespace) -> str: """ Generates range of responses based on the parameters sent from the parser :param args: The arguments assigned from argparse :type args: argparse.Namespace :raises otp2289.OTPChallengeException: If the challenge is invalid :raises otp2289.OTPGeneratorException: If generator parameters are wrong :return: The responses string :rtype: str """ generator = otp2289.generator.OTPGenerator( args.password.encode(), args.seed, args.hash_algo, ) if args.output_format == 'token': method = generator.generate_otp_words else: method = generator.generate_otp_hexdigest # handle most cases explicitly if args.range == 1: return f'{args.step}: ' + method(args.step) if args.range > args.step + 1: args.range = args.step + 1 # any need for quiet? header = '' if not args.quiet: header = ( f'Seed: {args.seed}, Step: {args.step}, ' f'Hash: {args.hash_algo}, Range: {args.range}{os.linesep}' ) return header + os.linesep.join( [ f'{step}: ' + method(step) for step in range( args.step, args.step - args.range, -1, ) ] )
[docs]def get_password(args: argparse.Namespace) -> str: """ Extract the provided password using the defined argparse arguments :param args: The arguments assigned from argparse :type args: argparse.Namespace :raises KeyboardInterrupt: If the password prompt is interrupted :return: The extrated password string :rtype: str """ if args.force_password_prompt: while True: password = getpass.getpass() if not args.initiate_new_sequence or password == getpass.getpass( 'Repeat password: ' ): break eprint('The passwords do not match') return password if not args.password: password = os.environ.get('OTP2289_PASSWORD') if password is not None: return password while True: password = getpass.getpass() if not args.initiate_new_sequence or password == getpass.getpass( 'Repeat password: ' ): break eprint('The passwords do not match') return password if os.path.isfile(args.password): with io.open(args.password, 'r', encoding='utf-8') as fp: return fp.readline().strip() return args.password
[docs]def get_rnd_seed() -> str: """ Returns a random seed in the format: 2 random letters (capitalize()) + 5 random digits """ rnd = secrets.SystemRandom() return ''.join( rnd.choices(string.ascii_lowercase, k=2) ).capitalize() + ''.join(rnd.choices(string.digits, k=5))
[docs]def initiate_new_sequence(args: argparse.Namespace) -> str: """ Generates a new sequence based on the parameters sent from the parser. :param args: The arguments assigned from argparse :type args: argparse.Namespace :raises otp2289.OTPChallengeException: If the challenge is invalid :raises otp2289.OTPGeneratorException: If generator parameters are wrong :return: The response string :rtype: str """ if not args.seed: args.seed = get_rnd_seed() header = '' if not args.quiet: header = ( f'Seed: {args.seed}, Step: {args.step}, ' f'Hash: {args.hash_algo}{os.linesep}' ) generator = otp2289.generator.OTPGenerator( args.password.encode(), args.seed, args.hash_algo, ) if args.challenge_string: return header + generator.generate_otp_hexdigest_from_challenge( args.challenge_string ) return header + generator.generate_otp_hexdigest(args.step)
[docs]def main(args=None): """the main entry point""" parser = argparse.ArgumentParser( prog=__package__, epilog=( f'%(prog)s {otp2289.__version__} by Simeon Simeonov ' '(sgs @ LiberaChat)' ), description='The following options are available', ) group = parser.add_mutually_exclusive_group(required=True) password_group = parser.add_mutually_exclusive_group() group.add_argument( '--generate-otp-range', action='store_true', dest='generate_otp_range', default=False, help='Generates a range of OTP responses', ) group.add_argument( '--generate-otp-response', action='store_true', dest='generate_otp_response', default=False, help='Generates a new OTP response', ) group.add_argument( '--initiate-new-sequence', action='store_true', dest='initiate_new_sequence', default=False, help=( 'Initiates a new OTP sequence. Essentially the same as ' '--generate-otp-response only it prompts twice for password ' 'and always outputs hex (ignores -f).' ), ) password_group.add_argument( '-P', '--force-password-prompt', dest='force_password_prompt', action='store_true', help=( 'Force password prompt even if the env. variable ' '"OTP2289_PASSWORD" is set' ), ) password_group.add_argument( '-p', '--password', metavar='<PASSWORD[FILE]>', type=str, dest='password', default='', help=( 'The password or path to password file ' '(default & recommended: prompt for passwd)' ), ) parser.add_argument( '-a', '--hash-algorithm', metavar='<md5 | sha1>', type=str, dest='hash_algo', default='md5', help='The hash algorithm to use. Possible values: md5 (default), sha1', ) parser.add_argument( '-c', '--challenge-string', metavar='<challenge string>', type=str, dest='challenge_string', default='', help='Use challenge string when generating response', ) parser.add_argument( '-f', '--output-format', metavar='<hex | token>', type=str, dest='output_format', default='hex', help='The output format to use. Possible values: hex (default), token', ) parser.add_argument( '-i', '--step', metavar='<step>', type=int, dest='step', default=500, help='The step. Default for initiating a new sequence is: 500', ) parser.add_argument( '-q', '--quiet', action='store_true', dest='quiet', default=False, help='Dot not show headers. Only hex / tokens', ) parser.add_argument( '-r', '--range', metavar='<range>', type=int, dest='range', default=1, help='Amount of consecutive OTP hex/tokens to generate. default: 1', ) parser.add_argument( '-s', '--seed', metavar='[seed]', type=str, dest='seed', default='', help=( 'The seed to use (1 to 16 alphanumeric characters) ' '(default & recommended: random seed)' ), ) parser.add_argument( '-v', '--version', action='version', version=f'%(prog)s {otp2289.__version__}', help='display program-version and exit', ) args = parser.parse_args(args) # handle the password before everything else try: args.password = get_password(args) except KeyboardInterrupt: eprint(os.linesep + 'Prompt terminated') sys.exit(errno.EACCES) except Exception as exp: eprint(f'Unable to fetch password: {exp}') sys.exit(1) try: if args.initiate_new_sequence: print(initiate_new_sequence(args)) if args.generate_otp_range: print(generate_otp_range(args)) if args.generate_otp_response: print(generate_otp_response(args)) sys.exit(0) except otp2289.generator.OTPGeneratorException as exp: eprint(f'GeneratorException: {exp}') except otp2289.generator.OTPChallengeException as exp: eprint(f'ChallengeException: {exp}') except Exception as exp: eprint(f'Unknown error: {exp}') sys.exit(1)
if __name__ == '__main__': main()