This is a program that allows you to store an initial password in an encrypted file and that is capable of generating a new TOTP one time password every time it is requested.
It has a CLI (Command Line Interface), and a GUI (Graphical User Interface) version.
- The
keys
folder contains a set of valid and invalid keys (for testing purpose). The valid keys are:key.hex
,key.base32
. - The file containing the secret key shouldn't end with a newline character.
do:
echo -n <key_string> > <key_file>
to put into a file a string that doesn't end with a newline. - The key should be either in Hex or Base32 formats.
- The key should have at least 64 characters.
- Crypto++ Library
Used for performing HMAC-SHA1.
sudo apt install libcrypto++X libcrypto++-dev libcrypto++-utils libcrypto++-doc
// Or, on Termux
pkg install cryptopp
- Qrencode
Used for producing QRcodes.
sudo apt install libqrencode-dev
// Or, on Termux
pkg install libqrencode
- PNG Library Used for producing QRcodes.
sudo apt install libpng-dev
// Or, on Termux
pkg install libpng
// Install
cd cli
make
// Usage: ./ft_otp [OPTIONS] <key file>
// Options:
-g, --generate Generate and save the encrypted key
-k, --key Generate password using the provided key
-q, --qrcode Generate a QR code containing the key (requires -g)
-v, --verbose Enable verbose output
-h, --help Show this help message and exit
// Generate the encrypted key file with its corresponding QR code
./ft_otp -gk <key_file>
The program receives as argument a hexadecimal key of at least 64 characters.
The program stores this key safely in a file called ft_otp.key,
which is encrypted with AES encryption using Crypto++.
// Generate the TOTP temporary password
./ft_otp -k ft_otp.key
The program generates a new temporary password based on the encrypted key
given as argument and prints it on the standard output.
// Check what TOTP code you should get with a hex key
oathtool --totp $(cat keys/key.hex) -v
// Check with a Base32 key
oathtool --totp -b $(cat keys/key.base32) -v
// Examples of usage
# With a Hex key #
// 1. Generate and save the encrypted key to the external file `ft_otp.key`.
make && ./ft_otp -g keys/key.hex
// 2. Decode the encrypted key and generate a TOTP code from it.
// Compare our TOTP code to the one delivered by `oathtool`.
./ft_otp ft_otp.key -k && echo "" && oathtool --totp $(cat keys/key.hex) -v
or: make hex
# With a Base32 key #
make && ./ft_otp -g keys/key.base32 && ./ft_otp ft_otp.key -k && echo "" && oathtool --totp -b $(cat keys/key.base32) -v
or: make b32
# With a Bad key #
make && ./ft_otp -g keys/key.base32hex
./ft_otp ft_otp.key -k
oathtool --totp $(cat keys/key.base32hex) -v
or: make bad
// Tests
make hex // Run with a Hex secret key
make b32 // Run with a Base32 secret key
make bad // Run with a bad secret key
make tests // Run all tests
// Run the GUI
cd gui
./run.sh
// Or
cd gui
./build/Desktop_Qt_6_8_1-Debug/ft_otp_gui
- QR codes can be generated and read from command line for testing:
// Generate QR code image
sudo apt install qrencode
qrencode -o qrcode.png $(cat keys/key.hex)
// Read QR code image
sudo apt install zbar-tools
zbarimg qrcode.png
- This program generates a QR code for a TOTP (Time-based One-Time Password) secret, saving it as a PNG image file. The process involves creating a QR code based on the Key URI Format, which can be scanned by standard QR code readers for seamless OTP setup.
-
QR Code Dimensions and Scaling:
- The QR code is generated at a scaled-up resolution for better readability.
- Dimensions: The total image size is
(QR_width × scale + 2 × margin) × (QR_width × scale + 2 × margin)
, ensuring sufficient size and compatibility with QR scanners. - Defaults:
- Scale: 10 (each module is scaled by 10 pixels).
- Margin: 4 modules.
-
PNG File Creation:
- The program uses the libpng library to create a grayscale PNG file where:
- Black pixels represent QR code modules (
0x00
). - White pixels fill the rest (
0xFF
).
- Black pixels represent QR code modules (
- The program uses the libpng library to create a grayscale PNG file where:
-
Key URI Format:
- The QR code encodes a TOTP URI in the following format:
otpauth://totp/<PROJECT_NAME>:<USER_EMAIL>?secret=<SECRET>&issuer=<PROJECT_NAME>
- Example:
otpauth://totp/MyService:[email protected]?secret=BASE32SECRET&issuer=MyService
- Example:
- The QR code encodes a TOTP URI in the following format:
-
Steps to Generate the QR Code:
- A TOTP URI is created dynamically using the provided secret and project name.
- The URI is encoded into a QR code using the
qrencode
library. - The generated QR code is saved as a PNG file in the current directory.
Endianness determines the byte order of data in memory.
- In TOTP, the timestamp must be in big-endian format (most significant byte first) when passed to the HMAC function. Incorrect endianness leads to invalid TOTP codes, as the hash depends on the precise byte order. So we had to ensure the timestamp matched the expected endianness for consistent results.
- To check the endianness of your system:
lscpu | grep Order # Output: Byte Order: Little Endian
- The TOTP one-time password is randomly generated by the HOTP algorithm (RFC 4226), and always contains the same format, i.e. 6 digits.
- The main difference between HOTP (HMAC-Based One-Time Password) and TOTP (Time-Based One-Time Password) lies in how they generate and validate one-time passwords (OTPs). While both are based on a shared secret and hashing algorithms, they differ in their reliance on counters versus time:
HOTP:
Uses a counter that is incremented on each OTP request.
Formula: HOTP = Truncate(HMAC(secret, counter)).
TOTP:
Uses the current time divided by a fixed time step (e.g., 30 seconds).
Formula: TOTP = Truncate(HMAC(secret, time_step)).
At first, we were trying to decode Base32 keys with Base32Decoder
:
Base32Decoder decoder;
decoder.Put((byte*)key.data(), key.size());
decoder.MessageEnd();
size_t size = decoder.MaxRetrievable();
decodedKey.resize(size);
decoder.Get(decodedKey, size);
However, the decoded string it returned didn't match the one returned by oathtool --totp -b $(cat key.base32) -v
.
We thought that the padding character =
was handled differently, so we tried our code and oathtool both with and without the padding character at the end of the key string.
However it did not change anything either with our code or with oathtool.
So, we concluded that the key difference between our implementation (decodeBase32RFC4648()
) and the behavior of oathtool was likely rooted in how the Base32 decoding process was handled, particularly with respect to strict adherence to RFC 4648.
In fact, we found that the default Base32Decoder
code was based on Differential Unicode Domain Encoding (DUDE), which doesn't even use the same character set:
// DUDE Decoder's set of characters
static const char base32[] = {
97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, /* a-k */
109, 110, /* m-n */
112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, /* p-z */
50, 51, 52, 53, 54, 55, 56, 57 /* 2-9 */
};
- We used Qt Creator (Qt6) in order to create the GUI.
- Install the latest open source free version of Qt from the official website.
- The error
In file included from srcs/FileHandler.cpp:1:
incs/FileHandler.hpp:4:10: fatal error: 'iostream' file not found
#include <iostream>
^~~~~~~~~~
1 error generated.
- The solution
Install the necessary development packages to provide standard headers and libraries:
sudo apt install build-essential clang libc++-14-dev libc++-dev libc++1-14 libc++abi-14-dev libc++abi-dev libc++abi1-14 libstdc++-12-dev libunwind-14 libunwind-14-dev
- https://www.cryptopp.com/wiki/Advanced_Encryption_Standard (AES)
- https://www.ietf.org/rfc/rfc4226.txt (HOTP)
- https://datatracker.ietf.org/doc/html/rfc6238#section-4 (TOTP)
- https://datatracker.ietf.org/doc/html/rfc4648#section-6 (Base32 encoding)
- https://datatracker.ietf.org/doc/html/rfc3986 (URI)
- https://github.com/google/google-authenticator/wiki/Key-Uri-Format (Key URI Format)