Skip to content
67 changes: 31 additions & 36 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,60 +1,55 @@
# MFA CLI
# Multi-Factor Authentication Command Line Interface

<div align="center">
<img src="https://github.com/EgydioBNeto/mfa-cli/assets/84047984/714533aa-22a2-4127-8d40-363e59a573fa" width="300px"/>
</div>

## Description

This is a simple Command Line Interface (CLI) tool written in Python for managing secrets and generating Multi-Factor Authentication (MFA) codes. The script allows users to add, delete, update, list, and export secrets, as well as generate MFA codes.

## Features

## Installation
### Commands

```
python3 -c "$(curl -fsSL https://raw.githubusercontent.com/EgydioBNeto/mfa-cli/main/install.py)"
```

## Features
- **add_secret**: Adds a new secret with a specified name and value.
- **delete_secret**: Deletes a stored secret by name.
- **list_secrets**: Lists all stored secrets.
- **update_secret**: Updates an existing secret with a new value.
- **generate_mfa**: Generates an MFA code for a given secret name.
- **export_secrets**: Exports stored secrets to a specified file.
- **help**: Displays a help message with information about available commands and examples.

## Usage

### Commands:
- **mfa_add** <name> <secret>: Add a new secret.
- **mfa_delete** <name>: Delete a stored secret.
- **mfa_add** &lt;name&gt; &lt;secret&gt;: Add a new secret.
- **mfa_delete** &lt;name&gt; Delete a stored secret.
- **mfa_list** List all stored secrets.
- **mfa_update** <name> <secret>: Update an existing secret.
- **mfa_generate** <name>: Generate an MFA code.
- **mfa_export** <file_path>: Export secrets to a file.
- **mfa_update** &lt;name&gt; &lt;secret&gt;: Update an existing secret.
- **mfa_generate** &lt;name&gt;: Generate an MFA code.
- **mfa_export** &lt;file_path&gt;: Export secrets to a file.
- **mfa_help** Show this help message.

#### Summarized Commands:
- **mfa** <name> <secret>: Add a new secret.
- **mfd** <name>: Delete a stored secret.
### Summarized Commands

- **mfa** &lt;name&gt; &lt;secret&gt;: Add a new secret.
- **mfd** &lt;name&gt; Delete a stored secret.
- **mfl** List all stored secrets.
- **mfu** <name> <secret>: Update an existing secret.
- **mfg** <name>: Generate an MFA code.
- **mfe** <file_path>: Export secrets to a file.
- **mfu** &lt;name&gt; &lt;secret&gt;: Update an existing secret.
- **mfg** &lt;name&gt;: Generate an MFA code.
- **mfe** &lt;file_path&gt;: Export secrets to a file.
- **mfh** Show this help message.

# Configuration
The script uses a secrets.json file to store secrets. Ensure that the file exists or is created in the same directory as the script.
## Requirements

- **Python3**

## Uninstallation
## Installation

```bash
python3 -c "$(curl -fsSL https://raw.githubusercontent.com/EgydioBNeto/mfa-cli/main/install.py)"
```

## Uninstallation

```bash
python3 -c "$(curl -fsSL https://raw.githubusercontent.com/EgydioBNeto/mfa-cli/main/uninstall.py)"
```

# Requirements
Python 3

## Author

