#!/usr/bin/env python3 import argparse import sqlite3 import pyotp import sys import os import configparser CLIENT_KEY_LENGTH = 64 TOTP_KEY_LENGTH = 24 CLIENT_ALIAS_INDEX = 0 CLIENT_KEY_INDEX = 1 CLIENT_TOTP_INDEX = 2 def die(msg): print(msg) sys.exit(1) def parse_arguments(): parser = argparse.ArgumentParser() parser.add_argument("--config",type=str,help="Config file location",\ default="/etc/mfa/mfa.conf") parser.add_argument("--database",type=str,help="Database location") parser.add_argument("--add-client",type=str,nargs="+",help="Add a client") parser.add_argument("--add-app",action="store_true",help="Add an application") parser.add_argument("--user",type=str,help="Application username") parser.add_argument("--host",type=str,help="Application hostname") parser.add_argument("--service",type=str,help="Application service name") parser.add_argument("--alias",type=str,help="Alias for new client") parser.add_argument("--methods",type=str,nargs="+",help="Allowed MFA methods") parser.add_argument("--get-client",action="store_true",help="Get a client key") parser.add_argument("--show-secret",action="store_true",help="Show TOTP secrets") parser.add_argument("--delete-client",nargs="+",help="Delete given clients") parser.add_argument("--update-totp",action="store_true",help="Update a TOTP") parser.add_argument("--get-totp",action="store_true",help="Get client's TOTP") parser.add_argument("--get-app",action="store_true",help="Get provisioned apps") parser.add_argument("--delete-app",action="store_true",help="Delete application") return parser.parse_args() def alias_exists(db,alias): client = None with sqlite3.connect(db) as conn: c = conn.cursor() c.execute("SELECT * FROM clients WHERE alias=?",(alias,)) client = c.fetchall() if len(client) == 0: return False elif len(client) == 1: return True def delete_app(db,alias,user,host,service): get_app(db,alias,user,host,service,None) confirm = input("These applications will be deleted. Continue [y/N]") if confirm != "y": die("operation cancelled") with sqlite3.connect(db) as conn: data = [alias,user,host,service] for var in data: if var == None: data[data.index(var)] = '%' c = conn.cursor() c.execute("""DELETE FROM applications WHERE alias LIKE ? AND username LIKE ? AND hostname LIKE ? AND service LIKE ? """,\ (data[0],data[1],data[2],data[3])) print("applications deleted") def get_app(db,alias,user,host,service,methods): # If variable is not defined set to % to get all results if methods != None: methods = " ".join(methods) data = [alias,user,host,service,methods] for var in data: if var == None: data[data.index(var)] = '%' with sqlite3.connect(db) as conn: c = conn.cursor() c.execute("""SELECT * FROM applications WHERE alias LIKE ? AND username LIKE ? AND hostname LIKE ? AND service LIKE ? and mfa_methods LIKE ?""",\ (data[0],data[1],data[2],data[3],data[4])) apps = c.fetchall() for app in apps: print("username: " + app[0]) print("hostname: " + app[1]) print("service: " + app[2]) print("alias: " + app[3]) print("mfa methods: " + app[4]) print("") def get_clients(db,show=False): with sqlite3.connect(db) as conn: c = conn.cursor() c.execute("SELECT * FROM clients") clients = c.fetchall() for client in clients: print("alias: " + client[0]) print("key: " + client[1]) if show: print("totp secret: " + client[2]) print("") def get_client_info(db,alias,info): if not alias_exists(db, alias): die("Error: alias does not exist") else: client = None with sqlite3.connect(db) as conn: c = conn.cursor() c.execute("SELECT * FROM clients WHERE alias=?",(alias,)) client = c.fetchone() if info == "key": return str(client[CLIENT_KEY_INDEX]) elif info == "totp": return str(client[CLIENT_TOTP_INDEX]) def update_totp(db,alias): if alias_exists(db, alias): totp_secret = pyotp.random_base32(TOTP_KEY_LENGTH) with sqlite3.connect(db) as conn: c = conn.cursor() c.execute("UPDATE clients SET totp_secret=? WHERE alias=?",\ (totp_secret,alias)) print("totp secret: " + totp_secret) else: die("error: alias does not exist") def add_client(db,aliases): for alias in aliases: if not alias_exists(db, alias): client_key = pyotp.random_base32(CLIENT_KEY_LENGTH) totp_secret = pyotp.random_base32(TOTP_KEY_LENGTH) with sqlite3.connect(db) as conn: c = conn.cursor() c.execute("INSERT INTO clients VALUES (?,?,?)",\ (alias,client_key,totp_secret)) print("alias: " + alias) print("client key: " + client_key) print("totp secret: " + totp_secret) print("uri: " + pyotp.TOTP(totp_secret).provisioning_uri(alias+"@mfad")) print("") else: print("error: " + alias + " already used") def delete_client(db,aliases): for alias in aliases: if alias_exists(db,alias): with sqlite3.connect(db) as conn: c = conn.cursor() c.execute("DELETE FROM clients WHERE alias=?",(alias,)) print(alias + " deleted") else: die("error: alias does not exist") def add_app(db,username, hostname, service, alias, mfa_methods): if not alias_exists(db, alias): die("Error: alias does not exist") else: client_key = get_client_info(db, alias, "key") with sqlite3.connect(db) as conn: c = conn.cursor() c.execute("INSERT INTO applications VALUES (?,?,?,?,?)", (username,hostname,service,alias,mfa_methods)) def read_config(config): parser = configparser.ConfigParser(inline_comment_prefixes="#") parser.read(config) return parser def get_vars(args,confparser): if not os.path.exists(args.config): die("Unable to open config file") database = None # Set values from config file first if confparser.has_section("mfad"): database = confparser.get("mfad","database",fallback=None) # Let command line args overwrite any values if args.database: database = args.database # Exit if any value is null if database == None: die("error: no database file given") return database def main(): args = parse_arguments() confparser = read_config(args.config) db = get_vars(args,confparser) # Sanity checks if args.update_totp and args.alias == None: die("Error: no alias specified") if args.get_totp and args.alias == None: die("Error: no alias specified") if args.add_app and (args.user == None or args.host == None or \ args.service == None or args.alias == None \ or args.methods == None): die("Error: --add-app requires all of --user,--host,--service,--alias,--methods") if args.add_client != None: add_client(db,args.add_client) elif args.get_client and args.alias == None: key = get_clients(db,args.show_secret) elif args.get_client: key = get_client_info(db,args.alias,"key") print(key) elif args.add_app: methods = " ".join(args.methods) add_app(db,args.user,args.host,args.service,args.alias,methods) elif args.update_totp: update_totp(db,args.alias) elif args.get_totp: secret = get_client_info(db,args.alias,"totp") print(secret) print(pyotp.TOTP(secret).provisioning_uri(args.alias+"@mfad")) elif args.get_app: get_app(db,args.alias,args.user,args.host,args.service,args.methods) elif args.delete_client: delete_client(db,args.delete_client) elif args.delete_app: delete_app(db,args.alias,args.user,args.host,args.service) if __name__ == '__main__': main()