[PATCH] stop using pwgen

Antoine Beaupré anarcat at debian.org
Sat Dec 17 23:02:57 CET 2016


pwgen has a long history of generating insecure passphrases. up until
2014 (pwgen 2.07, shipped only in Debian jessie, and Ubuntu Vivid) it
had two serious security vulnerabilities (CVE-2013-4440 and
CVE-2013-4442) that specifically affect pass. it still defaults to an
insecure "phoneme" password generation, although pass uses the more
secure "-s" flag. more information about those issues and more can be
found in those discussions:

http://www.openwall.com/lists/oss-security/2012/01/22/6
http://www.openwall.com/lists/oss-security/2013/05/24/7

it is still unclear how actually secure the `--secure` flag is: the
manpage doesn't say how much entropy is actually used to generate
passwords. (according to a quick review of the source code: each
character is chosen randomly based on a byte taken from the
non-blocking /dev/urandom PRNG, and not all bytes are used in some
cases, wasting possible entropy.)

since we use pwgen only to generate passphrases, it seems reasonable
we generate those ourselves. generating passphrases is simple: take a
source of entropy (UNIX has /dev/random) and turn bytes into
transportable strings (UNIX has base64):

head -c 18 /dev/random | base64

a 18 bytes password contains (naturally) 144 bits of entropy and
base64 turns that in a 25 character password, the current default
length in pass. 20 bytes may be better because we like to think in
round numbers, which would give us 160 bits of entropy and 28
character passwords. we could even cram an extra byte in there to get
168 bytes of entropy and keep 28 character passwords, but I chose to
retain the same default to make this patch more acceptable.

base64 passwords are more portable and incur only a ~13% size increase
compared to original byte stream. we also have a more direct control
over the entropy level than what pwgen provides: we don't actually
know how much entropy a given pwgen password has, as it's
implementation specific.

we depend on GNU coreutils' base46 command because its CLI is
different from the traditionnal BSD one (BSD requires -e or -d, GNU
refuses -e) - I assume this can be fixed in the BSD ports.

this removes a dependency and encourages stronger passwords.

ideally, the password generation program would be made configurable
instead of being hardcoded. that way people could use diceware, pwqgen
or other software more easily.
---
 README                              |  4 ++--
 contrib/emacs/password-store.el     |  1 -
 man/pass.1                          | 12 +++++-------
 src/completion/pass.bash-completion |  2 +-
 src/completion/pass.fish-completion |  1 -
 src/completion/pass.zsh-completion  |  4 +---
 src/password-store.sh               | 21 ++++++++++++---------
 7 files changed, 21 insertions(+), 24 deletions(-)

diff --git a/README b/README
index 1cc01b9..394d078 100644
--- a/README
+++ b/README
@@ -21,8 +21,8 @@ Depends on:
   http://www.git-scm.com/
 - xclip
   http://sourceforge.net/projects/xclip/
-- pwgen
-  http://sourceforge.net/projects/pwgen/
+- base64 from GNU coreutils
+  http://www.gnu.org/software/coreutils/coreutils.html
 - tree >= 1.7.0
   http://mama.indstate.edu/users/ice/tree/
 - GNU getopt
diff --git a/contrib/emacs/password-store.el b/contrib/emacs/password-store.el
index a1be788..0d12595 100644
--- a/contrib/emacs/password-store.el
+++ b/contrib/emacs/password-store.el
@@ -104,7 +104,6 @@ outputs error message on failure."
 (defun password-store--run-generate (entry password-length &optional force no-symbols)
   (password-store--run "generate"
                        (if force "--force")
-                       (if no-symbols "--no-symbols")
                        entry
                        (number-to-string password-length)))
 
diff --git a/man/pass.1 b/man/pass.1
index 33b6036..a47afc8 100644
--- a/man/pass.1
+++ b/man/pass.1
@@ -111,12 +111,10 @@ ensure that temporary files are created in \fI/dev/shm\fP in order to avoid writ
 difficult-to-erase disk sectors. If \fI/dev/shm\fP is not accessible, fallback to
 the ordinary \fITMPDIR\fP location, and print a warning.
 .TP
