summaryrefslogtreecommitdiff
path: root/otp2289/__main__.py
diff options
context:
space:
mode:
authorSimeon Simeonov2022-03-31 07:21:33 +0200
committerSimeon Simeonov2022-03-31 07:21:33 +0200
commit4ea3c077bd4111960af1b93195cb25bfb1d6edce (patch)
tree9ed0b8fdb4398cf79a8ecc3ab39ade2d374eb65c /otp2289/__main__.py
parentf19fde48c6b3f65c164c931286e72a0d254c1fb3 (diff)
Restructure the project in order to replace distutils with setuptoolsv1.1.0
Diffstat (limited to 'otp2289/__main__.py')
-rw-r--r--otp2289/__main__.py385
1 files changed, 0 insertions, 385 deletions
diff --git a/otp2289/__main__.py b/otp2289/__main__.py
deleted file mode 100644
index 78e60e8..0000000
--- a/otp2289/__main__.py
+++ /dev/null
@@ -1,385 +0,0 @@
1# -*- coding: utf-8 -*-
2# SPDX-License-Identifier: BSD-2-Clause-FreeBSD
3#
4# Copyright (c) 2020-2022 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 io
39import os
40import secrets
41import string
42import sys
43
44import otp2289
45
46
47def eprint(*arg, **kwargs):
48 """stdderr print wrapper"""
49 print(*arg, file=sys.stderr, flush=True, **kwargs)
50
51
52def generate_otp_response(args: argparse.Namespace) -> str:
53 """
54 Generates a response based on the parameters sent from the parser
55
56 :param args: The arguments assigned from argparse
57 :type args: argparse.Namespace
58
59 :raises otp2289.OTPChallengeException: If the challenge is invalid
60
61 :raises otp2289.OTPGeneratorException: If generator parameters are wrong
62
63 :return: The response string
64 :rtype: str
65 """
66 generator = otp2289.generator.OTPGenerator(
67 args.password.encode(),
68 args.seed,
69 args.hash_algo,
70 )
71 if args.challenge_string:
72 if args.output_format == 'token':
73 return generator.generate_otp_words_from_challenge(
74 args.challenge_string
75 )
76 return generator.generate_otp_hexdigest_from_challenge(
77 args.challenge_string
78 )
79 # regular parameters
80 header = ''
81 if not args.quiet:
82 header = (
83 f'Seed: {args.seed}, Step: {args.step}, '
84 f'Hash: {args.hash_algo}{os.linesep}'
85 )
86 if args.output_format == 'token':
87 return header + generator.generate_otp_words(args.step)
88 return header + generator.generate_otp_hexdigest(args.step)
89
90
91def generate_otp_range(args: argparse.Namespace) -> str:
92 """
93 Generates range of responses based on the parameters sent from the parser
94
95 :param args: The arguments assigned from argparse
96 :type args: argparse.Namespace
97
98 :raises otp2289.OTPChallengeException: If the challenge is invalid
99
100 :raises otp2289.OTPGeneratorException: If generator parameters are wrong
101
102 :return: The responses string
103 :rtype: str
104 """
105 generator = otp2289.generator.OTPGenerator(
106 args.password.encode(),
107 args.seed,
108 args.hash_algo,
109 )
110 if args.output_format == 'token':
111 method = generator.generate_otp_words
112 else:
113 method = generator.generate_otp_hexdigest
114 # handle most cases explicitly
115 if args.range == 1:
116 return f'{args.step}: ' + method(args.step)
117 if args.range > args.step + 1:
118 args.range = args.step + 1
119 # any need for quiet?
120 header = ''
121 if not args.quiet:
122 header = (
123 f'Seed: {args.seed}, Step: {args.step}, '
124 f'Hash: {args.hash_algo}, Range: {args.range}{os.linesep}'
125 )
126 return header + os.linesep.join(
127 [
128 f'{step}: ' + method(step)
129 for step in range(
130 args.step,
131 args.step - args.range,
132 -1,
133 )
134 ]
135 )
136
137
138def get_password(args: argparse.Namespace) -> str:
139 """
140 Extract the provided password using the defined argparse arguments
141
142 :param args: The arguments assigned from argparse
143 :type args: argparse.Namespace
144
145 :raises KeyboardInterrupt: If the password prompt is interrupted
146
147 :return: The extrated password string
148 :rtype: str
149 """
150 if args.force_password_prompt:
151 while True:
152 password = getpass.getpass()
153 if not args.initiate_new_sequence or password == getpass.getpass(
154 'Repeat password: '
155 ):
156 break
157 eprint('The passwords do not match')
158 return password
159
160 if not args.password:
161 password = os.environ.get('OTP2289_PASSWORD')
162 if password is not None:
163 return password
164 while True:
165 password = getpass.getpass()
166 if not args.initiate_new_sequence or password == getpass.getpass(
167 'Repeat password: '
168 ):
169 break
170 eprint('The passwords do not match')
171 return password
172
173 if os.path.isfile(args.password):
174 with io.open(args.password, 'r', encoding='utf-8') as fp:
175 return fp.readline().strip()
176
177 return args.password
178
179
180def get_rnd_seed() -> str:
181 """
182 Returns a random seed in the format:
183
184 2 random letters (capitalize()) + 5 random digits
185 """
186 rnd = secrets.SystemRandom()
187 return ''.join(
188 rnd.choices(string.ascii_lowercase, k=2)
189 ).capitalize() + ''.join(rnd.choices(string.digits, k=5))
190
191
192def initiate_new_sequence(args: argparse.Namespace) -> str:
193 """
194 Generates a new sequence based on the parameters sent from the parser.
195
196 :param args: The arguments assigned from argparse
197 :type args: argparse.Namespace
198
199 :raises otp2289.OTPChallengeException: If the challenge is invalid
200
201 :raises otp2289.OTPGeneratorException: If generator parameters are wrong
202
203 :return: The response string
204 :rtype: str
205 """
206 if not args.seed:
207 args.seed = get_rnd_seed()
208 header = ''
209 if not args.quiet:
210 header = (
211 f'Seed: {args.seed}, Step: {args.step}, '
212 f'Hash: {args.hash_algo}{os.linesep}'
213 )
214 generator = otp2289.generator.OTPGenerator(
215 args.password.encode(),
216 args.seed,
217 args.hash_algo,
218 )
219 if args.challenge_string:
220 return header + generator.generate_otp_hexdigest_from_challenge(
221 args.challenge_string
222 )
223 return header + generator.generate_otp_hexdigest(args.step)
224
225
226def main(args=None):
227 """the main entry point"""
228 parser = argparse.ArgumentParser(
229 prog=__package__,
230 epilog=(
231 f'%(prog)s {otp2289.__version__} by Simeon Simeonov '
232 '(sgs @ LiberaChat)'
233 ),
234 description='The following options are available',
235 )
236 group = parser.add_mutually_exclusive_group(required=True)
237 password_group = parser.add_mutually_exclusive_group()
238 group.add_argument(
239 '--generate-otp-range',
240 action='store_true',
241 dest='generate_otp_range',
242 default=False,
243 help='Generates a range of OTP responses',
244 )
245 group.add_argument(
246 '--generate-otp-response',
247 action='store_true',
248 dest='generate_otp_response',
249 default=False,
250 help='Generates a new OTP response',
251 )
252 group.add_argument(
253 '--initiate-new-sequence',
254 action='store_true',
255 dest='initiate_new_sequence',
256 default=False,
257 help=(
258 'Initiates a new OTP sequence. Essentially the same as '
259 '--generate-otp-response only it prompts twice for password '
260 'and always outputs hex (ignores -f).'
261 ),
262 )
263 password_group.add_argument(
264 '-P',
265 '--force-password-prompt',
266 dest='force_password_prompt',
267 action='store_true',
268 help=(
269 'Force password prompt even if the env. variable '
270 '"OTP2289_PASSWORD" is set'
271 ),
272 )
273 password_group.add_argument(
274 '-p',
275 '--password',
276 metavar='<PASSWORD[FILE]>',
277 type=str,
278 dest='password',
279 default='',
280 help=(
281 'The password or path to password file '
282 '(default & recommended: prompt for passwd)'
283 ),
284 )
285 parser.add_argument(
286 '-a',
287 '--hash-algorithm',
288 metavar='<md5 | sha1>',
289 type=str,
290 dest='hash_algo',
291 default='md5',
292 help='The hash algorithm to use. Possible values: md5 (default), sha1',
293 )
294 parser.add_argument(
295 '-c',
296 '--challenge-string',
297 metavar='<challenge string>',
298 type=str,
299 dest='challenge_string',
300 default='',
301 help='Use challenge string when generating response',
302 )
303 parser.add_argument(
304 '-f',
305 '--output-format',
306 metavar='<hex | token>',
307 type=str,
308 dest='output_format',
309 default='hex',
310 help='The output format to use. Possible values: hex (default), token',
311 )
312 parser.add_argument(
313 '-i',
314 '--step',
315 metavar='<step>',
316 type=int,
317 dest='step',
318 default=500,
319 help='The step. Default for initiating a new sequence is: 500',
320 )
321 parser.add_argument(
322 '-q',
323 '--quiet',
324 action='store_true',
325 dest='quiet',
326 default=False,
327 help='Dot not show headers. Only hex / tokens',
328 )
329 parser.add_argument(
330 '-r',
331 '--range',
332 metavar='<range>',
333 type=int,
334 dest='range',
335 default=1,
336 help='Amount of consecutive OTP hex/tokens to generate. default: 1',
337 )
338 parser.add_argument(
339 '-s',
340 '--seed',
341 metavar='[seed]',
342 type=str,
343 dest='seed',
344 default='',
345 help=(
346 'The seed to use (1 to 16 alphanumeric characters) '
347 '(default & recommended: random seed)'
348 ),
349 )
350 parser.add_argument(
351 '-v',
352 '--version',
353 action='version',
354 version=f'%(prog)s {otp2289.__version__}',
355 help='display program-version and exit',
356 )
357 args = parser.parse_args(args)
358 # handle the password before everything else
359 try:
360 args.password = get_password(args)
361 except KeyboardInterrupt:
362 eprint(os.linesep + 'Prompt terminated')
363 sys.exit(errno.EACCES)
364 except Exception as exp:
365 eprint(f'Unable to fetch password: {exp}')
366 sys.exit(1)
367 try:
368 if args.initiate_new_sequence:
369 print(initiate_new_sequence(args))
370 if args.generate_otp_range:
371 print(generate_otp_range(args))
372 if args.generate_otp_response:
373 print(generate_otp_response(args))
374 sys.exit(0)
375 except otp2289.generator.OTPGeneratorException as exp:
376 eprint(f'GeneratorException: {exp}')
377 except otp2289.generator.OTPChallengeException as exp:
378 eprint(f'ChallengeException: {exp}')
379 except Exception as exp:
380 eprint(f'Unknown error: {exp}')
381 sys.exit(1)
382
383
384if __name__ == '__main__':
385 main()