Skip to content

Commit 20fd472

Browse files
committed
Add toy TOTP generator
0 parents  commit 20fd472

File tree

7 files changed

+302
-0
lines changed

7 files changed

+302
-0
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
*.sw?
2+
.DS_Store

LICENSE.md

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
The MIT License (MIT)
2+
=====================
3+
4+
Copyright (c) 2019 Susam Pal
5+
6+
Permission is hereby granted, free of charge, to any person obtaining
7+
a copy of this software and associated documentation files (the
8+
"Software"), to deal in the Software without restriction, including
9+
without limitation the rights to use, copy, modify, merge, publish,
10+
distribute, sublicense, and/or sell copies of the Software, and to
11+
permit persons to whom the Software is furnished to do so, subject to
12+
the following conditions:
13+
14+
The above copyright notice and this permission notice shall be
15+
included in all copies or substantial portions of the Software.
16+
17+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
19+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
20+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
21+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
22+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
23+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Makefile

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
URI1 = otpauth://totp/alice:bob?secret=ZYTYYE5FOAGW5ML7LRWUL4WTZLNJAMZS
2+
URI2 = otpauth://totp/alice:cam?secret=PW4YAYYZVDE5RK2AOLKUATNZIKAFQLZO&issuer=alice
3+
4+
gen:
5+
python3 totp.py $$(zbarimg -q *.png | sed 's/.*secret=\([^&]*\).*/\1/')
6+
7+
qr:
8+
qrencode -s 10 -o secret1.png "$(URI1)"
9+
qrencode -s 10 -o secret2.png "$(URI2)"
10+
11+
key:
12+
python3 -c 'import base64, os; print(base64.b32encode(os.urandom(20)).decode())'

README.md

