Time Based One Time Password using Python

Time Based One Time Password using Python

What is a Time Based One Time Password

Time Based One Time Password(aka TOTP) is a common method of two-factor authentication. Used to create temporary passcodes from time and Secret key using HOTP algorithm. Generated passcode expires after some time, most common is every 30 seconds.

How to create a python app to display passcodes in the terminal?

Before starting lets add script to run our main function

if __name__ == "__main__":
    main()

That way we will run our main function every time we start this code from terminal

First of all, we will need the datetime library to get the time and a function named main to print passcodes every 30 seconds

import datetime

def main():
    print_keys()#printing passcode at start
    while True:
        time_sec = datetime.datetime.now().second
        if(time_sec == 00 or time_sec == 30):
            print_keys()
            time.sleep(1)

Next, we should function to convert secret keys to passcodes To store secret keys we will use secrets.txt named text file format will be like this

Name:SecretKey
Name:SecretKey

We have to clear terminal before starting

import os
def print_keys():
        os.system('cls'  if os.name ==  'nt'  else  'clear')

after that we will open secrets.txt file and save to table

longest = 0
table_data =  list()
with open("secrets.txt", "r") as f:
    for data in f.readlines():
        data = data.replace("\n", "")#Removing newline from line
        name, secret = data.split(":")
        if len(name) > longest:
            longest = len(name)
        secret = hotp(secret)
        table_data.append([name, secret])
    print_table(table_data, longest)#prints passcode in table

Now you see 2 diffrent things here longest and hotp function We use hotp function to convert secret keys to passcodes but what about longest Well i decided it will be more cool if we add table like

+------+--------+
| Name | Key    |
+------+--------+
| Name | 702837 |
| Name | 539807 |
| Name | 895028 |
+------+--------+

To create this without using external libraries I have to get the length of the longest name Now let's create hotp function. For this function, I used susam's mintotp repository and modified a little bit to fit in one function.

import base64
import hmac
import struct

def hotp(secret):
    padding = '=' * ((8 - len(secret)) % 8)
    secret_bytes = base64.b32decode(secret.upper() + padding)
    counter_bytes = struct.pack(">Q", int(time.time() / 30))
    mac = hmac.digest(secret_bytes, counter_bytes, "sha1")
    offset = mac[-1] & 0x0f
    truncated = struct.unpack('>L', mac[offset:offset+4])[0] & 0x7fffffff
    return str(truncated)[-6:].rjust(6, '0')

Now lets create table Because of the headers name lenght should not be smaller than 4 characters We will be create table header like this

def print_table(data, longest_lenght):
    if longest_lenght < 4:
        longest_lenght = 4
    print("+-{}-+--------+".format(longest_lenght*"-"))
    print("| Name{} | Key |".format((longest_lenght-4)*" "))
    print("+-{}-+--------+".format(longest_lenght*"-"))

Then we will print every name and passcode in the data list

for creds in data:
    blank_space = str(" "*(longest_lenght-len(creds[0])))
    print("| {} | {} |".format(creds[0]+blank_space, creds[1]))

and close our table

print("+-{}-+--------+".format(longest_lenght*"-"))

Now our final code looks like this

import datetime
import os
import time
import base64
import hmac
import struct


def print_table(data, longest_lenght):
    if longest_lenght < 4:
        longest_lenght = 4
    print("+-{}-+--------+".format(longest_lenght*"-"))
    print("| Name{} | Key    |".format((longest_lenght-4)*" "))
    print("+-{}-+--------+".format(longest_lenght*"-"))

    for creds in data:
        blank_space = str(" "*(longest_lenght-len(creds[0])))
        print("| {} | {} |".format(creds[0]+blank_space, creds[1]))

    print("+-{}-+--------+".format(longest_lenght*"-"))


def print_keys():
    os.system('cls' if os.name == 'nt' else 'clear')
    longest = 0
    table_data = list()
    with open("secrets.txt", "r") as f:
        for data in f.readlines():
            data = data.replace("\n", "")
            name, secret = data.split(":")
            if len(name) > longest:
                longest = len(name)
            secret = hotp(secret)
            table_data.append([name, secret])
        print_table(table_data, longest)

def hotp(secret):
    padding = '=' * ((8 - len(secret)) % 8)
    secret_bytes = base64.b32decode(secret.upper() + padding)
    counter_bytes = struct.pack(">Q", int(time.time() / 30))
    mac = hmac.digest(secret_bytes, counter_bytes, "sha1")
    offset = mac[-1] & 0x0f
    truncated = struct.unpack('>L', mac[offset:offset+4])[0] & 0x7fffffff
    return str(truncated)[-6:].rjust(6, '0')

