diff --git a/kkae.desktop b/Linux/kkae.desktop similarity index 100% rename from kkae.desktop rename to Linux/kkae.desktop diff --git a/README.md b/README.md index 8a6ebb7..5050b5e 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Written in Bash, works on Linux (Wayland/X11), macOS, as well as on Windows via ## Usage ``` -usage: kkae [-asmnpvbh] [-l LENGTH] [-c CASE] [-e LIST] [-r MIN-MAX] +usage: kkae [-asmdpwbvh] [-l LENGTH] [-c CASE] [-e LIST] Generate a random password and save it into the clipboard. -l LENGTH Character length of the password (default is 16). -c CASE Only include lowercase or uppercase letters. @@ -26,77 +26,26 @@ Generate a random password and save it into the clipboard. -m Save the password into the middle-click clipboard. -n Do not send a notification when the password has been saved. -p Print the password instead of saving it into the clipboard. + -P Same as -p, but without the trailing newline. -v Show current settings and exit. -b Enable debug mode. -h Print these instructions and exit. ``` + ## Configuration If the file `/etc/kkae.conf` exists, its content will become the default kkae settings when ran from the command-line or the application. See [the example file](https://github.com/Silejonu/kkae/blob/main/kkae.conf) for options. - - -## Dependencies -At least one of those two programs must be installed on Linux: -* for Wayland: `wl-clipboard` -* for X11: `xclip` - -`kkae` also uses some GNU coreutils. It will prompt for any missing dependency when ran from the command-line. - -To get notifications in Windows, you need to copy [wsl-notify-send.exe](https://github.com/stuartleeks/wsl-notify-send/releases) into your WSL `$PATH`. - ## Installation instructions -### Main program ``` git clone https://github.com/Silejonu/kkae -# cp kkae/kkae /usr/local/bin/ -# chmod 755 /usr/local/bin/kkae -``` - -### Example config file - -`# cp kkae/kkae.conf /etc/` - -### Clickable button/application - -#### On Linux - -`# cp kkae/kkae.desktop /usr/share/applications/` - -#### On macOS - -Open Script Editor, make sure AppleScript is selected in the dropdown menu in the top-left corner, and enter the following text: -``` -do shell script "/usr/local/bin/kkae" +cd kkae +chmod +x ./install.sh +sudo ./install.sh ``` -Then go to File -> Export… - -Export as: `kkae` - -Where: Applications - -File Format: Application - -Code Sign: Don't Code Sign - -#### On WSL - -Create a script named `kkae.bat` wherever you like, with the following content: -``` -@echo off -title kkae -wsl.exe kkae -exit -``` - -Then go into `%AppData%\Microsoft\Windows\Start Menu\Programs` and right-click -> New -> Shortcut. - -Location: `cmd.exe /c "\path\to\kkae.bat` - -Name: `kkae` ## To-do Here are the things I wish to implement in the future: -* Make success notifications on Windows and macOS non-persistent -* Make an installation script +* Make an uninstallation script +* Fix the app icon in macOS \ No newline at end of file diff --git a/Windows/kkae.ico b/Windows/kkae.ico new file mode 100644 index 0000000..b0eb6b7 Binary files /dev/null and b/Windows/kkae.ico differ diff --git a/install.sh b/install.sh new file mode 100644 index 0000000..c13095f --- /dev/null +++ b/install.sh @@ -0,0 +1,141 @@ +#!/bin/bash + +if [[ ${UID} -ne 0 ]] ; then + printf "Error: this script requires superuser privileges.\nRun it with: sudo ${0}\n" >&2 + exit 1 +fi + +# Determine the platform the script is running on +if [[ $(uname -r | grep -i Microsoft) ]] ; then + os='windows' + if ! touch /mnt/c/Windows &> /dev/null ; then + printf "Error: WSL needs to be launched with admin privileges to be properly installed.\n" >&2 + exit 1 + fi +elif [[ $(uname) == 'Darwin' ]] ; then + os='macos' +else + os='linux' +fi + +install_linux_application() { +cp -f Linux/kkae.desktop /usr/share/applications/kkae +} + +install_macos_application() { +mkdir -p /Applications/kkae.app/Contents/MacOS /Applications/kkae.app/Contents/Resources +cp -f macOS/kkae.icns /Applications/kkae.app/Contents/Resources/ +tee /Applications/kkae.app/Contents/Info.plist << EOF > /dev/null + + + + + CFBundleExecutable + kkae + CFBundleName + kkae + CFBundleIconFile + kkae.icns + CFBundleShortVersionString + v1.2 + + +EOF +tee /Applications/kkae.app/Contents/MacOS/kkae << EOF > /dev/null +#!/bin/bash +/usr/local/bin/kkae +EOF +chmod 755 /Applications/kkae.app/Contents/MacOS/kkae +} + +install_windows_application() { +# Create the script that'll be called from within Windows +user_dir=$(wslpath $("/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe" '$HOME') | tr -d '\r' ) +mkdir -p "${user_dir}/kkae" +tee "${user_dir}/kkae/kkae.bat" << EOF > /dev/null +@echo off +title kkae +wsl.exe kkae +exit +EOF +# Add the .ico file +cp Windows/kkae.ico "${user_dir}/kkae/" + +# Install wsl-notify-send's latest release +cd $(mktemp -d) +latest_wsl_notify_send_release=$(curl --silent https://api.github.com/repos/stuartleeks/wsl-notify-send/releases/latest | grep tag_name | cut -d'"' -f4) +wget "https://github.com/stuartleeks/wsl-notify-send/releases/download/${latest_wsl_notify_send_release}/wsl-notify-send_windows_amd64.zip" +sudo apt install -y unzip +unzip wsl-notify-send_windows_amd64.zip +# When inside the Linux $PATH, wsl-notify-send.exe is very slow, +# so installing it into the Windows $PATH instead +cp -f wsl-notify-send.exe /mnt/c/Windows + +# Create the shortcut to appear in the Start menu +cd "${user_dir}" +tee CreatekkaeShortcut.vbs << EOF > /dev/null +Set oWS = WScript.CreateObject("WScript.Shell") +sLinkFile = "AppData\Roaming\Microsoft\Windows\Start Menu\Programs\kkae.lnk" +Set oLink = oWS.CreateShortcut(sLinkFile) + oLink.TargetPath = "%HOMEDRIVE%%HOMEPATH%\kkae\kkae.bat" + oLink.Description = "A powerful password generator that saves directly into the clipboard, with lots of options." + oLink.IconLocation = "%HOMEDRIVE%%HOMEPATH%\kkae\kkae.ico" +oLink.Save +EOF +"/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe" './CreatekkaeShortcut.vbs' +rm ./CreatekkaeShortcut.vbs +} + +# Make sure all dependencies are met +if [[ ${os} == 'linux' ]] ; then + for clipboard_manager in xclip wl-clipboard ; do + apt install -y ${clipboard_manager} 2> /dev/null ||\ + dnf install -y ${clipboard_manager} 2> /dev/null ||\ + pacman --noconfirm -S ${clipboard_manager} 2> /dev/null ||\ + zypper -n install ${clipboard_manager} 2> /dev/null ||\ + xbps-install -y -S ${clipboard_manager} 2> /dev/null ||\ + eopkg install -y ${clipboard_manager} 2> /dev/null ||\ + { printf "\nError: missing dependency: %s\nPlease install it and re-launch this script.\n" "${clipboard_manager}" >&2 ; exit 1 ; } + done +fi +for dependency in tr cat cut fold head sort wc ; do + if ! which ${dependency} &> /dev/null ; then + apt install -y ${dependency} 2> /dev/null ||\ + dnf install -y ${dependency} 2> /dev/null ||\ + pacman --noconfirm -S ${dependency} 2> /dev/null ||\ + zypper -n install ${dependency} 2> /dev/null ||\ + xbps-install -y -S ${dependency} 2> /dev/null ||\ + eopkg install -y ${dependency} 2> /dev/null ||\ + { printf "\nError: missing dependency: %s\nPlease install it and re-launch this script.\n" "${dependency}" >&2 ; exit 1 ; } + fi +done + +# Install the command-line utility +mkdir -p /usr/local/bin +cp -f kkae /usr/local/bin/ +chmod 755 /usr/local/bin/kkae + +# Install the configuration file +if [[ -f /etc/kkae.conf ]] ; then + read -p 'The configuration file /etc/kkae.conf already exists. Do you want to overwrite it? [y/N] ' yn + case ${yn} in + [yY]|[yY][eE][sS] ) + cp -f kkae.conf /etc/ + printf 'The configuration file has been updated.\n' ;; + * ) + printf 'The configuration file has been kept untouched.\n' ;; + esac +else + cp kkae.conf /etc/ +fi + +case ${os} in + linux ) install_linux_application ;; + macos ) install_macos_application ;; + windows ) install_windows_application ;; +esac + +printf "\nInstallation finished.\nStart using kkae by running it in the terminal or launching the application.\nRun kkae -h to learn about all of its options!\n" + +exit 0 diff --git a/kkae b/kkae index 9f91cc2..424f52a 100644 --- a/kkae +++ b/kkae @@ -1,35 +1,18 @@ #!/bin/bash -kkae_version='1.1' +kkae_version='1.2' # Needed for macOS compatibility export LC_ALL=C debug_message() { -if [[ ${debug} == 'true' ]] ; then - echo "${@}" +if [[ "${debug}" == 'true' ]] ; then + printf "%s\n" "${@}" fi } -# Determine the display server used -if [[ "${XDG_SESSION_TYPE}" == "wayland" ]] ; then - os='linux' - clipboard_manager='wl-copy' -elif [[ "${XDG_SESSION_TYPE}" == "x11" ]] ; then - os='linux' - clipboard_manager='xclip' -fi - -# Make sure all dependencies are met -for dependency in tr cat cut fold head sort wc ${clipboard_manager} ; do - if ! which ${dependency} &> /dev/null ; then - printf "Missing dependency: %s.\n" "${dependency}" >&2 - exit 1 - fi -done - usage() { cat << EOF -usage: ${0} [-asmdpwbvh] [-l LENGTH] [-c CASE] [-e LIST] +usage: ${0} [-asmnpvbh] [-l LENGTH] [-c CASE] [-e LIST] [-r MIN-MAX] Generate a random password and save it into the clipboard. -l LENGTH Character length of the password (default is ${password_length}). -c CASE Only include lowercase or uppercase letters. @@ -40,6 +23,7 @@ Generate a random password and save it into the clipboard. -m Save the password into the middle-click clipboard. -n Do not send a notification when the password has been saved. -p Print the password instead of saving it into the clipboard. + -P Same as -p, but without the trailing newline. -v Show current settings and exit. -b Enable debug mode. -h Print these instructions and exit. @@ -64,22 +48,21 @@ if [[ -f /etc/kkae.conf ]] ; then fi # Parse the command-line options -while getopts l:c:ase:r:mnpvbh option ; do +while getopts l:c:ase:r:mnpPvbh option ; do case ${option} in l) password_length="${OPTARG}" - valid_number='^[0-9]+$' - if ! [[ ${password_length} =~ ${valid_number} ]] ||\ - [[ ${password_length} -lt 0 ]] ||\ - [[ ${password_length} -gt ${maximum_password_length} ]] + if ! [[ "${password_length}" =~ ^[0-9]+$ ]] ||\ + [[ "${password_length}" -lt 0 ]] ||\ + [[ "${password_length}" -gt "${maximum_password_length}" ]] then printf "Option -l requires a number between 0 and %s.\n" "${maximum_password_length}">&2 printf "Edit /etc/kkae.conf to change the maximum password length.\n" >&2 exit 1 fi ;; c) password_case="${OPTARG}" - if [[ ${password_case} == 'lowercase' ]] ; then + if [[ "${password_case}" == 'lowercase' ]] ; then excluded_letters='ABCDEFGHIJKLMNOPQRSTUVWXYZ' - elif [[ ${password_case} == 'uppercase' ]] ; then + elif [[ "${password_case}" == 'uppercase' ]] ; then excluded_letters='abcdefghijklmnopqrstuvwxyz' else printf "Option -c accepts the following paramaters: lowercase, uppercase.\n" >&2 @@ -88,7 +71,7 @@ while getopts l:c:ase:r:mnpvbh option ; do ;; a) tr_character_set='[:alnum:]' ;; s) similar_characters='' ;; - e) if [[ -f ${OPTARG} ]] ; then + e) if [[ -f "${OPTARG}" ]] ; then excluded_characters=$(tr -d '\n' < "${OPTARG}") else excluded_characters="${OPTARG}" @@ -96,17 +79,20 @@ while getopts l:c:ase:r:mnpvbh option ; do ;; r) min_special_characters_ratio="$(echo "${OPTARG}" | cut -d'-' -f1)" max_special_characters_ratio="$(echo "${OPTARG}" | cut -d'-' -f2)" - if [[ min_special_characters_ratio -gt max_special_characters_ratio ]] ; then - printf "Invalid range in -r.\n" >&2 + if ! [[ "${min_special_characters_ratio}" =~ ^[0-9]+$ ]] ||\ + ! [[ "${max_special_characters_ratio}" =~ ^[0-9]+$ ]] ||\ + [[ "${min_special_characters_ratio}" -gt "${max_special_characters_ratio}" ]] ; then + printf "Invalid range: -r %s\n" "${OPTARG}" >&2 exit 1 fi - if [[ max_special_characters_ratio -lt 1 ]] ; then + if [[ "${max_special_characters_ratio}" -lt 1 ]] ; then printf "Upper range of -r can't be lower than 1.\nIf you want to exclude special characters entirely, use -a instead.\n" >&2 exit 1 fi ;; m) middle_click_clipboard='true' ;; n) do_not_notify='true' ;; - p) print_password='true' ;; + p) print_password='newline' ;; + P) print_password='nonewline' ;; v) print_current_settings='true' ;; b) debug='true' ;; h) usage && exit 0 ;; @@ -118,24 +104,40 @@ done shift $(( OPTIND - 1 )) # Exit in case an invalid option has been entered -if [[ ${#} -gt 0 ]] ; then - printf "Invalid option or parameter.\n" >&2 +if [[ "${#}" -gt 0 ]] ; then + printf "Invalid option or parameter: %s\n" "${@}" >&2 usage exit 1 fi -if [[ ${os} == 'linux' ]] ; then - debug_message "Display server detected: ${XDG_SESSION_TYPE}" +# Determine the platform the script is running on +if [[ $(uname -r | grep -i Microsoft) ]] ; then + os='windows' + debug_message "System detected: Windows Subsystem for Linux" + kkae_notification() { + if [[ "${do_not_notify}" == 'false' ]] ; then + wsl-notify-send.exe --appId kkae --category kkae --expire-time 1 "${1}" + fi + } elif [[ $(uname) == 'Darwin' ]] ; then os='macos' debug_message "System detected: macOS" + kkae_notification() { + if [[ "${do_not_notify}" == 'false' ]] ; then + osascript -e "display notification \"${1}\" with title \"kkae\"" + fi + } else - os='windows' - debug_message "System detected: Windows Subsystem for Linux" - # Add support for notifications via wsl-notify-send - notify-send() { wsl-notify-send.exe --category "${WSL_DISTRO_NAME}" "${@}"; } + os='linux' + debug_message "System detected: Linux" + kkae_notification() { + if [[ "${do_not_notify}" == 'false' ]] ; then + notify-send -t 1000 --hint=int:transient:1 -i dialog-password-symbolic kkae "${1}" + fi + } fi +# Check which characters can be used with the current parameters valid_characters=$(tr -dc "${tr_character_set}" < /dev/urandom |\ tr -d "${excluded_characters}${similar_characters}${excluded_letters}" |\ head -c 10000 | fold -w1 | sort -u | tr -d '\n') @@ -144,16 +146,17 @@ valid_special_characters=$(echo "${valid_characters}" | tr -d '[:alnum:]' |\ head -c 10000 | fold -w1 | sort -u | tr -d '\n') debug_message "Valid special characters: ${valid_special_characters}" +# Determine the minimum and maximum amount of special characters the password must contain min_special_characters="$(( password_length * min_special_characters_ratio / 100 ))" debug_message "Minimum amount of special characters: ${min_special_characters} (${min_special_characters_ratio}%)" max_special_characters="$(( password_length * max_special_characters_ratio / 100 ))" debug_message "Maximum amount of special characters: ${max_special_characters} (${max_special_characters_ratio}%)" -if [[ max_special_characters -eq 0 ]] ; then +if [[ "${max_special_characters}" -eq 0 ]] ; then max_special_characters='1' debug_message "Maximum amount of special characters set to 1 to account for rounding." fi -if [[ ${print_current_settings} = 'true' ]] ; then +if [[ "${print_current_settings}" = 'true' ]] ; then cat << EOF $(tput bold)kkae v${kkae_version}$(tput sgr0) $(tput bold)Options passed in the terminal overwrite settings from /etc/kkae.conf$(tput sgr0) @@ -176,25 +179,18 @@ generate_password() { head -c "${password_length}") } -debug_message "Preparing to generate the password…" +debug_message "Generating the password…" # Pass a non-zero diversity target to trigger the while loop password_diversity_target='1' # Generate passwords until at least one character of each selected category is found in a single password -while [[ ${password_diversity} -ne ${password_diversity_target} ]] ; do +until [[ "${password_diversity}" -eq "${password_diversity_target}" ]] ; do + (( password_generation_attempts += 1 )) timeout() { + debug_message "${password_generation_attempts} passwords were generated, none passed the validity check." printf "Aborted: could not generate a valid password after %ss.\n" "${countdown}" >&2 printf "Try again, or review your parameters.\n" >&2 printf "Alternatively, edit /etc/kkae.conf to increase the timeout.\n" >&2 - if [[ ${do_not_notify} == 'false' ]] ; then - if [[ ${os} == 'linux' ]] ; then - notify-send -t 1000 -i dialog-password-symbolic kkae "Could not generate a valid password in time." - elif [[ ${os} == 'macos' ]] ; then - osascript -e 'display notification "Could not generate a valid password in time." with title "kkae"' - elif [[ ${os} == 'windows' ]] ; then - notify-send --category kkae --appId kkae "Could not generate a valid password in time." - fi - debug_message "Notification sent. Exit code: ${?}" - fi + kkae_notification "Could not generate a valid password in time." exit 3 } # Quit the program if a valid password could not be generated before the ${countdown} @@ -204,91 +200,84 @@ while [[ ${password_diversity} -ne ${password_diversity_target} ]] ; do password_diversity_target='0' # Check if the password should contain at least one character of a given category if [[ "${valid_characters}" == *[0-9]* ]] ; then - password_diversity_target=$(( password_diversity_target + 1 )) + (( password_diversity_target += 1 )) fi if [[ "${valid_characters}" == *[a-z]* ]] ; then - password_diversity_target=$(( password_diversity_target + 1 )) + (( password_diversity_target += 1 )) fi if [[ "${valid_characters}" == *[A-Z]* ]] ; then - password_diversity_target=$(( password_diversity_target + 1 )) + (( password_diversity_target += 1 )) fi if [[ "${#valid_special_characters}" -gt 0 ]] ; then - password_diversity_target=$(( password_diversity_target + 1 )) + (( password_diversity_target += 1 )) fi # Reset the password diversity on each new pass password_diversity='0' - # Create a function to record the diversity of the generated password - record_diversity() { - password_diversity=$(( password_diversity + 1 )) - } # Check if the password contains at least one number of a given category if [[ "${password}" == *[0-9]* ]] ; then - record_diversity + (( password_diversity += 1 )) fi if [[ "${password}" == *[a-z]* ]] ; then - record_diversity + (( password_diversity += 1 )) fi if [[ "${password}" == *[A-Z]* ]] ; then - record_diversity + (( password_diversity += 1 )) fi special_characters_in_password=$(printf "%s" "${password}" | tr -d 'a-zA-Z0-9') - if [[ "${#special_characters_in_password}" -gt min_special_characters ]] && [[ "${#special_characters_in_password}" -le max_special_characters ]] ; then - record_diversity + if [[ "${#special_characters_in_password}" -gt min_special_characters ]] &&\ + [[ "${#special_characters_in_password}" -le max_special_characters ]] ; then + (( password_diversity += 1 )) fi done -debug_message "Password generated: ${password}" +debug_message "A valid password was found after ${password_generation_attempts} attempt(s)." -if [[ ${print_password} = 'true' ]] ; then +if [[ "${print_password}" = 'newline' ]] ; then debug_message "Printing password:" echo "${password}" exit 0 +elif [[ "${print_password}" = 'nonewline' ]] ; then + debug_message "Printing password:" + printf "%s" "${password}" + exit 0 fi -# Save the password to the clipboard -case ${XDG_SESSION_TYPE} in - wayland) if [[ ${middle_click_clipboard} = 'true' ]] ; then +# Save the password in the clipboard +case "${os}" in + linux) save_password_in_wayland_clipboard() { + if [[ "${middle_click_clipboard}" = 'true' ]] ; then debug_message "Saving the password into the middle-click clipboard." selection='--primary' fi - printf "%s" "${password}" | wl-copy ${selection} - debug_message "Password copied into the Wayland clipboard." ;; - x11) if [[ ${middle_click_clipboard} = 'true' ]] ; then + printf "%s" "${password}" | wl-copy ${selection} 2> /dev/null || return 1 + debug_message "Password copied into the Wayland clipboard." + } + save_password_in_x11_clipboard() { + if [[ ${middle_click_clipboard} = 'true' ]] ; then debug_message "Saving the password into the middle-click clipboard." selection='primary' else selection='clipboard' fi printf "%s" "${password}" | xclip -selection ${selection} - debug_message "Password copied into the X11 clipboard." ;; - *) if [[ ${os} == 'macos' ]] ; then - if [[ ${middle_click_clipboard} = 'true' ]] ; then - printf "Error: middle-click clipboard not available in macOS.\n" >&2 - exit 1 - fi - printf "%s" "${password}" | pbcopy - debug_message "Password copied into the macOS clipboard." - elif [[ ${os} == 'windows' ]] ; then - if [[ ${middle_click_clipboard} = 'true' ]] ; then - printf "Error: middle-click clipboard not available in WSL.\n" >&2 - exit 1 - fi - printf "%s" "${password}" | clip.exe - debug_message "Password copied into the Windows clipboard." - fi ;; + debug_message "Password copied into the X11 clipboard." + } + save_password_in_wayland_clipboard || save_password_in_x11_clipboard ;; + macos) if [[ "${middle_click_clipboard}" = 'true' ]] ; then + printf "Error: middle-click clipboard not available on macOS.\n" >&2 + exit 1 + fi + printf "%s" "${password}" | pbcopy + debug_message "Password copied into the macOS clipboard." ;; + windows) if [[ "${middle_click_clipboard}" = 'true' ]] ; then + printf "Error: middle-click clipboard not available on WSL.\n" >&2 + exit 1 + fi + printf "%s" "${password}" | clip.exe + debug_message "Password copied into the Windows clipboard." ;; esac -if [[ ${do_not_notify} == 'true' ]] ; then - debug_message "Do not send a notification." - exit 0 -else - if [[ ${os} == 'linux' ]] ; then - notify-send -t 1000 --hint=int:transient:1 -i dialog-password-symbolic kkae "New random password saved in the clipboard." - elif [[ ${os} == 'macos' ]] ; then - osascript -e 'display notification "New random password saved in the clipboard." with title "kkae"' - elif [[ ${os} == 'windows' ]] ; then - notify-send --category kkae --appId kkae "New random password saved in the clipboard." - fi - debug_message "Notification sent. Exit code: ${?}" - exit 0 -fi +kkae_notification "New random password saved in the clipboard." + +exit 0 + diff --git a/kkae.conf b/kkae.conf index e72e4c8..7ac2dd4 100644 --- a/kkae.conf +++ b/kkae.conf @@ -34,7 +34,8 @@ #do_not_notify='true' # Print the password instead of copying it into the clipboard -#print_password='true' +# Can be set to either 'newline' or 'nonewline' +#print_password='nonewline' # Time in seconds searching for a valid password before giving up #countdown='5' diff --git a/macOS/create_icns_from_svg b/macOS/create_icns_from_svg new file mode 100644 index 0000000..14f21bb --- /dev/null +++ b/macOS/create_icns_from_svg @@ -0,0 +1,2 @@ +convert -background none -density 2000 -resize '256x256' dialog-password-symbolic.svg kkae.png +convert -background none -density 144 kkae.png kkae.icns diff --git a/macOS/kkae.icns b/macOS/kkae.icns new file mode 100644 index 0000000..8f96bab Binary files /dev/null and b/macOS/kkae.icns differ