diff --git a/README.md b/README.md index 6767c7c..011ab13 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ **⚠️ Disclaimer** -This software was not developed by an experienced security expert and should be used with caution. There may be bugs and missing features. Each vault chunk is limited to 50 KB and SeedPass periodically publishes a new snapshot to keep accumulated deltas small. The security of the program's memory management and logs has not been evaluated and may leak sensitive information. +This software was not developed by an experienced security expert and should be used with caution. There may be bugs and missing features. Each vault chunk is limited to 50 KB and SeedPass periodically publishes a new snapshot to keep accumulated deltas small. The security of the program's memory management and logs has not been evaluated and may leak sensitive information. Loss or exposure of the parent seed places all derived passwords, accounts, and other artifacts at risk. --- ### Supported OS @@ -53,6 +53,7 @@ SeedPass now uses the `portalocker` library for cross-platform file locking. No - **Optional External Backup Location:** Configure a second directory where backups are automatically copied. - **Auto‑Lock on Inactivity:** Vault locks after a configurable timeout for additional security. - **Secret Mode:** Copy retrieved passwords directly to your clipboard and automatically clear it after a delay. +- **Tagging Support:** Organize entries with optional tags and find them quickly via search. ## Prerequisites @@ -172,6 +173,7 @@ seedpass import --file "~/seedpass_backup.json" # Quickly find or retrieve entries seedpass search "github" +seedpass search --tags "work,personal" seedpass get "github" seedpass totp "email" # The code is printed and copied to your clipboard @@ -265,9 +267,10 @@ When choosing **Add Entry**, you can now select from: 1. From the main menu choose **Modify an Existing Entry** and enter the index of the 2FA code you want to edit. 2. SeedPass will show the current label, period, digit count, and archived status. 3. Enter new values or press **Enter** to keep the existing settings. -4. The updated entry is saved back to your encrypted vault. -5. Archived entries are hidden from lists but can be viewed or restored from the **List Archived** menu. -6. When editing an archived entry you'll be prompted to restore it after saving your changes. +4. When retrieving a 2FA entry you can press **E** to edit the label, period or digit count, or **A** to archive/unarchive it. +5. The updated entry is saved back to your encrypted vault. +6. Archived entries are hidden from lists but can be viewed or restored from the **List Archived** menu. +7. When editing an archived entry you'll be prompted to restore it after saving your changes. ### Using Secret Mode @@ -295,14 +298,14 @@ entry includes a `label`, while only password entries track a `url`. | Entry Type | Extra Fields | |---------------|---------------------------------------------------------------------------------------------------------------------------------------| -| Password | `username`, `url`, `length`, `archived`, optional `notes`, optional `custom_fields` (may include hidden fields) | -| 2FA (TOTP) | `index` or `secret`, `period`, `digits`, `archived`, optional `notes` | -| SSH Key | `index`, `archived`, optional `notes` | -| Seed Phrase | `index`, `word_count` *(mnemonic regenerated; never stored)*, `archived`, optional `notes` | -| PGP Key | `index`, `key_type`, `archived`, optional `user_id`, optional `notes` | -| Nostr Key Pair| `index`, `archived`, optional `notes` | -| Key/Value | `value`, `archived`, optional `notes`, optional `custom_fields` | -| Managed Account | `index`, `word_count`, `fingerprint`, `archived`, optional `notes` | +| Password | `username`, `url`, `length`, `archived`, optional `notes`, optional `custom_fields` (may include hidden fields), optional `tags` | +| 2FA (TOTP) | `index` or `secret`, `period`, `digits`, `archived`, optional `notes`, optional `tags` | +| SSH Key | `index`, `archived`, optional `notes`, optional `tags` | +| Seed Phrase | `index`, `word_count` *(mnemonic regenerated; never stored)*, `archived`, optional `notes`, optional `tags` | +| PGP Key | `index`, `key_type`, `archived`, optional `user_id`, optional `notes`, optional `tags` | +| Nostr Key Pair| `index`, `archived`, optional `notes`, optional `tags` | +| Key/Value | `value`, `archived`, optional `notes`, optional `custom_fields`, optional `tags` | +| Managed Account | `index`, `word_count`, `fingerprint`, `archived`, optional `notes`, optional `tags` | ### Managing Multiple Seeds diff --git a/docs/advanced_cli.md b/docs/advanced_cli.md index fa6298b..0ed3070 100644 --- a/docs/advanced_cli.md +++ b/docs/advanced_cli.md @@ -1,906 +1,159 @@ -# Advanced CLI Commands Documentation (Future Feature Set) +# SeedPass Advanced CLI and API Documentation ## Overview -The **Advanced CLI Commands** document provides an in-depth guide to the various command-line functionalities available in **SeedPass**, a secure password manager built on Bitcoin's BIP-85 standard. Designed for power users and developers, this guide outlines each command's purpose, usage, options, and examples to facilitate efficient and effective password management through the CLI. -> **Note:** This documentation describes planned functionality. The advanced CLI is not yet part of the stable release. It will align with the current SeedPass design, using fingerprint-based profiles and a forthcoming API to allow secure integration for power users and external applications. +Welcome to the **Advanced CLI and API Documentation** for **SeedPass**, a secure, deterministic password manager built on Bitcoin's BIP‑85 standard. This guide is designed for power users, developers, and system administrators who wish to leverage the full capabilities of SeedPass through the command line for scripting, automation, and integration. +SeedPass uses a `noun-verb` command structure (e.g., `seedpass entry get `) for a clear, scalable, and discoverable interface. You can explore the available actions for any command group with the `--help` flag (for example, `seedpass entry --help`). + +> **Note:** These commands describe planned functionality. The advanced CLI is not yet part of the stable release but will follow the current SeedPass design of fingerprint-based profiles and a local API for secure integrations. --- ## Table of Contents -1. [Command Reference](#command-reference) -2. [Detailed Command Descriptions](#detailed-command-descriptions) - - [1. Add a New Password Entry](#1-add-a-new-password-entry) - - [2. Retrieve a Password Entry](#2-retrieve-a-password-entry) - - [3. Modify an Existing Entry](#3-modify-an-existing-entry) - - [4. Delete an Entry](#4-delete-an-entry) - - [5. List All Entries](#5-list-all-entries) - - [6. Search for a Password Entry](#6-search-for-a-password-entry) - - [7. Get a Password by Query](#7-get-a-password-by-query) - - [8. Display a TOTP Code](#8-display-a-totp-code) - - [9. Export Passwords to a File](#9-export-passwords-to-a-file) - - [10. Import Passwords from a File](#8-import-passwords-from-a-file) - - [11. Display Help Information](#9-display-help-information) - - [12. Display Application Version](#10-display-application-version) - - [13. Change Master Password](#11-change-master-password) - - [14. Enable Auto-Lock](#12-enable-auto-lock) - - [15. Disable Auto-Lock](#13-disable-auto-lock) - - [16. Generate a Strong Password](#14-generate-a-strong-password) - - [17. Verify Script Checksum](#15-verify-script-checksum) - - [18. Post Encrypted Snapshots to Nostr](#16-post-encrypted-snapshots-to-nostr) - - [19. Retrieve from Nostr](#17-retrieve-from-nostr) - - [20. Display Nostr Public Key](#18-display-nostr-public-key) - - [21. Set Custom Nostr Relays](#19-set-custom-nostr-relays) - - [22. Enable "Secret" Mode](#20-enable-secret-mode) - - [23. Batch Post Snapshot Deltas to Nostr](#21-batch-post-snapshot-deltas-to-nostr) - - [24. Show All Passwords](#22-show-all-passwords) - - [23. Add Notes to an Entry](#23-add-notes-to-an-entry) - - [24. Add Tags to an Entry](#24-add-tags-to-an-entry) - - [25. Add Key/Value Entry](#25-add-keyvalue-entry) - - [26. Add Managed Account](#26-add-managed-account) - - [27. Search by Tag or Title](#26-search-by-tag-or-title) - - [28. Automatically Post Deltas to Nostr After Edit](#27-automatically-post-deltas-to-nostr-after-edit) - - [29. Initial Setup Prompt for Seed Generation/Import](#28-initial-setup-prompt-for-seed-generationimport) -3. [Notes on New CLI Commands](#notes-on-new-cli-commands) +1. [Global Options](#global-options) +2. [Command Group Reference](#command-group-reference) + - [Entry Commands](#entry-commands) + - [Vault Commands](#vault-commands) + - [Nostr Commands](#nostr-commands) + - [Config Commands](#config-commands) + - [Fingerprint Commands](#fingerprint-commands) + - [Utility Commands](#utility-commands) +3. [Detailed Command Descriptions](#detailed-command-descriptions) +4. [Planned API Integration](#planned-api-integration) +5. [Usage Guidelines](#usage-guidelines) --- -## Command Reference +## Global Options -The following table provides a quick reference to all available advanced CLI commands in SeedPass, including their actions, command syntax, short and long flags, and example usages. +These options can be used with any command. -| **Action** | **Command** | **Short Flag** | **Long Flag** | **Example Command** | -|-------------------------------------------|------------------------|----------------|-----------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| Add a new password entry | `add` | `-A` | `--add` | `seedpass add --title "GitHub" --url "https://github.com" --username "john_doe" --email "john@example.com" --notes "Primary GitHub account" --tags "work,development" --length 20` | -| Retrieve a password entry | `retrieve` | `-R` | `--retrieve` | `seedpass retrieve --index 3` or `seedpass retrieve --title "GitHub"` | -| Modify an existing entry | `modify` | `-M` | `--modify` | `seedpass modify --index 3 --title "GitHub Pro" --notes "Updated to pro account" --tags "work,development,pro" --length 22` | -| Delete an entry | `delete` | `-D` | `--delete` | `seedpass delete --index 3` | -| List all entries | `list` | `-L` | `--list` | `seedpass list --sort label` | -| Search for a password entry | `search` | `-S` | `--search` | `seedpass search "GitHub"` | -| Get password from query | `get` | | | `seedpass get "GitHub"` -| Display a TOTP code | `totp` | | | `seedpass totp "email"` -| | | | | `seedpass list --filter totp` -| Export passwords to a file | `export` | `-E` | `--export` | `seedpass export --file "backup_passwords.json"` | -| Import passwords from a file | `import` | `-I` | `--import` | `seedpass import --file "backup_passwords.json"` | -| Display help information | `help` | `-H` | `--help` | `seedpass help` | -| Display application version | `version` | `-V` | `--version` | `seedpass version` | -| Change master password | `changepw` | `-C` | `--changepw` | `seedpass changepw --new "NewSecureP@ssw0rd!"` | -| Enable auto-lock | `autolock --enable` | `-AL` | `--auto-lock --enable` | `seedpass autolock --enable --timeout 10` | -| Disable auto-lock | `autolock --disable` | `-DL` | `--auto-lock --disable` | `seedpass autolock --disable` | -| Generate a strong password | `generate` | `-G` | `--generate` | `seedpass generate --length 20` | -| Verify script checksum | `verify` | `-V` | `--verify` | `seedpass verify` | -| Post encrypted snapshots to Nostr | `post` | `-P` | `--post` | `seedpass post` | -| Retrieve snapshots from Nostr | `get-nostr` | `-GN` | `--get-nostr` | `seedpass get-nostr` | -| Display Nostr public key | `show-pubkey` | `-K` | `--show-pubkey` | `seedpass show-pubkey` | -| Set Custom Nostr Relays | `set-relays` | `-SR` | `--set-relays` | `seedpass set-relays --add "wss://relay1.example.com" --add "wss://relay2.example.com"` | -| Enable "Secret" Mode | `set-secret` | `-SS` | `--set-secret` | `seedpass set-secret --enable` or `seedpass set-secret --disable` | -| Batch Post Snapshot Deltas to Nostr | `batch-post` | `-BP` | `--batch-post` | `seedpass batch-post --start 0 --end 9` or `seedpass batch-post --range 10-19` | -| Show All Passwords | `show-all` | `-SA` | `--show-all` | `seedpass show-all` | -| Add Notes to an Entry | `add-notes` | `-AN` | `--add-notes` | `seedpass add-notes --index 3 --notes "This is a secured account"` | -| Add Tags to an Entry | `add-tags` | `-AT` | `--add-tags` | `seedpass add-tags --index 3 --tags "personal,finance"` | -| Add Key/Value entry | `add-kv` | `-KV` | `--add-kv` | `seedpass add-kv --label "API" --value "secret"` -| Add Managed Account | `add-managed` | `-AM` | `--add-managed` | `seedpass add-managed --label "Account"` -| Search by Tag or Title | `search-by` | `-SB` | `--search-by` | `seedpass search-by --tag "work"` or `seedpass search-by --title "GitHub"` | -| Automatically Post Deltas After Edit | `auto-post` | `-AP` | `--auto-post` | `seedpass auto-post --enable` or `seedpass auto-post --disable` | -| Initial Setup Prompt for Seed Generation/Import | `setup` | `-ST` | `--setup` | `seedpass setup` | +| Flag | Description | +| :--- | :--- | +| `--fingerprint ` | Specify which seed profile to use. If omitted, the most recently used profile is selected. | +| `--help`, `-h` | Display help information for a command or subcommand. | + +--- + +## Command Group Reference + +### Entry Commands + +Manage individual entries within a vault. + +| Action | Command | Examples | +| :--- | :--- | :--- | +| Add a new entry | `entry add` | `seedpass entry add --type password --label "GitHub" --username "user" --length 20` | +| Retrieve an entry's secret | `entry get` | `seedpass entry get "GitHub"` | +| List entries | `entry list` | `seedpass entry list --sort label` | +| Search for entries | `entry search` | `seedpass entry search "GitHub"` | +| Modify an entry | `entry modify` | `seedpass entry modify "GitHub" --notes "New note"` | +| Delete an entry | `entry delete` | `seedpass entry delete "GitHub"` | + +### Vault Commands + +Manage the entire vault for a profile. + +| Action | Command | Examples | +| :--- | :--- | :--- | +| Export the vault | `vault export` | `seedpass vault export --file backup.json` | +| Import a vault | `vault import` | `seedpass vault import --file backup.json` | +| Change master password | `vault changepw` | `seedpass vault changepw` | + +### Nostr Commands + +Interact with the Nostr network for backup and synchronization. + +| Action | Command | Examples | +| :--- | :--- | :--- | +| Sync with relays | `nostr sync` | `seedpass nostr sync` | +| Get public key | `nostr get-pubkey` | `seedpass nostr get-pubkey` | +| Manage relays | `nostr relays` | `seedpass nostr relays --add wss://relay.example.com` | + +### Config Commands + +Manage profile‑specific settings. + +| Action | Command | Examples | +| :--- | :--- | :--- | +| Get a setting value | `config get` | `seedpass config get inactivity_timeout` | +| Set a setting value | `config set` | `seedpass config set secret_mode true` | + +### Fingerprint Commands + +Manage seed profiles (fingerprints). + +| Action | Command | Examples | +| :--- | :--- | :--- | +| Add a new profile | `fingerprint add` | `seedpass fingerprint add` | +| List all profiles | `fingerprint list` | `seedpass fingerprint list` | +| Remove a profile | `fingerprint remove` | `seedpass fingerprint remove ` | +| Set active profile | `fingerprint use` | `seedpass fingerprint use ` | + +### Utility Commands + +Miscellaneous helper commands. + +| Action | Command | Examples | +| :--- | :--- | :--- | +| Generate a password | `util generate-password` | `seedpass util generate-password --length 24` | +| Verify script checksum | `util verify-checksum` | `seedpass util verify-checksum` | --- ## Detailed Command Descriptions -### 1. Add a New Password Entry +### `entry` Commands -**Command:** `add` -**Short Flag:** `-A` -**Long Flag:** `--add` +- **`seedpass entry add`** – Add a new entry. Use `--type` to specify `password`, `totp`, `ssh`, `pgp`, `nostr`, `key-value`, or `managed-account`. Include `--tags tag1,tag2` to categorize the entry. +- **`seedpass entry get `** – Retrieve the primary secret for one matching entry. +- **`seedpass entry list`** – List entries in the vault, optionally sorted or filtered. +- **`seedpass entry search `** – Search across labels, usernames, URLs, notes, and tags. +- **`seedpass entry modify `** – Update fields on an existing entry. Use `--archive` to hide or `--restore` to un‑archive. Specify `--tags tag1,tag2` to replace the entry's tags. +- **`seedpass entry delete `** – Permanently delete an entry after confirmation. -**Description:** -Adds a new password entry to the password manager. This command allows users to specify various attributes of the password entry, including title, URL, username, email, notes, tags, and desired password length. +### `vault` Commands -**Usage Example:** -```bash -seedpass add --title "GitHub" --url "https://github.com" --username "john_doe" --email "john@example.com" --notes "Primary GitHub account" --tags "work,development" --length 20 -``` +- **`seedpass vault export`** – Export the entire vault to an encrypted JSON file. +- **`seedpass vault import`** – Import entries from an exported file, replacing the current vault after creating a backup. +- **`seedpass vault changepw`** – Interactively change the master password for the current profile. -**Options:** -- `--title` (`-T`): The title or name of the service. -- `--url` (`-U`): The URL of the service. -- `--username` (`-UN`): The username associated with the account. -- `--email` (`-E`): The email address linked to the account. -- `--notes` (`-N`): Additional notes or comments about the account. -- `--tags` (`-TG`): Comma-separated tags for categorization. -- `--length` (`-L`): Desired length of the generated password. +### `nostr` Commands + +- **`seedpass nostr sync`** – Perform a two‑way sync with configured Nostr relays. +- **`seedpass nostr get-pubkey`** – Display the Nostr public key for the active profile. +- **`seedpass nostr relays`** – Manage the relay list (`--list`, `--add`, `--remove`, `--reset`). + +### `config` Commands + +- **`seedpass config get `** – Retrieve a configuration value such as `inactivity_timeout`, `secret_mode`, or `auto_sync`. +- **`seedpass config set `** – Set a configuration value for the active profile. + +### `fingerprint` Commands + +- **`seedpass fingerprint add`** – Add a new seed profile (interactive or via `--import-seed`). +- **`seedpass fingerprint list`** – List available profiles by fingerprint. +- **`seedpass fingerprint remove `** – Delete a profile and its data after confirmation. +- **`seedpass fingerprint use `** – Make the given fingerprint active in the current shell session. + +### `util` Commands + +- **`seedpass util generate-password`** – Generate a strong password of the requested length. +- **`seedpass util verify-checksum`** – Verify the program checksum for integrity. --- -### 2. Retrieve a Password Entry +## Planned API Integration -**Command:** `retrieve` -**Short Flag:** `-R` -**Long Flag:** `--retrieve` - -**Description:** -Retrieves a password entry based on either its index or title. This allows users to access specific passwords without browsing through all entries. - -**Usage Examples:** -```bash -seedpass retrieve --index 3 -seedpass retrieve --title "GitHub" -``` - -**Options:** -- `--index` (`-I`): The numerical index of the password entry. -- `--title` (`-T`): The title of the password entry. - ---- - -### 3. Modify an Existing Entry - -**Command:** `modify` -**Short Flag:** `-M` -**Long Flag:** `--modify` - -**Description:** -Modifies an existing password entry. Users can update various attributes of the entry, such as title, notes, tags, and password length. - -**Usage Example:** -```bash -seedpass modify --index 3 --title "GitHub Pro" --notes "Updated to pro account" --tags "work,development,pro" --length 22 -``` - -**Options:** -- `--index` (`-I`): The numerical index of the password entry to modify. -- `--title` (`-T`): New title for the password entry. -- `--notes` (`-N`): Updated notes or comments. -- `--tags` (`-TG`): Updated comma-separated tags. -- `--length` (`-L`): New desired password length. - ---- - -### 4. Delete an Entry - -**Command:** `delete` -**Short Flag:** `-D` -**Long Flag:** `--delete` - -**Description:** -Deletes a password entry from the password manager based on its index. - -**Usage Example:** -```bash -seedpass delete --index 3 -``` - -**Options:** -- `--index` (`-I`): The numerical index of the password entry to delete. - ---- - -### 5. List All Entries - -**Command:** `list` -**Short Flag:** `-L` -**Long Flag:** `--list` - -**Description:** -Lists all password entries stored in the password manager. You can sort the output by index, label, or username and filter by entry type. - -**Usage Example:** -```bash -seedpass list --sort label -seedpass list --filter totp -``` - ---- - -### 6. Search for a Password Entry - -**Command:** `search` -**Short Flag:** `-S` -**Long Flag:** `--search` - -**Description:** -Searches for password entries based on a query string, allowing users to find specific entries without knowing their exact titles or indices. - -**Usage Example:** -```bash -seedpass search "GitHub" -``` - -**Options:** -- ``: The search string to look for in titles, usernames, URLs or notes. - ---- - -### 7. Get a Password by Query - -**Command:** `get` - -**Description:** -Searches for a password entry and immediately prints the generated password when exactly one match is found. - -**Usage Example:** -```bash -seedpass get "GitHub" -``` - ---- - -### 8. Display a TOTP Code - -**Command:** `totp` - -**Description:** -Looks up a TOTP entry by query and prints the current code. The code is also copied to your clipboard if possible. - -**Usage Example:** -```bash -seedpass totp "email" -``` - ---- - -### 9. Export Passwords to a File - ---- - -### 7. Export Passwords to a File - -**Command:** `export` -**Short Flag:** `-E` -**Long Flag:** `--export` - -**Description:** -Exports password entries to a specified file in JSON format, enabling users to back up their data or transfer it to another system. - -**Usage Example:** -```bash -seedpass export --file "backup_passwords.json" -``` - -**Options:** -- `--file` (`-F`): The destination file path for the exported data. If omitted, the export - is saved to the current profile's `exports` directory under `~/.seedpass//exports/`. - ---- - -### 8. Import Passwords from a File - -**Command:** `import` -**Short Flag:** `-I` -**Long Flag:** `--import` - -**Description:** -Imports password entries from a specified JSON file into the password manager, allowing users to restore backups or migrate data. - -**Usage Example:** -```bash -seedpass import --file "backup_passwords.json" -``` - -**Options:** -- `--file` (`-F`): The source file path containing the password entries to import. - ---- - -### 9. Display Help Information - -**Command:** `help` -**Short Flag:** `-H` -**Long Flag:** `--help` - -**Description:** -Displays help information for SeedPass commands, providing users with guidance on available options and usage patterns. - -**Usage Example:** -```bash -seedpass help -``` - ---- - -### 10. Display Application Version - -**Command:** `version` -**Short Flag:** `-V` -**Long Flag:** `--version` - -**Description:** -Displays the current version of the SeedPass application, helping users verify their installed version or check for updates. - -**Usage Example:** -```bash -seedpass version -``` - ---- - -### 11. Change Master Password - -**Command:** `changepw` -**Short Flag:** `-C` -**Long Flag:** `--changepw` - -**Description:** -Allows users to change the master password used to encrypt and decrypt their password entries, enhancing account security. - -**Usage Example:** -```bash -seedpass changepw --new "NewSecureP@ssw0rd!" -``` - -**Options:** -- `--new` (`-N`): The new master password to set. - ---- - -### 12. Enable Auto-Lock - -**Command:** `autolock --enable` -**Short Flag:** `-AL` -**Long Flag:** `--auto-lock --enable` - -**Description:** -Enables the auto-lock feature, which automatically locks the password manager after a specified period of inactivity, enhancing security. - -**Usage Example:** -```bash -seedpass autolock --enable --timeout 10 -``` - -**Options:** -- `--enable`: Flag to enable the auto-lock feature. -- `--timeout` (`-T`): The duration (in minutes) of inactivity before auto-lock is triggered. - ---- - -### 13. Disable Auto-Lock - -**Command:** `autolock --disable` -**Short Flag:** `-DL` -**Long Flag:** `--auto-lock --disable` - -**Description:** -Disables the auto-lock feature, preventing the password manager from automatically locking after inactivity. - -**Usage Example:** -```bash -seedpass autolock --disable -``` - ---- - -### 14. Generate a Strong Password - -**Command:** `generate` -**Short Flag:** `-G` -**Long Flag:** `--generate` - -**Description:** -Generates a strong, random password of specified length, aiding users in creating secure credentials. - -**Usage Example:** -```bash -seedpass generate --length 20 -``` - -**Options:** -- `--length` (`-L`): The desired length of the generated password. - ---- - -### 15. Verify Script Checksum - -**Command:** `verify` -**Short Flag:** `-V` -**Long Flag:** `--verify` - -**Description:** -Verifies the integrity of the SeedPass script by checking its checksum, ensuring that the code has not been tampered with. - -**Usage Example:** -```bash -seedpass verify -``` - ---- - -### 16. Post Encrypted Snapshots to Nostr - -**Command:** `post` -**Short Flag:** `-P` -**Long Flag:** `--post` - -**Description:** -Posts encrypted snapshot chunks of the index to the Nostr network, followed by compact delta events for subsequent changes. This approach enables reliable backups and efficient synchronization across devices. - -**Usage Example:** -```bash -seedpass post -``` - ---- - -### 17. Retrieve from Nostr - -**Command:** `get-nostr` -**Short Flag:** `-GN` -**Long Flag:** `--get-nostr` - -**Description:** -Retrieves the encrypted snapshot chunks and any delta events from the Nostr network, allowing users to reconstruct the latest index on a new device. - -**Usage Example:** -```bash -seedpass get-nostr -``` - ---- - -### 18. Display Nostr Public Key - -**Command:** `show-pubkey` -**Short Flag:** `-K` -**Long Flag:** `--show-pubkey` - -**Description:** -Displays the user's Nostr public key (npub), which is used for identifying their account on the Nostr network. - -**Usage Example:** -```bash -seedpass show-pubkey -``` - ---- - -### 19. Set Custom Nostr Relays - -**Command:** `set-relays` -**Short Flag:** `-SR` -**Long Flag:** `--set-relays` - -**Description:** -Allows users to specify custom Nostr relays for publishing their encrypted backup snapshots, providing flexibility and control over data distribution. -Relay URLs are stored in an encrypted configuration file located in `~/.seedpass//seedpass_config.json.enc` and loaded each time the Nostr client starts. New accounts use the following default relays until changed: - -``` -wss://relay.snort.social -wss://nostr.oxtr.dev -wss://relay.primal.net -``` - -**Usage Example:** -```bash -seedpass set-relays --add "wss://relay1.example.com" --add "wss://relay2.example.com" -``` - -**Options:** -- `--add`: Adds a new relay URL to the list of custom relays. - ---- - -### 20. Enable "Secret" Mode - -**Command:** `set-secret` -**Short Flag:** `-SS` -**Long Flag:** `--set-secret` - -**Description:** -Enables or disables "secret" mode, where retrieved passwords are copied directly to the clipboard instead of being displayed on the screen, enhancing security. - -**Usage Examples:** -```bash -seedpass set-secret --enable -seedpass set-secret --disable -``` - -**Options:** -- `--enable`: Activates "secret" mode. -- `--disable`: Deactivates "secret" mode. - -You can also enable or disable secret mode from the interactive Settings menu by selecting **Toggle Secret Mode**. - ---- - -### 21. Batch Post Snapshot Deltas to Nostr - -**Command:** `batch-post` -**Short Flag:** `-BP` -**Long Flag:** `--batch-post` - -**Description:** -Posts a specified range of snapshot delta events to the Nostr network in batches, ensuring efficient and manageable data transmission. - -**Usage Examples:** -```bash -seedpass batch-post --start 0 --end 9 -seedpass batch-post --range 10-19 -``` - -**Options:** -- `--start` (`-S`): The starting index of the batch. -- `--end` (`-E`): The ending index of the batch. -- `--range` (`-R`): Specifies a range in the format `start-end`. - ---- - -### 22. Show All Passwords - -**Command:** `show-all` -**Short Flag:** `-SA` -**Long Flag:** `--show-all` - -**Description:** -Displays all stored password entries along with their associated index numbers, titles, and tags, providing a comprehensive view for management purposes. - -**Usage Example:** -```bash -seedpass show-all -``` - ---- - -### 23. Add Notes to an Entry - -**Command:** `add-notes` -**Short Flag:** `-AN` -**Long Flag:** `--add-notes` - -**Description:** -Adds or updates notes for a specific password entry, allowing users to include additional information or comments. - -**Usage Example:** -```bash -seedpass add-notes --index 3 --notes "This is a secured account" -``` - -**Options:** -- `--index` (`-I`): The numerical index of the password entry. -- `--notes` (`-N`): The notes or comments to add. - ---- - -### 24. Add Tags to an Entry - -**Command:** `add-tags` -**Short Flag:** `-AT` -**Long Flag:** `--add-tags` - -**Description:** -Adds or updates tags for a specific password entry, enabling better categorization and organization. - -**Usage Example:** -```bash -seedpass add-tags --index 3 --tags "personal,finance" -``` - -**Options:** -- `--index` (`-I`): The numerical index of the password entry. -- `--tags` (`-TG`): Comma-separated tags to add. - ---- - -### 25. Add Key/Value Entry - -**Command:** `add-kv` -**Short Flag:** `-KV` -**Long Flag:** `--add-kv` - -**Description:** -Creates a simple key/value entry for storing items like API keys or configuration snippets. The value can be copied to the clipboard when secret mode is enabled. - -**Usage Example:** -```bash -seedpass add-kv --label "API" --value "secret" --notes "Service token" -``` - -**Options:** -- `--label` (`-L`): Descriptive label for the entry. -- `--value` (`-V`): The secret value to store. -- `--notes` (`-N`): Optional notes about the entry. - ---- - -### 26. Add Managed Account - -**Command:** `add-managed` -**Short Flag:** `-AM` -**Long Flag:** `--add-managed` - -**Description:** -Creates a managed account derived from the current seed profile. The child profile is stored in `.seedpass//accounts/`. - -Managed account seeds are always **12 words** long. - -**Usage Example:** -```bash -seedpass add-managed --label "Account" -``` - -When loaded, the breadcrumb shows ` > Managed Account > `. Press Enter on the main menu to return to the parent profile. - -### 27. Search by Tag or Title - -**Command:** `search-by` -**Short Flag:** `-SB` -**Long Flag:** `--search-by` - -**Description:** -Allows users to search for password entries based on specific tags or titles, enhancing the ability to locate entries quickly. - -**Usage Examples:** -```bash -seedpass search-by --tag "work" -seedpass search-by --title "GitHub" -``` - -**Options:** -- `--tag` (`-T`): The tag to search for. -- `--title` (`-Ti`): The title to search for. - ---- - -### 28. Automatically Post Deltas to Nostr After Edit - -**Command:** `auto-post` -**Short Flag:** `-AP` -**Long Flag:** `--auto-post` - -**Description:** -Enables or disables the automatic posting of snapshot delta events to the Nostr network whenever an edit occurs, ensuring real-time backups. - -**Usage Examples:** -```bash -seedpass auto-post --enable -seedpass auto-post --disable -``` - -**Options:** -- `--enable`: Activates automatic posting. -- `--disable`: Deactivates automatic posting. - ---- - -### 29. Initial Setup Prompt for Seed Generation/Import - -**Command:** `setup` -**Short Flag:** `-ST` -**Long Flag:** `--setup` - -**Description:** -Guides users through the initial setup process, allowing them to choose between generating a new seed or importing an existing one. This command also handles the encryption of the seed and the creation of a profile. - -**Usage Example:** -```bash -seedpass setup -``` - -**Features to Implement:** -- **Seed Choice Prompt:** Asks users whether they want to generate a new seed or import an existing one. -- **Encryption of Seed:** Uses the user-selected password to encrypt the seed, whether generated or imported. -- **Profile Creation:** Upon first login, automatically generates a profile and checks for existing snapshot data that can be pulled and decrypted. - ---- - -## CLI Commands for Managing Fingerprints - -SeedPass provides a set of Command-Line Interface (CLI) commands to facilitate the management of fingerprints. These commands allow users to import, remove, list, and switch between fingerprints efficiently. - -### 1. List All Fingerprints - -**Command:** - -```bash -seedpass fingerprint list -``` - -**Description:** - -Displays all available fingerprints stored in the `~/.seedpass/` directory. - -**Example Output:** - -``` -Available Fingerprints: -1. A1B2C3D4 -2. E5F6G7H8 -3. I9J0K1L2 -``` - -### 2. Import a New Seed - -**Command:** - -```bash -seedpass fingerprint import -``` - -**Description:** - -Guides the user through the process of importing a new seed, which automatically generates a corresponding fingerprint. - -**Steps:** - -1. **Choose Seed Option:** - - **Generate:** SeedPass can generate a new seed. - - **Import:** Users can import an existing seed by entering their 12-word mnemonic phrase. - -2. **Provide Seed Details:** - - If importing, enter the 12-word mnemonic phrase. - - If generating, SeedPass creates a new seed complying with BIP-39 standards. - -3. **Set Password:** - - Enter a strong password to encrypt the seed and associated data. - -4. **Confirmation:** - - SeedPass generates the fingerprint and creates the corresponding directory structure. - -**Example:** - -```bash -seedpass fingerprint import -``` - -### 3. Remove an Existing Fingerprint - -**Command:** - -```bash -seedpass fingerprint remove -``` - -**Description:** - -Removes a specified fingerprint and deletes all associated data. - -**Parameters:** - -- ``: The identifier of the fingerprint to remove (e.g., `A1B2C3D4`). - -**Example:** - -```bash -seedpass fingerprint remove A1B2C3D4 -``` - -**Confirmation Prompt:** - -``` -Are you sure you want to remove A1B2C3D4? This action cannot be undone. (y/n): -``` - -### 4. Switch Active Fingerprint - -**Command:** - -```bash -seedpass fingerprint switch -``` - -**Description:** - -Switches the active fingerprint to the specified one, loading its data for use. - -**Parameters:** - -- ``: The identifier of the fingerprint to activate. - -**Example:** - -```bash -seedpass fingerprint switch E5F6G7H8 -``` - -### 5. View Current Active Fingerprint - -**Command:** - -```bash -seedpass fingerprint current -``` - -**Description:** - -Displays the currently active fingerprint. - -**Example Output:** - -``` -Current Active Fingerprint: -A1B2C3D4 -``` - -### 6. Rename a Fingerprint - -**Command:** - -```bash -seedpass fingerprint rename -``` - -**Description:** - -Renames an existing fingerprint for better identification. - -**Parameters:** - -- ``: The current identifier of the fingerprint. -- ``: The new desired identifier. - -**Example:** - -```bash -seedpass fingerprint rename A1B2C3D4 PersonalProfile -``` - -*Note: Renaming does not affect the underlying seed data but provides a more recognizable identifier for the user.* - ---- - -## Notes on New CLI Commands - -1. **Automatically Post Deltas to Nostr After Edit (`auto-post`):** - - **Purpose:** Enables or disables the automatic posting of snapshot deltas to Nostr whenever an edit occurs. - - **Usage Examples:** - - Enable auto-post: `seedpass auto-post --enable` - - Disable auto-post: `seedpass auto-post --disable` - -2. **Initial Setup Prompt for Seed Generation/Import (`setup`):** - - **Purpose:** Guides users through the initial setup process, allowing them to choose between generating a new seed or importing an existing one. - - **Features to Implement:** - - **Seed Choice Prompt:** Ask users whether they want to generate a new seed or import an existing one. - - **Encryption of Seed:** Use the user-selected password to encrypt the seed, whether generated or imported. - - **Profile Creation:** Upon first login, automatically generate a profile and check for existing snapshot data that can be pulled and decrypted. - - **Usage Example:** `seedpass setup` - -3. **Advanced CLI Enhancements:** - - **Toggle "Secret" Mode via CLI:** - - **Description:** Allows users to enable or disable "secret" mode directly through the CLI. - - **Usage Examples:** - - Enable secret mode: `seedpass set-secret --enable` - - Disable secret mode: `seedpass set-secret --disable` - - - **Initial Seed Setup Flow:** - - **Description:** When running `seedpass setup`, prompts users to either enter an existing seed or generate a new one, followed by password creation for encryption. - - **Usage Example:** `seedpass setup` - - - **Automatic Profile Generation and Snapshot Retrieval:** - - **Description:** During the initial setup or first login, generates a profile and attempts to retrieve and decrypt any existing snapshots and deltas from Nostr. - - **Usage Example:** `seedpass setup` (handles internally) +The advanced CLI will act as a client for a locally hosted REST API. Starting the API loads the vault into memory after prompting for the master password and prints a temporary API key. Third‑party clients include this key in the `Authorization` header when calling endpoints such as `GET /api/v1/entry?query=GitHub`. The server automatically shuts down after a period of inactivity or when `seedpass api stop` is run. --- ## Usage Guidelines -- **Help Commands:** For detailed information on any command, use the help flag. For example: - ```bash - seedpass add --help - ``` - -- **Consistent Flag Usage:** Use either the short flag or the long flag as per your preference, but maintain consistency for readability. - -- **Security Considerations:** - - Always use strong, unique master passwords. - - Regularly back up your encrypted index. - - Enable auto-lock to enhance security. - - Be cautious when using the `export` and `import` commands to handle sensitive data securely. - -- **Nostr Integration:** - - Ensure that your Nostr relays are reliable and secure. - - Regularly verify your Nostr public key and manage relays through the `set-relays` command. -## Planned API Integration - -SeedPass will expose a local API that allows third-party applications and browser extensions to interact with your encrypted vault. The advanced CLI will act as a power-user client for these endpoints, mapping commands directly to the API while preserving the fingerprint-based security model. This design enables automation and programmatic access without compromising security. - - ---- - -## Conclusion - -This **Advanced CLI Commands Documentation** serves as a comprehensive guide for utilizing SeedPass's full suite of command-line functionalities. By understanding and effectively leveraging these commands, users can manage their passwords securely and efficiently, ensuring both ease of use and robust security measures. - -For further assistance or to contribute to the development of SeedPass, please refer to the [Contributing Guidelines](CONTRIBUTING.md) or open an issue on the [GitHub Repository](https://github.com/PR0M3TH3AN/SeedPass/issues). - ---- \ No newline at end of file +- Use the `--help` flag for details on any command. +- Set a strong master password and regularly export encrypted backups. +- Adjust configuration values like `inactivity_timeout` or `secret_mode` through the `config` commands. +- `entry get` is script‑friendly and can be piped into other commands. diff --git a/docs/json_entries.md b/docs/json_entries.md index 0d9c374..e25673e 100644 --- a/docs/json_entries.md +++ b/docs/json_entries.md @@ -77,6 +77,7 @@ Each entry is stored within `seedpass_entries_db.json.enc` under the `entries` d "notes": "", "custom_fields": [], "origin": "", + "tags": [], "index": 0 } ``` @@ -97,6 +98,7 @@ Each entry is stored within `seedpass_entries_db.json.enc` under the `entries` d - **index** (`integer`, optional): BIP-85 derivation index for entries that derive material from a seed. - **word_count** (`integer`, managed_account only): Number of words in the child seed. Managed accounts always use `12`. - **fingerprint** (`string`, managed_account only): Identifier of the child profile, used for its directory name. +- **tags** (`array`, optional): Category labels to aid in organization and search. Example: ```json @@ -126,6 +128,7 @@ Each entry is stored within `seedpass_entries_db.json.enc` under the `entries` d "custom_fields": [ {"name": "department", "value": "finance"} ], + "tags": ["work"], "timestamp": "2024-04-27T12:34:56Z", "metadata": { "created_at": "2024-04-27T12:34:56Z", @@ -250,6 +253,7 @@ Each entry is stored within `seedpass_entries_db.json.enc` under the `entries` d "key": "api_key", "value": "" }, + "tags": ["api"], "timestamp": "2024-04-27T12:40:56Z" } ``` diff --git a/landing/index.html b/landing/index.html index 2ac9623..fa5cd60 100644 --- a/landing/index.html +++ b/landing/index.html @@ -155,6 +155,7 @@ flowchart TD
  • Auto-lock after inactivity
  • Derive nested managed account seeds
  • Secret Mode copies passwords to your clipboard
  • +
  • Group entries using tags for easy cross-type search
  • @@ -204,6 +205,7 @@ Enter your choice (1-7) or press Enter to exit:

    Disclaimer

    ⚠️ Disclaimer: This software was not developed by an experienced security expert and should be used with caution. There may be bugs and missing features. Additionally, the security of the program's memory management and logs has not been evaluated and may leak sensitive information.

    +

    Loss or exposure of the parent seed places all derived passwords, accounts, and other artifacts at risk.

    Snapshot chunks are limited to 50 KB and rotated when deltas accumulate.

    diff --git a/src/password_manager/entry_management.py b/src/password_manager/entry_management.py index d67bda8..260909e 100644 --- a/src/password_manager/entry_management.py +++ b/src/password_manager/entry_management.py @@ -79,6 +79,7 @@ class EntryManager: if "word_count" not in entry and "words" in entry: entry["word_count"] = entry["words"] entry.pop("words", None) + entry.setdefault("tags", []) logger.debug("Index loaded successfully.") return data except Exception as e: @@ -127,6 +128,7 @@ class EntryManager: archived: bool = False, notes: str = "", custom_fields: List[Dict[str, Any]] | None = None, + tags: list[str] | None = None, ) -> int: """ Adds a new entry to the encrypted JSON index file. @@ -154,6 +156,7 @@ class EntryManager: "kind": EntryType.PASSWORD.value, "notes": notes, "custom_fields": custom_fields or [], + "tags": tags or [], } logger.debug(f"Added entry at index {index}: {data['entries'][str(index)]}") @@ -197,6 +200,7 @@ class EntryManager: period: int = 30, digits: int = 6, notes: str = "", + tags: list[str] | None = None, ) -> str: """Add a new TOTP entry and return the provisioning URI.""" entry_id = self.get_next_index() @@ -216,6 +220,7 @@ class EntryManager: "digits": digits, "archived": archived, "notes": notes, + "tags": tags or [], } else: entry = { @@ -227,6 +232,7 @@ class EntryManager: "digits": digits, "archived": archived, "notes": notes, + "tags": tags or [], } data["entries"][str(entry_id)] = entry @@ -248,6 +254,7 @@ class EntryManager: index: int | None = None, notes: str = "", archived: bool = False, + tags: list[str] | None = None, ) -> int: """Add a new SSH key pair entry. @@ -268,6 +275,7 @@ class EntryManager: "label": label, "notes": notes, "archived": archived, + "tags": tags or [], } self._save_index(data) self.update_checksum() @@ -297,6 +305,7 @@ class EntryManager: user_id: str = "", notes: str = "", archived: bool = False, + tags: list[str] | None = None, ) -> int: """Add a new PGP key entry.""" @@ -314,6 +323,7 @@ class EntryManager: "user_id": user_id, "notes": notes, "archived": archived, + "tags": tags or [], } self._save_index(data) self.update_checksum() @@ -347,6 +357,7 @@ class EntryManager: index: int | None = None, notes: str = "", archived: bool = False, + tags: list[str] | None = None, ) -> int: """Add a new Nostr key pair entry.""" @@ -362,6 +373,7 @@ class EntryManager: "label": label, "notes": notes, "archived": archived, + "tags": tags or [], } self._save_index(data) self.update_checksum() @@ -376,6 +388,7 @@ class EntryManager: notes: str = "", custom_fields=None, archived: bool = False, + tags: list[str] | None = None, ) -> int: """Add a new generic key/value entry.""" @@ -391,6 +404,7 @@ class EntryManager: "notes": notes, "archived": archived, "custom_fields": custom_fields or [], + "tags": tags or [], } self._save_index(data) @@ -431,6 +445,7 @@ class EntryManager: words_num: int = 24, notes: str = "", archived: bool = False, + tags: list[str] | None = None, ) -> int: """Add a new derived seed phrase entry.""" @@ -447,6 +462,7 @@ class EntryManager: "word_count": words_num, "notes": notes, "archived": archived, + "tags": tags or [], } self._save_index(data) self.update_checksum() @@ -483,6 +499,7 @@ class EntryManager: index: int | None = None, notes: str = "", archived: bool = False, + tags: list[str] | None = None, ) -> int: """Add a new managed account seed entry. @@ -518,6 +535,7 @@ class EntryManager: "notes": notes, "fingerprint": fingerprint, "archived": archived, + "tags": tags or [], } self._save_index(data) @@ -642,6 +660,7 @@ class EntryManager: digits: Optional[int] = None, value: Optional[str] = None, custom_fields: List[Dict[str, Any]] | None = None, + tags: list[str] | None = None, **legacy, ) -> None: """ @@ -727,6 +746,10 @@ class EntryManager: f"Updated custom fields for index {index}: {custom_fields}" ) + if tags is not None: + entry["tags"] = tags + logger.debug(f"Updated tags for index {index}: {tags}") + data["entries"][str(index)] = entry logger.debug(f"Modified entry at index {index}: {entry}") @@ -890,8 +913,10 @@ class EntryManager: etype = entry.get("type", entry.get("kind", EntryType.PASSWORD.value)) label = entry.get("label", entry.get("website", "")) notes = entry.get("notes", "") + tags = entry.get("tags", []) label_match = query_lower in label.lower() notes_match = query_lower in notes.lower() + tags_match = any(query_lower in str(t).lower() for t in tags) if etype == EntryType.PASSWORD.value: username = entry.get("username", "") @@ -908,6 +933,7 @@ class EntryManager: or query_lower in url.lower() or notes_match or custom_match + or tags_match ): results.append( ( @@ -931,6 +957,7 @@ class EntryManager: or query_lower in value_field.lower() or notes_match or custom_match + or tags_match ): results.append( ( @@ -942,7 +969,7 @@ class EntryManager: ) ) else: - if label_match or notes_match: + if label_match or notes_match or tags_match: results.append( ( int(idx), diff --git a/src/password_manager/manager.py b/src/password_manager/manager.py index cb502f1..7b4a010 100644 --- a/src/password_manager/manager.py +++ b/src/password_manager/manager.py @@ -981,6 +981,12 @@ class PasswordManager: username = input("Enter the username (optional): ").strip() url = input("Enter the URL (optional): ").strip() notes = input("Enter notes (optional): ").strip() + tags_input = input("Enter tags (comma-separated, optional): ").strip() + tags = ( + [t.strip() for t in tags_input.split(",") if t.strip()] + if tags_input + else [] + ) custom_fields: list[dict[str, object]] = [] while True: @@ -1021,6 +1027,7 @@ class PasswordManager: archived=False, notes=notes, custom_fields=custom_fields, + tags=tags, ) # Mark database as dirty for background sync @@ -1084,6 +1091,14 @@ class PasswordManager: ) continue notes = input("Notes (optional): ").strip() + tags_input = input( + "Enter tags (comma-separated, optional): " + ).strip() + tags = ( + [t.strip() for t in tags_input.split(",") if t.strip()] + if tags_input + else [] + ) totp_index = self.entry_manager.get_next_totp_index() entry_id = self.entry_manager.get_next_index() uri = self.entry_manager.add_totp( @@ -1093,6 +1108,7 @@ class PasswordManager: period=int(period), digits=int(digits), notes=notes, + tags=tags, ) secret = TotpManager.derive_secret(self.parent_seed, totp_index) self.is_dirty = True @@ -1128,6 +1144,14 @@ class PasswordManager: period = int(input("Period (default 30): ").strip() or 30) digits = int(input("Digits (default 6): ").strip() or 6) notes = input("Notes (optional): ").strip() + tags_input = input( + "Enter tags (comma-separated, optional): " + ).strip() + tags = ( + [t.strip() for t in tags_input.split(",") if t.strip()] + if tags_input + else [] + ) entry_id = self.entry_manager.get_next_index() uri = self.entry_manager.add_totp( label, @@ -1136,6 +1160,7 @@ class PasswordManager: period=period, digits=digits, notes=notes, + tags=tags, ) self.is_dirty = True self.last_update = time.time() @@ -1181,7 +1206,15 @@ class PasswordManager: print(colored("Error: Label cannot be empty.", "red")) return notes = input("Notes (optional): ").strip() - index = self.entry_manager.add_ssh_key(label, self.parent_seed, notes=notes) + tags_input = input("Enter tags (comma-separated, optional): ").strip() + tags = ( + [t.strip() for t in tags_input.split(",") if t.strip()] + if tags_input + else [] + ) + index = self.entry_manager.add_ssh_key( + label, self.parent_seed, notes=notes, tags=tags + ) priv_pem, pub_pem = self.entry_manager.get_ssh_key_pair( index, self.parent_seed ) @@ -1230,12 +1263,18 @@ class PasswordManager: return words_input = input("Word count (12 or 24, default 24): ").strip() notes = input("Notes (optional): ").strip() + tags_input = input("Enter tags (comma-separated, optional): ").strip() + tags = ( + [t.strip() for t in tags_input.split(",") if t.strip()] + if tags_input + else [] + ) if words_input and words_input not in {"12", "24"}: print(colored("Invalid word count. Choose 12 or 24.", "red")) return words = int(words_input) if words_input else 24 index = self.entry_manager.add_seed( - label, self.parent_seed, words_num=words, notes=notes + label, self.parent_seed, words_num=words, notes=notes, tags=tags ) phrase = self.entry_manager.get_seed_phrase(index, self.parent_seed) self.is_dirty = True @@ -1296,12 +1335,19 @@ class PasswordManager: ) user_id = input("User ID (optional): ").strip() notes = input("Notes (optional): ").strip() + tags_input = input("Enter tags (comma-separated, optional): ").strip() + tags = ( + [t.strip() for t in tags_input.split(",") if t.strip()] + if tags_input + else [] + ) index = self.entry_manager.add_pgp_key( label, self.parent_seed, key_type=key_type, user_id=user_id, notes=notes, + tags=tags, ) priv_key, fingerprint = self.entry_manager.get_pgp_key( index, self.parent_seed @@ -1350,7 +1396,13 @@ class PasswordManager: print(colored("Error: Label cannot be empty.", "red")) return notes = input("Notes (optional): ").strip() - index = self.entry_manager.add_nostr_key(label, notes=notes) + tags_input = input("Enter tags (comma-separated, optional): ").strip() + tags = ( + [t.strip() for t in tags_input.split(",") if t.strip()] + if tags_input + else [] + ) + index = self.entry_manager.add_nostr_key(label, notes=notes, tags=tags) npub, nsec = self.entry_manager.get_nostr_key_pair(index, self.parent_seed) self.is_dirty = True self.last_update = time.time() @@ -1401,6 +1453,12 @@ class PasswordManager: return value = input("Value: ").strip() notes = input("Notes (optional): ").strip() + tags_input = input("Enter tags (comma-separated, optional): ").strip() + tags = ( + [t.strip() for t in tags_input.split(",") if t.strip()] + if tags_input + else [] + ) custom_fields: list[dict[str, object]] = [] while True: @@ -1419,7 +1477,11 @@ class PasswordManager: ) index = self.entry_manager.add_key_value( - label, value, notes=notes, custom_fields=custom_fields + label, + value, + notes=notes, + custom_fields=custom_fields, + tags=tags, ) self.is_dirty = True self.last_update = time.time() @@ -1465,8 +1527,14 @@ class PasswordManager: print(colored("Error: Label cannot be empty.", "red")) return notes = input("Notes (optional): ").strip() + tags_input = input("Enter tags (comma-separated, optional): ").strip() + tags = ( + [t.strip() for t in tags_input.split(",") if t.strip()] + if tags_input + else [] + ) index = self.entry_manager.add_managed_account( - label, self.parent_seed, notes=notes + label, self.parent_seed, notes=notes, tags=tags ) seed = self.entry_manager.get_managed_account_seed(index, self.parent_seed) self.is_dirty = True @@ -1555,6 +1623,8 @@ class PasswordManager: print(colored("C. Add Custom Field", "cyan")) print(colored("H. Add Hidden Field", "cyan")) print(colored("E. Edit", "cyan")) + print(colored("T. Edit Tags", "cyan")) + print(colored("Q. Show QR codes", "cyan")) choice = ( input("Select an action or press Enter to return: ").strip().lower() @@ -1591,8 +1661,29 @@ class PasswordManager: self.entry_manager.modify_entry(index, custom_fields=custom_fields) self.is_dirty = True self.last_update = time.time() + elif choice == "t": + current_tags = entry.get("tags", []) + print( + colored( + f"Current tags: {', '.join(current_tags) if current_tags else 'None'}", + "cyan", + ) + ) + tags_input = input( + "Enter tags (comma-separated, leave blank to remove all tags): " + ).strip() + tags = ( + [t.strip() for t in tags_input.split(",") if t.strip()] + if tags_input + else [] + ) + self.entry_manager.modify_entry(index, tags=tags) + self.is_dirty = True + self.last_update = time.time() elif choice == "e": self._entry_edit_menu(index, entry) + elif choice == "q": + self._entry_qr_menu(index, entry) else: print(colored("Invalid choice.", "red")) entry = self.entry_manager.retrieve_entry(index) or entry @@ -1606,6 +1697,9 @@ class PasswordManager: if entry_type == EntryType.PASSWORD.value: print(colored("U. Edit Username", "cyan")) print(colored("R. Edit URL", "cyan")) + elif entry_type == EntryType.TOTP.value: + print(colored("P. Edit Period", "cyan")) + print(colored("D. Edit Digits", "cyan")) choice = input("Select option or press Enter to go back: ").strip().lower() if not choice: break @@ -1625,10 +1719,79 @@ class PasswordManager: self.entry_manager.modify_entry(index, url=new_url) self.is_dirty = True self.last_update = time.time() + elif entry_type == EntryType.TOTP.value and choice == "p": + period_str = input("New period (seconds): ").strip() + if period_str.isdigit(): + self.entry_manager.modify_entry(index, period=int(period_str)) + self.is_dirty = True + self.last_update = time.time() + else: + print(colored("Invalid period value.", "red")) + elif entry_type == EntryType.TOTP.value and choice == "d": + digits_str = input("New digits: ").strip() + if digits_str.isdigit(): + self.entry_manager.modify_entry(index, digits=int(digits_str)) + self.is_dirty = True + self.last_update = time.time() + else: + print(colored("Invalid digits value.", "red")) else: print(colored("Invalid choice.", "red")) entry = self.entry_manager.retrieve_entry(index) or entry + def _entry_qr_menu(self, index: int, entry: dict) -> None: + """Display QR codes for the given ``entry``.""" + + entry_type = entry.get("type") + + try: + if entry_type in {EntryType.SEED.value, EntryType.MANAGED_ACCOUNT.value}: + if entry_type == EntryType.SEED.value: + seed = self.entry_manager.get_seed_phrase(index, self.parent_seed) + else: + seed = self.entry_manager.get_managed_account_seed( + index, self.parent_seed + ) + + print(color_text(seed, "deterministic")) + from password_manager.seedqr import encode_seedqr + + TotpManager.print_qr_code(encode_seedqr(seed)) + return + + if entry_type == EntryType.NOSTR.value: + while True: + print(colored("\n[+] QR Codes:", "green")) + print(colored("P. Public key", "cyan")) + print(colored("K. Private key", "cyan")) + choice = ( + input("Select option or press Enter to return: ") + .strip() + .lower() + ) + if not choice: + break + + npub, nsec = self.entry_manager.get_nostr_key_pair( + index, self.parent_seed + ) + + if choice == "p": + print(colored(f"npub: {npub}", "cyan")) + TotpManager.print_qr_code(f"nostr:{npub}") + elif choice == "k": + print(color_text(f"nsec: {nsec}", "deterministic")) + TotpManager.print_qr_code(nsec) + else: + print(colored("Invalid choice.", "red")) + entry = self.entry_manager.retrieve_entry(index) or entry + return + + print(colored("No QR codes available for this entry.", "yellow")) + except Exception as e: # pragma: no cover - best effort + logging.error(f"Error displaying QR menu: {e}", exc_info=True) + print(colored(f"Error: Failed to display QR codes: {e}", "red")) + def handle_retrieve_entry(self) -> None: """ Handles retrieving a password from the index by prompting the user for the index number @@ -1683,6 +1846,9 @@ class PasswordManager: print(color_text(f"Code: {code}", category)) if notes: print(colored(f"Notes: {notes}", "cyan")) + tags = entry.get("tags", []) + if tags: + print(colored(f"Tags: {', '.join(tags)}", "cyan")) remaining = self.entry_manager.get_totp_time_remaining(index) exit_loop = False while remaining > 0: @@ -1732,6 +1898,9 @@ class PasswordManager: print(colored(f"Label: {label}", "cyan")) if notes: print(colored(f"Notes: {notes}", "cyan")) + tags = entry.get("tags", []) + if tags: + print(colored(f"Tags: {', '.join(tags)}", "cyan")) print(colored("Public Key:", "cyan")) print(color_text(pub_pem, "default")) if self.secret_mode_enabled: @@ -1767,6 +1936,9 @@ class PasswordManager: print(colored(f"Label: {label}", "cyan")) if notes: print(colored(f"Notes: {notes}", "cyan")) + tags = entry.get("tags", []) + if tags: + print(colored(f"Tags: {', '.join(tags)}", "cyan")) if self.secret_mode_enabled: copy_to_clipboard(phrase, self.clipboard_clear_delay) print( @@ -1777,10 +1949,7 @@ class PasswordManager: ) else: print(color_text(phrase, "deterministic")) - if confirm_action("Show Compact Seed QR? (Y/N): "): - from password_manager.seedqr import encode_seedqr - - TotpManager.print_qr_code(encode_seedqr(phrase)) + # Removed QR code display prompt and output if confirm_action("Show derived entropy as hex? (Y/N): "): from local_bip85.bip85 import BIP85 from bip_utils import Bip39SeedGenerator @@ -1819,6 +1988,9 @@ class PasswordManager: print(colored(f"User ID: {label}", "cyan")) if notes: print(colored(f"Notes: {notes}", "cyan")) + tags = entry.get("tags", []) + if tags: + print(colored(f"Tags: {', '.join(tags)}", "cyan")) print(colored(f"Fingerprint: {fingerprint}", "cyan")) if self.secret_mode_enabled: copy_to_clipboard(priv_key, self.clipboard_clear_delay) @@ -1856,14 +2028,12 @@ class PasswordManager: ) else: print(color_text(f"nsec: {nsec}", "deterministic")) - if confirm_action("Show QR code for npub? (Y/N): "): - TotpManager.print_qr_code(f"nostr:{npub}") - if confirm_action( - "WARNING: Displaying the nsec QR reveals your private key. Continue? (Y/N): " - ): - TotpManager.print_qr_code(nsec) + # QR code display removed for npub and nsec if notes: print(colored(f"Notes: {notes}", "cyan")) + tags = entry.get("tags", []) + if tags: + print(colored(f"Tags: {', '.join(tags)}", "cyan")) except Exception as e: logging.error(f"Error deriving Nostr keys: {e}", exc_info=True) print(colored(f"Error: Failed to derive Nostr keys: {e}", "red")) @@ -1879,6 +2049,9 @@ class PasswordManager: print(colored(f"Retrieving value for '{label}'.", "cyan")) if notes: print(colored(f"Notes: {notes}", "cyan")) + tags = entry.get("tags", []) + if tags: + print(colored(f"Tags: {', '.join(tags)}", "cyan")) print( colored( f"Archived Status: {'Archived' if archived else 'Active'}", @@ -1937,6 +2110,9 @@ class PasswordManager: print(colored(f"Notes: {notes}", "cyan")) if fingerprint: print(colored(f"Fingerprint: {fingerprint}", "cyan")) + tags = entry.get("tags", []) + if tags: + print(colored(f"Tags: {', '.join(tags)}", "cyan")) print( colored( f"Archived Status: {'Archived' if archived else 'Active'}", @@ -1964,10 +2140,7 @@ class PasswordManager: ) else: print(color_text(seed, "deterministic")) - if confirm_action("Show Compact Seed QR? (Y/N): "): - from password_manager.seedqr import encode_seedqr - - TotpManager.print_qr_code(encode_seedqr(seed)) + # QR code display removed for managed account seed self._entry_actions_menu(index, entry) pause() return @@ -2030,6 +2203,9 @@ class PasswordManager: "cyan", ) ) + tags = entry.get("tags", []) + if tags: + print(colored(f"Tags: {', '.join(tags)}", "cyan")) custom_fields = entry.get("custom_fields", []) if custom_fields: print(colored("Additional Fields:", "cyan")) @@ -2190,6 +2366,15 @@ class PasswordManager: {"label": label, "value": value, "is_hidden": hidden} ) + tags_input = input( + "Enter tags (comma-separated, leave blank to keep current): " + ).strip() + tags = ( + [t.strip() for t in tags_input.split(",") if t.strip()] + if tags_input + else None + ) + self.entry_manager.modify_entry( index, archived=new_blacklisted, @@ -2198,6 +2383,7 @@ class PasswordManager: period=new_period, digits=new_digits, custom_fields=custom_fields, + tags=tags, ) elif entry_type in ( EntryType.KEY_VALUE.value, @@ -2273,6 +2459,15 @@ class PasswordManager: {"label": f_label, "value": f_value, "is_hidden": hidden} ) + tags_input = input( + "Enter tags (comma-separated, leave blank to keep current): " + ).strip() + tags = ( + [t.strip() for t in tags_input.split(",") if t.strip()] + if tags_input + else None + ) + self.entry_manager.modify_entry( index, archived=new_blacklisted, @@ -2280,6 +2475,7 @@ class PasswordManager: label=new_label, value=new_value, custom_fields=custom_fields, + tags=tags, ) else: website_name = entry.get("label", entry.get("website")) @@ -2366,6 +2562,15 @@ class PasswordManager: {"label": label, "value": value, "is_hidden": hidden} ) + tags_input = input( + "Enter tags (comma-separated, leave blank to keep current): " + ).strip() + tags = ( + [t.strip() for t in tags_input.split(",") if t.strip()] + if tags_input + else None + ) + self.entry_manager.modify_entry( index, new_username, @@ -2374,6 +2579,7 @@ class PasswordManager: notes=new_notes, label=new_label, custom_fields=custom_fields, + tags=tags, ) # Mark database as dirty for background sync @@ -2479,6 +2685,9 @@ class PasswordManager: notes = entry.get("notes", "") if notes: print(color_text(f" Notes: {notes}", "index")) + tags = entry.get("tags", []) + if tags: + print(color_text(f" Tags: {', '.join(tags)}", "index")) elif etype == EntryType.SEED.value: print(color_text(" Type: Seed Phrase", "index")) print(color_text(f" Label: {entry.get('label', '')}", "index")) @@ -2489,6 +2698,9 @@ class PasswordManager: notes = entry.get("notes", "") if notes: print(color_text(f" Notes: {notes}", "index")) + tags = entry.get("tags", []) + if tags: + print(color_text(f" Tags: {', '.join(tags)}", "index")) elif etype == EntryType.SSH.value: print(color_text(" Type: SSH Key", "index")) print(color_text(f" Label: {entry.get('label', '')}", "index")) @@ -2498,6 +2710,9 @@ class PasswordManager: notes = entry.get("notes", "") if notes: print(color_text(f" Notes: {notes}", "index")) + tags = entry.get("tags", []) + if tags: + print(color_text(f" Tags: {', '.join(tags)}", "index")) elif etype == EntryType.PGP.value: print(color_text(" Type: PGP Key", "index")) print(color_text(f" Label: {entry.get('label', '')}", "index")) @@ -2513,6 +2728,9 @@ class PasswordManager: notes = entry.get("notes", "") if notes: print(color_text(f" Notes: {notes}", "index")) + tags = entry.get("tags", []) + if tags: + print(color_text(f" Tags: {', '.join(tags)}", "index")) elif etype == EntryType.NOSTR.value: print(color_text(" Type: Nostr Key", "index")) print(color_text(f" Label: {entry.get('label', '')}", "index")) @@ -2522,6 +2740,9 @@ class PasswordManager: notes = entry.get("notes", "") if notes: print(color_text(f" Notes: {notes}", "index")) + tags = entry.get("tags", []) + if tags: + print(color_text(f" Tags: {', '.join(tags)}", "index")) else: website = entry.get("label", entry.get("website", "")) username = entry.get("username", "") diff --git a/src/password_manager/migrations.py b/src/password_manager/migrations.py index 6052e85..6ea8599 100644 --- a/src/password_manager/migrations.py +++ b/src/password_manager/migrations.py @@ -58,7 +58,17 @@ def _v2_to_v3(data: dict) -> dict: return data -LATEST_VERSION = 3 +@migration(3) +def _v3_to_v4(data: dict) -> dict: + """Add tags defaults to each entry.""" + entries = data.get("entries", {}) + for entry in entries.values(): + entry.setdefault("tags", []) + data["schema_version"] = 4 + return data + + +LATEST_VERSION = 4 def apply_migrations(data: dict) -> dict: diff --git a/src/tests/test_add_tags_from_retrieve.py b/src/tests/test_add_tags_from_retrieve.py new file mode 100644 index 0000000..fac5866 --- /dev/null +++ b/src/tests/test_add_tags_from_retrieve.py @@ -0,0 +1,49 @@ +import sys +from pathlib import Path +from tempfile import TemporaryDirectory +from types import SimpleNamespace + +from helpers import create_vault, TEST_SEED, TEST_PASSWORD + +sys.path.append(str(Path(__file__).resolve().parents[1])) + +from password_manager.entry_management import EntryManager +from password_manager.backup import BackupManager +from password_manager.manager import PasswordManager, EncryptionMode +from password_manager.config_manager import ConfigManager + + +class FakePasswordGenerator: + def generate_password(self, length: int, index: int) -> str: # noqa: D401 + return "pw" + + +def test_add_tags_from_retrieve(monkeypatch): + with TemporaryDirectory() as tmpdir: + tmp_path = Path(tmpdir) + vault, enc_mgr = create_vault(tmp_path, TEST_SEED, TEST_PASSWORD) + cfg_mgr = ConfigManager(vault, tmp_path) + backup_mgr = BackupManager(tmp_path, cfg_mgr) + entry_mgr = EntryManager(vault, backup_mgr) + + pm = PasswordManager.__new__(PasswordManager) + pm.encryption_mode = EncryptionMode.SEED_ONLY + pm.encryption_manager = enc_mgr + pm.vault = vault + pm.entry_manager = entry_mgr + pm.backup_manager = backup_mgr + pm.password_generator = FakePasswordGenerator() + pm.parent_seed = TEST_SEED + pm.nostr_client = SimpleNamespace() + pm.fingerprint_dir = tmp_path + pm.secret_mode_enabled = False + + index = entry_mgr.add_entry("example.com", 8) + + inputs = iter([str(index), "t", "work,personal", ""]) + monkeypatch.setattr("builtins.input", lambda *a, **k: next(inputs)) + + pm.handle_retrieve_entry() + + entry = entry_mgr.retrieve_entry(index) + assert set(entry.get("tags", [])) == {"work", "personal"} diff --git a/src/tests/test_backup_restore.py b/src/tests/test_backup_restore.py index eacc8eb..fdbc221 100644 --- a/src/tests/test_backup_restore.py +++ b/src/tests/test_backup_restore.py @@ -22,7 +22,7 @@ def test_backup_restore_workflow(monkeypatch): index_file = fp_dir / "seedpass_entries_db.json.enc" data1 = { - "schema_version": 3, + "schema_version": 4, "entries": { "0": { "label": "a", @@ -32,6 +32,7 @@ def test_backup_restore_workflow(monkeypatch): "notes": "", "custom_fields": [], "origin": "", + "tags": [], } }, } @@ -46,7 +47,7 @@ def test_backup_restore_workflow(monkeypatch): assert backup1.stat().st_mode & 0o777 == 0o600 data2 = { - "schema_version": 3, + "schema_version": 4, "entries": { "0": { "label": "b", @@ -56,6 +57,7 @@ def test_backup_restore_workflow(monkeypatch): "notes": "", "custom_fields": [], "origin": "", + "tags": [], } }, } @@ -69,11 +71,11 @@ def test_backup_restore_workflow(monkeypatch): if os.name != "nt": assert backup2.stat().st_mode & 0o777 == 0o600 - vault.save_index({"schema_version": 3, "entries": {"temp": {}}}) + vault.save_index({"schema_version": 4, "entries": {"temp": {}}}) backup_mgr.restore_latest_backup() assert vault.load_index()["entries"] == data2["entries"] - vault.save_index({"schema_version": 3, "entries": {}}) + vault.save_index({"schema_version": 4, "entries": {}}) backup_mgr.restore_backup_by_timestamp(1111) assert vault.load_index()["entries"] == data1["entries"] @@ -91,7 +93,7 @@ def test_additional_backup_location(monkeypatch): cfg_mgr.set_additional_backup_path(extra) backup_mgr = BackupManager(fp_dir, cfg_mgr) - vault.save_index({"schema_version": 3, "entries": {"a": {}}}) + vault.save_index({"schema_version": 4, "entries": {"a": {}}}) monkeypatch.setattr(time, "time", lambda: 3333) backup_mgr.create_backup() diff --git a/src/tests/test_cli_export_import.py b/src/tests/test_cli_export_import.py index de11ea4..5d4afef 100644 --- a/src/tests/test_cli_export_import.py +++ b/src/tests/test_cli_export_import.py @@ -31,7 +31,7 @@ def _setup_pm(tmp_path: Path): def test_cli_export_creates_file(monkeypatch, tmp_path): pm, vault = _setup_pm(tmp_path) data = { - "schema_version": 3, + "schema_version": 4, "entries": { "0": { "label": "example", @@ -39,6 +39,7 @@ def test_cli_export_creates_file(monkeypatch, tmp_path): "notes": "", "custom_fields": [], "origin": "", + "tags": [], } }, } @@ -58,7 +59,7 @@ def test_cli_export_creates_file(monkeypatch, tmp_path): def test_cli_import_round_trip(monkeypatch, tmp_path): pm, vault = _setup_pm(tmp_path) original = { - "schema_version": 3, + "schema_version": 4, "entries": { "0": { "label": "example", @@ -66,6 +67,7 @@ def test_cli_import_round_trip(monkeypatch, tmp_path): "notes": "", "custom_fields": [], "origin": "", + "tags": [], } }, } @@ -79,7 +81,7 @@ def test_cli_import_round_trip(monkeypatch, tmp_path): parent_seed=TEST_SEED, ) - vault.save_index({"schema_version": 3, "entries": {}}) + vault.save_index({"schema_version": 4, "entries": {}}) monkeypatch.setattr(main, "PasswordManager", lambda: pm) monkeypatch.setattr(main, "configure_logging", lambda: None) diff --git a/src/tests/test_edit_tags_from_retrieve.py b/src/tests/test_edit_tags_from_retrieve.py new file mode 100644 index 0000000..143da53 --- /dev/null +++ b/src/tests/test_edit_tags_from_retrieve.py @@ -0,0 +1,49 @@ +import sys +from pathlib import Path +from tempfile import TemporaryDirectory +from types import SimpleNamespace + +from helpers import create_vault, TEST_SEED, TEST_PASSWORD + +sys.path.append(str(Path(__file__).resolve().parents[1])) + +from password_manager.entry_management import EntryManager +from password_manager.backup import BackupManager +from password_manager.manager import PasswordManager, EncryptionMode +from password_manager.config_manager import ConfigManager + + +class FakePasswordGenerator: + def generate_password(self, length: int, index: int) -> str: # noqa: D401 + return "pw" + + +def test_edit_tags_from_retrieve(monkeypatch): + with TemporaryDirectory() as tmpdir: + tmp_path = Path(tmpdir) + vault, enc_mgr = create_vault(tmp_path, TEST_SEED, TEST_PASSWORD) + cfg_mgr = ConfigManager(vault, tmp_path) + backup_mgr = BackupManager(tmp_path, cfg_mgr) + entry_mgr = EntryManager(vault, backup_mgr) + + pm = PasswordManager.__new__(PasswordManager) + pm.encryption_mode = EncryptionMode.SEED_ONLY + pm.encryption_manager = enc_mgr + pm.vault = vault + pm.entry_manager = entry_mgr + pm.backup_manager = backup_mgr + pm.password_generator = FakePasswordGenerator() + pm.parent_seed = TEST_SEED + pm.nostr_client = SimpleNamespace() + pm.fingerprint_dir = tmp_path + pm.secret_mode_enabled = False + + index = entry_mgr.add_entry("example.com", 8, tags=["old"]) + + inputs = iter([str(index), "t", "newtag", ""]) + monkeypatch.setattr("builtins.input", lambda *a, **k: next(inputs)) + + pm.handle_retrieve_entry() + + entry = entry_mgr.retrieve_entry(index) + assert entry.get("tags", []) == ["newtag"] diff --git a/src/tests/test_entry_add.py b/src/tests/test_entry_add.py index ea9328e..8ea313d 100644 --- a/src/tests/test_entry_add.py +++ b/src/tests/test_entry_add.py @@ -39,6 +39,7 @@ def test_add_and_retrieve_entry(): "kind": "password", "notes": "", "custom_fields": custom, + "tags": [], } data = enc_mgr.load_json_data(entry_mgr.index_file) diff --git a/src/tests/test_index_import_export.py b/src/tests/test_index_import_export.py index d6fa622..ea86dd3 100644 --- a/src/tests/test_index_import_export.py +++ b/src/tests/test_index_import_export.py @@ -31,7 +31,7 @@ def test_index_export_import_round_trip(): vault = setup_vault(tmp) original = { - "schema_version": 3, + "schema_version": 4, "entries": { "0": { "label": "example", @@ -39,6 +39,7 @@ def test_index_export_import_round_trip(): "notes": "", "custom_fields": [], "origin": "", + "tags": [], } }, } @@ -49,7 +50,7 @@ def test_index_export_import_round_trip(): vault.save_index( { - "schema_version": 3, + "schema_version": 4, "entries": { "0": { "label": "changed", @@ -57,6 +58,7 @@ def test_index_export_import_round_trip(): "notes": "", "custom_fields": [], "origin": "", + "tags": [], } }, } diff --git a/src/tests/test_key_value_entry.py b/src/tests/test_key_value_entry.py index 9322d64..895dfbf 100644 --- a/src/tests/test_key_value_entry.py +++ b/src/tests/test_key_value_entry.py @@ -33,6 +33,7 @@ def test_add_and_modify_key_value(): "notes": "token", "archived": False, "custom_fields": [], + "tags": [], } em.modify_entry(idx, value="def456") diff --git a/src/tests/test_manager_add_totp.py b/src/tests/test_manager_add_totp.py index 0038511..c011d3e 100644 --- a/src/tests/test_manager_add_totp.py +++ b/src/tests/test_manager_add_totp.py @@ -48,6 +48,7 @@ def test_handle_add_totp(monkeypatch, capsys): "", # period "", # digits "", # notes + "", # tags ] ) monkeypatch.setattr("builtins.input", lambda *args, **kwargs: next(inputs)) @@ -66,5 +67,6 @@ def test_handle_add_totp(monkeypatch, capsys): "digits": 6, "archived": False, "notes": "", + "tags": [], } assert "ID 0" in out diff --git a/src/tests/test_manager_edit_totp.py b/src/tests/test_manager_edit_totp.py new file mode 100644 index 0000000..53e43d4 --- /dev/null +++ b/src/tests/test_manager_edit_totp.py @@ -0,0 +1,57 @@ +import sys +from pathlib import Path +from tempfile import TemporaryDirectory + +from helpers import create_vault, TEST_SEED, TEST_PASSWORD + +sys.path.append(str(Path(__file__).resolve().parents[1])) + +from password_manager.entry_management import EntryManager +from password_manager.backup import BackupManager +from password_manager.manager import PasswordManager, EncryptionMode +from password_manager.config_manager import ConfigManager + + +class FakeNostrClient: + def __init__(self, *args, **kwargs): + self.published = [] + + def publish_snapshot(self, data: bytes): + self.published.append(data) + return None, "abcd" + + +def test_edit_totp_period_from_retrieve(monkeypatch): + with TemporaryDirectory() as tmpdir: + tmp_path = Path(tmpdir) + vault, enc_mgr = create_vault(tmp_path, TEST_SEED, TEST_PASSWORD) + cfg_mgr = ConfigManager(vault, tmp_path) + backup_mgr = BackupManager(tmp_path, cfg_mgr) + entry_mgr = EntryManager(vault, backup_mgr) + + pm = PasswordManager.__new__(PasswordManager) + pm.encryption_mode = EncryptionMode.SEED_ONLY + pm.encryption_manager = enc_mgr + pm.vault = vault + pm.entry_manager = entry_mgr + pm.backup_manager = backup_mgr + pm.parent_seed = TEST_SEED + pm.nostr_client = FakeNostrClient() + pm.fingerprint_dir = tmp_path + pm.is_dirty = False + pm.secret_mode_enabled = False + + entry_mgr.add_totp("Example", TEST_SEED) + + inputs = iter(["0", "e", "p", "45", "", ""]) + monkeypatch.setattr("builtins.input", lambda *a, **k: next(inputs)) + monkeypatch.setattr(pm.entry_manager, "get_totp_code", lambda *a, **k: "123456") + monkeypatch.setattr( + pm.entry_manager, "get_totp_time_remaining", lambda *a, **k: 1 + ) + monkeypatch.setattr("password_manager.manager.time.sleep", lambda *a, **k: None) + monkeypatch.setattr("password_manager.manager.timed_input", lambda *a, **k: "b") + + pm.handle_retrieve_entry() + entry = entry_mgr.retrieve_entry(0) + assert entry["period"] == 45 diff --git a/src/tests/test_manager_workflow.py b/src/tests/test_manager_workflow.py index c12f39e..5d2dbd0 100644 --- a/src/tests/test_manager_workflow.py +++ b/src/tests/test_manager_workflow.py @@ -54,6 +54,7 @@ def test_manager_workflow(monkeypatch): "", # username "", # url "", # notes + "", # tags "n", # add custom field "", # length (default) "0", # retrieve index @@ -65,6 +66,7 @@ def test_manager_workflow(monkeypatch): "", # archive status "", # new notes "n", # edit custom fields + "", # tags keep ] ) monkeypatch.setattr("builtins.input", lambda *args, **kwargs: next(inputs)) diff --git a/src/tests/test_migrations.py b/src/tests/test_migrations.py index 4fb2f11..2371203 100644 --- a/src/tests/test_migrations.py +++ b/src/tests/test_migrations.py @@ -13,7 +13,7 @@ def setup(tmp_path: Path): return enc_mgr, vault -def test_migrate_v0_to_v3(tmp_path: Path): +def test_migrate_v0_to_v4(tmp_path: Path): enc_mgr, vault = setup(tmp_path) legacy = {"passwords": {"0": {"website": "a", "length": 8}}} enc_mgr.save_json_data(legacy) @@ -26,11 +26,12 @@ def test_migrate_v0_to_v3(tmp_path: Path): "notes": "", "custom_fields": [], "origin": "", + "tags": [], } assert data["entries"]["0"] == expected_entry -def test_migrate_v1_to_v3(tmp_path: Path): +def test_migrate_v1_to_v4(tmp_path: Path): enc_mgr, vault = setup(tmp_path) legacy = {"schema_version": 1, "passwords": {"0": {"website": "b", "length": 10}}} enc_mgr.save_json_data(legacy) @@ -43,11 +44,12 @@ def test_migrate_v1_to_v3(tmp_path: Path): "notes": "", "custom_fields": [], "origin": "", + "tags": [], } assert data["entries"]["0"] == expected_entry -def test_migrate_v2_to_v3(tmp_path: Path): +def test_migrate_v2_to_v4(tmp_path: Path): enc_mgr, vault = setup(tmp_path) legacy = { "schema_version": 2, @@ -65,6 +67,7 @@ def test_migrate_v2_to_v3(tmp_path: Path): "notes": "", "custom_fields": [], "origin": "", + "tags": [], } assert data["entries"]["0"] == expected_entry diff --git a/src/tests/test_modify_totp_entry.py b/src/tests/test_modify_totp_entry.py new file mode 100644 index 0000000..b1cb825 --- /dev/null +++ b/src/tests/test_modify_totp_entry.py @@ -0,0 +1,20 @@ +from helpers import create_vault, TEST_SEED, TEST_PASSWORD + +from password_manager.entry_management import EntryManager +from password_manager.backup import BackupManager +from password_manager.config_manager import ConfigManager + + +def test_modify_totp_entry_period_digits_and_archive(tmp_path): + vault, _ = create_vault(tmp_path, TEST_SEED, TEST_PASSWORD) + cfg_mgr = ConfigManager(vault, tmp_path) + backup_mgr = BackupManager(tmp_path, cfg_mgr) + em = EntryManager(vault, backup_mgr) + + em.add_totp("Example", TEST_SEED, period=30, digits=6) + em.modify_entry(0, period=60, digits=8, archived=True) + + entry = em.retrieve_entry(0) + assert entry["period"] == 60 + assert entry["digits"] == 8 + assert entry["archived"] is True diff --git a/src/tests/test_nostr_entry.py b/src/tests/test_nostr_entry.py index 569dfeb..c049850 100644 --- a/src/tests/test_nostr_entry.py +++ b/src/tests/test_nostr_entry.py @@ -31,6 +31,7 @@ def test_nostr_key_determinism(): "label": "main", "notes": "", "archived": False, + "tags": [], } npub1, nsec1 = entry_mgr.get_nostr_key_pair(idx, TEST_SEED) diff --git a/src/tests/test_nostr_qr.py b/src/tests/test_nostr_qr.py index aaa594f..e2af078 100644 --- a/src/tests/test_nostr_qr.py +++ b/src/tests/test_nostr_qr.py @@ -10,6 +10,7 @@ from password_manager.entry_management import EntryManager from password_manager.backup import BackupManager from password_manager.manager import PasswordManager, EncryptionMode, TotpManager from password_manager.config_manager import ConfigManager +from utils.color_scheme import color_text class FakeNostrClient: @@ -44,13 +45,8 @@ def test_show_qr_for_nostr_keys(monkeypatch): idx = entry_mgr.add_nostr_key("main") npub, _ = entry_mgr.get_nostr_key_pair(idx, TEST_SEED) - inputs = iter([str(idx), "n", "", ""]) + inputs = iter([str(idx), "q", "p", ""]) monkeypatch.setattr("builtins.input", lambda *a, **k: next(inputs)) - responses = iter([True, False]) - monkeypatch.setattr( - "password_manager.manager.confirm_action", - lambda *_a, **_k: next(responses), - ) called = [] monkeypatch.setattr( "password_manager.manager.TotpManager.print_qr_code", @@ -59,3 +55,41 @@ def test_show_qr_for_nostr_keys(monkeypatch): pm.handle_retrieve_entry() assert called == [f"nostr:{npub}"] + + +def test_show_private_key_qr(monkeypatch, capsys): + """Ensure nsec QR code is shown and output is colored.""" + with TemporaryDirectory() as tmpdir: + tmp_path = Path(tmpdir) + vault, enc_mgr = create_vault(tmp_path, TEST_SEED, TEST_PASSWORD) + cfg_mgr = ConfigManager(vault, tmp_path) + backup_mgr = BackupManager(tmp_path, cfg_mgr) + entry_mgr = EntryManager(vault, backup_mgr) + + pm = PasswordManager.__new__(PasswordManager) + pm.encryption_mode = EncryptionMode.SEED_ONLY + pm.encryption_manager = enc_mgr + pm.vault = vault + pm.entry_manager = entry_mgr + pm.backup_manager = backup_mgr + pm.parent_seed = TEST_SEED + pm.nostr_client = FakeNostrClient() + pm.fingerprint_dir = tmp_path + pm.is_dirty = False + pm.secret_mode_enabled = False + + idx = entry_mgr.add_nostr_key("main") + _, nsec = entry_mgr.get_nostr_key_pair(idx, TEST_SEED) + + inputs = iter([str(idx), "q", "k", ""]) + monkeypatch.setattr("builtins.input", lambda *a, **k: next(inputs)) + called = [] + monkeypatch.setattr( + "password_manager.manager.TotpManager.print_qr_code", + lambda data: called.append(data), + ) + + pm.handle_retrieve_entry() + out = capsys.readouterr().out + assert called == [nsec] + assert color_text(f"nsec: {nsec}", "deterministic") in out diff --git a/src/tests/test_search_entries.py b/src/tests/test_search_entries.py index 1d24c76..86e6f35 100644 --- a/src/tests/test_search_entries.py +++ b/src/tests/test_search_entries.py @@ -105,3 +105,26 @@ def test_search_no_results(): entry_mgr.add_entry("Example.com", 12, "alice") result = entry_mgr.search_entries("missing") assert result == [] + + +def test_search_by_tag_password(): + with TemporaryDirectory() as tmpdir: + tmp_path = Path(tmpdir) + entry_mgr = setup_entry_manager(tmp_path) + + idx = entry_mgr.add_entry("TaggedSite", 8, tags=["work"]) + + result = entry_mgr.search_entries("work") + assert result == [(idx, "TaggedSite", "", "", False)] + + +def test_search_by_tag_totp(): + with TemporaryDirectory() as tmpdir: + tmp_path = Path(tmpdir) + entry_mgr = setup_entry_manager(tmp_path) + + entry_mgr.add_totp("OTPAccount", TEST_SEED, tags=["mfa"]) + idx = entry_mgr.search_entries("OTPAccount")[0][0] + + result = entry_mgr.search_entries("mfa") + assert result == [(idx, "OTPAccount", None, None, False)] diff --git a/src/tests/test_seed_entry.py b/src/tests/test_seed_entry.py index 772fa55..d7d9d36 100644 --- a/src/tests/test_seed_entry.py +++ b/src/tests/test_seed_entry.py @@ -42,6 +42,7 @@ def test_seed_phrase_determinism(): "word_count": 12, "notes": "", "archived": False, + "tags": [], } assert entry24 == { @@ -52,6 +53,7 @@ def test_seed_phrase_determinism(): "word_count": 24, "notes": "", "archived": False, + "tags": [], } assert phrase12_a not in entry12.values() diff --git a/src/tests/test_ssh_entry.py b/src/tests/test_ssh_entry.py index 695034c..f037437 100644 --- a/src/tests/test_ssh_entry.py +++ b/src/tests/test_ssh_entry.py @@ -29,6 +29,7 @@ def test_add_and_retrieve_ssh_key_pair(): "label": "ssh", "notes": "", "archived": False, + "tags": [], } priv1, pub1 = entry_mgr.get_ssh_key_pair(index, TEST_SEED) diff --git a/src/tests/test_tag_persistence.py b/src/tests/test_tag_persistence.py new file mode 100644 index 0000000..487fb3c --- /dev/null +++ b/src/tests/test_tag_persistence.py @@ -0,0 +1,49 @@ +import sys +from pathlib import Path +from tempfile import TemporaryDirectory + +from helpers import create_vault, TEST_SEED, TEST_PASSWORD + +sys.path.append(str(Path(__file__).resolve().parents[1])) + +from password_manager.entry_management import EntryManager +from password_manager.backup import BackupManager +from password_manager.config_manager import ConfigManager + + +def setup_entry_manager(tmp_path: Path) -> EntryManager: + vault, _ = create_vault(tmp_path, TEST_SEED, TEST_PASSWORD) + cfg_mgr = ConfigManager(vault, tmp_path) + backup_mgr = BackupManager(tmp_path, cfg_mgr) + return EntryManager(vault, backup_mgr) + + +def test_tags_persist_on_new_entry(): + with TemporaryDirectory() as tmpdir: + tmp_path = Path(tmpdir) + entry_mgr = setup_entry_manager(tmp_path) + + idx = entry_mgr.add_entry("Site", 8, tags=["work"]) + + # Reinitialize to simulate application restart + entry_mgr = setup_entry_manager(tmp_path) + + result = entry_mgr.search_entries("work") + assert result == [(idx, "Site", "", "", False)] + + +def test_tags_persist_after_modify(): + with TemporaryDirectory() as tmpdir: + tmp_path = Path(tmpdir) + entry_mgr = setup_entry_manager(tmp_path) + + idx = entry_mgr.add_entry("Site", 8) + entry_mgr.modify_entry(idx, tags=["personal"]) + + # Ensure tag searchable before reload + assert entry_mgr.search_entries("personal") == [(idx, "Site", "", "", False)] + + # Reinitialize to simulate application restart + entry_mgr = setup_entry_manager(tmp_path) + result = entry_mgr.search_entries("personal") + assert result == [(idx, "Site", "", "", False)] diff --git a/src/tests/test_totp_entry.py b/src/tests/test_totp_entry.py index b25d610..6eb0b12 100644 --- a/src/tests/test_totp_entry.py +++ b/src/tests/test_totp_entry.py @@ -37,6 +37,7 @@ def test_add_totp_and_get_code(): "digits": 6, "archived": False, "notes": "", + "tags": [], } code = entry_mgr.get_totp_code(0, TEST_SEED, timestamp=0) @@ -76,6 +77,7 @@ def test_add_totp_imported(tmp_path): "digits": 6, "archived": False, "notes": "", + "tags": [], } code = em.get_totp_code(0, timestamp=0) assert code == pyotp.TOTP(secret).at(0)