def main():
    print_keys()
    while True:
        time_sec = datetime.datetime.now().second
        if(time_sec == 00 or time_sec == 30):
            print_keys()
            time.sleep(1)


if __name__ == "__main__":
    main()

If we run this we will get an error because we didn't put any secrets file lets quickly add example secrets file

Name1:JBSWY3DPEHPK3PXP
Name2:ACDWY3DAQHPK3ZDZ

Now if we did everything correctly our output will be like this

+-------+--------+
| Name  | Key    |
+-------+--------+
| Name1 | 155326 |
| Name2 | 446469 |
+-------+--------+

Looks like something missing We should add time bar to see how much time left to next password I Want something like

+-------+--------+
| Name  | Key    |
+-------+--------+
| Name1 | 155326 |
| Name2 | 446469 |
+-------+--------+
15########

We should add else to main function

def main():
    print_keys()#printing passcode at start
    while True:
        time_sec = datetime.datetime.now().second
        if(time_sec == 00 or time_sec == 30):
            print_keys()
            time.sleep(1)
       else:
            if time_sec <= 30:
                remaining_time = 30-time_sec
            else:
                remaining_time = 60-time_sec
            if remaining_time < 10:
                remaining_time_str = "0{}".format(remaining_time)
            else:
                remaining_time_str = str(remaining_time)
            table_len = longest_name+11
            loading = "#"*(table_len-((remaining_time*table_len)//30))
            print(remaining_time_str + loading, end="\r")# \r takes cursor to beginning

We still need the longest name we can get this from where we defined it first, from print keys Now we will return the longest name just by adding this to end of print_keys function

return longest

and lastly, in our main function, we have to create longest_name variable from print_keys we will add longest_name in front of print_keys function like this

longest_name = print_keys()

And were done our final code Should Look Like This

import datetime
import os
import time
import base64
import hmac
import struct


def print_table(data, longest_lenght):
    if longest_lenght < 4:
        longest_lenght = 4
    print("+-{}-+--------+".format(longest_lenght*"-"))
    print("| Name{} | Key    |".format((longest_lenght-4)*" "))
    print("+-{}-+--------+".format(longest_lenght*"-"))

    for creds in data:
        blank_space = str(" "*(longest_lenght-len(creds[0])))
        print("| {} | {} |".format(creds[0]+blank_space, creds[1]))

    print("+-{}-+--------+".format(longest_lenght*"-"))


def print_keys():
    os.system('cls' if os.name == 'nt' else 'clear')
    longest = 0
    table_data = list()
    with open("secrets.txt", "r") as f:
        for data in f.readlines():
            data = data.replace("\n", "")
            name, secret = data.split(":")
            if len(name) > longest:
                longest = len(name)
            secret = hotp(secret)
            table_data.append([name, secret])
        print_table(table_data, longest)
        return longest

def hotp(secret):
    padding = '=' * ((8 - len(secret)) % 8)
    secret_bytes = base64.b32decode(secret.upper() + padding)
    counter_bytes = struct.pack(">Q", int(time.time() / 30))
    mac = hmac.digest(secret_bytes, counter_bytes, "sha1")
    offset = mac[-1] & 0x0f
    truncated = struct.unpack('>L', mac[offset:offset+4])[0] & 0x7fffffff
    return str(truncated)[-6:].rjust(6, '0')

def main():
    longest_name = print_keys()
    while True:
        time_sec = datetime.datetime.now().second
        if(time_sec == 00 or time_sec == 30):
            longest_name = print_keys()
            time.sleep(1)
        else:
            if time_sec <= 30:
                remaining_time = 30-time_sec
            else:
                remaining_time = 60-time_sec
            if remaining_time < 10:
                remaining_time_str = "0{}".format(remaining_time)
            else:
                remaining_time_str = str(remaining_time)
            table_len = longest_name+11
            loading = "#"*(table_len-((remaining_time*table_len)//30))
            print(remaining_time_str + loading, end="\r")# \r takes cursor to beginning



if __name__ == "__main__":
    main()

If we run this we will get this output

+-------+--------+
| Name  | Key    |
+-------+--------+
| Name1 | 817811 |
| Name2 | 854363 |
+-------+--------+
09############

Github Repository

Thanks For Reading