diff --git a/README.md b/README.md index c301bc5..6256f76 100644 --- a/README.md +++ b/README.md @@ -173,6 +173,17 @@ python src/main.py Enter your choice (1-5): ``` + When choosing **Add Entry**, you can now select **Password** or **2FA (TOTP)**. + +### Adding a 2FA Entry + +1. From the main menu choose **Add Entry** and select **2FA (TOTP)**. +2. Provide a label for the account (for example, `GitHub`). +3. Enter the derivation index you wish to use for this 2FA code. +4. Optionally specify the TOTP period and digit count. +5. SeedPass will display an `otpauth://` URI and secret that you can manually + enter into your authenticator app. + ### Managing Multiple Seeds diff --git a/src/main.py b/src/main.py index 01eeb95..5838e6d 100644 --- a/src/main.py +++ b/src/main.py @@ -591,13 +591,17 @@ def display_menu( while True: print("\nAdd Entry:") print("1. Password") - print("2. Back") + print("2. 2FA (TOTP)") + print("3. Back") sub_choice = input("Select entry type: ").strip() password_manager.update_activity() if sub_choice == "1": password_manager.handle_add_password() break elif sub_choice == "2": + password_manager.handle_add_totp() + break + elif sub_choice == "3": break else: print(colored("Invalid choice.", "red")) diff --git a/src/password_manager/manager.py b/src/password_manager/manager.py index 444ffaf..154602c 100644 --- a/src/password_manager/manager.py +++ b/src/password_manager/manager.py @@ -862,6 +862,62 @@ class PasswordManager: logging.error(f"Error during password generation: {e}", exc_info=True) print(colored(f"Error: Failed to generate password: {e}", "red")) + def handle_add_totp(self) -> None: + """Prompt for details and add a new TOTP entry.""" + try: + label = input("Enter the account label: ").strip() + if not label: + print(colored("Error: Label cannot be empty.", "red")) + return + + index_input = input("Enter derivation index (number): ").strip() + if not index_input.isdigit(): + print(colored("Error: Index must be a number.", "red")) + return + totp_index = int(index_input) + + period_input = input("TOTP period in seconds (default 30): ").strip() + period = 30 + if period_input: + if not period_input.isdigit(): + print(colored("Error: Period must be a number.", "red")) + return + period = int(period_input) + + digits_input = input("Number of digits (default 6): ").strip() + digits = 6 + if digits_input: + if not digits_input.isdigit(): + print(colored("Error: Digits must be a number.", "red")) + return + digits = int(digits_input) + + entry_id = self.entry_manager.get_next_index() + uri = self.entry_manager.add_totp(label, totp_index, period, digits) + + self.is_dirty = True + self.last_update = time.time() + + secret = TotpManager.derive_secret(self.parent_seed, totp_index) + + print(colored(f"\n[+] TOTP entry added with ID {entry_id}.\n", "green")) + print(colored("Add this URI to your authenticator app:", "cyan")) + print(colored(uri, "yellow")) + print(colored(f"Secret: {secret}\n", "cyan")) + + try: + self.sync_vault() + logging.info("Encrypted index posted to Nostr after TOTP add.") + except Exception as nostr_error: + logging.error( + f"Failed to post updated index to Nostr: {nostr_error}", + exc_info=True, + ) + + except Exception as e: + logging.error(f"Error during TOTP setup: {e}", exc_info=True) + print(colored(f"Error: Failed to add TOTP: {e}", "red")) + def handle_retrieve_entry(self) -> None: """ Handles retrieving a password from the index by prompting the user for the index number diff --git a/src/tests/test_cli_invalid_input.py b/src/tests/test_cli_invalid_input.py index cb65f0d..5e29ba4 100644 --- a/src/tests/test_cli_invalid_input.py +++ b/src/tests/test_cli_invalid_input.py @@ -39,6 +39,7 @@ def _make_pm(called, locked=None): last_activity=time.time(), nostr_client=SimpleNamespace(close_client_pool=lambda: None), handle_add_password=add, + handle_add_totp=lambda: None, handle_retrieve_entry=retrieve, handle_modify_entry=modify, update_activity=update, @@ -76,7 +77,7 @@ def test_out_of_range_menu(monkeypatch, capsys): def test_invalid_add_entry_submenu(monkeypatch, capsys): called = {"add": False, "retrieve": False, "modify": False} pm, _ = _make_pm(called) - inputs = iter(["1", "3", "2", "5"]) + inputs = iter(["1", "4", "3", "5"]) monkeypatch.setattr(main, "timed_input", lambda *_: next(inputs)) monkeypatch.setattr("builtins.input", lambda *_: next(inputs)) with pytest.raises(SystemExit):