-\fBgenerate\fP [ \fI--no-symbols\fP, \fI-n\fP ] [ \fI--clip\fP, \fI-c\fP ] [ \fI--in-place\fP, \fI-i\fP | \fI--force\fP, \fI-f\fP ] \fIpass-name [pass-length]\fP
-Generate a new password using
-.BR pwgen (1)
-of length \fIpass-length\fP (or \fIPASSWORD_STORE_GENERATED_LENGTH\fP if unspecified)
-and insert into \fIpass-name\fP. If \fI--no-symbols\fP or \fI-n\fP
-is specified, do not use any non-alphanumeric characters in the generated password.
+\fBgenerate\fP [ \fI--clip\fP, \fI-c\fP ] [ \fI--in-place\fP, \fI-i\fP | \fI--force\fP, \fI-f\fP ] \fIpass-name [pass-entropy]\fP
+Generate a new password of given entropy \fIpass-entropy\fP (or \fIPASSWORD_STORE_GENERATED_ENTROPY\fP if unspecified)
+and insert into \fIpass-name\fP. Password is generated by extracting
+\fIpass-entropy\P bytes from \fI/dev/random\fP en base64 encoding them.
 If \fI--clip\fP or \fI-c\fP is specified, do not print the password but instead copy
 it to the clipboard using
 .BR xclip (1)
@@ -424,7 +422,7 @@ is unspecified.
 The location of the text editor used by \fBedit\fP.
 .SH SEE ALSO
 .BR gpg2 (1),
-.BR pwgen (1),
+.BR base64 (1),
 .BR git (1),
 .BR xclip (1).
 
diff --git a/src/completion/pass.bash-completion b/src/completion/pass.bash-completion
index 456485b..fdc3fe6 100644
--- a/src/completion/pass.bash-completion
+++ b/src/completion/pass.bash-completion
@@ -106,7 +106,7 @@ _pass()
 				_pass_complete_entries
 				;;
 			generate)
-				COMPREPLY+=($(compgen -W "-n --no-symbols -c --clip -f --force -i --in-place" -- ${cur}))
+				COMPREPLY+=($(compgen -W "-c --clip -f --force -i --in-place" -- ${cur}))
 				_pass_complete_entries
 				;;
 			cp|copy|mv|rename)
diff --git a/src/completion/pass.fish-completion b/src/completion/pass.fish-completion
index c32a42c..25fa835 100644
--- a/src/completion/pass.fish-completion
+++ b/src/completion/pass.fish-completion
@@ -74,7 +74,6 @@ complete -c $PROG -f -A -n '__fish_pass_uses_command insert' -s f -l force -d 'D
 complete -c $PROG -f -A -n '__fish_pass_uses_command insert' -a "(__fish_pass_print_entry_dirs)"
 
 complete -c $PROG -f -A -n '__fish_pass_needs_command' -a generate -d 'Command: generate new password'
-complete -c $PROG -f -A -n '__fish_pass_uses_command generate' -s n -l no-symbols -d 'Do not use special symbols'
 complete -c $PROG -f -A -n '__fish_pass_uses_command generate' -s c -l clip -d 'Put the password in clipboard'
 complete -c $PROG -f -A -n '__fish_pass_uses_command generate' -s f -l force -d 'Do not prompt before overwritting'
 complete -c $PROG -f -A -n '__fish_pass_uses_command generate' -s i -l in-place -d 'Replace only the first line with the generated password'
diff --git a/src/completion/pass.zsh-completion b/src/completion/pass.zsh-completion
index 27ce15a..06659c5 100644
--- a/src/completion/pass.zsh-completion
+++ b/src/completion/pass.zsh-completion
@@ -48,8 +48,6 @@ _pass () {
 				;;
 			generate)
 				_arguments : \
-					"-n[don't include symbols in password]" \
-					"--no-symbols[don't include symbols in password]" \
 					"-c[copy password to the clipboard]" \
 					"--clip[copy password to the clipboard]" \
 					"-f[force overwrite]" \
