Building a Simple GPG Password Manager in Bash (with Rofi UI)
A practical story on why and how to build your own password manager in Bash using GPG, and how to add a Rofi UI for a better experience.
Building a Simple GPG Password Manager in Bash with Rofi UI
Managing passwords can be challenging. Server passwords, database passwords, SSH keys, and other secrets often end up in different files or locations. This can make it difficult to work efficiently and access them quickly.
There are many ways to manage passwords. You could write them in a text file or a note, but this is not safe. You could also use password managers like LastPass, 1Password, or Bitwarden. These tools are secure and have many features, but they often require extra steps or a graphical program. If you work in the terminal, this can slow you down.
For terminal users, a better solution is a command-line password manager. One popular option is pass
. It is simple, secure, and terminal-based. It uses GPG (GNU Privacy Guard) to encrypt each password. GPG is a trusted encryption tool. Even if someone finds your password files, they cannot read them without your key or passphrase. However, using pass
still requires typing commands in the terminal.
I thought it would be useful to build our own simple version of pass
that is designed only for our needs. It should be fast, easy to use, and combine well with Rofi to provide a simple interface. You could access your passwords with a single command or a custom shortcut.
Below, we will build a simple password manager in Bash and add a Rofi interface for easier access.
Prerequisites
If you are new to Linux or Bash scripting, check my previous post: Mastering Bash Scripting: A Practical Guide. It is a complete guide for beginners.
For Rofi beginners, see my post: Rofi Configuration. It explains installation, configuration, and usage.
Steps / Instructions / Code
Step 1: What We Need
Before we start, make sure you have the following installed:
bash
gpg
rofi
xclip
orxsel
(for clipboard access)notify-send
(for desktop notifications)
Here is what these tools do:
- Bash: A Unix shell and command language. We will use it to write our script.
- GPG: A tool for secure communication and data storage. We will use it to encrypt and decrypt passwords.
- Rofi: A window switcher, application launcher, and dmenu replacement. We will use it to create a simple menu for our password manager.
- xclip/xsel: Command-line tools to access the clipboard. We will use them to copy passwords quickly.
- notify-send: A command to send desktop notifications. We will use it to notify the user about actions taken by the script.
Step 2: The Bash Script
First, create a new Bash script file. I will call it passmgr.sh
because I want a short name to type quickly in the terminal. Make it executable:
touch passmgr.sh chmod +x passmgr.sh
Now open it in your favorite text editor and start writing the script.
At the top of the script, we will set our constants:
PASSWORDS_DIR="$HOME/.passwords" # Directory to store encrypted passwords mkdir -p "$PASSWORDS_DIR" # Create the directory if it does not exist chmod 700 "$PASSWORDS_DIR" # Set the directory permissions to 700
PASSWORDS_DIR
is the folder where we will save our encrypted passwords. You can change it to any folder you like, but make sure it is hidden and has the right permissions (700) which means only you can read, write, and access it.
700 permission means: only the owner has read, write, and execute permissions, while group members and others have no permissions at all.
So now we have our constants set up, we can start building the main menu function.
As i mention above I want a simple password manager, whith a simple menu, I want to be able to add, get, and delete passwords, and exit the program.
So let's create the main menu function:
main_menu() { local CHOICE # Local variable to store the user's choice # We use rofi to show a menu with options: Add Password, Get Password, Delete Password, Exit, # and we store the user's choice in the CHOICE variable CHOICE=$(echo -e "Add Password\nGet Password\nDelete Password\nExit" | rofi -dmenu -p "Password Manager:") # Show the menu using Rofi # Then based on the user's choice, we call the corresponding function case "$CHOICE" in "Add Password") add_password ;; "Get Password") get_password ;; "Delete Password") delete_password ;; "Exit" | "") exit 0 ;; *) notify-send "Password Manager" "Invalid option." ;; # Notify if the option is invalid esac }
Adding a Password
When you add a password, the script asks for a name and the password. It checks if the name already exists. If not, it locks and saves it:
add_password() { # Define local variables local name password file # Use rofi to get the name of the new password entry from the user # -p sets the prompt text # -theme-str sets a placeholder text in the input field name=$(rofi -dmenu \ -p "Enter new password name:" \ -theme-str 'entry { placeholder: "e.g. github, aws"; }') # Check if the name is empty (user cancelled) # send a notification and exit if it is empty if [[ -z "$name" ]]; then notify-send "Password Manager" "Add password cancelled." exit 0 fi # Set the file path for the new password entry file="$SECRETS_DIR/$name.gpg" # Check if the file already exists # send a notification and exit if it exists if [[ -f "$file" ]]; then notify-send "Password Manager" "Error: Entry '$name' already exists." exit 1 fi # Use rofi to get the password from the user # -password makes the input hidden (like a password field) # -p sets the prompt text # -theme-str sets a placeholder text in the input field password=$(rofi -dmenu -password \ -p "Enter password for \"$name\":" \ -theme-str 'entry { placeholder: "e.g. myStrongPass123"; }') # Check if the password is empty (user cancelled) # send a notification and exit if it is empty if [[ -z "$password" ]]; then notify-send "Password Manager" "Add password cancelled." exit 0 fi # Encrypt the password using GPG and save it to the file # -n means do not add a newline at the end # --symmetric means we want to use symmetric encryption (same key for encrypting and decrypting) # --cipher-algo AES256 means we want to use the AES256 encryption algorithm # -o "$file" means we want to save the output to the file echo -n "$password" | gpg --symmetric --cipher-algo AES256 -o "$file" # Set the file permissions to 600 (only the owner can read and write) chmod 600 "$file" # Notify the user that the password was added successfully notify-send "Password Manager" "Password for '$name' added securely." }
Very simple, right? We use Rofi to get the name and password from the user, check if the name already exists, and if not, we encrypt the password with GPG and save it to a file.
Getting a Password
So we created a function to add passwords. Now we need a function to get passwords. How does this work?
We want to show a list of saved passwords and let the user pick one. When the user picks a password, the script will ask for the GPG passphrase to unlock it, and then copy it to the clipboard.
get_password() { # Define local variables local choices selected file # Get a list of all saved passwords by listing the files in the password directory # We use sed to remove the .gpg extension from the filenames choices=$(ls "$SECRETS_DIR" 2>/dev/null | sed 's/\.gpg$//') # Check if there are no saved passwords, if not, notify and exit if [[ -z "$choices" ]]; then notify-send "Password Manager" "No password entries found." exit 0 fi # Use rofi to show a menu with the list of saved passwords # The user can type to filter the list or select one from the list # -p sets the prompt text # -theme-str sets a placeholder text in the input field selected=$(echo "$choices" | rofi -dmenu \ -p "Select password:" \ -theme-str 'entry { placeholder: "Type or select a password"; }') # Check if the user cancelled the selection, if so, notify and exit if [[ -z "$selected" ]]; then notify-send "Password Manager" "Selection cancelled." exit 0 fi # Set the file path for the selected password entry file="$SECRETS_DIR/$selected.gpg" # Check if the file exists, if not, notify and exit if [[ ! -f "$file" ]]; then notify-send "Password Manager" "Error: Password file not found." exit 1 fi # Decrypt password and copy to clipboard # NOTE: If you are not prompted for a GPG passphrase, it may be because: # - You used symmetric encryption, which asks for a passphrase at encryption time # - GPG caches the passphrase for a short period # - You used no passphrase or did not set a passphrase for GPG # Always use a strong passphrase when adding passwords. # Check if xclip or xsel is installed and use it to copy the password if command -v xclip &>/dev/null; then gpg --quiet --batch --yes --decrypt "$file" | xclip -selection clipboard elif command -v xsel &>/dev/null; then gpg --quiet --batch --yes --decrypt "$file" | xsel --clipboard --input else notify-send "Password Manager" "No clipboard utility found (xclip or xsel)." exit 1 fi # Notify the user that the password was copied to the clipboard notify-send "Password Manager" "Password for '$selected' copied to clipboard." }
Pretty simple, right? We list all saved passwords, show them in a Rofi menu, let the user pick one, decrypt it with GPG, and copy it to the clipboard using xclip
or xsel
.
Deleting a Password
Sometimes we want to delete old or unused passwords. We need a function to do that, but we want to make sure the user really wants to delete it. We will ask for confirmation before deleting.
delete_password() { # Define local variables local choices selected file confirm # Get a list of all saved passwords by listing the files in the password directory # We use sed to remove the .gpg extension from the filenames choices=$(ls "$SECRETS_DIR" 2>/dev/null | sed 's/\.gpg$//') # Check if there are no saved passwords, if not, notify and exit if [[ -z "$choices" ]]; then notify-send "Password Manager" "No password entries found to delete." exit 0 fi # Use rofi to show a menu with the list of saved passwords # The user can type to filter the list or select one from the list # -p sets the prompt text # -theme-str sets a placeholder text in the input field selected=$(printf "%s\n" $choices | rofi -dmenu \ -p "Select password to DELETE:" \ -theme-str 'entry { placeholder: "Type or select a password to delete"; }') # Check if the user cancelled the selection, if so, notify and exit if [[ -z "$selected" ]]; then notify-send "Password Manager" "Deletion cancelled." exit 0 fi # Set the file path for the selected password entry file="$SECRETS_DIR/$selected.gpg" # Check if the file exists, if not, notify and exit if [[ ! -f "$file" ]]; then notify-send "Password Manager" "Error: Password file not found." exit 1 fi # Ask for confirmation before deleting the password entry confirm=$(printf "No\nYes" | rofi -dmenu \ -p "Confirm delete '$selected'?" \ -theme-str 'entry { placeholder: "Select Yes to confirm / No to cancel"; }') # If the user confirmed, delete the file and notify the user # If not, notify that the deletion was cancelled if [[ "$confirm" == "Yes" ]]; then rm -f "$file" notify-send "Password Manager" "Password entry '$selected' deleted." else notify-send "Password Manager" "Deletion cancelled." fi }
Step 3: Putting It All Together
Now that we have all our functions, we need to call the main menu function at the end of the script to start the program:
main_menu
Conclusion
Managing passwords does not have to be difficult. With Bash and GPG, you can create a simple, secure, and fast password manager that works entirely in your terminal. This approach keeps your secrets safe and allows you to work efficiently.
Full Script
Below is the full script, you can copy and paste it into your passmgr.sh
file, and you can find it also on my github gist simple-gpg-password-manager-with-rofi.
#!/bin/bash # # Simple GPG-encrypted Password Manager using rofi/dmenu # # Description: # This script allows you to securely store, retrieve, and delete passwords # using GPG symmetric encryption. Passwords are saved individually as # encrypted files in a hidden directory (~/passwords by default). # # Features: # - Add password: Encrypt and store a new password securely. # - Get password: Decrypt and copy a stored password to the clipboard. # - Delete password: Remove a stored password securely after confirmation. # # The script uses rofi (or dmenu) for a graphical menu interface, # and supports clipboard utilities (xclip or xsel) for easy copying. # # Requirements: # - gpg (GnuPG) installed for encryption/decryption. # - rofi or dmenu for menus. # - xclip or xsel for clipboard operations. # - notify-send for desktop notifications (optional but recommended). # # Usage: # Run the script and choose from the menu: # ./passmgr.sh # # License: MIT License # Author: Edmond Kaçaj <info@edmondkacaj.com> # PASSWORDS_DIR="$HOME/passwords" # Directory to store encrypted passwords mkdir -p "$PASSWORDS_DIR" # Create the directory if it does not exist chmod 700 "$PASSWORDS_DIR" # Set the directory permissions to 700 main_menu() { local CHOICE # Local variable to store the user's choice # We use rofi to show a menu with options: Add Password, Get Password, Delete Password, Exit, # and we store the user's choice in the CHOICE variable CHOICE=$(echo -e "Add Password\nGet Password\nDelete Password\nExit" | rofi -dmenu -p "Password Manager:") # Show the menu using Rofi # Then based on the user's choice, we call the corresponding function case "$CHOICE" in "Add Password") add_password ;; "Get Password") get_password ;; "Delete Password") delete_password ;; "Exit" | "") exit 0 ;; *) notify-send "Password Manager" "Invalid option." ;; # Notify if the option is invalid esac } add_password() { # Define local variables local name password file # Use rofi to get the name of the new password entry from the user # -p sets the prompt text # -theme-str sets a placeholder text in the input field name=$(rofi -dmenu \ -p "Enter new password name:" \ -theme-str 'entry { placeholder: "e.g. github, aws"; }') # Check if the name is empty (user cancelled) # send a notification and exit if it is empty if [[ -z "$name" ]]; then notify-send "Password Manager" "Add password cancelled." exit 0 fi # Set the file path for the new password entry file="$PASSWORDS_DIR/$name.gpg" # Check if the file already exists # send a notification and exit if it exists if [[ -f "$file" ]]; then notify-send "Password Manager" "Error: Entry '$name' already exists." exit 1 fi # Use rofi to get the password from the user # -password makes the input hidden (like a password field) # -p sets the prompt text # -theme-str sets a placeholder text in the input field password=$(rofi -dmenu -password \ -p "Enter password for \"$name\":" \ -theme-str 'entry { placeholder: "e.g. myStrongPass123"; }') # Check if the password is empty (user cancelled) # send a notification and exit if it is empty if [[ -z "$password" ]]; then notify-send "Password Manager" "Add password cancelled." exit 0 fi # Encrypt the password using GPG and save it to the file # -n means do not add a newline at the end # --symmetric means we want to use symmetric encryption (same key for encrypting and decrypting) # --cipher-algo AES256 means we want to use the AES256 encryption algorithm # -o "$file" means we want to save the output to the file echo -n "$password" | gpg --symmetric --cipher-algo AES256 -o "$file" # Set the file permissions to 600 (only the owner can read and write) chmod 600 "$file" # Notify the user that the password was added successfully notify-send "Password Manager" "Password for '$name' added securely." } get_password() { # Define local variables local choices selected file # Get a list of all saved passwords by listing the files in the password directory # We use sed to remove the .gpg extension from the filenames choices=$(ls "$PASSWORDS_DIR" 2>/dev/null | sed 's/\.gpg$//') # Check if there are no saved passwords, if not, notify and exit if [[ -z "$choices" ]]; then notify-send "Password Manager" "No password entries found." exit 0 fi # Use rofi to show a menu with the list of saved passwords # The user can type to filter the list or select one from the list # -p sets the prompt text # -theme-str sets a placeholder text in the input field selected=$(echo "$choices" | rofi -dmenu \ -p "Select password:" \ -theme-str 'entry { placeholder: "Type or select a password"; }') # Check if the user cancelled the selection, if so, notify and exit if [[ -z "$selected" ]]; then notify-send "Password Manager" "Selection cancelled." exit 0 fi # Set the file path for the selected password entry file="$PASSWORDS_DIR/$selected.gpg" # Check if the file exists, if not, notify and exit if [[ ! -f "$file" ]]; then notify-send "Password Manager" "Error: Password file not found." exit 1 fi # Decrypt password and copy to clipboard # NOTE: If you are NOT prompted for a GPG passphrase, it's because: # - You are using symmetric encryption, which asks for a passphrase at encryption time. # - GPG caches the passphrase for a short period, or you used no passphrase. # - Or you haven't set a passphrase for GPG itself (rare). # # To ensure security, ALWAYS use a strong passphrase when adding passwords. # Check if xclip or xsel is installed and use it to copy the password to the clipboard if command -v xclip &>/dev/null; then gpg --quiet --batch --yes --decrypt "$file" | xclip -selection clipboard elif command -v xsel &>/dev/null; then gpg --quiet --batch --yes --decrypt "$file" | xsel --clipboard --input else notify-send "Password Manager" "No clipboard utility found (xclip or xsel)." exit 1 fi # Notify the user that the password was copied to the clipboard notify-send "Password Manager" "Password for '$selected' copied to clipboard." } delete_password() { # Define local variables local choices selected file confirm # Get a list of all saved passwords by listing the files in the password directory # We use sed to remove the .gpg extension from the filenames choices=$(ls "$PASSWORDS_DIR" 2>/dev/null | sed 's/\.gpg$//') # Check if there are no saved passwords, if not, notify and exit if [[ -z "$choices" ]]; then notify-send "Password Manager" "No password entries found to delete." exit 0 fi # Use rofi to show a menu with the list of saved passwords # The user can type to filter the list or select one from the list # -p sets the prompt text # -theme-str sets a placeholder text in the input field selected=$(printf "%s\n" $choices | rofi -dmenu \ -p "Select password to DELETE:" \ -theme-str 'entry { placeholder: "Type or select a password to delete"; }') # Check if the user cancelled the selection, if so, notify and exit if [[ -z "$selected" ]]; then notify-send "Password Manager" "Deletion cancelled." exit 0 fi # Set the file path for the selected password entry file="$PASSWORDS_DIR/$selected.gpg" # Check if the file exists, if not, notify and exit if [[ ! -f "$file" ]]; then notify-send "Password Manager" "Error: Password file not found." exit 1 fi # Ask for confirmation before deleting the password entry confirm=$(printf "No\nYes" | rofi -dmenu \ -p "Confirm delete '$selected'?" \ -theme-str 'entry { placeholder: "Select Yes to confirm / No to cancel"; }') # If the user confirmed, delete the file and notify the user # If not, notify that the deletion was cancelled if [[ "$confirm" == "Yes" ]]; then rm -f "$file" notify-send "Password Manager" "Password entry '$selected' deleted." else notify-send "Password Manager" "Deletion cancelled." fi } main_menu