Skip to content

Commit 8dbd28c

Browse files
committed
Add arguments for time-step, digits, and digest
1 parent 3b10190 commit 8dbd28c

File tree

3 files changed

+129
-40
lines changed

3 files changed

+129
-40
lines changed

README.md

+72-11
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ Contents
3030
* [With QR Code](#with-qr-code)
3131
* [With Encrypted QR Code](#with-encrypted-qr-code)
3232
* [Multiple Keys](#multiple-keys)
33+
* [Command Line Arguments](#command-line-arguments)
3334
* [Tradeoff](#tradeoff)
3435
* [Alternative: OATH Toolkit](#alternative-oath-toolkit)
3536
* [Resources](#resources)
@@ -96,23 +97,23 @@ import sys
9697
import time
9798

9899

99-
def hotp(secret, counter, digits=6, digest='sha1'):
100-
padding = '=' * ((8 - len(secret)) % 8)
101-
secret_bytes = base64.b32decode(secret.upper() + padding)
102-
counter_bytes = struct.pack(">Q", counter)
103-
mac = hmac.new(secret_bytes, counter_bytes, digest).digest()
100+
def hotp(key, counter, digits=6, digest='sha1'):
101+
key = base64.b32decode(key.upper() + '=' * ((8 - len(key)) % 8))
102+
counter = struct.pack('>Q', counter)
103+
mac = hmac.new(key, counter, digest).digest()
104104
offset = mac[-1] & 0x0f
105-
truncated = struct.unpack('>L', mac[offset:offset+4])[0] & 0x7fffffff
106-
return str(truncated)[-digits:].rjust(digits, '0')
105+
binary = struct.unpack('>L', mac[offset:offset+4])[0] & 0x7fffffff
106+
return str(binary)[-digits:].rjust(digits, '0')
107107

108108

109-
def totp(secret, interval=30):
110-
return hotp(secret, int(time.time() / interval))
109+
def totp(key, time_step=30, digits=6, digest='sha1'):
110+
return hotp(key, int(time.time() / time_step), digits, digest)
111111

112112

113113
def main():
114-
for secret in sys.stdin:
115-
print(totp(secret.strip()))
114+
args = [int(x) if x.isdigit() else x for x in sys.argv[1:]]
115+
for key in sys.stdin:
116+
print(totp(key.strip(), *args))
116117

117118

118119
if __name__ == '__main__':
@@ -474,6 +475,66 @@ key must occur in its own line.
474475
zbarimg -q *.png | sed 's/.*secret=\([^&]*\).*/\1/' | mintotp
475476
```
476477
478+
### Command Line Arguments
479+
480+
In order to keep this tool as minimal as possible, it does not come with
481+
any command line options. In fact, it does not even have the `--help`
482+
option. It does support a few command line arguments though. Since there
483+
is no help output from the tool, this section describes the command line
484+
arguments for this tool.
485+
486+
Here is a synopsis of the command line arguments supported by this tool:
487+
488+
```
489+
mintotp [TIME_STEP [DIGITS [DIGEST]]]
490+
```
491+
492+
Here is a description of each argument:
493+
494+
- `TIME_STEP`
495+
496+
TOTP time-step duration (in seconds) during which a TOTP value is
497+
valid. A new TOTP value is generated after time-step duration
498+
elapses. (Default: `30`)
499+
500+
- `DIGITS`
501+
502+
Number of digits in TOTP value. (Default: `6`)
503+
504+
- `DIGEST`
505+
506+
Cryptographic hash algorithm to use while generating TOTP value.
507+
(Default: `sha1)
508+
509+
Possible values are `sha1`, `sha224`, `sha256`, `sha384`, and
510+
`sha512`.
511+
512+
Here are some usage examples of these command line arguments:
513+
514+
1. Generate TOTP value with a time-step size of 60 seconds:
515+
516+
```shell
517+
mintotp 60 <<< ZYTYYE5FOAGW5ML7LRWUL4WTZLNJAMZS
518+
```
519+
520+
2. Generate 8-digit TOTP value:
521+
522+
```shell
523+
mintotp 60 8 <<< ZYTYYE5FOAGW5ML7LRWUL4WTZLNJAMZS
524+
```
525+
526+
3. Use SHA-256 hash algorithm to generate TOTP value:
527+
528+
```shell
529+
mintotp 60 6 sha256 <<< ZYTYYE5FOAGW5ML7LRWUL4WTZLNJAMZS
530+
```
531+
532+
The behaviour of the tool is undefined if it is used in any way other
533+
than what is described above. For example, although surplus command line
534+
arguments are ignored currently, this behaviour may change in future, so
535+
what should happen in case of surplus arguments is left undefined in
536+
this document.
537+
477538
478539
Tradeoff
479540
--------

mintotp.py

+7-7
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,22 @@
88

99

1010
def hotp(key, counter, digits=6, digest='sha1'):
11-
padding = '=' * ((8 - len(key)) % 8)
12-
key_bytes = base64.b32decode(key.upper() + padding)
13-
counter_bytes = struct.pack(">Q", counter)
14-
mac = hmac.new(key_bytes, counter_bytes, digest).digest()
11+
key = base64.b32decode(key.upper() + '=' * ((8 - len(key)) % 8))
12+
counter = struct.pack('>Q', counter)
13+
mac = hmac.new(key, counter, digest).digest()
1514
offset = mac[-1] & 0x0f
1615
binary = struct.unpack('>L', mac[offset:offset+4])[0] & 0x7fffffff
1716
return str(binary)[-digits:].rjust(digits, '0')
1817

1918

20-
def totp(key, time_step=30):
21-
return hotp(key, int(time.time() / time_step))
19+
def totp(key, time_step=30, digits=6, digest='sha1'):
20+
return hotp(key, int(time.time() / time_step), digits, digest)
2221

2322

2423
def main():
24+
args = [int(x) if x.isdigit() else x for x in sys.argv[1:]]
2525
for key in sys.stdin:
26-
print(totp(key.strip()))
26+
print(totp(key.strip(), *args))
2727

2828

2929
if __name__ == '__main__':

test.py

+50-22
Original file line numberDiff line numberDiff line change
@@ -29,25 +29,53 @@ def test_totp(self):
2929
self.assertEqual(mintotp.totp(SECRET1), '626854')
3030
self.assertEqual(mintotp.totp(SECRET2), '093610')
3131

32-
def test_main(self):
33-
with mock.patch('sys.stdin', [SECRET1]):
34-
with mock.patch('time.time', return_value=0):
35-
with mock.patch('builtins.print') as mock_print:
36-
mintotp.main()
37-
mock_print.assert_called_once_with('549419')
38-
with mock.patch('sys.stdin', [SECRET1, SECRET2]):
39-
with mock.patch('time.time', return_value=0):
40-
with mock.patch('builtins.print') as mock_print:
41-
mintotp.main()
42-
self.assertEqual(mock_print.mock_calls,
43-
[mock.call('549419'),
44-
mock.call('009551')])
45-
46-
def test_name(self):
47-
with mock.patch('sys.stdin', [SECRET1]):
48-
with mock.patch('time.time', return_value=0):
49-
with mock.patch('builtins.print') as mock_print:
50-
runpy.run_module('mintotp', run_name='mintotp')
51-
mock_print.assert_not_called()
52-
runpy.run_module('mintotp', run_name='__main__')
53-
mock_print.assert_called_once_with('549419')
32+
@mock.patch('time.time', mock.Mock(return_value=0))
33+
@mock.patch('sys.argv', ['prog'])
34+
@mock.patch('sys.stdin', [SECRET1])
35+
@mock.patch('builtins.print')
36+
def test_main_one_secret(self, mock_print):
37+
mintotp.main()
38+
mock_print.assert_called_once_with('549419')
39+
40+
@mock.patch('time.time', mock.Mock(return_value=0))
41+
@mock.patch('sys.argv', ['prog'])
42+
@mock.patch('sys.stdin', [SECRET1, SECRET2])
43+
@mock.patch('builtins.print')
44+
def test_main_two_secrets(self, mock_print):
45+
mintotp.main()
46+
self.assertEqual(mock_print.mock_calls, [mock.call('549419'),
47+
mock.call('009551')])
48+
49+
@mock.patch('time.time', mock.Mock(return_value=2520))
50+
@mock.patch('sys.argv', ['prog', '60'])
51+
@mock.patch('sys.stdin', [SECRET1])
52+
@mock.patch('builtins.print')
53+
def test_main_step(self, mock_print):
54+
mintotp.main()
55+
mock_print.assert_called_once_with('626854')
56+
57+
@mock.patch('time.time', mock.Mock(return_value=0))
58+
@mock.patch('sys.argv', ['prog', '30', '8'])
59+
@mock.patch('sys.stdin', [SECRET1])
60+
@mock.patch('builtins.print')
61+
def test_main_digits(self, mock_print):
62+
mintotp.main()
63+
mock_print.assert_called_once_with('49549419')
64+
65+
@mock.patch('time.time', mock.Mock(return_value=0))
66+
@mock.patch('sys.argv', ['prog', '30', '6', 'sha256'])
67+
@mock.patch('sys.stdin', [SECRET1])
68+
@mock.patch('builtins.print')
69+
def test_main_digest(self, mock_print):
70+
mintotp.main()
71+
mock_print.assert_called_once_with('473535')
72+
73+
@mock.patch('time.time', mock.Mock(return_value=0))
74+
@mock.patch('sys.argv', ['prog'])
75+
@mock.patch('sys.stdin', [SECRET1])
76+
@mock.patch('builtins.print')
77+
def test_module(self, mock_print):
78+
runpy.run_module('mintotp', run_name='mintotp')
79+
mock_print.assert_not_called()
80+
runpy.run_module('mintotp', run_name='__main__')
81+
mock_print.assert_called_once_with('549419')

0 commit comments

Comments
 (0)