@@ -97,7 +95,7 @@ _pass () {
 			"grep:Search inside decrypted password files for matching pattern"
 			"show:Decrypt and print a password"
 			"insert:Insert a new password"
-			"generate:Generate a new password using pwgen"
+			"generate:Generate a new password"
 			"edit:Edit a password with \$EDITOR"
 			"mv:Rename the password"
 			"cp:Copy the password"
diff --git a/src/password-store.sh b/src/password-store.sh
index 63be840..8de4eac 100755
--- a/src/password-store.sh
+++ b/src/password-store.sh
@@ -15,7 +15,9 @@ which gpg2 &>/dev/null && GPG="gpg2"
 PREFIX="${PASSWORD_STORE_DIR:-$HOME/.password-store}"
 X_SELECTION="${PASSWORD_STORE_X_SELECTION:-clipboard}"
 CLIP_TIME="${PASSWORD_STORE_CLIP_TIME:-45}"
-GENERATED_LENGTH="${PASSWORD_STORE_GENERATED_LENGTH:-25}"
+# backwards compatibility
+PASSWORD_STORE_GENERATED_ENTROPY="${PASSWORD_STORE_GENERATED_LENGTH:-$PASSWORD_STORE_GENERATED_ENTROPY}"
+GENERATED_ENTROPY="${PASSWORD_STORE_GENERATED_ENTROPY:-18}"
 
 export GIT_DIR="${PASSWORD_STORE_GIT:-$PREFIX}/.git"
 export GIT_WORK_TREE="${PASSWORD_STORE_GIT:-$PREFIX}"
@@ -235,8 +237,8 @@ cmd_usage() {
 	        overwriting existing password unless forced.
 	    $PROGRAM edit pass-name
 	        Insert a new password or edit an existing password using ${EDITOR:-vi}.
-	    $PROGRAM generate [--no-symbols,-n] [--clip,-c] [--in-place,-i | --force,-f] pass-name [pass-length]
-	        Generate a new password of pass-length (or $GENERATED_LENGTH if unspecified) with optionally no symbols.
+	    $PROGRAM generate [--clip,-c] [--in-place,-i | --force,-f] pass-name [entropy]
+	        Generate a new password of given entropy (or $GENERATED_ENTROPY if unspecified).
 	        Optionally put it on the clipboard and clear board after $CLIP_TIME seconds.
 	        Prompt before overwriting existing password unless forced.
 	        Optionally replace only the first line of an existing file with a new password.
@@ -431,30 +433,31 @@ cmd_edit() {
 }
 
 cmd_generate() {
-	local opts clip=0 force=0 symbols="-y" inplace=0
+	local opts clip=0 force=0 symbols="" inplace=0
 	opts="$($GETOPT -o ncif -l no-symbols,clip,in-place,force -n "$PROGRAM" -- "$@")"
 	local err=$?
 	eval set -- "$opts"
 	while true; do case $1 in
-		-n|--no-symbols) symbols=""; shift ;;
+		-n|--no-symbols) echo '--no-symbols deprecated'; shift ;;
 		-c|--clip) clip=1; shift ;;
 		-f|--force) force=1; shift ;;
 		-i|--in-place) inplace=1; shift ;;
 		--) shift; break ;;
 	esac done
 
-	[[ $err -ne 0 || ( $# -ne 2 && $# -ne 1 ) || ( $force -eq 1 && $inplace -eq 1 ) ]] && die "Usage: $PROGRAM $COMMAND [--no-symbols,-n] [--clip,-c] [--in-place,-i | --force,-f] pass-name [pass-length]"
+	[[ $err -ne 0 || ( $# -ne 2 && $# -ne 1 ) || ( $force -eq 1 && $inplace -eq 1 ) ]] && die "Usage: $PROGRAM $COMMAND [--clip,-c] [--in-place,-i | --force,-f] pass-name [pass-length]"
 	local path="$1"
-	local length="${2:-$GENERATED_LENGTH}"
+	local entropy="${2:-$GENERATED_ENTROPY}"
 	check_sneaky_paths "$path"
-	[[ ! $length =~ ^[0-9]+$ ]] && die "Error: pass-length \"$length\" must be a number."
+	[[ ! $entropy =~ ^[0-9]+$ ]] && die "Error: pass-entropy \"$entropy\" must be a number."
 	mkdir -p -v "$PREFIX/$(dirname "$path")"
 	set_gpg_recipients "$(dirname "$path")"
 	local passfile="$PREFIX/$path.gpg"
 
 	[[ $inplace -eq 0 && $force -eq 0 && -e $passfile ]] && yesno "An entry already exists for $path. Overwrite it?"
 
-	local pass="$(pwgen -s $symbols $length 1)"
+        # strip possible newlines if output is wrapped and trailing = signs as they add nothing to the password's entropy
+	local pass="$(head -c $entropy /dev/random | base64 | tr -d '\n=')"
 	[[ -n $pass ]] || exit 1
 	if [[ $inplace -eq 0 ]]; then
 		$GPG -e "${GPG_RECIPIENT_ARGS[@]}" -o "$passfile" "${GPG_OPTS[@]}" <<<"$pass" || die "Password encryption aborted."
-- 
2.10.2



More information about the Password-Store mailing list