+239
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
Toy TOTP Generator
2+
==================
3+
4+
This is a tiny toy TOTP generator.
5+
6+
[![View Source][Source SVG]][Source File]
7+
[![MIT License][License SVG]][L]
8+
9+
[Source SVG]: https://img.shields.io/badge/view-source-brightgreen.svg
10+
[Source File]: totp.py
11+
[License SVG]: https://img.shields.io/badge/license-MIT-blue.svg
12+
[L]: LICENSE.md
13+
14+
15+
Contents
16+
--------
17+
18+
* [Introduction](#introduction)
19+
* [Get Started](#get-started)
20+
* [With Base32 Key](#with-base32-key)
21+
* [With QR Code](#with-qr-code)
22+
* [Usage](#usage)
23+
* [Caution](#caution)
24+
* [License](#license)
25+
26+
27+
Introduction
28+
------------
29+
30+
The source code in [totp.py](totp.py) contains toy code to show how TOTP
31+
values are generated from a secret key and current time. It's just 26
32+
lines of code (actually 19 lines if we ignore the blank lines). There
33+
are no comments in the code, so a brief description of the code is
34+
presented in this section. Here is the entire code presented once again
35+
for convenience:
36+
37+
```python
38+
#!/usr/bin/python3
39+
40+
import base64
41+
import hmac
42+
import struct
43+
import sys
44+
import time
45+
46+
47+
def hotp(secret, counter, digits=6, algo='sha1'):
48+
padding = '=' * ((8 - len(secret)) % 8)
49+
secret_bytes = base64.b32decode(secret.upper() + padding)
50+
counter_bytes = struct.pack(">Q", counter)
51+
mac = hmac.digest(secret_bytes, counter_bytes, algo)
52+
offset = mac[-1] & 0x0f
53+
truncated = struct.unpack('>L', mac[offset:offset+4])[0] & 0x7fffffff
54+
return str(truncated)[-digits:].rjust(digits, '0')
55+
56+
57+
def totp(secret, interval=30):
58+
return hotp(secret, int(time.time() / interval))
59+
60+
61+
if __name__ == '__main__':
62+
for secret in sys.argv[1:]:
63+
print(totp(secret))
64+
```
65+
66+
TOTP stands for Time-based One-Time Password. At the heart of the TOTP
67+
algorithm lies the HOTP algorithm. HOTP stands for HMAC-based One-Time
68+
Password. Here are the relevant RFCs to learn more about these
69+
algorithms:
70+
71+
- [RFC 2104]: HMAC: Keyed-Hashing for Message Authentication
72+
- [RFC 4226]: HOTP: An HMAC-Based One-Time Password Algorithm
73+
- [RFC 6238]: TOTP: Time-Based One-Time Password Algorithm
74+
75+
[RFC 2104]: https://tools.ietf.org/html/rfc2104
76+
[RFC 4226]: https://tools.ietf.org/html/rfc4226
77+
[RFC 6238]: https://tools.ietf.org/html/rfc6238
78+
[RFC 2104-5]: https://tools.ietf.org/html/rfc4226#section-5
79+
80+
In the code above, we use the `hmac` module available in the Python
81+
standard library to implement HOTP. The implementation can be found in
82+
the `hotp()` function. It is a simple function with just 7 lines of
83+
code. It is a pretty straightforward implementation of [RFC 2104:
84+
Section 5: HOTP Algorithm][RFC 2104-5]. It takes a Base32-encoded secret
85+
key and a counter as input. It returns a 6-digit HOTP value.
86+
87+
The `totp()` function implements the TOTP algorithm. It is a thin
88+
wrapper around the HOTP algorithm. The TOTP value is obtained by
89+
invoking the HOTP function with the secret key and the number of time
90+
intervals (30 second intervals by default) that have elapsed since Unix
91+
epoch (1970-01-01 00:00:00 UTC).
92+
93+
94+
Get Started
95+
-----------
96+
97+
### With Base32 Key
98+
99+
1. Enter this command:
100+
101+
python3 totp.py ZYTYYE5FOAGW5ML7LRWUL4WTZLNJAMZS
102+
103+
The output should be a 6-digit TOTP value.
104+
105+
2. If you have Google Authenticator on your mobile phone, open it, tap
106+
its add button (`+` sign), select "Enter a provided key", enter any
107+
account name and "Time-based" and enter the following key:
108+
109+
ZYTYYE5FOAGW5ML7LRWUL4WTZLNJAMZS
110+
111+
Set the dropdown menu to "Time-based" and tap the "Add" button. A
112+
6-digit TOTP value should appear for the new key.
113+
114+
3. Run the command in step 1 again and verify that the TOTP value
115+
printed by the Python script matches the TOTP value that appears in
116+
Google Authenticator.
117+
118+
119+
### With QR Code
120+
121+
1. Install `zbarimg` to scan QR codes:
122+
123+
```shell
124+
# On macOS
125+
brew install zbar
126+
127+
# On Debian, Ubuntu, etc.
128+
apt-get install zbar-tools
129+
```
130+
131+
2. Download and save the following QR code on your system:\
132+
[![QR code for TOTP secret key](secret1.png)](secret1.png)\
133+
The QR code above can also be found in this file:
134+
[secret1.png](secret1.png).
135+
136+
3. Enter this command to data in the QR code:
137+
138+
139+
```shell
140+
zbarimg -q secret1.png
141+
```
142+
143+
The output should be:
144+
145+
```
146+
QR-Code:otpauth://totp/alice:bob?secret=ZYTYYE5FOAGW5ML7LRWUL4WTZLNJAMZS
147+
```
148+
149+
Note that the secret key in the URI is same as the secret key we
150+
used in the previous section.
151+
152+
4. Now enter this command to extract the secret key from the QR code
153+
and feed it to the Python script.
154+
155+
```shell
156+
python3 totp.py $(zbarimg -q secret1.png | sed 's/.*secret=\([^&]*\).*/\1/')
157+
```
158+
159+
5. If you have Google Authenticator on your mobile phone, open it, tap
160+
its add button (`+` sign), select "Scan a barcode", and scan the QR
161+
code shown above in step 3. A 6-digit TOTP value should appear for
162+
the new key.
163+
164+
6. Run the command in step 3 again and verify that the TOTP value
165+
printed by the Python script matches the TOTP value that appears in
166+
Google Authenticator.
167+
168+
169+
Usage
170+
-----
171+
172+
The script [totp.py](totp.py) accepts one or more Base32 secret keys as
173+
command line arguments and generates TOTP values from the secret keys.
174+
Here are a few examples:
175+
176+
1. Generate multiple TOTP values, one for each of multiple Base32 keys:
177+
178+
```shell
179+
python3 totp.py ZYTYYE5FOAGW5ML7LRWUL4WTZLNJAMZS PW4YAYYZVDE5RK2AOLKUATNZIKAFQLZO
180+
```
181+
182+
2. Generate TOTP values for multiple keys in multiple QR codes:
183+
184+
```shell
185+
python3 totp.py $(zbarimg -q *.png | sed 's/.*secret=\([^&]*\).*/\1/')
186+
```
187+
188+
3. Generate TOTP value for a key and copy it to clipboard :wink: on macOS:
189+
190+
```shell
191+
python3 totp.py ZYTYYE5FOAGW5ML7LRWUL4WTZLNJAMZS | pbcopy
192+
```
193+
194+
4. Generate TOTP value for a key, print it, and copy it to clipboard on
195+
macOS.
196+
197+
```shell
198+
python3 totp.py ZYTYYE5FOAGW5ML7LRWUL4WTZLNJAMZS | tee /dev/stderr | pbcopy
199+
```
200+
201+
202+
Caution
203+
-------
204+
205+
This project is only a proof of concept to demonstrate how TOTP values
206+
are generated. It can be tempting to use this to generate TOTP values on
207+
a desktop/laptop device while logging into a website that requires
208+
TOTP-based two-factor authentication from the same device. However,
209+
doing so defeats the purpose of two-factor authentication (2FA). If your
210+
desktop/laptop device is compromised, then both authentication factors
211+
would be compromised. The attacker can steal the first authentication
212+
factor that only you should know (e.g., password) by running a key
213+
logger on the compromised device. The attacker can also steal the second
214+
authentication factor that only you should have (e.g., TOTP secret key)
215+
because it would be read by this script on the same compromised device;
216+
if this script can read the TOTP secret key on the compromised device,
217+
so can the attacker.
218+
219+
220+
License
221+
-------
222+
223+
This is free and open source software. You can use, copy, modify,
224+
merge, publish, distribute, sublicense, and/or sell copies of it,
225+
under the terms of the MIT License. See [LICENSE.md][L] for details.
226+
227+
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
228+
express or implied. See [LICENSE.md][L] for details.
229+
230+
231+
Thanks
232+
------
233+
234+
Thanks to [Prateek Nischal][PN] for getting me involved with TOTP. I
235+
referred to his TOTP implementation at
236+
[prateeknischal/qry/util/totp.py][PNTOTP] while writing my own.
237+
238+
[PN]: https://github.com/prateeknischal
239+
[PNTOTP]: https://github.com/prateeknischal/qry/blob/master/util/totp.py

secret1.png

652 Bytes
Loading

secret2.png

636 Bytes
Loading

totp.py

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
#!/usr/bin/python3
2+
3+
import base64
4+
import hmac
5+
import struct
6+
import sys
7+
import time
8+
9+
10+
def hotp(secret, counter, digits=6, algo='sha1'):
11+
padding = '=' * ((8 - len(secret)) % 8)
12+
secret_bytes = base64.b32decode(secret.upper() + padding)
13+
counter_bytes = struct.pack(">Q", counter)
14+
mac = hmac.digest(secret_bytes, counter_bytes, algo)
15+
offset = mac[-1] & 0x0f
16+
truncated = struct.unpack('>L', mac[offset:offset+4])[0] & 0x7fffffff
17+
return str(truncated)[-digits:].rjust(digits, '0')
18+
19+
20+
def totp(secret, interval=30):
21+
return hotp(secret, int(time.time() / interval))
22+
23+
24+
if __name__ == '__main__':
25+
for secret in sys.argv[1:]:
26+
print(totp(secret))

0 commit comments

Comments
 (0)