├── .ci └── before-install.sh ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── passbox └── test ├── add-field-tests.bats ├── delete-tests.bats ├── gen-tests.bats ├── get-tests.bats ├── new-tests.bats ├── remove-field-tests.bats ├── search-tests.bats ├── test-key-gen.conf ├── test_helper.bash └── update-tests.bats /.ci/before-install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -ex 3 | 4 | wget -O bats-v0.4.0.tar.gz https://github.com/sstephenson/bats/archive/v0.4.0.tar.gz 5 | tar -zxvf bats-v0.4.0.tar.gz 6 | cd bats-0.4.0 7 | mkdir ~/env 8 | ./install.sh ~/env 9 | 10 | if [ "${TRAVIS_OS_NAME}" == 'osx' ]; then 11 | brew update 12 | brew install gnupg 13 | fi 14 | 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | shunit2-2.0.3 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: bash 3 | notifications: 4 | email: 5 | on_success: never 6 | matrix: 7 | include: 8 | - os: linux 9 | env: PASSBOX_ASYMMETRIC=false 10 | - os: osx 11 | env: PASSBOX_ASYMMETRIC=false 12 | - os: linux 13 | env: PASSBOX_RECIPIENT=test@example.com PASSBOX_ASYMMETRIC=true 14 | - os: osx 15 | env: PASSBOX_RECIPIENT=test@example.com PASSBOX_ASYMMETRIC=true 16 | before_install: 17 | - .ci/before-install.sh 18 | - export PATH=$PATH:~/env/bin 19 | - gpg --batch --gen-key test/test-key-gen.conf 20 | script: bats -t test 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Rob Bollons 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/RobBollons/passbox.svg?branch=master)](https://travis-ci.org/RobBollons/passbox) 2 | 3 | # PASSBOX 4 | #### A simple command line password manager using bash and a GPG encrypted flat file 5 | Passbox is a tool for managing a GPG encrypted text file as a password database. 6 | 7 | Please bear in mind that due to the highly configurable nature of GnuPG and passbox, all responsibility for keeping your passwords secure and backed up is on you. 8 | 9 | Credit to [drduh/pwd.sh](https://github.com/drduh/pwd.sh) for some ideas. Please check that project out as it might suit your needs better. 10 | 11 | ### Features 12 | - [x] Search/Add/Update/Delete password entries 13 | - [x] Generate random passwords 14 | - [x] Manage additional custom fields other than just username/password 15 | - [x] Configurable symmetric/asymmetric encryption 16 | - [ ] Cross platform clipboard support 17 | - [ ] Configure settings such as passbox file location from '.passboxrc' config file 18 | 19 | ### Pre-requisites 20 | The aim is to support as many OSs as possible, the tests are ran against OSX and Linux only but Windows support could potentially be achieved though the use of Cygwin or MSYS (MSYS comes along with a standard install of Git on Windows) 21 | 22 | - GnuPG 23 | - Grep 24 | - Bash 25 | 26 | ### Installing 27 | ```` 28 | curl -L https://raw.githubusercontent.com/RobBollons/passbox/master/passbox > ./passbox && chmod +x ./passbox 29 | ```` 30 | 31 | ### Usage 32 | ```` 33 | usage: passbox [action] 34 | 35 | Passbox - command line password manager utility 36 | 37 | ACTIONS 38 | add-field Update an existing entry to add additional fields to 39 | delete Remove an entry from the password database 40 | get Get a particular password entry by it's name 41 | generate Generate a new random password 42 | new Prompt to create a new passbox entry 43 | remove-field Update an existing entry to remove additional fields 44 | search Search the password database for a particular string, returns all matching entries 45 | update Update an existing entry in the password database 46 | ```` 47 | 48 | The default location of the passbox file is '~/passbox.gpg' however this can be overridden using the PASSBOX_LOCATION env variable. So for example you could put this in your .bashrc: 49 | ```` 50 | export PASSBOX_LOCATION='~/dropbox/passwords.gpg' 51 | ```` 52 | 53 | Passbox uses *symmetric* encryption by default, this means that the data is encrypted using a simple passphrase. You can enable *asymmetric* encryption by setting the following environment variables: 54 | ```` 55 | export PASSBOX_ASYMMETRIC=true 56 | export PASSBOX_RECIPIENT=yourkeyuser@example.com 57 | ```` 58 | Asymmetric cryptography uses a public key for encryption and the private key for decryption. You can generate yourself a public/private key pair by using `gpg --gen-key` and following the prompts. 59 | 60 | ### Tests 61 | Tests are ran locally against the 'test/' directory using [bats](https://github.com/sstephenson/bats) e.g. `bats test/` 62 | 63 | **WARNING:** The tests will temporarily override the 'PASSBOX_LOCATION' env variable before each test but will restore it again after 64 | 65 | ### Similar Projects 66 | - [drudh/pwd.sh](https://github.com/drduh/pwd.sh) - Script to manage passwords in an encrypted file using gpg 67 | - [pass](http://www.passwordstore.org/) - Standard UNIX password manager 68 | -------------------------------------------------------------------------------- /passbox: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Script for managing passwords in a symmetrically encrypted file using GnuPG. 4 | # 5 | # Copyright (C) 2015 Robert Bollons - All Rights Reserved 6 | # Permission to copy and modify is granted under the MIT license 7 | # Version 2.0.0 8 | # Licence: MIT 9 | 10 | set -o errtrace 11 | set -o nounset 12 | set -o pipefail 13 | 14 | # Defaults 15 | PASSBOX=${PASSBOX_LOCATION:=~/passbox.gpg} 16 | GPG="$(command -v gpg2 || command -v gpg)" 17 | SEPARATOR="|" 18 | GPG_USEASYM="${PASSBOX_ASYMMETRIC:=false}" 19 | GPG_RECIPIENT="${PASSBOX_RECIPIENT:=}" 20 | 21 | ####################################### 22 | # HELPERS 23 | ####################################### 24 | 25 | ####################################### 26 | # Print an error message and then exit 27 | ####################################### 28 | fail () { 29 | local message="${1}" 30 | 31 | echo "Error: ${message}"; 32 | exit 1 33 | } 34 | 35 | ####################################### 36 | # Make sure required programs are installed and can be executed. 37 | ####################################### 38 | check_deps () { 39 | if [[ -z ${GPG} && ! -x ${GPG} ]] ; then 40 | fail "GnuPG is not available" 41 | fi 42 | } 43 | 44 | # ############################################################################################### 45 | # Retrieves password records matching search term which can be ether a string literal or a regex. 46 | # Matching records are stored in the $records global array. An individual record is a SEPARATOR 47 | # delimited string of the format: 48 | # [nameusernamepassword] 49 | # ############################################################################################### 50 | get_records () { 51 | if [ ! -s "${PASSBOX}" ] ; then 52 | fail "No passwords found" 53 | fi 54 | get_pass "Enter password to unlock ${PASSBOX}: " ; echo 55 | fail_on_invalid_pass 56 | records=(); 57 | while read -r line; do 58 | records+=("${line}"); 59 | done < <(decrypt "${password}" | grep -i "$1") 60 | } 61 | 62 | # ####################################################################### 63 | # Formats and prints to standard output the password records stored 64 | # in the $records global array. Prints a failure message if null or empty 65 | # ####################################################################### 66 | print_records () { 67 | if [ ! -z "${records+1}" ] && [ ! ${#records[@]} -eq 0 ] ; then 68 | for record in "${records[@]}" 69 | do 70 | split_line "${record}" 71 | if [[ ${#pass_vals[@]} -gt 0 ]] ; then 72 | echo 73 | echo "Name: ${pass_vals[0]}" 74 | echo "Username: ${pass_vals[1]}" 75 | echo "Password: ${pass_vals[2]}" 76 | 77 | # If there are >2 items in the array then there must be extra values 78 | if [[ ${#pass_vals[@]} -gt 2 ]] ; then 79 | for l in "${!pass_vals[@]}" 80 | do 81 | # TODO: There could be a better way to do this e.g. start the loop at 2 82 | if [[ $l -gt 2 ]]; then 83 | 84 | # Split the additional values and echo them out 85 | split_extra_val "${pass_vals[l]}" 86 | echo "${extra_val[0]}: ${extra_val[1]}" 87 | fi 88 | done 89 | fi 90 | fi 91 | done 92 | else 93 | fail "No entries found" 94 | fi 95 | } 96 | 97 | ####################################### 98 | # Generate a random password using GPG 99 | # Based on https://github.com/drduh/pwd.sh 100 | ####################################### 101 | gen_pass () { 102 | len=20 103 | max=100 104 | read -p "Password length? (default: ${len}, max: ${max}) " length 105 | if [[ ${length} =~ ^[0-9]+$ ]] ; then 106 | len=${length} 107 | fi 108 | 109 | # base64: 4 characters for every 3 bytes 110 | ${GPG} --gen-random -a 0 "$((max * 3/4))" | cut -c -"${len}" 111 | } 112 | 113 | ####################################### 114 | # Decrypt the password file with a given password 115 | ####################################### 116 | decrypt () { 117 | echo "${1}" | ${GPG} \ 118 | --decrypt --armor --batch --quiet \ 119 | --passphrase-fd 0 "${PASSBOX}" 2>/dev/null 120 | } 121 | 122 | ####################################### 123 | # Encrypt the file contents 124 | ####################################### 125 | encrypt () { 126 | local recipient_switch="" 127 | local enctype_switch="--symmetric" 128 | local local_user_switch="" 129 | local sign_switch="" 130 | 131 | if [ "$GPG_USEASYM" == true ]; then 132 | enctype_switch="--encrypt" 133 | recipient_switch="--recipient=$GPG_RECIPIENT" 134 | local_user_switch="--local-user=$GPG_RECIPIENT" 135 | sign_switch="--sign" 136 | if [[ "$GPG_RECIPIENT" == "" ]]; then 137 | recipient_switch="--default-recipient-self" 138 | local_user_switch="" 139 | fi 140 | fi 141 | 142 | ${GPG} \ 143 | $enctype_switch \ 144 | $recipient_switch \ 145 | $local_user_switch \ 146 | $sign_switch \ 147 | --armor --batch --yes \ 148 | --passphrase-fd 3 \ 149 | --output "${PASSBOX}" "${2}" 3< <(echo "${1}") 150 | 151 | } 152 | 153 | ####################################### 154 | # Remove an entry from the file contents 155 | ####################################### 156 | strip_entry () { 157 | local input="${1}" 158 | local passname="${2}" 159 | echo "${input}" | grep -v -e "^${passname}[${SEPARATOR}]" 160 | } 161 | 162 | ####################################### 163 | # Append an entry to the file contents 164 | ####################################### 165 | append_entry () { 166 | local input="${1}" 167 | local new_entry="${2}" 168 | ( echo "${input}"; 169 | echo "${new_entry}" ) 170 | } 171 | 172 | ####################################### 173 | # Re-write the file 174 | ####################################### 175 | write_entries () { 176 | local input="${1}" 177 | echo "${input}" | 178 | grep -v -e "^[[:space:]]*$" | 179 | encrypt "${gpgpass}" - 180 | 181 | echo "Passbox saved"; 182 | } 183 | 184 | ####################################### 185 | # Prompts for new password details 186 | ####################################### 187 | new_details () { 188 | if [[ $# -gt 0 ]] ; then 189 | passname="$1" 190 | else 191 | read -p "Name: " passname 192 | fi 193 | 194 | if [[ $# -gt 1 ]] ; then 195 | read -p "Username (${2}): " username 196 | if [[ -z "${username}" ]]; then 197 | username="${2}" 198 | fi 199 | else 200 | read -p "Username: " username 201 | fi 202 | 203 | read -p "Generate password? (y/n, default: y) " rand_pass 204 | if [[ "${rand_pass}" =~ ^([nN][oO]|[nN])$ ]]; then 205 | if [[ $# -gt 1 ]] ; then 206 | get_pass "Enter password for \"${username}\" (${3}): " ; echo 207 | if [[ -z ${password} ]] ; then 208 | userpass="${3}" 209 | else 210 | userpass="$password" 211 | fi 212 | else 213 | get_pass "Enter password for \"${username}\": " ; echo 214 | if [[ -z ${password} ]] ; then 215 | fail "No password provided" 216 | else 217 | userpass="${password}" 218 | fi 219 | fi 220 | else 221 | userpass=$(gen_pass) 222 | echo "Password: ${userpass}" 223 | fi 224 | } 225 | 226 | ####################################### 227 | # Write to the password file 228 | ####################################### 229 | write_pass () { 230 | local gpgpass=$password 231 | local addlfields=${1:-} 232 | if [ -z "${userpass+x}" ] ; then 233 | new_entry_string="" 234 | else 235 | new_entry_string="${passname}${SEPARATOR}${username}${SEPARATOR}${userpass}${addlfields}" 236 | fi 237 | 238 | if [ -z "${gpgpass+x}" ]; then 239 | get_pass "Enter password to unlock ${PASSBOX}: " ; echo 240 | gpgpass=$password 241 | fi 242 | 243 | if [ -f $PASSBOX_LOCATION ]; then 244 | fail_on_invalid_pass 245 | fi 246 | 247 | local result=$(decrypt "${password}") 248 | result=$(strip_entry "${result}" "${passname}") 249 | result=$(append_entry "${result}" "${new_entry_string}") 250 | write_entries "${result}" 251 | } 252 | 253 | ####################################### 254 | # Prompts for a password, hiding the contents with stars 255 | # Based on https://github.com/drduh/pwd.sh 256 | # Argument $1 is the string to use for the prompt message 257 | ####################################### 258 | get_pass () { 259 | local prompt="${1}" 260 | 261 | password='' 262 | while IFS= read -p "${prompt}" -r -s -n 1 char ; do 263 | if [[ ${char} == $'\0' ]] ; then 264 | break 265 | elif [[ ${char} == $'\177' ]] ; then 266 | if [[ -z "${password}" ]] ; then 267 | prompt="" 268 | else 269 | prompt=$'\b \b' 270 | password="${password%?}" 271 | fi 272 | else 273 | prompt="*" 274 | password+="${char}" 275 | fi 276 | done 277 | } 278 | 279 | ####################################### 280 | # Splits a line based on the given separator into an array 281 | ####################################### 282 | split_line () { 283 | local oldIFS=IFS 284 | IFS="${SEPARATOR}" read -ra pass_vals <<< "$1" 285 | IFS=$oldIFS 286 | } 287 | 288 | ####################################### 289 | # Splits a line based on a colon into an array 290 | ####################################### 291 | split_extra_val () { 292 | local oldIFS=IFS 293 | IFS=":" read -ra extra_val <<< "$1" 294 | IFS=$oldIFS 295 | } 296 | 297 | ####################################### 298 | # Gets an individual extra val key value pair from a line 299 | ####################################### 300 | get_extra_val () { 301 | local line="$1" 302 | 303 | split_line "${line}" 304 | if [[ ${#pass_vals[@]} -gt 2 ]] ; then 305 | for i in "${!pass_vals[@]}" 306 | do 307 | # TODO: There could be a better way to do this e.g. start the loop at 2 308 | if [[ $i -gt 2 ]]; then 309 | 310 | # Split the additional values and echo them out 311 | split_extra_val "${pass_vals[i]}" 312 | if [[ "$2" = "${extra_val[0]}" ]]; then 313 | break 314 | fi 315 | fi 316 | done 317 | fi 318 | } 319 | 320 | 321 | fail_on_invalid_pass () { 322 | decrypt "${password}" >/dev/null || fail "Incorrect password" 323 | } 324 | 325 | ####################################### 326 | # OPTION METHODS 327 | ####################################### 328 | 329 | ####################################### 330 | # Display the usage of the script 331 | ####################################### 332 | _usage () { 333 | cat << EOF 334 | usage: $(basename "$0") [action] 335 | 336 | Passbox - command line password manager utility 337 | 338 | ACTIONS 339 | add-field Update an existing entry to add additional fields to 340 | delete Remove an entry from the password database 341 | get Get a particular password entry by it's name 342 | generate Generate a new random password 343 | new Prompt to create a new passbox entry 344 | remove-field Update an existing entry to remove additional fields 345 | search Search the password database for a particular string, returns all matchin entries 346 | update Update an existing entry in the password database 347 | EOF 348 | } 349 | 350 | ####################################### 351 | # Search the password database for a given string and output the result 352 | ####################################### 353 | _search () { 354 | get_records "$1" 355 | print_records 356 | } 357 | 358 | ####################################### 359 | # Generate a random password 360 | ####################################### 361 | _gen_pass () { 362 | gen_pass 363 | } 364 | 365 | ####################################### 366 | # Prompt to create a new entry in the password database 367 | ####################################### 368 | _new () { 369 | new_details && 370 | get_pass "Enter password to unlock ${PASSBOX}: " ; 371 | echo && 372 | write_pass 373 | } 374 | 375 | ####################################### 376 | # Update an existing entry in the database 377 | ####################################### 378 | _update () { 379 | local gpgpass="" 380 | local addlfields="" 381 | 382 | get_pass "Enter password to unlock ${PASSBOX}: " ; echo 383 | gpgpass=${password} 384 | fail_on_invalid_pass 385 | details=$(decrypt "${password}" | grep -i "^$1[${SEPARATOR}]") 386 | split_line "${details}" 387 | for i in "${!pass_vals[@]}"; do 388 | if [[ $i -gt 2 ]]; then 389 | addlfields="$addlfields$SEPARATOR${pass_vals[i]}" 390 | fi 391 | done 392 | if [[ ${#pass_vals[@]} -gt 1 ]] ; then 393 | new_details "${pass_vals[0]}" "${pass_vals[1]}" "${pass_vals[2]}" && 394 | password=$gpgpass && 395 | write_pass "$addlfields" 396 | else 397 | fail "Could not find a password entry for '${1}'" 398 | fi 399 | } 400 | 401 | ####################################### 402 | # Get a single entry from the database based on the entries name 403 | ####################################### 404 | _get () { 405 | get_records "^$1[${SEPARATOR}]" 406 | print_records 407 | echo 408 | } 409 | 410 | ####################################### 411 | # Deletes an entry from the database based on the entries name 412 | ####################################### 413 | _delete () { 414 | local gpgpass="" 415 | 416 | get_pass "Enter password to unlock ${PASSBOX}: " ; echo 417 | gpgpass=${password} 418 | fail_on_invalid_pass 419 | details=$(decrypt "${password}" | grep -i "^$1[${SEPARATOR}]") 420 | split_line "${details}" 421 | 422 | if [[ ${#pass_vals[@]} == 0 ]] ; then 423 | fail "Could not find a password entry for '${1}'" 424 | fi 425 | 426 | read -p "Are you sure you want to delete the entry for '${pass_vals[0]}'? (y/n, default: n) " confirm_delete 427 | if [[ "${confirm_delete}" =~ ^([nN][oO]|[nN])$ ]]; then 428 | echo "Delete aborted" 429 | return 1 430 | fi 431 | 432 | passname=${pass_vals[0]} && 433 | password=$gpgpass && 434 | write_pass 435 | } 436 | 437 | 438 | ####################################### 439 | # Adds an additional field to an existing password entry 440 | ####################################### 441 | _add_field () { 442 | local gpgpass="" 443 | 444 | get_pass "Enter password to unlock ${PASSBOX}: " ; echo 445 | gpgpass=${password} 446 | fail_on_invalid_pass 447 | details=$(decrypt "${password}" | grep -i "^$1[${SEPARATOR}]") 448 | split_line "${details}" 449 | if [[ ${#pass_vals[@]} -lt 2 ]]; then 450 | fail "Could not find a password entry for '${1}'" 451 | fi 452 | 453 | passname="${pass_vals[0]}" 454 | password=$gpgpass 455 | read -p "Field Name: " field_name 456 | read -p "Field Value: " field_value 457 | new_entry_string="${details}${SEPARATOR}${field_name}:${field_value}" 458 | 459 | local result=$(decrypt "${password}") 460 | result=$(strip_entry "${result}" "${passname}") 461 | result=$(append_entry "${result}" "${new_entry_string}") 462 | write_entries "${result}" 463 | } 464 | 465 | ####################################### 466 | # Removes an additional field to an existing password entry 467 | ####################################### 468 | _remove_field () { 469 | local gpgpass="" 470 | 471 | get_pass "Enter password to unlock ${PASSBOX}: " ; echo 472 | gpgpass=${password} 473 | fail_on_invalid_pass 474 | details=$(decrypt "${password}" | grep -i "^${1}[${SEPARATOR}]") 475 | split_line "${details}" 476 | if [[ ${#pass_vals[@]} -lt 1 ]]; then 477 | fail "Could not find a password entry for '${1}'" 478 | fi 479 | 480 | passname="${pass_vals[0]}" 481 | password=$gpgpass 482 | 483 | get_extra_val "$details" "$2" 484 | field_to_remove="|${extra_val[0]}:${extra_val[1]}" 485 | new_entry_string="${details/${field_to_remove}/}" 486 | 487 | local result=$(decrypt "${password}") 488 | result=$(strip_entry "${result}" "${passname}") 489 | result=$(append_entry "${result}" "${new_entry_string}") 490 | write_entries "${result}" 491 | } 492 | 493 | # MAIN 494 | check_deps 495 | 496 | if [[ $# == 0 ]] ; then 497 | _usage 498 | else 499 | case $1 in 500 | search) 501 | if ! [ -n "${2+1}" ] ; then 502 | fail "Please specify a string to search for" 503 | fi 504 | _search "$2" 505 | ;; 506 | get) 507 | if ! [ -n "${2+1}" ] ; then 508 | fail "Please specify the name of an entry to get" 509 | fi 510 | _get "$2" 511 | ;; 512 | generate|gen) 513 | _gen_pass 514 | ;; 515 | new) 516 | _new 517 | ;; 518 | update) 519 | if ! [ -n "${2+1}" ] ; then 520 | fail "Please specify the name of an entry to update" 521 | fi 522 | _update "$2" 523 | ;; 524 | delete) 525 | if ! [ -n "${2+1}" ] ; then 526 | fail "Please specify the name of an entry to delete" 527 | fi 528 | _delete "$2" 529 | ;; 530 | add-field) 531 | if ! [ -n "${2+1}" ] ; then 532 | fail "Please specify the name of an entry to add a field to" 533 | fi 534 | _add_field "$2" 535 | ;; 536 | remove-field) 537 | if ! [ -n "${2+1}" ] ; then 538 | fail "Please specify the name of an entry to remove a field from" 539 | fi 540 | if ! [ -n "${3+1}" ] ; then 541 | fail "Please specify the name of a field to remove" 542 | fi 543 | _remove_field "$2" "$3" 544 | ;; 545 | --help|help) 546 | _usage 547 | ;; 548 | *) 549 | _usage 550 | ;; 551 | esac 552 | fi 553 | -------------------------------------------------------------------------------- /test/add-field-tests.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load test_helper 4 | 5 | @test "add-field: Adds additional fields to an existing entry" { 6 | local db_password="test" 7 | 8 | ( echo "Entry 1|entry1@test.com|pass1234"; 9 | echo "Entry 2|entry2@test.com|1234pass" ) | encrypt "$db_password" 10 | 11 | ( echo "$db_password"; 12 | echo "Field Name"; 13 | echo "Field Value" ) | ./passbox add-field "Entry 2" >/dev/null 14 | 15 | run decrypt "$db_password" "$PASSBOX_LOCATION" 16 | 17 | assert_line 1 "Entry 2|entry2@test.com|1234pass|Field Name:Field Value" 18 | } 19 | 20 | @test "add-field: Adds multiple additional fields to an existing entry" { 21 | local db_password="test" 22 | 23 | ( echo "Entry 1|entry1@test.com|pass1234"; 24 | echo "Entry 2|entry2@test.com|1234pass|Field Name:Field Value" ) | encrypt "$db_password" 25 | 26 | ( echo "$db_password"; 27 | echo "Field 2 Name"; 28 | echo "Field 2 Value" ) | ./passbox add-field "Entry 2" >/dev/null 29 | 30 | run decrypt "$db_password" "$PASSBOX_LOCATION" 31 | 32 | assert_line 1 "Entry 2|entry2@test.com|1234pass|Field Name:Field Value|Field 2 Name:Field 2 Value" 33 | } 34 | 35 | @test "add-field: Displays an error if no entry name argument is specified" { 36 | run ./passbox update 37 | 38 | assert_output "Error: Please specify the name of an entry to update" 39 | } 40 | -------------------------------------------------------------------------------- /test/delete-tests.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load test_helper 4 | 5 | @test "delete: Removes an entry from the passbox file" { 6 | local db_password="test" 7 | 8 | ( echo "Entry 1|entry1@test.com|pass1234"; 9 | echo "Entry 2|entry2@test.com|1234pass" ) | encrypt "$db_password" 10 | 11 | ( echo "$db_password"; 12 | echo "y" ) | ./passbox delete "Entry 2" >/dev/null 13 | 14 | run decrypt "$db_password" "$PASSBOX_LOCATION" 15 | 16 | assert_output "Entry 1|entry1@test.com|pass1234" 17 | } 18 | 19 | @test "delete: Displays an error if no entry name argument is specified" { 20 | run ./passbox delete 21 | 22 | assert_output "Error: Please specify the name of an entry to delete" 23 | } 24 | -------------------------------------------------------------------------------- /test/gen-tests.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load test_helper 4 | 5 | @test "gen: Generates a 20 character password by default" { 6 | run bash -c "echo | ./passbox gen" 7 | 8 | assert_line_length 0 20 9 | } 10 | 11 | @test "gen: Generates a password of a given length" { 12 | run bash -c "echo 30 | ./passbox gen" 13 | 14 | assert_line_length 0 30 15 | } 16 | -------------------------------------------------------------------------------- /test/get-tests.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load test_helper 4 | 5 | @test "get: Gets a formatted entry from the passbox file" { 6 | local db_password="test" 7 | 8 | ( echo "Entry 1|entry1@test.com|pass1234"; 9 | echo "Entry 2|entry2@test.com|1234pass" ) | encrypt "$db_password" 10 | 11 | run bash -c "echo \"$db_password\" | ./passbox get \"Entry 2\"" 12 | 13 | assert_line 0 "Name: Entry 2" 14 | assert_line 1 "Username: entry2@test.com" 15 | assert_line 2 "Password: 1234pass" 16 | } 17 | 18 | @test "get: Can return additional fields" { 19 | local db_password="test" 20 | 21 | ( echo "Entry 1|entry1@test.com|pass1234"; 22 | echo "Entry 2|entry2@test.com|1234pass|Field 1:Field 1 value|Field 2:Field 2 value" ) | encrypt "$db_password" 23 | 24 | run bash -c "echo \"$db_password\" | ./passbox get \"Entry 2\"" 25 | 26 | assert_line 0 "Name: Entry 2" 27 | assert_line 1 "Username: entry2@test.com" 28 | assert_line 2 "Password: 1234pass" 29 | assert_line 3 "Field 1: Field 1 value" 30 | assert_line 4 "Field 2: Field 2 value" 31 | } 32 | 33 | @test "get: Displays an error message if no 'entry name' argument is specified" { 34 | run ./passbox get 35 | 36 | assert_output "Error: Please specify the name of an entry to get" 37 | } 38 | 39 | @test "get: Displays an error message if an entry cannot be found" { 40 | local db_password="test" 41 | 42 | ( echo "Entry 1|entry1@test.com|pass1234"; 43 | echo "Entry 2|entry2@test.com|1234pass" ) | encrypt "$db_password" 44 | 45 | run bash -c "echo \"$db_password\" | ./passbox get \"Entry 3\"" 46 | 47 | assert_line 0 "Error: No entries found" 48 | } 49 | -------------------------------------------------------------------------------- /test/new-tests.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load test_helper 4 | 5 | @test "new: Creates a new passbox file if it doesn't exist" { 6 | local entry_name="Entry 1" 7 | local entry_username="entry1@test.com" 8 | local entry_password="pass1234" 9 | local db_password="test" 10 | 11 | run bash -c "( echo $entry_name; 12 | echo $entry_username; 13 | echo n; 14 | echo $entry_pasword; 15 | echo $db_password; ) | ./passbox new" 16 | 17 | assert_file_exists ./test/passbox.gpg 18 | } 19 | 20 | @test "new: Creates a new entry in the passbox file" { 21 | local entry_name="Entry 1" 22 | local entry_username="entry1@test.com" 23 | local entry_password="pass1234" 24 | local db_password="test" 25 | 26 | ( echo "$entry_name"; 27 | echo "$entry_username"; 28 | echo "n"; 29 | echo "$entry_password"; 30 | echo "$db_password"; ) | ./passbox new >/dev/null 31 | 32 | run decrypt "$db_password" "$PASSBOX_LOCATION" 33 | 34 | assert_line 0 "$entry_name|$entry_username|$entry_password" 35 | } 36 | 37 | @test "new: Will not overwrite a passbox file if the password is incorrect" { 38 | local entry_name="Entry 1" 39 | local entry_username="entry1@test.com" 40 | local entry_password="pass1234" 41 | local db_password="test" 42 | 43 | ( echo "Entry 1|entry1@test.com|pass1234"; 44 | echo "Entry 2|entry2@test.com|1234pass" ) | encrypt "$db_password" >/dev/null 45 | 46 | ( echo "$entry_name"; 47 | echo "$entry_username"; 48 | echo "n"; 49 | echo "$entry_password"; 50 | echo "wr0ngp455"; ) | ./passbox new &>/dev/null && echo 51 | 52 | run decrypt "$db_password" "$PASSBOX_LOCATION" 53 | 54 | assert_line_count 2 55 | assert_line 0 "Entry 1|entry1@test.com|pass1234" 56 | assert_line 1 "Entry 2|entry2@test.com|1234pass" 57 | } 58 | -------------------------------------------------------------------------------- /test/remove-field-tests.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load test_helper 4 | 5 | @test "remove-field: Removes an existing field from an existing entry" { 6 | local db_password="test" 7 | 8 | ( echo "Entry 1|entry1@test.com|pass1234"; 9 | echo "Entry 2|entry2@test.com|1234pass|MyField:MyFieldValue|My Second Field:My Second Field Value" ) | encrypt "$db_password" 10 | 11 | echo "$db_password" | ./passbox remove-field "Entry 2" "MyField" >/dev/null 12 | 13 | run decrypt "$db_password" "$PASSBOX_LOCATION" 14 | 15 | assert_line 0 "Entry 1|entry1@test.com|pass1234" 16 | assert_line 1 "Entry 2|entry2@test.com|1234pass|My Second Field:My Second Field Value" 17 | } 18 | 19 | @test "remove-field: Displays an error if no entry name argument is specified" { 20 | run ./passbox remove-field 21 | 22 | assert_output "Error: Please specify the name of an entry to remove a field from" 23 | } 24 | 25 | @test "remove-field: Displays an error if no field name argument is specified" { 26 | run ./passbox remove-field "Test" 27 | 28 | assert_output "Error: Please specify the name of a field to remove" 29 | } 30 | -------------------------------------------------------------------------------- /test/search-tests.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load test_helper 4 | 5 | 6 | @test "search: Returns an entry that contains the search value in the Name " { 7 | local db_password="test" 8 | 9 | ( echo "Entry 1|entry1@test.com|123456"; 10 | echo "Entry 2|entry2@test.com|test1234") | encrypt "$db_password" 11 | 12 | run bash -c "echo \"$db_password\" | ./passbox search \"Entry 1\"" 13 | 14 | assert_line 0 "Name: Entry 1" 15 | } 16 | 17 | @test "search: Can return multiple results" { 18 | local db_password="test" 19 | 20 | ( echo "Entry 1|entry1@test.com|123456"; 21 | echo "Entry 2|entry2@test.com|test1234") | encrypt "$db_password" 22 | 23 | run bash -c "echo \"$db_password\" | ./passbox search \"Entry\"" 24 | 25 | assert_line 0 "Name: Entry 1" 26 | assert_line 3 "Name: Entry 2" 27 | } 28 | 29 | @test "search: Can return additional fields" { 30 | local db_password="test" 31 | 32 | ( echo "Entry 1|entry1@test.com|123456"; 33 | echo "Entry 2|entry2@test.com|test1234|Field 1:field 1 value|Field 2:field 2 value") | encrypt "$db_password" 34 | 35 | run bash -c "echo \"$db_password\" | ./passbox search \"Entry 2\"" 36 | 37 | assert_line 3 "Field 1: field 1 value" 38 | assert_line 4 "Field 2: field 2 value" 39 | } 40 | 41 | @test "search: Displays an error if no search string argument is specified" { 42 | run ./passbox search 43 | 44 | assert_output "Error: Please specify a string to search for" 45 | } 46 | -------------------------------------------------------------------------------- /test/test-key-gen.conf: -------------------------------------------------------------------------------- 1 | %echo Generating Key 2 | Key-Type: RSA 3 | Key-Length: 2048 4 | Name-Real: Test Test 5 | Name-Comment: This is a test key 6 | Name-Email: test@example.com 7 | Expire-Date: 0 8 | Passphrase: test 9 | %commit 10 | %echo done 11 | -------------------------------------------------------------------------------- /test/test_helper.bash: -------------------------------------------------------------------------------- 1 | encrypt () { 2 | if [ "$PASSBOX_ASYMMETRIC" == true ]; then 3 | gpg --encrypt --sign --armor --batch --quiet \ 4 | --command-fd 0 --passphrase="$1" \ 5 | --local-user="$PASSBOX_RECIPIENT" \ 6 | --recipient="$PASSBOX_RECIPIENT" \ 7 | --output=$PASSBOX_LOCATION 2>/dev/null 8 | else 9 | gpg --symmetric --armor --batch --yes --quiet \ 10 | --command-fd 0 --passphrase="$1" \ 11 | --output=$PASSBOX_LOCATION 2>/dev/null 12 | fi 13 | } 14 | 15 | decrypt () { 16 | gpg \ 17 | --decrypt --armor --batch --quiet \ 18 | --command-fd 0 --passphrase "${1}" "${2}" 2>/dev/null 19 | } 20 | 21 | setup () { 22 | 23 | # Don't screw up env variables if ran locally 24 | PREV_PASSBOX_LOCATION="$PASSBOX_LOCATION" 25 | export PASSBOX_LOCATION="./test/passbox.gpg" 26 | } 27 | 28 | teardown () { 29 | PASSBOX_LOCATION="$PREV_PASSBOX_LOCATION" 30 | if [ -f ./test/passbox.gpg ]; then 31 | rm ./test/passbox.gpg 32 | fi 33 | unset PREV_PASSBOX_LOCATION 34 | } 35 | 36 | flunk() { 37 | { if [ "$#" -eq 0 ]; then 38 | cat - 39 | else 40 | echo "$@" 41 | fi 42 | } >&2 43 | return 1 44 | } 45 | 46 | assert_success() { 47 | if [ "$status" -ne 0 ]; then 48 | flunk "command failed with exit status $status" 49 | elif [ "$#" -gt 0 ]; then 50 | assert_output "$1" 51 | fi 52 | } 53 | 54 | assert_failure() { 55 | if [ "$status" -eq 0 ]; then 56 | flunk "expected failed exit status" 57 | elif [ "$#" -gt 0 ]; then 58 | assert_output "$1" 59 | fi 60 | } 61 | 62 | assert_equal() { 63 | if [ "$1" != "$2" ]; then 64 | { echo "expected: ${1}" 65 | echo "actual: ${2}" 66 | } | flunk 67 | fi 68 | } 69 | 70 | assert_length () { 71 | local p1=$1 72 | local p2=$2 73 | if [ "${#p1}" -ne "${p2}" ]; then 74 | { echo "expected length: ${p2}" 75 | echo "actual length: ${#p1}" 76 | } | flunk 77 | fi 78 | } 79 | 80 | assert_output() { 81 | local expected 82 | if [ $# -eq 0 ]; then expected="$(cat -)" 83 | else expected="$1" 84 | fi 85 | assert_equal "$expected" "$output" 86 | } 87 | 88 | assert_line () { 89 | if [ "$1" -ge 0 ] 2>/dev/null; then 90 | assert_equal "$2" "${lines[$1]}" 91 | else 92 | local line 93 | for line in "${lines[@]}"; do 94 | if [ "$line" = "$1" ]; then return 0; fi 95 | done 96 | flunk "expected line \`$1'" 97 | fi 98 | } 99 | 100 | assert_line_contains () { 101 | if [ "$1" -ge 0 ] 2>/dev/null; then 102 | assert_contains "${lines[$1]}" "$2" 103 | else 104 | local line 105 | for line in "${lines[@]}"; do 106 | if [ "$line" == *"$1"* ]; then return 0; fi 107 | done 108 | flunk "expected line \`$1'" 109 | fi 110 | } 111 | 112 | assert_line_length () { 113 | if [ "$1" -ge 0 ] 2>/dev/null; then 114 | assert_length ${lines[$1]} "$2" 115 | else 116 | flunk "No line number >1 specified" 117 | fi 118 | } 119 | 120 | assert_line_count () { 121 | local num_lines="${#lines[@]}" 122 | if [ "$1" -ne "$num_lines" ]; then 123 | flunk "output has $num_lines lines, not $1" 124 | fi 125 | } 126 | 127 | refute_line () { 128 | if [ "$1" -ge 0 ] 2>/dev/null; then 129 | local num_lines="${#lines[@]}" 130 | if [ "$1" -lt "$num_lines" ]; then 131 | flunk "output has $num_lines lines" 132 | fi 133 | else 134 | local line 135 | for line in "${lines[@]}"; do 136 | if [ "$line" = "$1" ]; then 137 | flunk "expected to not find line \`$line'" 138 | fi 139 | done 140 | fi 141 | } 142 | 143 | assert () { 144 | if ! "$@"; then 145 | flunk "failed: $@" 146 | fi 147 | } 148 | 149 | assert_file_doesnt_exist () { 150 | local file="$1" 151 | if [ -f $file ]; then 152 | { echo "file exists: ${1}" 153 | } | flunk 154 | fi 155 | } 156 | 157 | assert_file_exists () { 158 | local file="$1" 159 | if ![ -f $file ]; then 160 | { echo "file does not exist: ${1}" 161 | } | flunk 162 | fi 163 | } 164 | -------------------------------------------------------------------------------- /test/update-tests.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load test_helper 4 | 5 | @test "update: Updates an existing entry in the passbox file" { 6 | local new_entry_username="updatedentry@test.com" 7 | local new_entry_pass="newpassword" 8 | local db_password="test" 9 | 10 | ( echo "Entry 1|entry1@test.com|pass1234"; 11 | echo "Entry 2|entry2@test.com|1234pass" ) | encrypt "$db_password" 12 | 13 | ( echo "$db_password"; 14 | echo "$new_entry_username"; 15 | echo "n"; 16 | echo "$new_entry_pass" ) | ./passbox update "Entry 2" >/dev/null 17 | 18 | run decrypt "$db_password" "$PASSBOX_LOCATION" 19 | 20 | assert_line 0 "Entry 1|entry1@test.com|pass1234" 21 | assert_line 1 "Entry 2|updatedentry@test.com|newpassword" 22 | } 23 | 24 | @test "update: Updates an existing entry in the passbox file that has a additional field" { 25 | local new_entry_username="updatedentry@test.com" 26 | local new_entry_pass="newpassword" 27 | local db_password="test" 28 | 29 | ( echo "Entry 1|entry1@test.com|pass1234|Foo:Bar"; 30 | echo "Entry 2|entry2@test.com|1234pass|Foo:Baz" ) | encrypt "$db_password" 31 | 32 | ( echo "$db_password"; 33 | echo "$new_entry_username"; 34 | echo "n"; 35 | echo "$new_entry_pass" ) | ./passbox update "Entry 2" >/dev/null 36 | 37 | run decrypt "$db_password" "$PASSBOX_LOCATION" 38 | 39 | assert_line 0 "Entry 1|entry1@test.com|pass1234|Foo:Bar" 40 | assert_line 1 "Entry 2|updatedentry@test.com|newpassword|Foo:Baz" 41 | } 42 | 43 | @test "update: Displays an error if no entry name argument is specified" { 44 | run ./passbox update 45 | 46 | assert_output "Error: Please specify the name of an entry to update" 47 | } 48 | --------------------------------------------------------------------------------