Enhance OTP script with QR code generation and improve secret key handling

This commit is contained in:
whaffman 2025-07-03 14:18:17 +02:00
parent f6ae60147c
commit 5ce8ac58c5
4 changed files with 65 additions and 25 deletions

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
.venv/ .venv/
ft_otp.key ft_otp.key
key2.hex

View File

@ -1,66 +1,102 @@
import argparse import argparse
from cryptography.fernet import Fernet from cryptography.fernet import Fernet
from hashlib import sha256 from hashlib import sha256
from base64 import urlsafe_b64encode import base64
import hmac
import struct
import hashlib
import qrcode_terminal
import time
def main(): def main():
parser = argparse.ArgumentParser(description="Run the FT OTP script.") parser = argparse.ArgumentParser(description="Run the FT OTP script.")
parser.add_argument("-g", metavar="hexfile", type=str, help="Generate a new OTP key and save it to the specified file.") parser.add_argument("-g", metavar="secret_file", type=str, help="Generate a new OTP key and save it to the specified file.")
parser.add_argument("-k", metavar="keyfile", type=str, help="Generate a OTP from the specified key file.") parser.add_argument("-k", metavar="keyfile", type=str, help="Generate a OTP from the specified key file.")
parser.add_argument("-q", action="store_true", help="Generate a QR code for the OTP key.")
args = parser.parse_args() args = parser.parse_args()
if args.g: if args.g:
# Call the function to generate a new OTP key # Call the function to generate a new OTP key
generate_otp_key(args.g) generate_secret_key(args.g)
elif args.k: elif args.k:
# Call the function to generate a OTP from the specified key file # Call the function to generate a OTP from the specified key file
generate_otp_from_key(args.k) generate_otp_from_secret_key(args.k)
elif args.q:
# Call the function to generate a QR code for the OTP key
generate_qr_code()
print("QR code generated successfully.")
else: else:
print("No action specified. Use -g to generate a new OTP key or -k to generate a OTP from a key file.") print("No action specified. Use -g to generate a new OTP key or -k to generate a OTP from a key file.")
parser.print_help() parser.print_help()
def generate_otp_key(hexfile): def is_hex(s):
print(f"Generating OTP key from {hexfile}...") try:
machine_id = get_machine_hash() int(s, 16)
with open(hexfile, 'rb') as f: return True
hex_key = f.read().strip() except ValueError:
fernet = Fernet(machine_id) return False
otp_key = fernet.encrypt(hex_key)
with open('./ft_otp.key', 'wb') as f:
f.write(otp_key)
def generate_otp_from_key(keyfile): def generate_secret_key(secret_file):
import time print(f"Generating OTP key from {secret_file}...")
machine_id = get_machine_hash()
with open(secret_file, 'rb') as f:
secret = f.read().strip()
if not is_hex(secret.decode()):
print("The provided secret is not a valid hexadecimal string.")
return
if len(secret) < 64:
print("The provided secret is too short. It should be at least 64 characters long.")
return
fernet = Fernet(machine_id)
secret_enc = fernet.encrypt(secret)
with open('./ft_otp.key', 'wb') as f:
f.write(secret_enc)
def read_secret_key(keyfile):
"""Read the secret key from the specified file."""
machine_id = get_machine_hash() machine_id = get_machine_hash()
with open(keyfile, 'rb') as f: with open(keyfile, 'rb') as f:
otp_key = f.read().strip() secret_enc = f.read().strip()
fernet = Fernet(machine_id) fernet = Fernet(machine_id)
otp = fernet.decrypt(otp_key) secret = fernet.decrypt(secret_enc)
return secret
def generate_qr_code():
"""Generate a QR code for the OTP key."""
keyfile = './ft_otp.key'
secret = read_secret_key(keyfile)
print(f"Secret key read from {keyfile}: {secret.decode()}")
base32 = base64.b32encode(base64.b16decode(secret.upper())).decode().strip('=')
print(str(base32))
otp_uri = f"otpauth://totp/FTOTP?secret={base32}&issuer=FTOTP&algo=SHA1&digits=6&period=30"
qrcode_terminal.draw(otp_uri)
def generate_otp_from_secret_key(keyfile):
secret = read_secret_key(keyfile)
counter = int(time.time() // 30) # Using time-based counter for HOTP counter = int(time.time() // 30) # Using time-based counter for HOTP
otp = HOTP(otp, counter) otp = HOTP(secret, counter)
# Print the generated OTP # Print the generated OTP
print(f"Generated OTP: {otp}") print(f"Generated OTP: {otp}")
def get_machine_hash() -> bytes: def get_machine_hash() -> bytes:
"""Retrieve the machine ID from /etc/machine-id.""" """Retrieve the machine ID from /etc/machine-id."""
try: try:
with open('/etc/machine-id', 'r') as f: with open('/etc/machine-id', 'r') as f:
machine_id = f.read().strip() machine_id = f.read().strip()
sha256_hash = sha256(machine_id.encode()).digest() sha256_hash = sha256(machine_id.encode()).digest()
return urlsafe_b64encode(sha256_hash) return base64.urlsafe_b64encode(sha256_hash)
except FileNotFoundError: except FileNotFoundError:
print("Machine ID file not found. Please ensure the system is properly configured.") print("Machine ID file not found. Please ensure the system is properly configured.")
return None return None
def HOTP(key, counter): def HOTP(key, counter):
"""Generate a one-time password using the HOTP algorithm. using RFC4226""" """Generate a one-time password using the HOTP algorithm. using RFC4226"""
import hmac
import struct
import hashlib
counter = struct.pack('>Q', counter) counter = struct.pack('>Q', counter)
key = base64.b16decode(key.upper())
hmac_hash = hmac.new(key, counter, hashlib.sha1).digest() hmac_hash = hmac.new(key, counter, hashlib.sha1).digest()
offset = hmac_hash[-1] & 0x0F offset = hmac_hash[-1] & 0x0F
otp = (struct.unpack('>I', hmac_hash[offset:offset + 4])[0] & 0x7FFFFFFF) % 1000000 otp = (struct.unpack('>I', hmac_hash[offset:offset + 4])[0] & 0x7FFFFFFF) % 1000000

View File

@ -1 +1 @@
d779574a5815a54089b260694be7b29838e28a72e943bdd41e47656885118c7a3f29f6611ca1383f7e67e9868a80b8fd68c71be79b418e8984bdaef4991932e4 d779574a5815a54089b260694be7b29838e28a72e943bdd41e47656885118c7a

3
requirements.txt Normal file
View File

@ -0,0 +1,3 @@
cffi==1.17.1
cryptography==45.0.4
pycparser==2.22