[EgydioBNeto](https://github.com/EgydioBNeto)

## License
This project is licensed under the [MIT License]([URL_do_Link](https://github.com/EgydioBNeto/mfa-cli/blob/main/SECURITY.md).

This project is licensed under the [MIT License](https://github.com/EgydioBNeto/mfa-cli/blob/main/LICENSE).
76 changes: 65 additions & 11 deletions install.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,19 @@

"""
Install Script

This script is used to install the 'mfa-cli' tool. It downloads the necessary files from a GitHub repository,
sets up aliases in the user's configuration files, and provides a convenient way to manage multi-factor authentication secrets.

The script first checks if 'mfa-cli' is already installed and, if so, it uninstalls it before proceeding with the installation.

Usage:
python install.py

Note: After installation, it is recommended to restart the terminal for the aliases to take effect.
"""

import sys
import os
import subprocess

Expand All @@ -21,23 +32,47 @@
def uninstall_mfa_cli():
"""
Uninstall the 'mfa-cli' script.

This function downloads the uninstall script from the GitHub repository and runs it.
"""
print("Uninstalling existing 'mfa-cli'...")
subprocess.run(["curl", "-fsSL", UNINSTALL_URL, "-o", f"{INSTALL_DIR}/uninstall.py"], check=True)

try:
subprocess.run(["curl", "-fsSL", UNINSTALL_URL, "-o", f"{INSTALL_DIR}/uninstall.py"], check=True)
except subprocess.CalledProcessError:
print(f"Cannot download uninstall script from '{UNINSTALL_URL}'.")
print("Please uninstall manually.")
return

os.chmod(f"{INSTALL_DIR}/uninstall.py", 0o755)
subprocess.run([f"{INSTALL_DIR}/uninstall.py"], check=True)

try:
subprocess.run([f"{INSTALL_DIR}/uninstall.py"], check=True)
except subprocess.CalledProcessError:
print("Uninstall script failed.")
print("Please uninstall manually.")
return

def install_mfa_cli():
"""
Install the 'mfa-cli' script.

This function downloads the script from the GitHub repository, sets up aliases in the user's configuration files,
"""
print("Installing 'mfa-cli'...")
os.makedirs(INSTALL_DIR, exist_ok=True)
subprocess.run(["curl", "-fsSL", INSTALL_URL, "-o", f"{INSTALL_DIR}/{SCRIPT_NAME}"], check=True)
try:
subprocess.run(["curl", "-fsSL", INSTALL_URL, "-o", f"{INSTALL_DIR}/{SCRIPT_NAME}"], check=True)
except subprocess.CalledProcessError as error:
print(RED + f"Failed to install 'mfa-cli' ({error})" + RESET)
sys.exit(1)
os.chmod(f"{INSTALL_DIR}/{SCRIPT_NAME}", 0o755)
for config_file in CONFIG_FILES:
with open(config_file, "a", encoding="utf-8") as alias_file:
alias_file.write(f"""
try:
with open(config_file, "r+", encoding="utf-8") as alias_file:
file_content = alias_file.read()
if "MFA CLI aliases start" not in file_content:
file_content += f"""
# MFA CLI aliases start
alias mfa_export='{INSTALL_DIR}/{SCRIPT_NAME} export_secrets'
alias mfa_add='{INSTALL_DIR}/{SCRIPT_NAME} add_secret'
Expand All @@ -54,12 +89,31 @@ def install_mfa_cli():
alias mfd='{INSTALL_DIR}/{SCRIPT_NAME} delete_secret'
alias mfh='{INSTALL_DIR}/{SCRIPT_NAME} mfa_help'
# MFA CLI aliases end
""")
"""
alias_file.seek(0)
alias_file.write(file_content)
alias_file.truncate()
except FileNotFoundError:
print(RED + f"Failed to add 'mfa-cli' aliases to {config_file}, file not found." + RESET)
except PermissionError:
print(RED + f"Failed to add 'mfa-cli' aliases to {config_file}, permission denied." + RESET)
print(GREEN + "mfa-cli installed successfully, please restart your terminal!" + RESET)

# Check if 'mfa-cli' already exists and uninstall it
if os.path.exists(INSTALL_DIR):
uninstall_mfa_cli()

def main():
"""
Main function.

This function checks if 'mfa-cli' is already installed and, if so, it uninstalls it before proceeding with the installation.
"""
if os.path.exists(INSTALL_DIR):
uninstall_mfa_cli()

install_mfa_cli()

# Proceed with installation
install_mfa_cli()
if __name__ == "__main__":
try:
main()
except Exception as exception_error:
print(exception_error)
sys.exit(1)
90 changes: 82 additions & 8 deletions script.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,30 @@
#!/usr/bin/env python3

"""
mfa-cli Script

This script provides a command-line interface (CLI) for storing secrets and generating MFA (Multi-Factor Authentication) codes.
The script allows users to add, delete, update, and list secrets, as well as generate MFA codes for specific secrets.
Secrets are stored in a JSON file located at ~/.mfa-cli/secrets.json.

Usage:
python script.py <command> [options]

Commands:
add_secret <name> <secret> Add a new secret
delete_secret <name> Delete a stored secret
list_secrets List all stored secrets
update_secret <name> <secret> Update an existing secret
generate_mfa <name> Generate an MFA code
export_secrets <file_path> Export secrets to a file
mfa_help Show this help message

Examples:
python script.py add_secret my_secret_name my_secret_key
python script.py delete_secret my_secret_name
python script.py list_secrets
python script.py update_secret my_secret_name new_secret_key
python script.py generate_mfa my_secret_name
python script.py export_secrets export_file.json
python script.py help
"""

import argparse
Expand All @@ -13,18 +36,26 @@
import time
import binascii
import os
import sys

MAX_SECRET_LENGTH = 256
SECRET_FILE = os.path.join(os.path.expanduser("~"), "mfa-cli", "secrets.json")

GREEN = "\033[92m"
RED = "\033[91m"
YELLOW = "\033[93m"
RESET = "\033[0m"

def load_secrets():
"""
Load secrets.
Load secrets from a file.

This function reads the contents of a secret file, parses it as JSON,
and returns the secrets as a dictionary. If the file does not exist,
it creates an empty file and returns an empty dictionary.

Returns:
dict: A dictionary containing the loaded secrets.

"""
try:
with open(SECRET_FILE, "r", encoding="utf-8") as file:
Expand All @@ -44,6 +75,10 @@ def load_secrets():
def save_secrets(secrets):
"""
Save secrets.

This function saves the provided secrets to a file named SECRET_FILE.
The secrets are serialized as JSON and written to the file.

"""
mfa_cli_dir = os.path.join(os.path.expanduser("~"), "mfa-cli")
os.makedirs(mfa_cli_dir, exist_ok=True)
Expand All @@ -56,6 +91,11 @@ def save_secrets(secrets):
def add_secret(name, secret):
"""
Add a secret.

This function adds a secret to the secrets file. If the secret already exists,
it prints an error message and does not add the secret. If the secret does not
exist, it adds the secret to the secrets file.

"""
if name and secret is not None:
if name not in load_secrets():
Expand All @@ -74,6 +114,11 @@ def add_secret(name, secret):
def delete_secret(name):
"""
Delete a secret.

This function deletes a secret from the secrets file. If the secret does not exist,
it prints an error message and does not delete the secret. If the secret exists,
it deletes the secret from the secrets file.

"""
secrets = load_secrets()
if name in secrets:
Expand All @@ -86,6 +131,8 @@ def delete_secret(name):
def list_secrets():
"""
List all stored secrets.

This function lists all the secrets stored in the secrets file.
"""
secrets = load_secrets()
if secrets:
Expand All @@ -98,6 +145,8 @@ def list_secrets():
def update_secret(name, secret):
"""
Update an existing secret.

This function updates an existing secret in the secrets file. If the secret does not exist, it prints an error message.
"""
try:
secrets = load_secrets()
Expand All @@ -122,6 +171,8 @@ def update_secret(name, secret):
def export_secrets(file_path):
"""
Export secrets to a file.

This function exports the secrets to a file. If the export fails, it prints an error message.
"""
file_path = os.path.join(os.getcwd(), file_path)
secrets = load_secrets()
Expand All @@ -135,6 +186,8 @@ def export_secrets(file_path):
def mfa_help():
"""
Show help message.

This function shows a help message with the available commands.
"""
print ("""
MFA CLI - Help
Expand Down Expand Up @@ -175,22 +228,37 @@ def mfa_help():
def generate_mfa(name):
"""
Generate MFA code.

This function generates an MFA code for the specified secret. If the secret does not exist, it prints an error message.
"""
# Load secrets from the secrets file
secrets = load_secrets()

# Check if the named secret exists
if name in secrets:
# Get the secret and pad it to a multiple of 8 bytes
secret = secrets[name]
try:
# Add padding if necessary
secret = secret.ljust((len(secret) + 7) // 8 * 8, '=')
counter = int(time.time()) // 30

# Decode the secret from base32
secret_bytes = base64.b32decode(secret, casefold=True)

# Calculate the hash for the current time
message = struct.pack(">Q", counter)
# Calculate the HMAC-SHA1 of the hash
hmac_digest = hmac.new(secret_bytes, message, hashlib.sha1).digest()
# Get the offset
offset = hmac_digest[-1] & 0x0F
# Get the 4 bytes at the offset
truncated_hash = hmac_digest[offset:offset + 4]
# Unpack the 4 bytes as a 32-bit big-endian integer
code = struct.unpack(">I", truncated_hash)[0] & 0x7FFFFFFF
# Get the code as a 6-digit integer
code = code % 1000000
print (GREEN + f"MFA code for {name}: {code:06}" + RESET)
# Print the code
print (GREEN + f"{code:06}" + RESET)
return code
except binascii.Error as export_error:
return print(RED + f"Error generating MFA for {name}: {str(export_error)}" + RESET)
Expand All @@ -199,7 +267,9 @@ def generate_mfa(name):

def main():
"""
Parse command-line arguments and execute the corresponding command.
Main function.

This function parses the command-line arguments and executes the corresponding command.
"""
parser = argparse.ArgumentParser(description="CLI to store secrets and generate MFA codes.")
parser.add_argument("command", choices=["add_secret", "generate_mfa", "delete_secret", "list_secrets", "update_secret", "export_secrets", "mfa_help"], help="Command to execute.")
Expand Down Expand Up @@ -249,4 +319,8 @@ def main():
print(RED + "Error: help command requires no arguments." + RESET)

if __name__ == "__main__":
main()
try:
main()
except Exception as exception_error:
print(exception_error)
sys.exit(1)
Loading