From 814877ba3d1823b048845a7b13a0a58bd4d91664 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Wed, 25 Mar 2015 20:54:19 +0100 Subject: Fix bash completion. --- cli/bash-completion.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/bash-completion.sh b/cli/bash-completion.sh index 81bee51..bbef124 100644 --- a/cli/bash-completion.sh +++ b/cli/bash-completion.sh @@ -58,7 +58,7 @@ _icevault() { if [ -z "$cur" -a $(($OPTIND + 1)) -eq $cword -a "${words[OPTIND]}" = insert ]; then return elif [ -z "$cur" -a $OPTIND -eq $cword ] || [ -z "$cur" -a $(($OPTIND + 1)) -eq $cword -a "${words[OPTIND]}" = fill ]; then - cur="$(./icevault _geturi)"/ # get URI from webpage + cur="$(icevault _geturi)"/ # get URI from webpage else cur=$(dequote "$cur") # trim words with : or = in $COMP_WORDBREAKS; see __ltrim_colon_completions @@ -78,7 +78,7 @@ _icevault() { # quote manually (so we don't quote the : and =) uri=$( echo "${uri#$trim}" | sed "s/[][\\{}*?~<>;'\"|&()\!$\` \t]/\\\\&/g" ) COMPREPLY+=( "$uri" ) - done < <(./icevault -0 _complete "$cur") + done < <(icevault -0 _complete "$cur") [ "${#COMPREPLY[@]}" -eq 1 -a "${COMPREPLY[0]: -1:1}" = / ] && compopt -o nospace return 0 fi -- cgit v1.2.3 From 731f5516a26610aed2d8bdaa988fa8c805f0b5d2 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Thu, 26 Mar 2015 09:41:54 +0100 Subject: Croak when mktemp, unlink or move fails. --- cli/icevault | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/cli/icevault b/cli/icevault index e0c2757..ef186d0 100755 --- a/cli/icevault +++ b/cli/icevault @@ -379,7 +379,7 @@ sub saveIdentityFile($$) { # don't encrypt directly into the destination file so we don't # end up with a messed up file if something goes wrong - my $outfh = File::Temp->new(SUFFIX => '.gpg', UNLINK => 0, TMPDIR => 1); + my $outfh = File::Temp->new(SUFFIX => '.gpg', UNLINK => 0, TMPDIR => 1) or die; my $pid = IPC::Open2::open2( ">&".$outfh->fileno , (ref $form ? my $infh : "<&".fileno($NULL)) , $CONFIG{gpg}, qw/-o - --no-encrypt-to --recipient/, $CONFIG{keyid} @@ -398,10 +398,10 @@ sub saveIdentityFile($$) { my $parent_dir = $filename =~ s/\/[^\/]+$//r; File::Path::make_path($parent_dir) unless -d $parent_dir; # create parent directories recursively - unless (File::Copy::move($outfh->filename, $filename)){ + unless (File::Copy::move($outfh->filename, $filename)) { my $r = $!; - unlink $outfh->filename; - error "Can't move C<%s>: %s", $outfh->filename, $r; + unlink $outfh->filename or error "Can't unlink C<%s>: %s", $outfh->filename, $!; + error "Can't move C<%s> to C<%s>: %s", $outfh->filename, $filename, $r; } # TODO: git add $filename; git commit @@ -838,14 +838,13 @@ elsif ($command eq 'edit') { my $id = shift; my $filename = getIdentityFile $id; error "No such identity C<%s>", $id unless -f $filename; - require 'File/Copy.pm'; require 'File/Temp.pm'; error "C<%s> is not set", '$EDITOR' unless defined $ENV{EDITOR}; $ENV{EDITOR} =~ /\A(\p{Print}+)\z/ or error "Insecure C<%s>", "\$EDITOR"; my $EDITOR = $1; # untaint $EDITOR - my $fh = File::Temp->new(SUFFIX => '.yaml', UNLINK => 0, TMPDIR => 1); + my $fh = File::Temp->new(SUFFIX => '.yaml', UNLINK => 0, TMPDIR => 1) or die; END { unlink $fh->filename if defined $fh; } # never leave cleartext lying around loadIdentityFile $filename, $fh; -- cgit v1.2.3 From 641f8168bc9084b808620ec00add1355e2d9d1fb Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Thu, 26 Mar 2015 09:42:36 +0100 Subject: Validate form content after manual edition. --- cli/icevault | 67 +++++++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 46 insertions(+), 21 deletions(-) diff --git a/cli/icevault b/cli/icevault index ef186d0..73136d1 100755 --- a/cli/icevault +++ b/cli/icevault @@ -343,7 +343,9 @@ sub getIdentityFile($) { return $1; } -# Decrypt the given identity file and return the YAML-parsed form. +# Decrypt the given identity file. In scalar context, return the +# YAML-parsed form; in void context, must be given a file handle (closed +# afterwards) where to dump the (unparsed) decrypted content. open my $NULL, '<', '/dev/null'; sub loadIdentityFile($;$) { my ($filename, $fh) = @_; @@ -366,7 +368,10 @@ sub loadIdentityFile($;$) { return YAML::Tiny::Load(decode_utf8 $str) if defined wantarray; } -# Dump and encrypt a form into the given filename. +# Encrypt a form into the given filename. If $form is a HASH +# reference, its YAML-formatted (and UTF8-encoded) content is encrypted; +# if $form is a GLOB reference, the file handle is duped, given as input +# to gpg(1), and closed afterwards. sub saveIdentityFile($$) { my ($form, $filename) = @_; myprintf \*STDERR, "Saving identity file C<%s>", $filename if $CONFIG{debug}; @@ -375,23 +380,23 @@ sub saveIdentityFile($$) { require 'File/Path.pm'; require 'File/Temp.pm'; require 'IPC/Open2.pm'; - require 'YAML/Tiny.pm' if ref $form; # XXX use Tiny::YAML instead? + + if (ref $form eq 'HASH') { + require 'YAML/Tiny.pm'; # XXX use Tiny::YAML instead? + $form->{fields} = [ grep defined, @{$form->{fields}} ]; # remove undefined fields + $form = encode_utf8(YAML::Tiny::Dump($form)); # dump the form as UTF8 + } # don't encrypt directly into the destination file so we don't # end up with a messed up file if something goes wrong + my $infh = "<&".fileno($form) if ref $form eq 'GLOB'; my $outfh = File::Temp->new(SUFFIX => '.gpg', UNLINK => 0, TMPDIR => 1) or die; - my $pid = IPC::Open2::open2( ">&".$outfh->fileno - , (ref $form ? my $infh : "<&".fileno($NULL)) + my $pid = IPC::Open2::open2( ">&".$outfh->fileno, $infh , $CONFIG{gpg}, qw/-o - --no-encrypt-to --recipient/, $CONFIG{keyid} - , '--encrypt', '--', (ref $form ? () : $form) - ) + , '--encrypt' ) or error "Can't fork: %s", $!; - - if (ref $form) { - $form->{fields} = [ grep defined, @{$form->{fields}} ]; # remove undefined fields - print $infh encode_utf8(YAML::Tiny::Dump($form)); # dump the form as UTF8 - close $infh; - } + print $infh $form unless ref $form; + close $infh; waitpid $pid, 0; error "C<%s> exited with value %d", $CONFIG{gpg}, ($? >> 8) if $? and $? != -1; $outfh->close; @@ -849,15 +854,35 @@ elsif ($command eq 'edit') { loadIdentityFile $filename, $fh; my $h = sha256_file $fh->filename; - system $EDITOR, $fh->filename; - error "C<%s> exited with value %d", $EDITOR, ($? >> 8) if $? and $? != -1; + while (1) { + system $EDITOR, $fh->filename; + error "C<%s> exited with value %d", $EDITOR, ($? >> 8) if $? and $? != -1; + my $h2 = sha256_file $fh->filename; + + my $fh2; + unless ($h eq $h2) { + require 'YAML/Tiny.pm'; # XXX use Tiny::YAML instead? + eval { my $str = YAML::Tiny::LoadFile($fh->filename) }; + if ($@ eq '') { + open $fh2, '<', $fh->filename or error "Can't open C<%s>: %s", $fh->filename, $!; + } else { + print STDERR $@; + my $r = promptYN "Not a valid YAML file! Reedit?", 1; + next if $r; + } + } - if ($h eq sha256_file $fh->filename) { - print "No modification made\n"; - } - else { - myprintf "Saving user changes for identity C<%s>", $id; - saveIdentityFile($fh->filename, $filename); + unlink $fh->filename or error "Can't unlink C<%s>: %s", $fh->filename, $!; + if ($h eq $h2) { + print "No modification made\n"; + } elsif (defined $fh2) { + myprintf "Saving user changes for identity C<%s>", $id; + saveIdentityFile($fh2, $filename); # use the FH we opened before unlinking + } else { + print "Aborting\n"; + exit 1; + } + last; } } -- cgit v1.2.3 From bb0ab83b5a10b0ff812484df11667765f43ecd44 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Fri, 27 Mar 2015 01:23:12 +0100 Subject: Refactorization. --- cli/icevault | 58 +++++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 37 insertions(+), 21 deletions(-) diff --git a/cli/icevault b/cli/icevault index 73136d1..992ee6a 100755 --- a/cli/icevault +++ b/cli/icevault @@ -80,6 +80,11 @@ sub usage($) { exit $rv; } +sub mysystem(@) { + system {$_[0]} @_; + error "C<%s> exited with value %d", $_[0], ($? >> 8) if $? and $? != -1; +} + # grepIdx BLOCK LIST # Evaluates the BLOCK for each element of LIST (locally setting $_ to # each element) and returns the list value consisting of the index of @@ -134,7 +139,7 @@ sub loadConfig($) { chomp; s/#.*//; # ignore comments next if /^\s*$/; # ignore empty and blank lines - /^([-\@\w.]+)(?:\s*=\s*)(\p{print}+)/ or error "Can't parse config line: C<%s>", $_; + /^([-\@a-zA-Z0-9.]+)(?:\s*=\s*)(\p{Print}+)/ or error "Can't parse config line: C<%s>", $_; $CONFIG{$1} //= $2; } close $CONFIG; @@ -162,10 +167,11 @@ sub connect($) { } closedir $dh; error "No Firefox profile found under C<%s>", $ffdir unless defined $profile; - "$ffdir/$profile" =~ /\A(\p{Print}+)\z/ or error "Insecure C<%s>", "$ffdir/$profile"; # untaint $ffdir/$profile - $sockname = "$1/$sockname"; - myprintf \*STDERR, "Using socket C<%s>", $sockname if $CONFIG{debug}; + $sockname = "$$ffdir/$profile/$sockname"; } + myprintf \*STDERR, "Using socket C<%s>", $sockname if $CONFIG{debug}; + $sockname =~ /\A(\/\p{Print}+)\z/ or error "Insecure C<%s>", $sockname; # untaint $sockname + $sockname = $1; require 'IO/Socket/UNIX.pm'; $SOCKET = IO::Socket::UNIX->new( Type => IO::Socket::UNIX::SOCK_STREAM(), Peer => $sockname ) @@ -224,13 +230,31 @@ sub sendCommand(@) { getResponse(); } + +# Glob over all form templates. Note that the output needs to be +# $LOCALE-decode'ed. +sub myglob(;$$$) { + my ($s, $h, $i) = @_; + my $glob = $LOCALE->encode($CONFIG{store}); + require 'File/Glob.pm'; + + $glob =~ s/([\\\[\]\{\}\*\?\~])/\\$1/g; + $glob =~ s{\%(.)}{ $1 eq '%' ? '%' : + $1 eq 's' ? $s // '*' : + $1 eq 'h' ? $h // '*' : + $1 eq 'i' ? $i // '*' : + die "Invalid placeholder %$1" }ge; + + myprintf \*STDERR, "Using glob pattern C<%s>", $glob if $CONFIG{debug}; + return File::Glob::bsd_glob($glob); +} + # Get all identities with the given $prefix. If there are multiple # matches and $all is false, limit the output to one depth more than the # longuest common prefix. sub complete($;$) { my $prefix = shift // ''; my $all = shift; - require 'File/Glob.pm'; my $pat = $CONFIG{store}; my ($s, $h, $i); # extract URI components from the prefix @@ -253,14 +277,6 @@ sub complete($;$) { $gh .= '*' if defined $gh and !defined $gi; $gi .= '*' if defined $gi; - my $glob = $pat; - $glob =~ s/([\\\[\]\{\}\*\?\~])/\\$1/g; - $glob =~ s{\%(.)}{ $1 eq '%' ? '%' : - $1 eq 's' ? $gs // '*' : - $1 eq 'h' ? $gh // '*' : - $1 eq 'i' ? $gi // '*' : - die "Invalid placeholder %$1" }ge; - # construct regexp to extract the URI compontents of the matching URIs my ($ps, $ph, $pi) = ($s, $h, $i); $ps = defined $h ? qr/(?\Q$s\E)/ : (defined $s and $s ne '') ? qr/(?\Q$s\E[A-Za-z0-9-]*)/ : qr/(?[A-Za-z0-9-]+)/; @@ -275,11 +291,11 @@ sub complete($;$) { die "Invalid placeholder %$1"}ge; $pat = qr/\A$pat\z/; - myprintf \*STDERR, "Using glob pattern C<%s>", $glob if $CONFIG{debug}; - myprintf \*STDERR, "Using regexp C<%s>", "$pat" if $CONFIG{debug}; + myprintf \*STDERR, "Using regexp C<%s>", "$pat" if $CONFIG{debug}; my @matches; - foreach my $filename (File::Glob::bsd_glob($glob)) { + foreach my $filename (myglob($gs, $gh, $gi)) { + next unless -f $filename; $LOCALE->decode($filename) =~ $pat or die "$filename doesn't match $pat"; push @matches, "$+{s}://$+{h}/$+{i}"; } @@ -339,8 +355,7 @@ sub getIdentityFile($) { $1 eq 'h' ? $h : $1 eq 'i' ? $i : die "Invalid placeholder %$1" }ge; - $filename =~ /\A(\p{Print}+)\z/ or error "Insecure C<%s>", $filename; # untaint $filename - return $1; + return $filename; } # Decrypt the given identity file. In scalar context, return the @@ -846,8 +861,9 @@ elsif ($command eq 'edit') { require 'File/Temp.pm'; error "C<%s> is not set", '$EDITOR' unless defined $ENV{EDITOR}; - $ENV{EDITOR} =~ /\A(\p{Print}+)\z/ or error "Insecure C<%s>", "\$EDITOR"; - my $EDITOR = $1; # untaint $EDITOR + my $EDITOR = $ENV{EDITOR} // 'editor'; + $EDITOR =~ /\A(\p{Print}+)\z/ or error "Insecure C<%s>", "\$EDITOR"; + $EDITOR = $1; # untaint $EDITOR my $fh = File::Temp->new(SUFFIX => '.yaml', UNLINK => 0, TMPDIR => 1) or die; END { unlink $fh->filename if defined $fh; } # never leave cleartext lying around @@ -855,7 +871,7 @@ elsif ($command eq 'edit') { my $h = sha256_file $fh->filename; while (1) { - system $EDITOR, $fh->filename; + mysystem $EDITOR, $fh->filename; error "C<%s> exited with value %d", $EDITOR, ($? >> 8) if $? and $? != -1; my $h2 = sha256_file $fh->filename; -- cgit v1.2.3 From b8a6e7339f3f200b3a25867a658e0d6aa18b01d4 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Fri, 27 Mar 2015 01:23:57 +0100 Subject: Fix space damage. --- cli/icevault | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cli/icevault b/cli/icevault index 992ee6a..cc95729 100755 --- a/cli/icevault +++ b/cli/icevault @@ -470,10 +470,10 @@ sub guessPasswordPage(@) { } } else { - if ($pw1->{value} eq $pw2->{value}) { + if ($pw1->{value} eq $pw2->{value}) { ($idx0, $idx1, $idx2) = (0, 1, 2); } - elsif ($pw0->{value} eq $pw1->{value}) { + elsif ($pw0->{value} eq $pw1->{value}) { ($idx0, $idx1, $idx2) = (2, 0, 1); } elsif ($pw0->{value} eq $pw2->{value}) { @@ -541,7 +541,7 @@ sub promptYN($;$) { # Get the SHA-256 digest of the given file. sub sha256_file($) { my $filename = shift; - + require 'Digest.pm'; my $sha256 = Digest->new('SHA-256'); open my $fh, '<', $filename or error "Can't open C<%s>: %s", $filename, $!; -- cgit v1.2.3 From 421e0afd790b5061d3bf71baf2915945cfb584e8 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Fri, 27 Mar 2015 01:55:51 +0100 Subject: Configuration validation; separate store/template. --- cli/icevault | 122 +++++++++++++++++++++++++++++++++++---------------------- cli/icevault.1 | 32 +++++++++------ 2 files changed, 96 insertions(+), 58 deletions(-) diff --git a/cli/icevault b/cli/icevault index cc95729..3b69b3a 100755 --- a/cli/icevault +++ b/cli/icevault @@ -34,7 +34,7 @@ delete @ENV{qw/IFS CDPATH ENV BASH_ENV/}; my $LANGINFO = I18N::Langinfo::langinfo(I18N::Langinfo::CODESET()); my $LOCALE = Encode::find_encoding $LANGINFO; -my %CONFIG; +my (%CONFIG, @GPG); my $SOCKET; @@ -130,10 +130,14 @@ sub delete($$) { ####################################################################### -# Load the given configuration file -sub loadConfig($) { - my $configFile = shift; - error "Configuration file C<%s> doesn't exist", $configFile unless -f $configFile; +# Load and validate the given configuration file +sub loadConfig() { + my $XDG_CONFIG_HOME = $ENV{XDG_CONFIG_HOME} // "$ENV{HOME}/.config"; + my $XDG_DATA_HOME = $ENV{XDG_DATA_HOME} // "$ENV{HOME}/.local/share"; + + # load config + my $configFile = "$XDG_CONFIG_HOME/icevault"; + error "Missing configuration file C<%s>", $configFile unless -f $configFile; open my $CONFIG, '<', $configFile or error "Can't open C<%s>: %s", $configFile, $!; while (<$CONFIG>) { chomp; @@ -143,6 +147,42 @@ sub loadConfig($) { $CONFIG{$1} //= $2; } close $CONFIG; + + # set config defaults and validate + $CONFIG{store} //= 'icevault'; + $CONFIG{store} =~ s#\A~/#$ENV{HOME}#; + $CONFIG{store} = "$XDG_DATA_HOME/$CONFIG{store}" unless $CONFIG{store} =~ /\A\//; + $CONFIG{store} =~ /\A(\/\p{Print}+)\z/ or error "Insecure C<%s>", $CONFIG{store}; + $CONFIG{store} = $1; # untaint $CONFIG{store} + + $CONFIG{template} //= '%s/%h/%i.gpg'; + error "C<%s> must contain %%s, %%h and %%i placeholders", 'template' + unless $CONFIG{template} =~ /%s/ and $CONFIG{template} =~ /%h/ and $CONFIG{template} =~ /%i/; + + $CONFIG{socket} //= 'socket'; + $CONFIG{socket} =~ s#\A~/#$ENV{HOME}#; + + error "Missing keyid in configuration file" unless defined $CONFIG{keyid}; + $CONFIG{keyid} = [ map { /^((?:0x)?\p{AHex}{16}|\p{AHex}{40})$/ + or error "C<%s> is not a 64-bits key ID or fingerprint", $_; + $1 } + split /,/, $CONFIG{keyid} ]; + + $CONFIG{'max-password-length'} //= 32; + error "C<%s> must be a positive integer", 'max-password-length' + unless $CONFIG{'max-password-length'} =~ /\A\d+\z/ and $CONFIG{'max-password-length'} > 0; + + $CONFIG{pwgen} //= 'pwgen -s -cyn %d'; + $CONFIG{pwgen} =~ s#\A~/#$ENV{HOME}#; + error "Insecure C<%s>", $CONFIG{pwgen} unless $CONFIG{pwgen} =~ /\A([-_\@a-zA-Z0-9\.%\/= ]+)\z/; + $CONFIG{pwgen} = $1; + + $CONFIG{gpg} //= 'gpg'; + $CONFIG{gpg} =~ s#\A~/#$ENV{HOME}#; + error "Insecure C<%s>", $CONFIG{gpg} unless $CONFIG{gpg} =~ /\A([-_\@a-zA-Z0-9\.%\/= ]+)\z/; + $CONFIG{gpg} = $1; + + @GPG = split / /, $CONFIG{gpg}; } # Connect to the given socket and assign the IO::Socket object to @@ -235,16 +275,18 @@ sub sendCommand(@) { # $LOCALE-decode'ed. sub myglob(;$$$) { my ($s, $h, $i) = @_; - my $glob = $LOCALE->encode($CONFIG{store}); + my $store = $CONFIG{store}; + my $template = $CONFIG{template}; require 'File/Glob.pm'; - $glob =~ s/([\\\[\]\{\}\*\?\~])/\\$1/g; - $glob =~ s{\%(.)}{ $1 eq '%' ? '%' : - $1 eq 's' ? $s // '*' : - $1 eq 'h' ? $h // '*' : - $1 eq 'i' ? $i // '*' : - die "Invalid placeholder %$1" }ge; + s/([\\\[\]\{\}\*\?\~])/\\$1/g foreach ($store, $template); + $template =~ s{\%(.)}{ $1 eq '%' ? '%' : + $1 eq 's' ? $s // '*' : + $1 eq 'h' ? $h // '*' : + $1 eq 'i' ? $i // '*' : + die "Invalid placeholder %$1" }ge; + my $glob = "$store/$template"; myprintf \*STDERR, "Using glob pattern C<%s>", $glob if $CONFIG{debug}; return File::Glob::bsd_glob($glob); } @@ -256,7 +298,6 @@ sub complete($;$) { my $prefix = shift // ''; my $all = shift; - my $pat = $CONFIG{store}; my ($s, $h, $i); # extract URI components from the prefix if ($prefix =~ /\A([A-Za-z0-9-]+):\/\/([^\P{Graph}:\/]+(?::\d+)?)\/([^\P{Print}\/]*)\z/) { ($s, $h, $i) = ($1, $2, $3); @@ -283,20 +324,21 @@ sub complete($;$) { $ph = defined $i ? qr/(?\Q$h\E)/ : (defined $h and $h ne '') ? qr/(?\Q$h\E[^\P{Graph}\/]*)/ : qr/(?[^\P{Graph}\/]+)/; $pi = (defined $i and $i ne '') ? qr/(?\Q$i\E[^\P{Print}\/]*)/ : qr/(?[^\P{Print}\/]+)/; - $pat =~ s/(\%.)([^\%]*)\z/$1.quotemeta($2)/e; - $pat =~ s{(.*?)\%(.)}{$2 eq '%' ? '%' : - $2 eq 's' ? quotemeta($1).$ps : - $2 eq 'h' ? quotemeta($1).$ph : - $2 eq 'i' ? quotemeta($1).$pi : - die "Invalid placeholder %$1"}ge; - $pat = qr/\A$pat\z/; - - myprintf \*STDERR, "Using regexp C<%s>", "$pat" if $CONFIG{debug}; + my $store = $LOCALE->encode($CONFIG{store}); + my $template = $LOCALE->encode($CONFIG{template}); + $template =~ s/(\%.)([^\%]*)\z/$1.quotemeta($2)/e; + $template =~ s{(.*?)\%(.)}{$2 eq '%' ? '%' : + $2 eq 's' ? quotemeta($1).$ps : + $2 eq 'h' ? quotemeta($1).$ph : + $2 eq 'i' ? quotemeta($1).$pi : + die "Invalid placeholder %$1"}ge; + my $pattern = qr/\A\Q$store\/\E$template\z/; + myprintf \*STDERR, "Using regexp C<%s>", "$pattern" if $CONFIG{debug}; my @matches; foreach my $filename (myglob($gs, $gh, $gi)) { next unless -f $filename; - $LOCALE->decode($filename) =~ $pat or die "$filename doesn't match $pat"; + $LOCALE->decode($filename) =~ $pattern or die "$filename doesn't match $pattern"; push @matches, "$+{s}://$+{h}/$+{i}"; } return @matches if $all or $#matches < 1; @@ -349,13 +391,13 @@ sub getIdentityFile($) { or error "Invalid identity C<%s>", $id; my ($s, $h, $i) = ($1, $2, $3); - my $filename = $CONFIG{store}; - $filename =~ s{\%(.)}{ $1 eq '%' ? '%' : + my $template = $LOCALE->encode($CONFIG{template}); + $template =~ s{\%(.)}{ $1 eq '%' ? '%' : $1 eq 's' ? $s : $1 eq 'h' ? $h : $1 eq 'i' ? $i : die "Invalid placeholder %$1" }ge; - return $filename; + return "$CONFIG{store}/$template"; } # Decrypt the given identity file. In scalar context, return the @@ -369,11 +411,11 @@ sub loadIdentityFile($;$) { require 'IPC/Open2.pm'; my $pid = IPC::Open2::open2( (defined wantarray ? $fh : ">&".$fh->fileno) , "<&".fileno($NULL) - , $CONFIG{gpg}, qw/-o - --decrypt --/, $filename) + , @GPG, qw/-o - --decrypt --/, $filename) or error "Can't fork: %s", $!; my $str = do { local $/ = undef; <$fh> } if defined wantarray; waitpid $pid, 0; - error "C<%s> exited with value %d", $CONFIG{gpg}, ($? >> 8) if $? and $? != -1; + error "C<%s> exited with value %d", $GPG[0], ($? >> 8) if $? and $? != -1; close $fh; return unless defined wantarray; @@ -407,13 +449,14 @@ sub saveIdentityFile($$) { my $infh = "<&".fileno($form) if ref $form eq 'GLOB'; my $outfh = File::Temp->new(SUFFIX => '.gpg', UNLINK => 0, TMPDIR => 1) or die; my $pid = IPC::Open2::open2( ">&".$outfh->fileno, $infh - , $CONFIG{gpg}, qw/-o - --no-encrypt-to --recipient/, $CONFIG{keyid} + , @GPG, qw/-o - --no-encrypt-to/ + , (map {('--recipient', $_)} @{$CONFIG{keyid}}) , '--encrypt' ) or error "Can't fork: %s", $!; print $infh $form unless ref $form; close $infh; waitpid $pid, 0; - error "C<%s> exited with value %d", $CONFIG{gpg}, ($? >> 8) if $? and $? != -1; + error "C<%s> exited with value %d", $GPG[0], ($? >> 8) if $? and $? != -1; $outfh->close; my $parent_dir = $filename =~ s/\/[^\/]+$//r; @@ -553,26 +596,13 @@ sub sha256_file($) { ####################################################################### +usage(1) unless @ARGV; +@ARGV = map { $LOCALE->decode($_) } @ARGV; +my $confFilename = ($ENV{XDG_CONFIG_HOME} // "$ENV{HOME}/.data") . "/icevault"; GetOptions(\%CONFIG, qw/debug show-passwords|p socket|s=s help|? zero|0/) or usage(1); usage(0) if $CONFIG{help}; # Load configuration -my $XDG_CONFIG_HOME = $ENV{XDG_CONFIG_HOME} // "$ENV{HOME}/.config"; -my $XDG_DATA_HOME = $ENV{XDG_DATA_HOME} // "$ENV{HOME}/.data"; -loadConfig "$XDG_CONFIG_HOME/icevault"; - -# Default options -$CONFIG{gpg} //= 'gpg'; -$CONFIG{socket} //= 'S.IceVault'; -$CONFIG{store} //= "$XDG_DATA_HOME/icevault/%s/%h/%i.gpg"; -$CONFIG{pwgen} //= 'pwgen -s -cyn %d'; -$CONFIG{'max-password-length'} //= 32; -$CONFIG{keyid} // error "Missing keyid in configuration file"; -error "C<%s> is not a 64-bits key ID or fingerprint", $CONFIG{keyid} - unless $CONFIG{keyid} =~ /^(?:(?:0x)?\p{AHex}{16}|\p{AHex}{40})$/; - -usage(1) unless @ARGV; -@ARGV = map { $LOCALE->decode($_) } @ARGV; my $command = $ARGV[0] =~ /\A[A-Za-z0-9-]+:\/\//aa ? 'fill' : shift; # Process the commands diff --git a/cli/icevault.1 b/cli/icevault.1 index 0eac11f..7db6be9 100644 --- a/cli/icevault.1 +++ b/cli/icevault.1 @@ -117,9 +117,9 @@ output. This flags turns off this behavior. .TP .B \-s\fR \fIsockpath\fR, \fB\-\-socket=\fR\fIsockpath\fR Specify the path of the UNIX socket used to communicate with the -browser. If the path does not start with a slash "/", it is assumed to -be relative to the default Firefox profile (or first profile found if -there is no default profile) in the "~/.mozilla/firefox" directory. +browser. Can be an absolute path or a path relative to the default +Firefox profile (or first profile found if there is no default profile) +in the "~/.mozilla/firefox" directory. The socket path and permissions can be configured on the Iceweasel/Firefox side with the "extensions.icevault.socketPath" and "extensions.icevault.socketPerms" preferences in "about:config", @@ -144,12 +144,14 @@ Valid options are: .TP .I gpg -The \fIgpg\fR(1) binary to use. (Default: "gpg".) +The \fIgpg\fR(1) command to use. Note that users of GnuPG 1.4.x will +probably want to add the \fB--use-agent\fR option. (Default: "gpg".) .TP .I keyid -The OpenPGP key ID used as encryption recipient. Must be given a -64-bits keyid or full fingerprint. +A comma-separated list of OpenPGP key ID(s) used as encryption +recipient(s). Each component must be given as 64-bits keyid or full +fingerprint. .TP .I max-password-length @@ -166,10 +168,10 @@ is not considered part of the password. .TP .I socket -The path of the UNIX socket used to communicate with the browser. If -the path does not start with a slash "/", it is assumed to be relative -to the default Firefox profile (or first profile found if there is no -default profile) in the "~/.mozilla/firefox" directory. +The path of the UNIX socket used to communicate with the browser. Can +be an absolute path or a path relative to the default Firefox profile +(or first profile found if there is no default profile) in the +"~/.mozilla/firefox" directory. The socket path and permissions can be configured on the Iceweasel/Firefox side with the "extensions.icevault.socketPath" and "extensions.icevault.socketPerms" preferences in "about:config", @@ -178,12 +180,18 @@ respectively. .TP .I store +The working directory. Can be an absolute path or a path relative +to \fI$XDG_CONFIG_HOME\fR (or \fI~/.local/share\fR if XDG_CONFIG_HOME is +unset). +(Default: "icevault".) + +.TP +.I template The template mapping \fIscheme\fR://\fIhostname\fR/\fIidentity\fR URIs to (encrypted) files on disk. Must contain "%s", "%h", and "%i", which respectively expand to the \fIscheme\fR, \fIhostname\fR and \fIidentity\fR parts of the URI. -(Default: "$XDG_DATA_HOME/icevault/%s/%h/%i.gpg", or -"~/.data/icevault/%s/%h/%i.gpg" if $XDG_DATA_HOME is unset.) +(Default: "%s/%h/%i.gpg".) .SH AUTHOR Guilhem Moulin -- cgit v1.2.3 From 2da97abb9caf281e159267d4f6d17538a471253c Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Sat, 28 Mar 2015 22:01:22 +0100 Subject: icevault [COMMAND] [OPTION ...] [ARG ...] --- cli/icevault | 146 ++++++++++++++++++++++++++++++++++++++++----------------- cli/icevault.1 | 93 ++++++++++++++---------------------- 2 files changed, 140 insertions(+), 99 deletions(-) diff --git a/cli/icevault b/cli/icevault index 3b69b3a..c93a608 100755 --- a/cli/icevault +++ b/cli/icevault @@ -20,6 +20,7 @@ use strict; use warnings; our $VERSION = '0.1'; +my $NAME = 'icevault'; use Getopt::Long qw/:config posix_default no_ignore_case gnu_compat bundling auto_version/; use Encode qw/decode_utf8 encode_utf8/; @@ -66,20 +67,6 @@ sub warning($@) { myprintf \*STDERR, shift, @_; } -# Print usage and exit. -sub usage($) { - my $rv = shift; - my $fh = $rv ? \*STDERR : \*STDOUT; - print $fh "Usage: $0 [OPTIONS] [fill] scheme://hostname/identity\n" - ." or: $0 [OPTIONS] insert [identity]\n" - ." or: $0 [OPTIONS] dump scheme://hostname/identity\n" - ." or: $0 [OPTIONS] clip scheme://hostname/identity\n" - ." or: $0 [OPTIONS] edit scheme://hostname/identity\n" - ." or: $0 [OPTIONS] ls [scheme://[hostname/[identity]]]\n" - . "Consult the manual page for more information.\n"; - exit $rv; -} - sub mysystem(@) { system {$_[0]} @_; error "C<%s> exited with value %d", $_[0], ($? >> 8) if $? and $? != -1; @@ -594,35 +581,96 @@ sub sha256_file($) { } + ####################################################################### -usage(1) unless @ARGV; +unless (@ARGV) { + print STDERR "Usage: $NAME [COMMAND] [OPTION ...] [ARG ...]\n"; + error "Missing command. Try C<%s> or consult the manpage for more information.", "$NAME --help"; +} + +my @USAGE = ( + fill => "[-f, --force] [-p, --show-passwords] [-s, --socket=PATH] scheme://hostname/identity", + clip => "scheme://hostname/identity", + dump => "[-p, --show-passwords] scheme://hostname/identity", + edit => "scheme://hostname/identity", + insert => "[-f, --force] [-s, --socket=PATH] [identity]", + ls => "[-0, --zero] [scheme://[hostname/[identity]]]", +); + +if ($ARGV[0] eq '--help' or $ARGV[0] eq '-?') { + my $default_cmd = shift @USAGE; + my $default_usage = shift @USAGE; + print "Usage: $NAME [$default_cmd] $default_usage\n"; + while (@USAGE) { + my $cmd = shift @USAGE; + my $usage = shift @USAGE; + print " or: $NAME $cmd $usage\n"; + } + myprintf "Try C<%s> or consult the manpage for more information.", "$NAME COMMAND --help"; + exit 0; +} + @ARGV = map { $LOCALE->decode($_) } @ARGV; -my $confFilename = ($ENV{XDG_CONFIG_HOME} // "$ENV{HOME}/.data") . "/icevault"; -GetOptions(\%CONFIG, qw/debug show-passwords|p socket|s=s help|? zero|0/) or usage(1); -usage(0) if $CONFIG{help}; +my $COMMAND = ($ARGV[0] =~ /\A[A-Za-z0-9-]+:\/\//aa or $ARGV[0] =~ /\A--?[^-]/) ? 'fill' : shift; + +# Print $COMMAND usage (detailed if --help) +sub usage(@) { + my @opts = @_; + my %usage = @USAGE; + print "$NAME $COMMAND $usage{$COMMAND} \n"; + if ($CONFIG{help}) { + if (@opts) { + print "Options:\n"; + while (@opts) { + shift @opts; + print " ".shift(@opts)."\n"; + } + } + printf "Consult the manpage for more information.\n"; + exit 0; + } else { + myprintf "Try C<%s> or consult the manpage for more information.", "$NAME $COMMAND --help"; + exit 1; + } +} + +# Get options, load and validate config +sub getopts(%) { + my @opts = @_; + my %opts = @opts; + usage(@opts) unless GetOptions(\%CONFIG, qw/debug help|?/, keys %opts) and !$CONFIG{help}; + loadConfig(); +} -# Load configuration -my $command = $ARGV[0] =~ /\A[A-Za-z0-9-]+:\/\//aa ? 'fill' : shift; +####################################################################### # Process the commands -if ($command eq '_complete') { + +if ($COMMAND eq '_complete') { # used internaly for auto-completion - usage(1) unless $#ARGV == 0; + GetOptions(\%CONFIG, qw/zero|0/) or die; + die unless $#ARGV == 0; my $delim = $CONFIG{zero} ? "\0" : "\n"; print $LOCALE->encode($_), $delim foreach complete(shift @ARGV); exit; } -elsif ($command eq '_geturi') { +elsif ($COMMAND eq '_geturi') { # used internaly for auto-completion - usage(1) if @ARGV; + GetOptions(\%CONFIG, qw/socket|s=s/) or die; + die if @ARGV; print $LOCALE->encode( &connect($CONFIG{socket}) ), "\n"; sendCommand 'QUIT'; exit; } -elsif ($command eq 'insert') { - usage(1) unless $#ARGV < 1; + +elsif ($COMMAND eq 'insert') { + getopts( 'force|f' => "-f, --force \tOverwrite preexisting identity" + , 'socket|s=s' => "-s, --socket=PATH\tSpecifiy the path to the Icevault socket" + ); + usage() unless $#ARGV < 1; + my $uri = &connect($CONFIG{socket}); myprintf "Importing HTML form from URI C<%s>", $uri; @@ -660,7 +708,7 @@ elsif ($command eq 'insert') { if ($r !~ /\A[^\P{Print}\/]+\z/) { myprintf \*STDERR, "Invalid identity: C<%s>", $r; } - elsif (-e getIdentityFile "$uri/$r") { + elsif (-e getIdentityFile "$uri/$r" and !$CONFIG{force}) { myprintf \*STDERR, "Identity C<%s> already exists", "$uri/$r"; } else { @@ -671,7 +719,7 @@ elsif ($command eq 'insert') { } my $filename = getIdentityFile "$uri/$id"; - error "Identity C<%s> already exists", "$uri/$id" if -e $filename; + error "Identity C<%s> already exists", "$uri/$id" if -e $filename and !$CONFIG{force}; my @passIdx = grepIdx { $_->{type} eq 'password' } @{$form->{fields}}; my @dontsave; @@ -725,8 +773,13 @@ elsif ($command eq 'insert') { saveIdentityFile $form, $filename; } -elsif ($command eq 'fill') { - usage(1) unless $#ARGV == 0; +elsif ($COMMAND eq 'fill') { + getopts( 'force|f' => "-f, --force \tDon't ask before updating the form" + , 'show-passwords|p=s' => "-p, --show-passwords\tDon't redact passwords" + , 'socket|s=s' => "-s, --socket=PATH \tSpecifiy the path to the Icevault socket" + ); + usage() unless $#ARGV == 0; + my $id = shift; my $filename = getIdentityFile $id; error "No such identity C<%s>", $id unless -f $filename; @@ -770,7 +823,7 @@ elsif ($command eq 'fill') { $changed = 1; } - if ($pass->{value} eq '') { # fill the password with the known value + if ($pass->{value} eq '' or $CONFIG{force}) { # fill the password with the known value $fill[$passIdx[0]] = $mypass->{value}; } elsif ($mypass->{value} ne $pass->{value}) { # update the password @@ -819,7 +872,8 @@ elsif ($command eq 'fill') { my $myidx = shift @{$myfields{$name}}; my $idx = shift @{$fields{$name}}; next unless defined $myidx and defined $idx; # was taken care of before - if ($form->{fields}->[$idx]->{value} eq '' and $myform->{fields}->[$myidx]->{value} ne '') { + if (($form->{fields}->[$idx]->{value} eq '' or $CONFIG{force}) + and $myform->{fields}->[$myidx]->{value} ne '') { # fill with the known value $fill[$idx] = $myform->{fields}->[$myidx]->{value}; } @@ -870,8 +924,10 @@ elsif ($command eq 'fill') { } } -elsif ($command eq 'dump') { - usage(1) unless $#ARGV == 0; +elsif ($COMMAND eq 'dump') { + getopts('show-passwords|p=s' => "-p, --show-passwords\tDon't redact passwords"); + usage() unless $#ARGV == 0; + my $id = shift; my $filename = getIdentityFile $id; error "No such identity C<%s>", $id unless -f $filename; @@ -883,8 +939,10 @@ elsif ($command eq 'dump') { print STDOUT (defined $LOCALE ? $LOCALE->encode($str) : $str) } -elsif ($command eq 'edit') { - usage(1) unless $#ARGV == 0; +elsif ($COMMAND eq 'edit') { + getopts(); + usage() unless $#ARGV == 0; + my $id = shift; my $filename = getIdentityFile $id; error "No such identity C<%s>", $id unless -f $filename; @@ -932,8 +990,10 @@ elsif ($command eq 'edit') { } } -elsif ($command eq 'clip') { - usage(1) unless $#ARGV == 0; +elsif ($COMMAND eq 'clip') { + getopts(); + usage() unless $#ARGV == 0; + my $id = shift; my $filename = getIdentityFile $id; error "No such identity C<%s>", $id unless -f $filename; @@ -951,8 +1011,10 @@ elsif ($command eq 'clip') { exit 0; } -elsif ($command eq 'ls') { - usage(1) if $#ARGV > 0; +elsif ($COMMAND eq 'ls') { + getopts( 'zero|0' => "-0, --zero\tUse NUL instead of newline as line delimiter" ); + usage() if $#ARGV > 0; + my $prefix = shift @ARGV; my @matches = complete $prefix // '', 1; @@ -978,6 +1040,6 @@ elsif ($command eq 'ls') { } else { - myprintf "Unknown command: C<%s>", $command; - usage(1); + print STDERR "Usage: $NAME [COMMAND] [OPTION ...] [ARG ...]\n"; + error "Unknown command C<%s>. Try C<%s> for more information.", $COMMAND, "$NAME --help"; } diff --git a/cli/icevault.1 b/cli/icevault.1 index 7db6be9..0768b68 100644 --- a/cli/icevault.1 +++ b/cli/icevault.1 @@ -4,17 +4,7 @@ IceVault \- IceVault client user interface .SH SYNOPSIS -.B icevault\fR [\fIOPTIONS\fR] [\fBfill\fR] \fIscheme\fR://\fIhostname\fR/\fIidentity\fR -.br -.B icevault\fR [\fIOPTIONS\fR] \fBinsert\fR [\fIidentity\fR] -.br -.B icevault\fR [\fIOPTIONS\fR] \fBdump\fR \fIscheme\fR://\fIhostname\fR/\fIidentity\fR -.br -.B icevault\fR [\fIOPTIONS\fR] \fBclip\fR \fIscheme\fR://\fIhostname\fR/\fIidentity\fR -.br -.B icevault\fR [\fIOPTIONS\fR] \fBedit\fR \fIscheme\fR://\fIhostname\fR/\fIidentity\fR -.br -.B icevault\fR [\fIOPTIONS\fR] \fBls\fR [\fIscheme\fR://[\fIhostname\fR/[\fIidentity\fR]]] +.B icevault\fR [\fICOMMAND\fR] [\fIOPTION\fR ...] [\fIARG\fR ...] .SH DESCRIPTION @@ -46,8 +36,10 @@ using \fIpwgen\fR(1). .SH COMMANDS +If \fICOMMAND\fR is omitted, \fBfill\fR is assumed. + .TP -.B fill\fR \fIscheme\fR://\fIhostname\fR/\fIidentity\fR +.B fill\fR [\fB-f\fR, \fB--force\fR] [\fB-p\fR, \fB--show-passwords\fR] [\fB-s\fR, \fB--socket=\fR\fIPATH\fR] \fIscheme\fR://\fIhostname\fR/\fIidentity\fR If the scheme (resp. hostname) of the active tab of the active window is not \fIscheme\fR (resp. \fIhostname\fR) the program assumes a phishing attempt and aborts. Otherwise, the \fIidentity\fR file is decrypted and @@ -60,11 +52,34 @@ If \fIidentity\fR has a single password whereas the webpage has 2 (resp. 3), a signup (resp. password changing) page is assumed, and a new password is randomly generated using \fIpwgen\fR(1) if the fields are left blank. +Use \fB--socket=\fR\fIPATH\fR to specify the path to the IceVault +socket. If \fB-f\fR is set, existing values on the browser are ignored. +Passwords are redacted unless the flag \fB-p\fR is set. + +.TP +.B clip\fR \fIscheme\fR://\fIhostname\fR/\fIidentity\fR +Decrypt the \fIidentity\fR file and copy its first password to the +clipboard using \fIxclip\fR(1), with a maximum number of pastes of 1. + +.TP +.B dump\fR [\fB-p\fR, \fB--show-passwords\fR] \fIscheme\fR://\fIhostname\fR/\fIidentity\fR +Decrypt the \fIidentity\fR file and dump its content on the standard +output. Note that while the output is a valid YAML document, original +formatting may not be preserved; in particular, comments and empty lines +are stripped. Passwords are redacted unless the flag \fB-p\fR is set. .TP -.B insert\fR [\fIidentity\fR] +.B edit\fR \fIscheme\fR://\fIhostname\fR/\fIidentity\fR +Decrypt the \fIidentity\fR file to a temporary file and open it using +the editor specified by the EDITOR environment variable (or \fIeditor\fR +if EDITOR is unset). Upon exit, the file is reencrypted if the SHA-256 +digest of its content differs. Note that formatting and comments may +not be preserved by subsequent updates of the \fIidentity\fR file. + +.TP +.B insert\fR [\fB-f\fR, \fB--force\fR] [\fB-s\fR, \fB--socket=\fR\fIPATH\fR] [\fIidentity\fR] Create a new \fIscheme\fR://\fIhostname\fR/\fIidentity\fR URI available -for further \fBfill\fR and other commands. +for further commands. Store the first visible form on the active tab of the active window which contains a password (or the first visible form with a non-empty field if no visible form has a password). If \fIidentity\fR is omitted, it @@ -74,33 +89,17 @@ password). If the webpage has 2 (resp. 3), a signup (resp. password changing) page is assumed, and a new password is randomly generated using \fIpwgen\fR(1) if the fields are left blank. +Use \fB--socket=\fR\fIPATH\fR to specify the path to the IceVault +socket. If the flag \fB-f\fR is set, override the \fIidentity\fR file +if it already exists (the default is to abort). .TP -.B dump\fR \fIscheme\fR://\fIhostname\fR/\fIidentity\fR -Decrypt the \fIidentity\fR file and dump its content on the standard -output. Note that while the output is a valid YAML document, original -formatting may not be preserved; in particular, comments and empty lines -are stripped. +.B ls\fR [\fB-0\fR, \fB--zero\fR] [\fIscheme\fR://[\fIhostname\fR/[\fIidentity\fR]]] +List content of the given identity prefix. If the flag \fB-0\fR is set, +use NUL as line separator. -.TP -.B clip\fR \fIscheme\fR://\fIhostname\fR/\fIidentity\fR -Decrypt the \fIidentity\fR file and copy the first password to the -clipboard using \fIxclip\fR(1), with a maximum number of pastes of 1. -.TP -.B edit\fR \fIscheme\fR://\fIhostname\fR/\fIidentity\fR -Decrypt the \fIidentity\fR file to a temporary file and opens it using -the editor specified by the EDITOR environment variable. When the -editor exits, the file is reencrypted if the SHA-256 digest of its -content differs. Note that formatting and comments may not be preserved -by subsequent updates of the \fIidentity\fR file. - -.TP -.B ls\fR [\fIscheme\fR://[\fIhostname\fR/[\fIidentity\fR]]] -List content of the given identity prefix. - - -.SH OPTIONS +.SH GLOBAL OPTIONS .TP .B \-\-debug Turn on debug mode. @@ -109,30 +108,10 @@ Turn on debug mode. .B \-h\fR, \fB\-\-help\fR Output a brief help and exit. -.TP -.B \-p\fR, \fB\-\-show\-passwords\fR -By default passwords are redacted when printing forms to the standard -output. This flags turns off this behavior. - -.TP -.B \-s\fR \fIsockpath\fR, \fB\-\-socket=\fR\fIsockpath\fR -Specify the path of the UNIX socket used to communicate with the -browser. Can be an absolute path or a path relative to the default -Firefox profile (or first profile found if there is no default profile) -in the "~/.mozilla/firefox" directory. -The socket path and permissions can be configured on the -Iceweasel/Firefox side with the "extensions.icevault.socketPath" and -"extensions.icevault.socketPerms" preferences in "about:config", -respectively. - .TP .B \-\-version Show the version number and exit. -.TP -.B \-0\fR, \fB\-\-zero -With the \fBls\fR command, use NUL instead of newline as line delimiter. - .SH CONFIGURATION FILE -- cgit v1.2.3 From 0db12ef8b87b37a9b7d55be5f8d4c2545b1dd0e4 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Fri, 27 Mar 2015 03:10:05 +0100 Subject: Add a 'git' command. --- cli/icevault | 51 ++++++++++++++++++++++++++++++++++++++++++++++++--- cli/icevault.1 | 47 +++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 89 insertions(+), 9 deletions(-) diff --git a/cli/icevault b/cli/icevault index c93a608..67e06ec 100755 --- a/cli/icevault +++ b/cli/icevault @@ -35,7 +35,7 @@ delete @ENV{qw/IFS CDPATH ENV BASH_ENV/}; my $LANGINFO = I18N::Langinfo::langinfo(I18N::Langinfo::CODESET()); my $LOCALE = Encode::find_encoding $LANGINFO; -my (%CONFIG, @GPG); +my (%CONFIG, @GIT, @GPG); my $SOCKET; @@ -146,6 +146,12 @@ sub loadConfig() { error "C<%s> must contain %%s, %%h and %%i placeholders", 'template' unless $CONFIG{template} =~ /%s/ and $CONFIG{template} =~ /%h/ and $CONFIG{template} =~ /%i/; + $CONFIG{'git-dir'} //= '.git'; + $CONFIG{'git-dir'} =~ s#\A~/#$ENV{HOME}#; + $CONFIG{'git-dir'} = "$CONFIG{store}/$CONFIG{'git-dir'}" unless $CONFIG{'git-dir'} =~ /\A\//; + $CONFIG{'git-dir'} =~ /\A(\/\p{Print}+)\z/ or error "Insecure C<%s>", $CONFIG{'git-dir'}; + $CONFIG{'git-dir'} = $1; # untaint $CONFIG{'git-dir'} + $CONFIG{socket} //= 'socket'; $CONFIG{socket} =~ s#\A~/#$ENV{HOME}#; @@ -169,6 +175,7 @@ sub loadConfig() { error "Insecure C<%s>", $CONFIG{gpg} unless $CONFIG{gpg} =~ /\A([-_\@a-zA-Z0-9\.%\/= ]+)\z/; $CONFIG{gpg} = $1; + @GIT = ('git', '--work-tree='.$CONFIG{store}, '--git-dir='.$CONFIG{'git-dir'}); @GPG = split / /, $CONFIG{gpg}; } @@ -453,8 +460,6 @@ sub saveIdentityFile($$) { unlink $outfh->filename or error "Can't unlink C<%s>: %s", $outfh->filename, $!; error "Can't move C<%s> to C<%s>: %s", $outfh->filename, $filename, $r; } - - # TODO: git add $filename; git commit } # Get the visible form list from the server, and croak if it's empty. @@ -580,6 +585,25 @@ sub sha256_file($) { return $sha256->digest; } +# Add the given @filenames to the index and if there are any staged +# changes for these files, commit them with the given message. +sub commit($@) { + my $msg = shift; + my @filenames = @_; + + return unless -d $CONFIG{'git-dir'} and @filenames; + mysystem @GIT, 'add', @filenames; + + # check if there are any staged changes on @filenames + system {$GIT[0]} @GIT, 'diff', '--quiet', '--staged', '--', @filenames; + return unless $?; # exit value 0: nothing staged + error "C<%s> exited with value %d", $GIT[0], ($? >> 8) if $? and $? != -1 and $? != 256; + + $msg =~ /\A(\p{Print}*)\z/ or error "Insecure C<%s>", $msg; + $msg = $1; # untaint $msg + mysystem @GIT, 'commit', '-m', $msg, '--', @filenames; +} + ####################################################################### @@ -594,6 +618,7 @@ my @USAGE = ( clip => "scheme://hostname/identity", dump => "[-p, --show-passwords] scheme://hostname/identity", edit => "scheme://hostname/identity", + git => "GIT-COMMAND [GIT-ARG ...]", insert => "[-f, --force] [-s, --socket=PATH] [identity]", ls => "[-0, --zero] [scheme://[hostname/[identity]]]", ); @@ -771,6 +796,7 @@ elsif ($COMMAND eq 'insert') { undef @{$form->{fields}}[@dontsave] if @dontsave; # remove the field we don't want to save myprintf "Saving identity C<%s>", "$uri/$id"; saveIdentityFile $form, $filename; + commit "Add new identity $uri/$id", $filename; } elsif ($COMMAND eq 'fill') { @@ -921,6 +947,7 @@ elsif ($COMMAND eq 'fill') { if ($changed) { my $r = promptYN "Save changes?", 0; saveIdentityFile ($myform, $filename) if $r; + commit "Save imported changes for $id", $filename; } } @@ -982,6 +1009,7 @@ elsif ($COMMAND eq 'edit') { } elsif (defined $fh2) { myprintf "Saving user changes for identity C<%s>", $id; saveIdentityFile($fh2, $filename); # use the FH we opened before unlinking + commit "Save manual (using $EDITOR) changes for $id", $filename; } else { print "Aborting\n"; exit 1; @@ -1039,6 +1067,23 @@ elsif ($COMMAND eq 'ls') { print $LOCALE->encode($_), $delim foreach sort keys %matches; } +elsif ($COMMAND eq 'git') { + getopts(); + usage() unless @ARGV; + + unless (-d $CONFIG{store}) { + require 'File/Path.pm'; + File::Path::make_path($CONFIG{store}); + myprintf "Created directory C<%s>", $CONFIG{store}; + } + + for (my $i = 0; $i <= $#ARGV; $i++) { + $ARGV[$i] =~ /\A(\p{print}*)\z/ or die; + $ARGV[$i] = $1; + } + exec {$GIT[0]} @GIT, @ARGV; +} + else { print STDERR "Usage: $NAME [COMMAND] [OPTION ...] [ARG ...]\n"; error "Unknown command C<%s>. Try C<%s> for more information.", $COMMAND, "$NAME --help"; diff --git a/cli/icevault.1 b/cli/icevault.1 index 0768b68..906cc8d 100644 --- a/cli/icevault.1 +++ b/cli/icevault.1 @@ -22,12 +22,14 @@ browser and the \fBicevault\fR client. Each form is stored in a separate file, encrypted separately with \fIgpg\fR(1); cleartext are never stored on disk. Form history can be kept -track of by adding the encrypted files to a VCS as binary blobs. File -paths are of the form ".../\fIscheme\fR/\fIhostname\fR/\fIidentity\fR" -where \fIidentity\fR is an arbitrary user-chosen value (allowing -multiple identities for a given site); since the URI of the active tab -can be retrieved from the socket and since the URI of a stored form can -be recovered from its file path, phishing attacks are easily detected. +track of by versioning the encrypted files to a Git repository as binary +blobs. (Modification of the stored forms are then automatically +committed to said repository.) File paths are of the form +".../\fIscheme\fR/\fIhostname\fR/\fIidentity\fR" where \fIidentity\fR is +an arbitrary user-chosen value (allowing multiple identities for a given +site); since the URI of the active tab can be retrieved from the socket +and since the URI of a stored form can be recovered from its file path, +phishing attacks are easily detected. Like Firefox's builtin password manager, IceVault has some heuristics to detect signup and password changing pages. In these cases, and if the @@ -76,6 +78,33 @@ if EDITOR is unset). Upon exit, the file is reencrypted if the SHA-256 digest of its content differs. Note that formatting and comments may not be preserved by subsequent updates of the \fIidentity\fR file. +.TP +.B git\fR \fIGIT-COMMAND\fR [\fIGIT-ARG\fR...] +Pass \fIGIT-COMMAND\fR [\fIGIT-ARG\fR...] as arguments to \fIgit\fR(1) +using the configuration value for \fIstore\fR and that for \fIgit-dir\fR +as the Git working tree and Git repository, respectively. +\fIstore\fR is automatically created if it is not an existing directory. + +It is recommended to initialize the repository as follows: + + \fBicevault git\fR init + echo '*.gpg diff=gpg' >"${XDG_DATA_HOME:-$HOME/.local/share}/icevault/.gitattributes" + \fBicevault git\fR add .gitattributes + \fBicevault git\fR commit \-m 'Add Git attributes for .gpg binary files.' + \fBicevault git\fR config diff.gpg.binary true + \fBicevault git\fR config diff.gpg.textconv 'gpg2 \-o \- \-\-decrypt' + +The textconv config option enable on-the-fly decryption prior to Git +operations such as \fIdiff\fR or \fIgrep\fR, see \fIgitattributes\fR(5). +For instance, grep'ing through the cleartext becomes trivial: + + \fBicevault git\fR grep \-\-textconv \fIpattern\fR + +Signing each commit can be achieved as follows, see \fIgit-config\fR(1): + + \fBicevault git\fR config commit.gpgsign true + \fBicevault git\fR config user.signingkey 0x39278DA8109E6244 + .TP .B insert\fR [\fB-f\fR, \fB--force\fR] [\fB-s\fR, \fB--socket=\fR\fIPATH\fR] [\fIidentity\fR] Create a new \fIscheme\fR://\fIhostname\fR/\fIidentity\fR URI available @@ -121,6 +150,12 @@ XDG_CONFIG_HOME is unset. Empty lines and comments (starting with a "#" characters are ignored). Valid options are: +.TP +.I git-dir +Path to the Git directory. Can be an absolute path or a path relative +to the working directory (specified with \fIstore\fR). +(Default: ".git") + .TP .I gpg The \fIgpg\fR(1) command to use. Note that users of GnuPG 1.4.x will -- cgit v1.2.3 From ed3adbf9fad33794fe3892bd2598060374cb0b95 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Sat, 28 Mar 2015 23:16:10 +0100 Subject: Add a 'reencrypt' command. --- cli/icevault | 66 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++-- cli/icevault.1 | 8 +++++++ 2 files changed, 72 insertions(+), 2 deletions(-) diff --git a/cli/icevault b/cli/icevault index 67e06ec..20a8f63 100755 --- a/cli/icevault +++ b/cli/icevault @@ -285,6 +285,30 @@ sub myglob(;$$$) { return File::Glob::bsd_glob($glob); } +# Find identities matching a given prefix +sub matches($) { + my $prefix = shift; + + my ($s, $h, $i); + if (!defined $prefix) { + } elsif ($prefix =~ /\A([A-Za-z0-9-]+):\/\/([^\P{Graph}:\/]+(?::\d+)?)\/([^\P{Print}\/]*)\z/) { + ($s, $h, $i) = ($1, $2, ($3 eq '' ? undef : $3)); + } elsif ($prefix =~ /\A([A-Za-z0-9-]+):\/\/([^\P{Graph}\/]*)\z/) { + ($s, $h, $i) = ($1, ($2 eq '' ? undef : $2), undef); + } elsif ($prefix =~ /\A([A-Za-z0-9-]*)(:\/?)?\z/) { + ($s, $h, $i) = ($1, undef, undef); + } else { + error "Invalid identity prefix C<%s>", $prefix; + } + + s/([\\\[\]\{\}\*\?\~])/\\$1/g foreach grep defined, ($s, $h, $i); # escape meta chars + + my @matches = myglob($s,$h,$i); + error "No matches for identity prefix C<%s>", $prefix unless @matches; + error "No such identity C<%s>", $prefix if defined $i and ! -f $matches[0]; + return @matches; +} + # Get all identities with the given $prefix. If there are multiple # matches and $all is false, limit the output to one depth more than the # longuest common prefix. @@ -395,8 +419,9 @@ sub getIdentityFile($) { } # Decrypt the given identity file. In scalar context, return the -# YAML-parsed form; in void context, must be given a file handle (closed -# afterwards) where to dump the (unparsed) decrypted content. +# YAML-parsed form; in list context, return the list of the forked PID +# and its standard output; in void context, must be given a file handle +# (closed afterwards) where to dump the (unparsed) decrypted content. open my $NULL, '<', '/dev/null'; sub loadIdentityFile($;$) { my ($filename, $fh) = @_; @@ -407,6 +432,7 @@ sub loadIdentityFile($;$) { , "<&".fileno($NULL) , @GPG, qw/-o - --decrypt --/, $filename) or error "Can't fork: %s", $!; + return ($pid, $fh) if wantarray; my $str = do { local $/ = undef; <$fh> } if defined wantarray; waitpid $pid, 0; error "C<%s> exited with value %d", $GPG[0], ($? >> 8) if $? and $? != -1; @@ -462,6 +488,20 @@ sub saveIdentityFile($$) { } } +# Copy the given filename to a new destination, and reencrypt it the +# file. The filenames may be identical since 'saveIdentityFile' uses a +# temporary destination. +sub copyIdentityFile($$) { + my ($oldname, $newname) = @_; + + my ($pid, $fh) = loadIdentityFile $oldname; + saveIdentityFile($fh, $newname); + + waitpid $pid, 0; + error "C<%s> exited with value %d", $GPG[0], ($? >> 8) if $? and $? != -1; + close $fh; +} + # Get the visible form list from the server, and croak if it's empty. sub getForms() { my $forms = sendCommand 'GETFORMS'; @@ -621,6 +661,7 @@ my @USAGE = ( git => "GIT-COMMAND [GIT-ARG ...]", insert => "[-f, --force] [-s, --socket=PATH] [identity]", ls => "[-0, --zero] [scheme://[hostname/[identity]]]", + reencrypt => "[scheme://[hostname/[identity]] ...]", ); if ($ARGV[0] eq '--help' or $ARGV[0] eq '-?') { @@ -1084,6 +1125,27 @@ elsif ($COMMAND eq 'git') { exec {$GIT[0]} @GIT, @ARGV; } +elsif ($COMMAND eq 'reencrypt') { + getopts(); + + my @matches = @ARGV ? map {matches($_)} @ARGV : myglob(undef, undef, undef); + error "No such identity C<%s>", $_ foreach grep { ! -f $_ } @matches; + + my @filenames; + foreach my $filename (@matches) { + $filename = $LOCALE->decode($filename); + myprintf "Reencrypting C<%s>", $filename; + + $filename =~ /\A(\/\p{Print}+)\z/ or error "Insecure C<%s>", $filename; + $filename = $1; # untaint $filename + + copyIdentityFile $filename, $filename; + push @filenames, $filename; + } + + commit 'Reencryption.', @filenames; +} + else { print STDERR "Usage: $NAME [COMMAND] [OPTION ...] [ARG ...]\n"; error "Unknown command C<%s>. Try C<%s> for more information.", $COMMAND, "$NAME --help"; diff --git a/cli/icevault.1 b/cli/icevault.1 index 906cc8d..b0308a5 100644 --- a/cli/icevault.1 +++ b/cli/icevault.1 @@ -127,6 +127,14 @@ if it already exists (the default is to abort). List content of the given identity prefix. If the flag \fB-0\fR is set, use NUL as line separator. +.TP +.B reencrypt\fR [\fIscheme\fR://[\fIhostname\fR/[\fIidentity\fR]] ...] +Reencrypt each given identity prefix(es) with the \fIkeyid\fR(s) found in +the configuration file as recpient(s). If no argument is given, +reencrypt the entire store. If \fIidentity\fR (resp. +\fIidentity\fR/\fIhostname\fR) is omitted, reencrypt all identities +found under \fIscheme\fR://\fIhostname\fR/ (resp. \fIscheme\fR://). + .SH GLOBAL OPTIONS .TP -- cgit v1.2.3 From 157c7d4e24cc11da132fcce30e384970b0aaa005 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Sat, 28 Mar 2015 23:51:23 +0100 Subject: Fix 'ls', and add a --recursive flag. --- cli/icevault | 107 +++++++++++++++++++++++++++++++++++++++------------------ cli/icevault.1 | 7 ++-- 2 files changed, 79 insertions(+), 35 deletions(-) diff --git a/cli/icevault b/cli/icevault index 20a8f63..3c31f1d 100755 --- a/cli/icevault +++ b/cli/icevault @@ -403,7 +403,7 @@ sub fill($$@) { # Parse a scheme://hostname(:port)?/identity, and return the associated # file. -sub getIdentityFile($) { +sub identity2File($) { my $id = shift; $id =~ /\A([A-Za-z0-9-]+):\/\/([^\P{Graph}:\/]+(?::\d+)?)\/([^\P{Print}\/]+)\z/ or error "Invalid identity C<%s>", $id; @@ -418,6 +418,24 @@ sub getIdentityFile($) { return "$CONFIG{store}/$template"; } +# Parse a filename as a scheme://hostname(:port)?/identity. +sub file2Identity($) { + my $filename = shift; + my $store = $CONFIG{store}; + my $template = $CONFIG{template}; + + $template =~ s/(\%.)([^\%]*)\z/$1.quotemeta($2)/e; + $template =~ s{(.*?)\%(.)}{$2 eq '%' ? '%' : + $2 eq 's' ? quotemeta($1).'(?[A-Za-z0-9-]+)' : + $2 eq 'h' ? quotemeta($1).'(?[^\P{Graph}:\/]+(?::\d+)?)' : + $2 eq 'i' ? quotemeta($1).'(?[^\P{Print}\/]+)' : + die "Invalid placeholder %$1"}ge; + my $pattern = qr/\A\Q$store\/\E$template\z/; + myprintf \*STDERR, "Using regexp C<%s>", "$pattern" if $CONFIG{debug}; + $filename =~ $pattern or die; + return "$+{s}://$+{h}/$+{i}"; +} + # Decrypt the given identity file. In scalar context, return the # YAML-parsed form; in list context, return the list of the forked PID # and its standard output; in void context, must be given a file handle @@ -644,6 +662,45 @@ sub commit($@) { mysystem @GIT, 'commit', '-m', $msg, '--', @filenames; } +# List all identites matching the given prefixes, along with their +# filename +sub list(@) { + my @args = @_; + + my @matches; + if (!@args) { + @matches = map { my $file = $LOCALE->decode($_); { filename => $file, id => file2Identity $file } } + myglob(undef, undef, undef); + unless ($CONFIG{recursive}) { + $_->{id} =~ s/:\/\/.*// foreach @matches; # keep the scheme only + } + } + else { + foreach my $prefix (@args) { + my @matches1 = map { my $file = $LOCALE->decode($_); { filename => $file, id => file2Identity $file } } + matches($prefix); + if ($CONFIG{recursive}) { + # don't remove suffix + } elsif ($prefix =~ /\A[A-Za-z0-9-]+:\/\/[^\P{Graph}:\/]+(?::\d+)?\/[^\P{Print}\/]+\z/) { + @matches1 = grep { $_->{id} =~ /\A\Q$prefix\E\z/ } @matches1; + } elsif ($prefix =~ /\A([A-Za-z0-9-]+:\/\/[^\P{Graph}\/]+)\/?\z/) { + my $x = $1; + @matches1 = grep defined, map { $_->{id} !~ s/\A\Q$x\E\/// ? undef : $_ } @matches1; + } elsif ($prefix =~ /\A([A-Za-z0-9-]+)(:\/{0,2})?\z/) { + my $x = $1; + @matches1 = grep defined, map { $_->{id} !~ s/\A\Q$x\E:\/\/// ? undef : + $_->{id} !~ s/\/[^\P{Print}\/]+\z// ? undef : $_ } @matches1; + } else { + @matches1 = (); + } + error "No such identity C<%s>", $prefix unless @matches1; + push @matches, @matches1; + } + } + map { $_->{filename} =~ /\A(\/\p{Print}+)\z/ or error "Insecure C<%s>", $_->{filename}; + $_->{filename} = $1; $_; # untaint $_->{filename} + } @matches; +} ####################################################################### @@ -660,7 +717,7 @@ my @USAGE = ( edit => "scheme://hostname/identity", git => "GIT-COMMAND [GIT-ARG ...]", insert => "[-f, --force] [-s, --socket=PATH] [identity]", - ls => "[-0, --zero] [scheme://[hostname/[identity]]]", + ls => "[-0, --zero] [-r, --recursive] [scheme://[hostname/[identity]] ...]", reencrypt => "[scheme://[hostname/[identity]] ...]", ); @@ -774,7 +831,7 @@ elsif ($COMMAND eq 'insert') { if ($r !~ /\A[^\P{Print}\/]+\z/) { myprintf \*STDERR, "Invalid identity: C<%s>", $r; } - elsif (-e getIdentityFile "$uri/$r" and !$CONFIG{force}) { + elsif (-e identity2File "$uri/$r" and !$CONFIG{force}) { myprintf \*STDERR, "Identity C<%s> already exists", "$uri/$r"; } else { @@ -784,7 +841,7 @@ elsif ($COMMAND eq 'insert') { } } - my $filename = getIdentityFile "$uri/$id"; + my $filename = identity2File "$uri/$id"; error "Identity C<%s> already exists", "$uri/$id" if -e $filename and !$CONFIG{force}; my @passIdx = grepIdx { $_->{type} eq 'password' } @{$form->{fields}}; @@ -848,7 +905,7 @@ elsif ($COMMAND eq 'fill') { usage() unless $#ARGV == 0; my $id = shift; - my $filename = getIdentityFile $id; + my $filename = identity2File $id; error "No such identity C<%s>", $id unless -f $filename; my $uri = &connect($CONFIG{socket}); @@ -997,7 +1054,7 @@ elsif ($COMMAND eq 'dump') { usage() unless $#ARGV == 0; my $id = shift; - my $filename = getIdentityFile $id; + my $filename = identity2File $id; error "No such identity C<%s>", $id unless -f $filename; my $form = loadIdentityFile $filename; @@ -1012,7 +1069,7 @@ elsif ($COMMAND eq 'edit') { usage() unless $#ARGV == 0; my $id = shift; - my $filename = getIdentityFile $id; + my $filename = identity2File $id; error "No such identity C<%s>", $id unless -f $filename; require 'File/Temp.pm'; @@ -1064,7 +1121,7 @@ elsif ($COMMAND eq 'clip') { usage() unless $#ARGV == 0; my $id = shift; - my $filename = getIdentityFile $id; + my $filename = identity2File $id; error "No such identity C<%s>", $id unless -f $filename; my $form = loadIdentityFile $filename; @@ -1081,30 +1138,12 @@ elsif ($COMMAND eq 'clip') { } elsif ($COMMAND eq 'ls') { - getopts( 'zero|0' => "-0, --zero\tUse NUL instead of newline as line delimiter" ); - usage() if $#ARGV > 0; - - my $prefix = shift @ARGV; - my @matches = complete $prefix // '', 1; - - if (!defined $prefix) { - s/:\/\/.*// foreach @matches; - } elsif ($prefix =~ /\A[A-Za-z0-9-]+:\/\/[^\P{Graph}:\/]+(?::\d+)?\/[^\P{Print}\/]+\z/) { - @matches = grep /\A\Q$prefix\E\z/, @matches; - } elsif ($prefix =~ /\A([A-Za-z0-9-]+:\/\/[^\P{Graph}\/]+)\/?\z/) { - my $x = $1; - @matches = grep defined, map { !s/\A\Q$x\E\/// ? undef : $_ } @matches; - } elsif ($prefix =~ /\A([A-Za-z0-9-]+)(:\/{0,2})?\z/) { - my $x = $1; - @matches = grep defined, map { !s/\A\Q$x\E:\/\/// ? undef : - !s/\/[^\P{Print}\/]+\z// ? undef : $_ } @matches; - } else { - @matches = (); - } - error "No such identity C<%s>", $prefix // '' unless @matches; + getopts( 'zero|0' => "-0, --zero \tUse NUL instead of newline as line delimiter" + , 'recursive|r' => "-r, --recursive\tList identities recursively" + ); my $delim = $CONFIG{zero} ? "\0" : "\n"; - my %matches = map {($_ => 1)} @matches; + my %matches = map {($_->{id} => 1)} list(@ARGV); print $LOCALE->encode($_), $delim foreach sort keys %matches; } @@ -1134,16 +1173,18 @@ elsif ($COMMAND eq 'reencrypt') { my @filenames; foreach my $filename (@matches) { $filename = $LOCALE->decode($filename); - myprintf "Reencrypting C<%s>", $filename; + my $id = file2Identity($filename); + myprintf "Reencrypting C<%s>", $id; $filename =~ /\A(\/\p{Print}+)\z/ or error "Insecure C<%s>", $filename; $filename = $1; # untaint $filename copyIdentityFile $filename, $filename; - push @filenames, $filename; + push @filenames, { filename => $filename, id => $id }; } - commit 'Reencryption.', @filenames; + commit( "Reencryption.".$filenames[0]->{id}.(scalar @filenames > 1 ? ' ...' : '') + , map {$_->{filename}} @filenames ); } else { diff --git a/cli/icevault.1 b/cli/icevault.1 index b0308a5..d49601f 100644 --- a/cli/icevault.1 +++ b/cli/icevault.1 @@ -123,9 +123,12 @@ socket. If the flag \fB-f\fR is set, override the \fIidentity\fR file if it already exists (the default is to abort). .TP -.B ls\fR [\fB-0\fR, \fB--zero\fR] [\fIscheme\fR://[\fIhostname\fR/[\fIidentity\fR]]] +.B ls\fR [\fB-0\fR, \fB--zero\fR] [\fB-r\fR, \fB--recursive\fR] [\fIscheme\fR://[\fIhostname\fR/[\fIidentity\fR]] ...] List content of the given identity prefix. If the flag \fB-0\fR is set, -use NUL as line separator. +use NUL as line separator. If the flag \fB-r\fR is set and +\fIidentity\fR (resp. \fIhostname\fR/\fIidentity\fR) is omitted, list +recursively all identities under \fIscheme\fR://\fIhostname\fR/ (resp. +\fIscheme\fR://). .TP .B reencrypt\fR [\fIscheme\fR://[\fIhostname\fR/[\fIidentity\fR]] ...] -- cgit v1.2.3 From 59106c868accfb88d7b545ba88f9b2c8b0e07080 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Sun, 29 Mar 2015 00:25:29 +0100 Subject: Don't make myprintf auto append a newline. --- cli/icevault | 62 ++++++++++++++++++++++++++++++------------------------------ 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/cli/icevault b/cli/icevault index 3c31f1d..5ad8ff2 100755 --- a/cli/icevault +++ b/cli/icevault @@ -47,24 +47,23 @@ my $SOCKET; sub myprintf($@) { my $fh = ref $_[0] eq 'GLOB' ? shift : \*STDOUT; my $format = shift; - chomp $format; $LANGINFO =~ /^utf-?8$/aai ? $format =~ s/C<%s>/\N{U+2018}%s\N{U+2019}/g : $format =~ s/C<%s>/'%s'/g; $format =~ s/I<%s>/%s/g; $format =~ s/B<%s>/%s/g; die "Illegal markup '$1' in message:\n", $format if $format =~ /([A-Z])<%s>/; - printf $fh map {$LOCALE->encode($_)} ($format."\n", @_); + printf $fh map {$LOCALE->encode($_)} ($format, @_); } # error FORMAT, LIST sub error($@) { - myprintf \*STDERR, shift, @_; + myprintf \*STDERR, shift."\n", @_; exit 1; } # warning FORMAT, LIST sub warning($@) { - myprintf \*STDERR, shift, @_; + myprintf \*STDERR, shift."\n", @_; } sub mysystem(@) { @@ -203,7 +202,7 @@ sub connect($) { error "No Firefox profile found under C<%s>", $ffdir unless defined $profile; $sockname = "$$ffdir/$profile/$sockname"; } - myprintf \*STDERR, "Using socket C<%s>", $sockname if $CONFIG{debug}; + myprintf \*STDERR, "Using socket C<%s>\n", $sockname if $CONFIG{debug}; $sockname =~ /\A(\/\p{Print}+)\z/ or error "Insecure C<%s>", $sockname; # untaint $sockname $sockname = $1; @@ -231,7 +230,7 @@ sub connect($) { sub getResponse() { my $buf = $SOCKET->getline; chomp $buf; - myprintf \*STDERR, "S: %s", decode_utf8 $buf if $CONFIG{debug}; + myprintf \*STDERR, "S: %s\n", decode_utf8 $buf if $CONFIG{debug}; my ($code, $msg) = split / /, $buf, 2; # allow $msg to be decoded to a string $msg = JSON->new->utf8->allow_nonref->decode($msg) if defined $msg; @@ -255,7 +254,7 @@ sub getResponse() { # socket, flush the handle, then return the server response. sub sendCommand(@) { my $command = join(' ',@_); - myprintf \*STDERR, "C: %s", Encode::decode_utf8 $command if $CONFIG{debug}; + myprintf \*STDERR, "C: %s\n", Encode::decode_utf8 $command if $CONFIG{debug}; $command .= "\n"; for (my $offset = 0; $offset < length $command;) { $offset += $SOCKET->syswrite($command, length($command) - $offset, $offset); @@ -281,7 +280,7 @@ sub myglob(;$$$) { die "Invalid placeholder %$1" }ge; my $glob = "$store/$template"; - myprintf \*STDERR, "Using glob pattern C<%s>", $glob if $CONFIG{debug}; + myprintf \*STDERR, "Using glob pattern C<%s>\n", $glob if $CONFIG{debug}; return File::Glob::bsd_glob($glob); } @@ -351,7 +350,7 @@ sub complete($;$) { $2 eq 'i' ? quotemeta($1).$pi : die "Invalid placeholder %$1"}ge; my $pattern = qr/\A\Q$store\/\E$template\z/; - myprintf \*STDERR, "Using regexp C<%s>", "$pattern" if $CONFIG{debug}; + myprintf \*STDERR, "Using regexp C<%s>\n", "$pattern" if $CONFIG{debug}; my @matches; foreach my $filename (myglob($gs, $gh, $gi)) { @@ -392,7 +391,7 @@ sub fill($$@) { return if none {defined} @fill; # noop for (my $i = 0; $i <= $#fill; $i++) { - myprintf "Filling field C<%s>, value C<%s>", + myprintf "Filling field C<%s>, value C<%s>\n", $form->{fields}->[$i]->{name}, safeValue($form->{fields}->[$i], $fill[$i]) if defined $fill[$i]; @@ -431,7 +430,7 @@ sub file2Identity($) { $2 eq 'i' ? quotemeta($1).'(?[^\P{Print}\/]+)' : die "Invalid placeholder %$1"}ge; my $pattern = qr/\A\Q$store\/\E$template\z/; - myprintf \*STDERR, "Using regexp C<%s>", "$pattern" if $CONFIG{debug}; + myprintf \*STDERR, "Using regexp C<%s>\n", "$pattern" if $CONFIG{debug}; $filename =~ $pattern or die; return "$+{s}://$+{h}/$+{i}"; } @@ -443,7 +442,7 @@ sub file2Identity($) { open my $NULL, '<', '/dev/null'; sub loadIdentityFile($;$) { my ($filename, $fh) = @_; - myprintf \*STDERR, "Decrypting identity file C<%s>", $filename if $CONFIG{debug}; + myprintf \*STDERR, "Decrypting identity file C<%s>\n", $filename if $CONFIG{debug}; require 'IPC/Open2.pm'; my $pid = IPC::Open2::open2( (defined wantarray ? $fh : ">&".$fh->fileno) @@ -469,7 +468,7 @@ sub loadIdentityFile($;$) { # to gpg(1), and closed afterwards. sub saveIdentityFile($$) { my ($form, $filename) = @_; - myprintf \*STDERR, "Saving identity file C<%s>", $filename if $CONFIG{debug}; + myprintf \*STDERR, "Saving identity file C<%s>\n", $filename if $CONFIG{debug}; require 'File/Copy.pm'; require 'File/Path.pm'; @@ -589,7 +588,7 @@ sub pwgen(@) { (defined $x and $x > 0 and $x < $l) ? $x : $l } @fields); $pwgen =~ s/%d/$l/g; myprintf "Generating $l-char long random value for field".($#fields > 0 ? '(s) ' : ' ') - .join(',', map {'C<%s>'} @fields), + .join(',', map {'C<%s>'} @fields)."\n", map {$_->{name}} @fields; my @pw = `$pwgen`; @@ -611,12 +610,13 @@ sub prompt($;$) { } # Prompt a Yes/No question. -sub promptYN($;$) { +sub promptYN($;$@) { my $prompt = shift; my $default = shift // 0; + my @args = @_; while (1) { - print $LOCALE->encode($prompt), " [", ($default ? 'Y/n' : 'y/N'), "] "; + myprintf "$prompt [".($default ? 'Y/n' : 'y/N')."] ", @args; my $r = ; die "\n" unless defined $r; chomp $r; @@ -627,7 +627,7 @@ sub promptYN($;$) { elsif (lc $r eq 'n' or lc $r eq 'no' or ($r eq '' and !$default)) { return 0 } - myprintf \*STDERR, "Answer: C<%s> or C<%s>", 'y', 'n'; + myprintf \*STDERR, "Answer: C<%s> or C<%s>\n", 'y', 'n'; } } @@ -730,7 +730,7 @@ if ($ARGV[0] eq '--help' or $ARGV[0] eq '-?') { my $usage = shift @USAGE; print " or: $NAME $cmd $usage\n"; } - myprintf "Try C<%s> or consult the manpage for more information.", "$NAME COMMAND --help"; + myprintf "Try C<%s> or consult the manpage for more information.\n", "$NAME COMMAND --help"; exit 0; } @@ -753,7 +753,7 @@ sub usage(@) { printf "Consult the manpage for more information.\n"; exit 0; } else { - myprintf "Try C<%s> or consult the manpage for more information.", "$NAME $COMMAND --help"; + myprintf "Try C<%s> or consult the manpage for more information.\n", "$NAME $COMMAND --help"; exit 1; } } @@ -795,7 +795,7 @@ elsif ($COMMAND eq 'insert') { usage() unless $#ARGV < 1; my $uri = &connect($CONFIG{socket}); - myprintf "Importing HTML form from URI C<%s>", $uri; + myprintf "Importing HTML form from URI C<%s>\n", $uri; my @forms = getForms(); my $formIdx = findForm undef, @forms; @@ -829,10 +829,10 @@ elsif ($COMMAND eq 'insert') { # ask for confirmation my $r = prompt 'Identity name?', $id; if ($r !~ /\A[^\P{Print}\/]+\z/) { - myprintf \*STDERR, "Invalid identity: C<%s>", $r; + myprintf \*STDERR, "Invalid identity: C<%s>\n", $r; } elsif (-e identity2File "$uri/$r" and !$CONFIG{force}) { - myprintf \*STDERR, "Identity C<%s> already exists", "$uri/$r"; + myprintf \*STDERR, "Identity C<%s> already exists\n", "$uri/$r"; } else { $id = $r; @@ -892,7 +892,7 @@ elsif ($COMMAND eq 'insert') { sendCommand 'QUIT'; undef @{$form->{fields}}[@dontsave] if @dontsave; # remove the field we don't want to save - myprintf "Saving identity C<%s>", "$uri/$id"; + myprintf "Saving identity C<%s>\n", "$uri/$id"; saveIdentityFile $form, $filename; commit "Add new identity $uri/$id", $filename; } @@ -941,7 +941,7 @@ elsif ($COMMAND eq 'fill') { delete $fields{$pass->{name}}; if ($mypass->{name} ne $pass->{name}) { # use the new name - myprintf "Renaming field C<%s> as C<%s>", + myprintf "Renaming field C<%s> as C<%s>\n", $mypass->{name}, $pass->{name}; $mypass->{name} = $pass->{name}; $changed = 1; @@ -951,7 +951,7 @@ elsif ($COMMAND eq 'fill') { $fill[$passIdx[0]] = $mypass->{value}; } elsif ($mypass->{value} ne $pass->{value}) { # update the password - myprintf "Updating field C<%s> to C<%s> (former value: C<%s>)", + myprintf "Updating field C<%s> to C<%s> (former value: C<%s>)\n", $mypass->{name}, safeValue($pass), safeValue($mypass); $mypass->{value} = $pass->{value}; $changed = 1; @@ -1003,7 +1003,7 @@ elsif ($COMMAND eq 'fill') { } elsif ($myform->{fields}->[$myidx]->{value} ne $form->{fields}->[$idx]->{value}) { # update the known value with that found in the page - myprintf "Updating field C<%s> to C<%s> (former value: C<%s>)", + myprintf "Updating field C<%s> to C<%s> (former value: C<%s>)\n", $myform->{fields}->[$myidx]->{name}, safeValue($form->{fields}->[$idx]), safeValue($myform->{fields}->[$myidx]); @@ -1024,7 +1024,7 @@ elsif ($COMMAND eq 'fill') { @fieldIdx = @fieldIdx[(0 .. $i)] if $i >= 0; next unless @fieldIdx and $i >= 0; myprintf "Adding field C<%s>, value".($#fieldIdx > 0 ? '(s) ' : ' '). - join(',', map {'C<%s>'} @fieldIdx), + join(',', map {'C<%s>'} @fieldIdx)."\n", $name, map {safeValue $form->{fields}->[$_]} @fieldIdx; push @{$myform->{fields}}, @{$form->{fields}}[@fieldIdx]; $changed = 1; @@ -1033,7 +1033,7 @@ elsif ($COMMAND eq 'fill') { my @fieldIdx = grep defined, @{$myfields{$name}}; next unless @fieldIdx; myprintf "Deleting field C<%s>, value".($#fieldIdx > 0 ? '(s) ' : ' '). - join(',', map {'C<%s>'} @fieldIdx), + join(',', map {'C<%s>'} @fieldIdx)."\n", $name, map {safeValue $myform->{fields}->[$_]} @fieldIdx; undef $myform->{fields}->[$_] foreach @fieldIdx; $changed = 1; @@ -1105,7 +1105,7 @@ elsif ($COMMAND eq 'edit') { if ($h eq $h2) { print "No modification made\n"; } elsif (defined $fh2) { - myprintf "Saving user changes for identity C<%s>", $id; + myprintf "Saving user changes for identity C<%s>\n", $id; saveIdentityFile($fh2, $filename); # use the FH we opened before unlinking commit "Save manual (using $EDITOR) changes for $id", $filename; } else { @@ -1154,7 +1154,7 @@ elsif ($COMMAND eq 'git') { unless (-d $CONFIG{store}) { require 'File/Path.pm'; File::Path::make_path($CONFIG{store}); - myprintf "Created directory C<%s>", $CONFIG{store}; + myprintf "Created directory C<%s>\n", $CONFIG{store}; } for (my $i = 0; $i <= $#ARGV; $i++) { @@ -1174,7 +1174,7 @@ elsif ($COMMAND eq 'reencrypt') { foreach my $filename (@matches) { $filename = $LOCALE->decode($filename); my $id = file2Identity($filename); - myprintf "Reencrypting C<%s>", $id; + myprintf "Reencrypting C<%s>\n", $id; $filename =~ /\A(\/\p{Print}+)\z/ or error "Insecure C<%s>", $filename; $filename = $1; # untaint $filename -- cgit v1.2.3 From d86d75224bf26aacabb93cd5496e15e03d87753e Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Sun, 29 Mar 2015 00:29:52 +0100 Subject: Add 'cp' and 'mv' commands. --- cli/icevault | 29 +++++++++++++++++++++++++++++ cli/icevault.1 | 14 ++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/cli/icevault b/cli/icevault index 5ad8ff2..fdc1b74 100755 --- a/cli/icevault +++ b/cli/icevault @@ -713,11 +713,13 @@ unless (@ARGV) { my @USAGE = ( fill => "[-f, --force] [-p, --show-passwords] [-s, --socket=PATH] scheme://hostname/identity", clip => "scheme://hostname/identity", + cp => "[-f, --force] scheme://hostname/identity1 scheme://hostname/identity2", dump => "[-p, --show-passwords] scheme://hostname/identity", edit => "scheme://hostname/identity", git => "GIT-COMMAND [GIT-ARG ...]", insert => "[-f, --force] [-s, --socket=PATH] [identity]", ls => "[-0, --zero] [-r, --recursive] [scheme://[hostname/[identity]] ...]", + mv => "[-f, --force] scheme://hostname/identity1 scheme://hostname/identity2", reencrypt => "[scheme://[hostname/[identity]] ...]", ); @@ -1187,6 +1189,33 @@ elsif ($COMMAND eq 'reencrypt') { , map {$_->{filename}} @filenames ); } +elsif ($COMMAND eq 'cp' or $COMMAND eq 'mv') { + getopts('force|f' => "-f, --force\tOverwrite preexisting destination"); + usage() if $#ARGV != 1; + my $source = shift; + my $target = shift; + + my $sourceFilename = identity2File($source); + $sourceFilename =~ /\A(\/\p{Print}+)\z/ or error "Insecure C<%s>", $sourceFilename; + $sourceFilename = $1; # untaint $sourceFilename + + my $targetFilename = identity2File($target); + $targetFilename =~ /\A(\/\p{Print}+)\z/ or error "Insecure C<%s>", $targetFilename; + $targetFilename = $1; # untaint $target + + error "Source and destination are the same identity C<%s>", $source if $source eq $target; + error "No such identity C<%s>", $source unless -f $sourceFilename; + exit 1 if -f $targetFilename and !$CONFIG{force} and !promptYN "Overwrite C<%s>?", 0, $target; + + copyIdentityFile $sourceFilename, $targetFilename; + my @filenames = $targetFilename; + if ($COMMAND eq 'mv') { + unlink $sourceFilename or error "Can't unlink C<%s>: %s", $sourceFilename, $!; + push @filenames, $sourceFilename; + } + commit(($COMMAND eq 'cp' ? 'copy' : 'move')." $source to $target", @filenames); +} + else { print STDERR "Usage: $NAME [COMMAND] [OPTION ...] [ARG ...]\n"; error "Unknown command C<%s>. Try C<%s> for more information.", $COMMAND, "$NAME --help"; diff --git a/cli/icevault.1 b/cli/icevault.1 index d49601f..2e40af3 100644 --- a/cli/icevault.1 +++ b/cli/icevault.1 @@ -63,6 +63,13 @@ Passwords are redacted unless the flag \fB-p\fR is set. Decrypt the \fIidentity\fR file and copy its first password to the clipboard using \fIxclip\fR(1), with a maximum number of pastes of 1. +.TP +.B cp\fR [\fB-f\fR, \fB--force\fR] \fIscheme\fR://\fIhostname\fR/\fIidentity1\fR \fIscheme\fR://\fIhostname\fR/\fIidentity2\fR +Copy \fIscheme\fR://\fIhostname\fR/\fIidentity1\fR to +\fIscheme\fR://\fIhostname\fR/\fIidentity2\fR. The destination is +reencrypted on the fly. If \fB-f\fR is set, don't ask before overriding +an existing destination identity. + .TP .B dump\fR [\fB-p\fR, \fB--show-passwords\fR] \fIscheme\fR://\fIhostname\fR/\fIidentity\fR Decrypt the \fIidentity\fR file and dump its content on the standard @@ -130,6 +137,13 @@ use NUL as line separator. If the flag \fB-r\fR is set and recursively all identities under \fIscheme\fR://\fIhostname\fR/ (resp. \fIscheme\fR://). +.TP +.B mv\fR [\fB-f\fR, \fB--force\fR] \fIscheme\fR://\fIhostname\fR/\fIidentity1\fR \fIscheme\fR://\fIhostname\fR/\fIidentity2\fR +Rename \fIscheme\fR://\fIhostname\fR/\fIidentity1\fR as +\fIscheme\fR://\fIhostname\fR/\fIidentity2\fR. The destination is +reencrypted on the fly. If \fB-f\fR is set, don't ask before overriding +an existing destination identity. + .TP .B reencrypt\fR [\fIscheme\fR://[\fIhostname\fR/[\fIidentity\fR]] ...] Reencrypt each given identity prefix(es) with the \fIkeyid\fR(s) found in -- cgit v1.2.3 From cb159c7f64cc64fafffd9dcc7c605bb12497fd02 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Sun, 29 Mar 2015 01:11:30 +0100 Subject: Add a 'rm' command. --- cli/icevault | 27 +++++++++++++++++++++++++++ cli/icevault.1 | 6 ++++++ 2 files changed, 33 insertions(+) diff --git a/cli/icevault b/cli/icevault index fdc1b74..be7902f 100755 --- a/cli/icevault +++ b/cli/icevault @@ -721,6 +721,7 @@ my @USAGE = ( ls => "[-0, --zero] [-r, --recursive] [scheme://[hostname/[identity]] ...]", mv => "[-f, --force] scheme://hostname/identity1 scheme://hostname/identity2", reencrypt => "[scheme://[hostname/[identity]] ...]", + rm => "[-f, --force] [-r, --recursive] scheme://[hostname/[identity]] ...", ); if ($ARGV[0] eq '--help' or $ARGV[0] eq '-?') { @@ -1216,6 +1217,32 @@ elsif ($COMMAND eq 'cp' or $COMMAND eq 'mv') { commit(($COMMAND eq 'cp' ? 'copy' : 'move')." $source to $target", @filenames); } +elsif ($COMMAND eq 'rm') { + getopts( 'force|f' => "-f, --force \tNever prompt" + , 'recursive|r' => "-r, --recursive\tList identities recursively" + ); + + my @matches = list(@ARGV); + my @deleted; + foreach my $m (@matches) { + error "Use C<%s> for recursive deletion", '-r' unless + $m->{id} =~ /\A[A-Za-z0-9-]+:\/\/[^\P{Graph}:\/]+(?::\d+)?\/[^\P{Print}\/]+\z/; + + if ($CONFIG{force} or promptYN "Really delete C<%s>?", 0, $m->{id}) { + unlink $m->{filename} or warning "Can't unlink C<%s>: %s", $m->{filename}, $!; + push @deleted, $m; + } + } + exit 1 unless @deleted; # nothing to do + commit( "Remove ".$deleted[0]->{id}.(scalar @deleted > 1 ? ' ...' : ''), map {$_->{filename}} @deleted ); + + foreach (@deleted) { + # try to delete empty parent directories + my $filename = $_->{filename} =~ s/\/[^\/]+$//r; + rmdir $filename and rmdir $filename =~ s/\/[^\/]+$//r; + } +} + else { print STDERR "Usage: $NAME [COMMAND] [OPTION ...] [ARG ...]\n"; error "Unknown command C<%s>. Try C<%s> for more information.", $COMMAND, "$NAME --help"; diff --git a/cli/icevault.1 b/cli/icevault.1 index 2e40af3..299e591 100644 --- a/cli/icevault.1 +++ b/cli/icevault.1 @@ -152,6 +152,12 @@ reencrypt the entire store. If \fIidentity\fR (resp. \fIidentity\fR/\fIhostname\fR) is omitted, reencrypt all identities found under \fIscheme\fR://\fIhostname\fR/ (resp. \fIscheme\fR://). +.TP +.B rm\fR [\fB-f\fR, \fB--force\fR] [\fB-r\fR, \fB--recursive\fR] [\fIscheme\fR://[\fIhostname\fR/[\fIidentity\fR]] ...] +Delete the given identity prefix(es). Croak if \fIidentity\fR is +omitted, unless \fB-r\fR is set. If \fB-f\fR is set, don't prompt before +each deletion. + .SH GLOBAL OPTIONS .TP -- cgit v1.2.3 From d5c7b0189ae05b99a2ed496df5445089cbf562e8 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Sun, 29 Mar 2015 05:43:42 +0200 Subject: Fix bash completion. --- cli/bash-completion.sh | 74 +++++++++++++++++++++++++++++++---------- cli/icevault | 89 ++++++++++++-------------------------------------- 2 files changed, 77 insertions(+), 86 deletions(-) diff --git a/cli/bash-completion.sh b/cli/bash-completion.sh index bbef124..e1c6424 100644 --- a/cli/bash-completion.sh +++ b/cli/bash-completion.sh @@ -20,15 +20,53 @@ _icevault() { COMPREPLY=() declare -a files - local opts="--debug -h --help -p --show-passwords -s --socket= --version" - local args="fill insert dump clip edit ls" - local OPTIND IFS - # find out if our $cur is an optional argument or a command - for (( OPTIND = 1; OPTIND < cword; OPTIND++ )); do + local cmd firstopt=2 opts nargs + if [ $cword -eq 1 -a -z "$cur" ] || [[ "${words[1]}" =~ :// ]] || [[ "${words[1]}" =~ ^- ]]; then + firstopt=1 + cmd=fill # default command + elif [ $cword -gt 1 ]; then + cmd="${words[1]}" + fi + + local commands='fill clip cp dump edit git insert ls mv reencrypt rm' + case "$cmd" in + fill) opts='-f --force -p --show-passwords -s --socket='; nargs=1;; + clip) opts=; nargs=1 ;; + cp) opts='-f --force'; nargs=2;; + dump) opts='-p --show-passwords'; nargs=1;; + edit) opts=; nargs=1 ;; + git) opts= ;; + insert) opts='-f --force -s --socket='; nargs=0;; + ls) opts='-0 --zero -r --recursive'; nargs=-1;; + mv) opts='-f --force'; nargs=2;; + reencrypt) opts=; nargs=-1 ;; + rm) opts='-f --force -r --recursive'; nargs=-1;; + esac + opts="${opts:+$opts }--debug -? --help --version" + + if [ "$cmd" = git ]; then + # use bash completion for git by ignoring the first word ('icevault') + [ -f /usr/share/bash-completion/completions/git ] && . /usr/share/bash-completion/completions/git || return + COMP_CWORD=$(( $COMP_CWORD - 1 )) + COMP_WORDS=( "${COMP_WORDS[@]:1}" ) + local GIT_DIR GIT_WORK_TREE # fetch store and git-dir from the config-file + GIT_WORK_TREE=$(sed -n '/^store\s*=\s*/{s///p;q}' "${XDG_CONFIG_HOME:-$HOME/.config}/icevault") + GIT_WORK_TREE=$( echo "${GIT_WORK_TREE:-icevault}" | sed -re "s@^~/@$HOME@; t; s@^/@/@; t; s@^@${XDG_DATA_HOME:-$HOME/.local/share}/@" ) + GIT_DIR=$(sed -n '/^git-dir\s*=\s*/{s///p;q}' "${XDG_CONFIG_HOME:-$HOME/.config}/icevault") + GIT_DIR=$( echo "${GIT_DIR:-.git}" | sed -re "s@^~/@$HOME@; t; s@^/@/@; t; s@^@$GIT_WORK_TREE/@" ) + export GIT_DIR GIT_WORK_TREE + _git + return + fi + + local OPTIND socket + # find out if our $cur is an optional argument or not + for (( OPTIND = firstopt; OPTIND < cword; OPTIND++ )); do case "${words[OPTIND]}" in - -s) (( OPTIND++ ));; + -s) (( OPTIND++ )); [ $OPTIND -lt $cword ] && socket="${words[OPTIND]}";; --) break;; + --socket=*) [ $OPTIND -lt $cword ] && socket="${words[OPTIND]#--socket=}";; -*) ;; *) break;; esac @@ -45,20 +83,19 @@ _icevault() { [ -d "$p" -o -S "$p" ] && COMPREPLY+=( "$p" ) done return + elif [[ $OPTIND -eq $cword && "$cur" =~ ^- ]]; then + COMPREPLY+=( $(compgen -W "$opts" -- "$cur") ) + [ "${#COMPREPLY[@]}" -eq 1 -a "${COMPREPLY[0]}" = '--socket=' ] && compopt -o nospace + return fi - # complete options and commands - if [ $OPTIND -eq $cword -a "$cur" ]; then - COMPREPLY+=( $(compgen -W "$opts $args" -- "$cur") ) - [ "${#COMPREPLY[@]}" -eq 1 -a "${COMPREPLY[0]}" = --socket= ] && compopt -o nospace - fi - [ $(($OPTIND + 1)) -lt $cword ] && return - local trim= - if [ -z "$cur" -a $(($OPTIND + 1)) -eq $cword -a "${words[OPTIND]}" = insert ]; then + if [ "$cmd" = fill -a \( $OPTIND -eq $cword -o $(( $OPTIND + 1)) -eq $cword \) -a -z "$cur" ]; then + cur="$(icevault _geturi ${socket:+--socket="$socket"})"/ # get URI from webpage + elif [ -z "$cmd" -a "$cword" -eq 1 -a "$cur" ]; then + # autocomplete command + COMPREPLY+=( $(compgen -W "$commands" -- "$cur") ) return - elif [ -z "$cur" -a $OPTIND -eq $cword ] || [ -z "$cur" -a $(($OPTIND + 1)) -eq $cword -a "${words[OPTIND]}" = fill ]; then - cur="$(icevault _geturi)"/ # get URI from webpage else cur=$(dequote "$cur") # trim words with : or = in $COMP_WORDBREAKS; see __ltrim_colon_completions @@ -72,13 +109,14 @@ _icevault() { fi local uri - if [[ $OPTIND -eq $cword || ! "${words[OPTIND]}" =~ :// ]]; then + # autocomplete with identities as long as we don't exeed $nargs + if [ $nargs -lt 0 -o $(($cword - $OPTIND + 1)) -le $nargs ]; then compopt -o filenames -o noquote while read -r -d $'\0' uri; do # quote manually (so we don't quote the : and =) uri=$( echo "${uri#$trim}" | sed "s/[][\\{}*?~<>;'\"|&()\!$\` \t]/\\\\&/g" ) COMPREPLY+=( "$uri" ) - done < <(icevault -0 _complete "$cur") + done < <(icevault _complete -0 "$cur" 2>/dev/null) [ "${#COMPREPLY[@]}" -eq 1 -a "${COMPREPLY[0]: -1:1}" = / ] && compopt -o nospace return 0 fi diff --git a/cli/icevault b/cli/icevault index be7902f..1f662bf 100755 --- a/cli/icevault +++ b/cli/icevault @@ -308,73 +308,6 @@ sub matches($) { return @matches; } -# Get all identities with the given $prefix. If there are multiple -# matches and $all is false, limit the output to one depth more than the -# longuest common prefix. -sub complete($;$) { - my $prefix = shift // ''; - my $all = shift; - - my ($s, $h, $i); # extract URI components from the prefix - if ($prefix =~ /\A([A-Za-z0-9-]+):\/\/([^\P{Graph}:\/]+(?::\d+)?)\/([^\P{Print}\/]*)\z/) { - ($s, $h, $i) = ($1, $2, $3); - } elsif ($prefix =~ /\A([A-Za-z0-9-]+):\/\/([^\P{Graph}\/]*)\z/) { - ($s, $h, $i) = ($1, $2, undef); - } elsif ($prefix =~ /\A([A-Za-z0-9-]*)(:\/?)?\z/) { - ($s, $h, $i) = ($1, (defined $2 ? '' : undef), undef); - } else { - exit; - } - - # construct a glob pattern with these URI components - my ($gs, $gh, $gi) = ($s, $h, $i); - s/([\\\[\]\{\}\*\?\~])/\\$1/g foreach grep defined, ($gs, $gh, $gi); # escape meta chars - - # add trailing wildcards - $gs .= '*' if defined $gs and !defined $gh; - $gh .= '*' if defined $gh and !defined $gi; - $gi .= '*' if defined $gi; - - # construct regexp to extract the URI compontents of the matching URIs - my ($ps, $ph, $pi) = ($s, $h, $i); - $ps = defined $h ? qr/(?\Q$s\E)/ : (defined $s and $s ne '') ? qr/(?\Q$s\E[A-Za-z0-9-]*)/ : qr/(?[A-Za-z0-9-]+)/; - $ph = defined $i ? qr/(?\Q$h\E)/ : (defined $h and $h ne '') ? qr/(?\Q$h\E[^\P{Graph}\/]*)/ : qr/(?[^\P{Graph}\/]+)/; - $pi = (defined $i and $i ne '') ? qr/(?\Q$i\E[^\P{Print}\/]*)/ : qr/(?[^\P{Print}\/]+)/; - - my $store = $LOCALE->encode($CONFIG{store}); - my $template = $LOCALE->encode($CONFIG{template}); - $template =~ s/(\%.)([^\%]*)\z/$1.quotemeta($2)/e; - $template =~ s{(.*?)\%(.)}{$2 eq '%' ? '%' : - $2 eq 's' ? quotemeta($1).$ps : - $2 eq 'h' ? quotemeta($1).$ph : - $2 eq 'i' ? quotemeta($1).$pi : - die "Invalid placeholder %$1"}ge; - my $pattern = qr/\A\Q$store\/\E$template\z/; - myprintf \*STDERR, "Using regexp C<%s>\n", "$pattern" if $CONFIG{debug}; - - my @matches; - foreach my $filename (myglob($gs, $gh, $gi)) { - next unless -f $filename; - $LOCALE->decode($filename) =~ $pattern or die "$filename doesn't match $pattern"; - push @matches, "$+{s}://$+{h}/$+{i}"; - } - return @matches if $all or $#matches < 1; - - # find the longest common prefix to determine the depth level of completion - $matches[0] =~ /\A([A-Za-z0-9-]+):\/\/([^\P{Graph}:\/]+(?::\d+)?)\// or die; - ($s, $h) = ($1, $2); - - if (all { /\A\Q$s\E:\/\/\Q$h\E\// } @matches) { # common host: list all ids - } elsif (all { /\A\Q$s\E:\/\// } @matches) { # common scheme: list only hosts - s#/[^\P{Print}\/]+\z#/# foreach @matches; - } else { # no common scheme: list only schemes - s#://[^\P{Graph}\/]+/[^\P{Print}\/]+\z#://# foreach @matches; - } - - my %matches = map {( $_ => 1 )} @matches; - return sort keys %matches; -} - # Redact passwords, unless $CONFIG{'show-passwords'} is set. sub safeValue($;$) { my $field = shift; @@ -776,15 +709,35 @@ sub getopts(%) { if ($COMMAND eq '_complete') { # used internaly for auto-completion GetOptions(\%CONFIG, qw/zero|0/) or die; + loadConfig(); die unless $#ARGV == 0; + + $CONFIG{recursive} = 1; + my @matches = grep defined, + map { $_->{id} =~ /\A\Q$ARGV[0]\E/ ? $_->{id} : undef} + list( $ARGV[0] =~ /\A(.*\/)/ ? $1 : $ARGV[0] =~ /\A([A-Za-z0-9-]+):\z/ ? "$1://" : () ); + + # find the longest common prefix to determine the depth level of completion + $matches[0] =~ /\A([A-Za-z0-9-]+):\/\/([^\P{Graph}:\/]+(?::\d+)?)\// or die; + my ($s, $h) = ($1, $2); + + if (all { /\A\Q$s\E:\/\/\Q$h\E\// } @matches) { # common host: list all ids + } elsif (all { /\A\Q$s\E:\/\// } @matches) { # common scheme: list only hosts + s#/[^\P{Print}\/]+\z#/# foreach @matches; + } else { # no common scheme: list only schemes + s#://[^\P{Graph}\/]+/[^\P{Print}\/]+\z#://# foreach @matches; + } + my %matches = map {($_ => 1)} grep defined, @matches; + my $delim = $CONFIG{zero} ? "\0" : "\n"; - print $LOCALE->encode($_), $delim foreach complete(shift @ARGV); + print $LOCALE->encode($_), $delim foreach sort keys %matches; exit; } elsif ($COMMAND eq '_geturi') { # used internaly for auto-completion GetOptions(\%CONFIG, qw/socket|s=s/) or die; + loadConfig(); die if @ARGV; print $LOCALE->encode( &connect($CONFIG{socket}) ), "\n"; sendCommand 'QUIT'; -- cgit v1.2.3 From 5827d41f6f7ca96eda7ffa91a375217bda0bb4dd Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Sun, 29 Mar 2015 14:52:36 +0200 Subject: Use -h not -? as short hand for --help --- cli/bash-completion.sh | 2 +- cli/icevault | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cli/bash-completion.sh b/cli/bash-completion.sh index e1c6424..e1ae871 100644 --- a/cli/bash-completion.sh +++ b/cli/bash-completion.sh @@ -43,7 +43,7 @@ _icevault() { reencrypt) opts=; nargs=-1 ;; rm) opts='-f --force -r --recursive'; nargs=-1;; esac - opts="${opts:+$opts }--debug -? --help --version" + opts="${opts:+$opts }--debug -h --help --version" if [ "$cmd" = git ]; then # use bash completion for git by ignoring the first word ('icevault') diff --git a/cli/icevault b/cli/icevault index 1f662bf..d9a6792 100755 --- a/cli/icevault +++ b/cli/icevault @@ -657,7 +657,7 @@ my @USAGE = ( rm => "[-f, --force] [-r, --recursive] scheme://[hostname/[identity]] ...", ); -if ($ARGV[0] eq '--help' or $ARGV[0] eq '-?') { +if ($ARGV[0] eq '--help' or $ARGV[0] eq '-h') { my $default_cmd = shift @USAGE; my $default_usage = shift @USAGE; print "Usage: $NAME [$default_cmd] $default_usage\n"; @@ -698,7 +698,7 @@ sub usage(@) { sub getopts(%) { my @opts = @_; my %opts = @opts; - usage(@opts) unless GetOptions(\%CONFIG, qw/debug help|?/, keys %opts) and !$CONFIG{help}; + usage(@opts) unless GetOptions(\%CONFIG, qw/debug help|h/, keys %opts) and !$CONFIG{help}; loadConfig(); } @@ -1198,5 +1198,5 @@ elsif ($COMMAND eq 'rm') { else { print STDERR "Usage: $NAME [COMMAND] [OPTION ...] [ARG ...]\n"; - error "Unknown command C<%s>. Try C<%s> for more information.", $COMMAND, "$NAME --help"; + error "Unknown command C<%s>. Try C<%s> or consult the manpage for more information.", $COMMAND, "$NAME --help"; } -- cgit v1.2.3 From 1399dc5cf6e5bf1a88cab91c21b2f10ab4be754b Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Sun, 29 Mar 2015 21:15:11 +0200 Subject: Be consistent gpg2 vs. gpg. --- cli/icevault.1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/icevault.1 b/cli/icevault.1 index 299e591..bd58cdf 100644 --- a/cli/icevault.1 +++ b/cli/icevault.1 @@ -99,7 +99,7 @@ It is recommended to initialize the repository as follows: \fBicevault git\fR add .gitattributes \fBicevault git\fR commit \-m 'Add Git attributes for .gpg binary files.' \fBicevault git\fR config diff.gpg.binary true - \fBicevault git\fR config diff.gpg.textconv 'gpg2 \-o \- \-\-decrypt' + \fBicevault git\fR config diff.gpg.textconv 'gpg \-o \- \-\-decrypt' The textconv config option enable on-the-fly decryption prior to Git operations such as \fIdiff\fR or \fIgrep\fR, see \fIgitattributes\fR(5). -- cgit v1.2.3 From 3831bd402d5adc5c05b8817a623ca87f93c5335c Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Sun, 29 Mar 2015 21:15:54 +0200 Subject: Fix the 'insert' command. --- cli/icevault | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/cli/icevault b/cli/icevault index d9a6792..41fb5d8 100755 --- a/cli/icevault +++ b/cli/icevault @@ -459,10 +459,16 @@ sub getForms() { return @$forms; } -# Guess which form is to be filled: take the first form with a password -# field if there are any, otherwise the first non-empty form. If the -# first argument is defined, only consider forms with a matching base -# URI. +# Guess which form is to be filled: +# * If $myform is defined, consider only the forms with matching action / base +# URI; and if there is a single match take it +# * If one of the considered forms has a password field and a (possibly +# different) non-empty field, take the first one found. +# * Otherwise (if all forms with a password field are empty), and if +# there is a form with a password field, take the first one found. +# * Otherwise (if no form has a password field), and if there +# is a non-empty form, take the first one found. +# * Otherwise (if all forms are empty), take the first form in the list. sub findForm($@) { my $myform = shift; my @forms = @_; @@ -476,8 +482,11 @@ sub findForm($@) { return $formIdx[0] if $#formIdx == 0; # single match } - my $idx = firstIdx { defined $_ and any {$_->{type} eq 'password'} @{$_->{fields}} } @forms; - $idx //= firstIdx { defined $_ and any {$_->{value} ne ''} @{$_->{fields}} } @forms; + my @formIdx = grepIdx { defined $_ and any {$_->{type} eq 'password'} @{$_->{fields}} } @forms; + my $idx = first { any {$_->{value} ne ''} @{$forms[$_]->{fields}} } @formIdx; # first non-empty form with a password + $idx //= $formIdx[0]; # first form with a password + $idx //= firstIdx { defined $_ and any {$_->{value} ne ''} @{$_->{fields}} } @forms; # first non-empty form + $idx //= 0 if @forms; # first form error 'Dunno which form to '. (defined $myform ? 'fill' : 'import') unless defined $idx; return $idx; } @@ -777,7 +786,7 @@ elsif ($COMMAND eq 'insert') { my $pwIdx = firstIdx { $_->{type} eq 'password' } @{$form->{fields}}; my $idx = first { $form->{fields}->[$_]->{value} ne '' and ($form->{fields}->[$_]->{type} eq 'text' or $form->{fields}->[$_]->{type} eq 'email') - } ($pwIdx-1 .. 0) + } reverse (0 .. $pwIdx-1) if defined $pwIdx; $idx //= firstIdx { $_->{value} ne '' and ($_->{type} eq 'text' or $_->{type} eq 'email') } @{$form->{fields}}; $id = $form->{fields}->[$idx]->{value} if defined $idx; @@ -804,8 +813,10 @@ elsif ($COMMAND eq 'insert') { my @dontsave; if ($#passIdx == 0) { # single password in the form if ($form->{fields}->[$passIdx[0]]->{value} eq '') { - warning "Warning! Empty password for field C<%s>", $form->{fields}->[$passIdx[0]]->{name}; - exit 1 unless promptYN "Continue?"; + exit 1 unless promptYN "Empty password for field C<%s>. Generate and continue?", 0, + $form->{fields}->[$passIdx[0]]->{name}; + pwgen $form->{fields}->[$passIdx[0]]; + $fill[$passIdx[0]] = $form->{fields}->[$passIdx[0]]->{value}; } } elsif ($#passIdx < 3) { # 2 or 3 passwords in the form -- cgit v1.2.3 From 1f9333be49708ead26a1dba4748775dfeff145e5 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Sun, 29 Mar 2015 21:16:30 +0200 Subject: Describe the form selection algorithm in a dedicated section. --- cli/icevault.1 | 36 ++++++++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/cli/icevault.1 b/cli/icevault.1 index bd58cdf..8aaa76c 100644 --- a/cli/icevault.1 +++ b/cli/icevault.1 @@ -45,10 +45,8 @@ If \fICOMMAND\fR is omitted, \fBfill\fR is assumed. If the scheme (resp. hostname) of the active tab of the active window is not \fIscheme\fR (resp. \fIhostname\fR) the program assumes a phishing attempt and aborts. Otherwise, the \fIidentity\fR file is decrypted and -used to fill a visible form on the browser. -Form selection is done by matching on the base URI; it fallbacks to the -first form containing a password; and further fallbacks to the first -form with a non-empty field. +used to fill a visible form on the browser. The HTML form selection +algorithm is described in the \fBHTML FORM SELECTION\fR section. Changes to the \fIidentity\fR are detected and can be saved on demand. If \fIidentity\fR has a single password whereas the webpage has 2 (resp. 3), a signup (resp. password changing) page is assumed, and a new @@ -115,10 +113,8 @@ Signing each commit can be achieved as follows, see \fIgit-config\fR(1): .TP .B insert\fR [\fB-f\fR, \fB--force\fR] [\fB-s\fR, \fB--socket=\fR\fIPATH\fR] [\fIidentity\fR] Create a new \fIscheme\fR://\fIhostname\fR/\fIidentity\fR URI available -for further commands. -Store the first visible form on the active tab of the active window which -contains a password (or the first visible form with a non-empty field if -no visible form has a password). If \fIidentity\fR is omitted, it +for further commands. The HTML form selection algorithm is described in +the \fBHTML FORM SELECTION\fR section. If \fIidentity\fR is omitted, it defaults to the value of the last textual value before the first password (or the first textual value if the selected form has no password). @@ -238,6 +234,30 @@ respectively expand to the \fIscheme\fR, \fIhostname\fR and \fIidentity\fR parts of the URI. (Default: "%s/%h/%i.gpg".) +.SH HTML FORM SELECTION +The HTML form selection for the \fBinsert\fR and \fBfill\fR commands is +performed in the following order: + +.nr step 1 1 +.IP \n[step]. 3 +Consider only the visible forms of the active tab of the active window. +.IP \n+[step]. +If there is a matching identity in the store, consider only the forms +with matching action / base URI; and if there is a single match take it +(\fBfill\fR only). +.IP \n+[step]. +If one of the considered forms has a password field and a (possibly +different) non-empty field, take the first one found. +.IP \n+[step]. +Otherwise (if all forms with a password field are empty), and if there +is a form with a password field, take the first one found. +.IP \n+[step]. +Otherwise (if no form has a password field), and if there +is a non-empty form, take the first one found. +form. +.IP \n+[step]. +Otherwise (if all forms are empty), take the first form in the list. + .SH AUTHOR Guilhem Moulin .SH SEE ALSO -- cgit v1.2.3 From c2e536c9ce1be626731bfc177a15ad5d6c45caaf Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Sun, 29 Mar 2015 21:20:53 +0200 Subject: s/insert/import/g --- cli/bash-completion.sh | 4 ++-- cli/icevault | 4 ++-- cli/icevault.1 | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cli/bash-completion.sh b/cli/bash-completion.sh index e1ae871..5ae1c63 100644 --- a/cli/bash-completion.sh +++ b/cli/bash-completion.sh @@ -29,7 +29,7 @@ _icevault() { cmd="${words[1]}" fi - local commands='fill clip cp dump edit git insert ls mv reencrypt rm' + local commands='fill clip cp dump edit git import ls mv reencrypt rm' case "$cmd" in fill) opts='-f --force -p --show-passwords -s --socket='; nargs=1;; clip) opts=; nargs=1 ;; @@ -37,7 +37,7 @@ _icevault() { dump) opts='-p --show-passwords'; nargs=1;; edit) opts=; nargs=1 ;; git) opts= ;; - insert) opts='-f --force -s --socket='; nargs=0;; + import) opts='-f --force -s --socket='; nargs=0;; ls) opts='-0 --zero -r --recursive'; nargs=-1;; mv) opts='-f --force'; nargs=2;; reencrypt) opts=; nargs=-1 ;; diff --git a/cli/icevault b/cli/icevault index 41fb5d8..e4c517a 100755 --- a/cli/icevault +++ b/cli/icevault @@ -659,7 +659,7 @@ my @USAGE = ( dump => "[-p, --show-passwords] scheme://hostname/identity", edit => "scheme://hostname/identity", git => "GIT-COMMAND [GIT-ARG ...]", - insert => "[-f, --force] [-s, --socket=PATH] [identity]", + import => "[-f, --force] [-s, --socket=PATH] [identity]", ls => "[-0, --zero] [-r, --recursive] [scheme://[hostname/[identity]] ...]", mv => "[-f, --force] scheme://hostname/identity1 scheme://hostname/identity2", reencrypt => "[scheme://[hostname/[identity]] ...]", @@ -753,7 +753,7 @@ elsif ($COMMAND eq '_geturi') { exit; } -elsif ($COMMAND eq 'insert') { +elsif ($COMMAND eq 'import') { getopts( 'force|f' => "-f, --force \tOverwrite preexisting identity" , 'socket|s=s' => "-s, --socket=PATH\tSpecifiy the path to the Icevault socket" ); diff --git a/cli/icevault.1 b/cli/icevault.1 index 8aaa76c..d1a2084 100644 --- a/cli/icevault.1 +++ b/cli/icevault.1 @@ -111,7 +111,7 @@ Signing each commit can be achieved as follows, see \fIgit-config\fR(1): \fBicevault git\fR config user.signingkey 0x39278DA8109E6244 .TP -.B insert\fR [\fB-f\fR, \fB--force\fR] [\fB-s\fR, \fB--socket=\fR\fIPATH\fR] [\fIidentity\fR] +.B import\fR [\fB-f\fR, \fB--force\fR] [\fB-s\fR, \fB--socket=\fR\fIPATH\fR] [\fIidentity\fR] Create a new \fIscheme\fR://\fIhostname\fR/\fIidentity\fR URI available for further commands. The HTML form selection algorithm is described in the \fBHTML FORM SELECTION\fR section. If \fIidentity\fR is omitted, it @@ -235,7 +235,7 @@ respectively expand to the \fIscheme\fR, \fIhostname\fR and (Default: "%s/%h/%i.gpg".) .SH HTML FORM SELECTION -The HTML form selection for the \fBinsert\fR and \fBfill\fR commands is +The HTML form selection for the \fBimport\fR and \fBfill\fR commands is performed in the following order: .nr step 1 1 -- cgit v1.2.3 From 3d18927234445d5355fb05914c203d6dafa3d656 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Mon, 30 Mar 2015 02:24:29 +0200 Subject: Document the configuration syntax. --- cli/icevault | 2 +- cli/icevault.1 | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/cli/icevault b/cli/icevault index e4c517a..8a313ca 100755 --- a/cli/icevault +++ b/cli/icevault @@ -129,7 +129,7 @@ sub loadConfig() { chomp; s/#.*//; # ignore comments next if /^\s*$/; # ignore empty and blank lines - /^([-\@a-zA-Z0-9.]+)(?:\s*=\s*)(\p{Print}+)/ or error "Can't parse config line: C<%s>", $_; + /^([-\@a-zA-Z0-9.]+)\s*=\s*(\p{Graph}\p{Print}*)/ or error "Can't parse config line: C<%s>", $_; $CONFIG{$1} //= $2; } close $CONFIG; diff --git a/cli/icevault.1 b/cli/icevault.1 index d1a2084..81c8322 100644 --- a/cli/icevault.1 +++ b/cli/icevault.1 @@ -173,8 +173,12 @@ Show the version number and exit. \fBicevault\fR reads it configuration from \fI$XDG_CONFIG_HOME/icevault\fR, or \fI~/.config/icevault\fR if -XDG_CONFIG_HOME is unset. -Empty lines and comments (starting with a "#" characters are ignored). +the XDG_CONFIG_HOME environment variable is unset. +Options given on the command line override those found in the +configuration file. +The syntax of the configuration file is a serie of +\fIOPTION\fR=\fIVALUE\fR lines; +everything after a "#" is considered a comment and ignored. Valid options are: .TP -- cgit v1.2.3 From 3b2e8ee11ecd3fa8604f0d503c6732b99f2d24ea Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Mon, 30 Mar 2015 02:25:17 +0200 Subject: Make it clearer that 'keyid' is required. --- cli/icevault | 2 +- cli/icevault.1 | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/cli/icevault b/cli/icevault index 8a313ca..9f58502 100755 --- a/cli/icevault +++ b/cli/icevault @@ -154,7 +154,7 @@ sub loadConfig() { $CONFIG{socket} //= 'socket'; $CONFIG{socket} =~ s#\A~/#$ENV{HOME}#; - error "Missing keyid in configuration file" unless defined $CONFIG{keyid}; + error "Missing mandatory option C<%s> in configuration file", 'keyid' unless defined $CONFIG{keyid}; $CONFIG{keyid} = [ map { /^((?:0x)?\p{AHex}{16}|\p{AHex}{40})$/ or error "C<%s> is not a 64-bits key ID or fingerprint", $_; $1 } diff --git a/cli/icevault.1 b/cli/icevault.1 index 81c8322..5052416 100644 --- a/cli/icevault.1 +++ b/cli/icevault.1 @@ -197,6 +197,7 @@ probably want to add the \fB--use-agent\fR option. (Default: "gpg".) A comma-separated list of OpenPGP key ID(s) used as encryption recipient(s). Each component must be given as 64-bits keyid or full fingerprint. +(Required.) .TP .I max-password-length -- cgit v1.2.3 From 821ff99e80961888fd05ea05ad66889708f77cb7 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Mon, 30 Mar 2015 02:25:43 +0200 Subject: manpage wibble --- cli/icevault.1 | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/cli/icevault.1 b/cli/icevault.1 index 5052416..46e80e5 100644 --- a/cli/icevault.1 +++ b/cli/icevault.1 @@ -184,13 +184,15 @@ Valid options are: .TP .I git-dir Path to the Git directory. Can be an absolute path or a path relative -to the working directory (specified with \fIstore\fR). +to the working directory (specified with the \fIstore\fR configuration +option). (Default: ".git") .TP .I gpg -The \fIgpg\fR(1) command to use. Note that users of GnuPG 1.4.x will -probably want to add the \fB--use-agent\fR option. (Default: "gpg".) +The \fIgpg\fR(1) command to use. Users of GnuPG 1.4.x will probably +want to add the \fB--use-agent\fR and possibly \fB--batch\fR options. +(Default: "gpg".) .TP .I keyid @@ -227,8 +229,8 @@ respectively. .TP .I store The working directory. Can be an absolute path or a path relative -to \fI$XDG_CONFIG_HOME\fR (or \fI~/.local/share\fR if XDG_CONFIG_HOME is -unset). +to the XDG_DATA_HOME environment variable (or \fI~/.local/share\fR if +XDG_DATA_HOME is unset). (Default: "icevault".) .TP @@ -259,7 +261,6 @@ is a form with a password field, take the first one found. .IP \n+[step]. Otherwise (if no form has a password field), and if there is a non-empty form, take the first one found. -form. .IP \n+[step]. Otherwise (if all forms are empty), take the first form in the list. -- cgit v1.2.3 From 5af89eb57e11b29dfd82d3a52618b0c0dad1b24a Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Mon, 30 Mar 2015 03:38:38 +0200 Subject: Replace " with \(rq / \(rq in the manpage. --- cli/icevault.1 | 89 ++++++++++++++++++++++++++++++++++------------------------ 1 file changed, 52 insertions(+), 37 deletions(-) diff --git a/cli/icevault.1 b/cli/icevault.1 index 46e80e5..b8c089b 100644 --- a/cli/icevault.1 +++ b/cli/icevault.1 @@ -8,6 +8,7 @@ IceVault \- IceVault client user interface .SH DESCRIPTION +.PP .B icevault\fR is an external password/login manager for Firefox. Its threat model is arguably more secure than the builtin manager's, as the browser is not granted direct access to the list of known HTML forms nor @@ -19,18 +20,18 @@ via a UNIX socket, which the browser creates upon startup; usual UNIX permissions can (and should) be used to restrict access to the socket. Further isolation can be achieved by using different UIDs for the browser and the \fBicevault\fR client. - +.PP Each form is stored in a separate file, encrypted separately with \fIgpg\fR(1); cleartext are never stored on disk. Form history can be kept track of by versioning the encrypted files to a Git repository as binary blobs. (Modification of the stored forms are then automatically committed to said repository.) File paths are of the form -".../\fIscheme\fR/\fIhostname\fR/\fIidentity\fR" where \fIidentity\fR is -an arbitrary user-chosen value (allowing multiple identities for a given -site); since the URI of the active tab can be retrieved from the socket -and since the URI of a stored form can be recovered from its file path, -phishing attacks are easily detected. - +\(lq.../\fIscheme\fR/\fIhostname\fR/\fIidentity\fR\(rq where +\fIidentity\fR is an arbitrary user-chosen value (allowing multiple +identities for a given site); since the URI of the active tab can be +retrieved from the socket and since the URI of a stored form can be +recovered from its file path, phishing attacks are easily detected. +.PP Like Firefox's builtin password manager, IceVault has some heuristics to detect signup and password changing pages. In these cases, and if the password fields are left blank, the (new) password is randomly chosen @@ -90,25 +91,39 @@ using the configuration value for \fIstore\fR and that for \fIgit-dir\fR as the Git working tree and Git repository, respectively. \fIstore\fR is automatically created if it is not an existing directory. +.RS It is recommended to initialize the repository as follows: - \fBicevault git\fR init - echo '*.gpg diff=gpg' >"${XDG_DATA_HOME:-$HOME/.local/share}/icevault/.gitattributes" - \fBicevault git\fR add .gitattributes - \fBicevault git\fR commit \-m 'Add Git attributes for .gpg binary files.' - \fBicevault git\fR config diff.gpg.binary true - \fBicevault git\fR config diff.gpg.textconv 'gpg \-o \- \-\-decrypt' +.nf +.RS +\fBicevault git\fR init +echo '*.gpg diff=gpg' >"${XDG_DATA_HOME:-$HOME/.local/share}/icevault/.gitattributes" +\fBicevault git\fR add .gitattributes +\fBicevault git\fR commit \-m 'Add Git attributes for .gpg binary files.' +\fBicevault git\fR config diff.gpg.binary true +\fBicevault git\fR config diff.gpg.textconv 'gpg \-o \- \-\-decrypt' +.RE +.fi The textconv config option enable on-the-fly decryption prior to Git operations such as \fIdiff\fR or \fIgrep\fR, see \fIgitattributes\fR(5). For instance, grep'ing through the cleartext becomes trivial: - \fBicevault git\fR grep \-\-textconv \fIpattern\fR +.nf +.RS +\fBicevault git\fR grep \-\-textconv \fIpattern\fR +.RE +.fi Signing each commit can be achieved as follows, see \fIgit-config\fR(1): - \fBicevault git\fR config commit.gpgsign true - \fBicevault git\fR config user.signingkey 0x39278DA8109E6244 +.nf +.RS +\fBicevault git\fR config commit.gpgsign true +\fBicevault git\fR config user.signingkey 0x39278DA8109E6244 +.RE +.fi +.RE .TP .B import\fR [\fB-f\fR, \fB--force\fR] [\fB-s\fR, \fB--socket=\fR\fIPATH\fR] [\fIidentity\fR] @@ -178,21 +193,21 @@ Options given on the command line override those found in the configuration file. The syntax of the configuration file is a serie of \fIOPTION\fR=\fIVALUE\fR lines; -everything after a "#" is considered a comment and ignored. -Valid options are: +everything after a \(lq#\(rq character is considered a comment and +ignored. Valid options are: .TP .I git-dir Path to the Git directory. Can be an absolute path or a path relative to the working directory (specified with the \fIstore\fR configuration option). -(Default: ".git") +(Default: \(lq.git\(rq) .TP .I gpg The \fIgpg\fR(1) command to use. Users of GnuPG 1.4.x will probably want to add the \fB--use-agent\fR and possibly \fB--batch\fR options. -(Default: "gpg".) +(Default: \(lqgpg\(rq.) .TP .I keyid @@ -203,43 +218,43 @@ fingerprint. .TP .I max-password-length -The maximum length for new passwords. (Default: "32".) +The maximum length for new passwords. (Default: \(lq32\(rq.) .TP .I pwgen -The command to use to generate new random passwords. May contain "%d", -which expands to the password's "maxLength" attribute (capped with the -\fImax-password-length\fR option). The command is expected to output to -the standard output, and may add a newline character afterwards, which -is not considered part of the password. -(Default: "pwgen \-s \-cyn %d".) +The command to use to generate new random passwords. May contain +\(lq%d\(rq, which expands to the password's \(lqmaxLength\(rq attribute +(capped with the \fImax-password-length\fR option). The command is +expected to output to the standard output, and may add a newline +character afterwards, which is not considered as part of the password. +(Default: \(lqpwgen \-s \-cyn %d\(rq.) .TP .I socket The path of the UNIX socket used to communicate with the browser. Can be an absolute path or a path relative to the default Firefox profile (or first profile found if there is no default profile) in the -"~/.mozilla/firefox" directory. +\(lq~/.mozilla/firefox\(rq directory. The socket path and permissions can be configured on the -Iceweasel/Firefox side with the "extensions.icevault.socketPath" and -"extensions.icevault.socketPerms" preferences in "about:config", -respectively. -(Default: "S.IceVault".) +Iceweasel/Firefox side with the \(lqextensions.icevault.socketPath\(rq +and \(lqextensions.icevault.socketPerms\(rq preferences in +\(lqabout:config\(rq, respectively. +(Default: \(lqS.IceVault\(rq.) .TP .I store The working directory. Can be an absolute path or a path relative to the XDG_DATA_HOME environment variable (or \fI~/.local/share\fR if XDG_DATA_HOME is unset). -(Default: "icevault".) +(Default: \(lqicevault\(rq.) .TP .I template The template mapping \fIscheme\fR://\fIhostname\fR/\fIidentity\fR URIs -to (encrypted) files on disk. Must contain "%s", "%h", and "%i", which -respectively expand to the \fIscheme\fR, \fIhostname\fR and -\fIidentity\fR parts of the URI. -(Default: "%s/%h/%i.gpg".) +to (encrypted) files on disk. Must contain \(lq%s\(rq, \(lq%h\(rq, and +\(lq%i\(rq, which respectively expand to the \fIscheme\fR, +\fIhostname\fR and \fIidentity\fR parts of the URI. +(Default: \(lq%s/%h/%i.gpg\(rq.) .SH HTML FORM SELECTION The HTML form selection for the \fBimport\fR and \fBfill\fR commands is -- cgit v1.2.3 From 08ff966676630a2446cdbb1bd493d14787b091be Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Mon, 30 Mar 2015 14:31:37 +0200 Subject: Add installation intructions. --- INSTALL | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 INSTALL diff --git a/INSTALL b/INSTALL new file mode 100644 index 0000000..090e834 --- /dev/null +++ b/INSTALL @@ -0,0 +1,21 @@ +To install the CLI and Iceweasel/Firefox addon, run (requires 'gzip' and +'zip'): + + make -C cli + sudo make -C cli install + + make -C xul-ext ../icevault@guilhem.org.xpi + cp icevault@guilhem.org.xpi ~/.mozilla/firefox/*.default/extensions + + +However users familiar with Debian packaging will probably prefer to +build their own package: + + git checkout debian + AUTO_DEBSIGN=no git buildpackage --git-force-create + VERSION=$(sed -nr '/^icevault \(([^)]+)\).*/ {s//\1/p;q}' debian/changelog) + sudo dpkg -i ../icevault_${VERSION}_all.deb ../xul-ext-icevault_${VERSION}_all.deb + +(Add the option '--git-upstream-tree=BRANCH' to the 'git buildpackage' +command to package the development version rather than the last tagged +release.) -- cgit v1.2.3 From 1703c96398f9fa202f00a01e46c45ffb6eb2fe25 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Mon, 30 Mar 2015 17:16:49 +0200 Subject: wibble --- cli/icevault.1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/icevault.1 b/cli/icevault.1 index b8c089b..7eb268c 100644 --- a/cli/icevault.1 +++ b/cli/icevault.1 @@ -99,9 +99,9 @@ It is recommended to initialize the repository as follows: \fBicevault git\fR init echo '*.gpg diff=gpg' >"${XDG_DATA_HOME:-$HOME/.local/share}/icevault/.gitattributes" \fBicevault git\fR add .gitattributes -\fBicevault git\fR commit \-m 'Add Git attributes for .gpg binary files.' +\fBicevault git\fR commit \-m 'Add an attribute for .gpg binary files.' \fBicevault git\fR config diff.gpg.binary true -\fBicevault git\fR config diff.gpg.textconv 'gpg \-o \- \-\-decrypt' +\fBicevault git\fR config diff.gpg.textconv 'gpg \-o \- \-\-decrypt \-\-' .RE .fi -- cgit v1.2.3 From e27989d46022f18d7937017a1f41f05c4378fe3a Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Mon, 30 Mar 2015 22:03:16 +0200 Subject: Prefer single quotes in the manpage. --- cli/icevault.1 | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/cli/icevault.1 b/cli/icevault.1 index 7eb268c..88c0902 100644 --- a/cli/icevault.1 +++ b/cli/icevault.1 @@ -193,7 +193,7 @@ Options given on the command line override those found in the configuration file. The syntax of the configuration file is a serie of \fIOPTION\fR=\fIVALUE\fR lines; -everything after a \(lq#\(rq character is considered a comment and +everything after a \(oq#\(cq character is considered a comment and ignored. Valid options are: .TP @@ -223,7 +223,7 @@ The maximum length for new passwords. (Default: \(lq32\(rq.) .TP .I pwgen The command to use to generate new random passwords. May contain -\(lq%d\(rq, which expands to the password's \(lqmaxLength\(rq attribute +\(oq%d\(cq, which expands to the password's \(oqmaxLength\(cq attribute (capped with the \fImax-password-length\fR option). The command is expected to output to the standard output, and may add a newline character afterwards, which is not considered as part of the password. @@ -234,11 +234,11 @@ character afterwards, which is not considered as part of the password. The path of the UNIX socket used to communicate with the browser. Can be an absolute path or a path relative to the default Firefox profile (or first profile found if there is no default profile) in the -\(lq~/.mozilla/firefox\(rq directory. +\(oq~/.mozilla/firefox\(cq directory. The socket path and permissions can be configured on the -Iceweasel/Firefox side with the \(lqextensions.icevault.socketPath\(rq -and \(lqextensions.icevault.socketPerms\(rq preferences in -\(lqabout:config\(rq, respectively. +Iceweasel/Firefox side with the \(oqextensions.icevault.socketPath\(cq +and \(oqextensions.icevault.socketPerms\(cq preferences in +\(oqabout:config\(cq, respectively. (Default: \(lqS.IceVault\(rq.) .TP @@ -251,8 +251,8 @@ XDG_DATA_HOME is unset). .TP .I template The template mapping \fIscheme\fR://\fIhostname\fR/\fIidentity\fR URIs -to (encrypted) files on disk. Must contain \(lq%s\(rq, \(lq%h\(rq, and -\(lq%i\(rq, which respectively expand to the \fIscheme\fR, +to (encrypted) files on disk. Must contain \(oq%s\(cq, \(oq%h\(cq, and +\(oq%i\(cq, which respectively expand to the \fIscheme\fR, \fIhostname\fR and \fIidentity\fR parts of the URI. (Default: \(lq%s/%h/%i.gpg\(rq.) -- cgit v1.2.3 From 020ac04b7f743b707538fd40ae85ee862cdc31ec Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Tue, 31 Mar 2015 22:14:34 +0200 Subject: Add --quiet to the default gpg options. --- cli/icevault | 2 +- cli/icevault.1 | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cli/icevault b/cli/icevault index 9f58502..7bbc37f 100755 --- a/cli/icevault +++ b/cli/icevault @@ -169,7 +169,7 @@ sub loadConfig() { error "Insecure C<%s>", $CONFIG{pwgen} unless $CONFIG{pwgen} =~ /\A([-_\@a-zA-Z0-9\.%\/= ]+)\z/; $CONFIG{pwgen} = $1; - $CONFIG{gpg} //= 'gpg'; + $CONFIG{gpg} //= qw/gpg --quiet/; $CONFIG{gpg} =~ s#\A~/#$ENV{HOME}#; error "Insecure C<%s>", $CONFIG{gpg} unless $CONFIG{gpg} =~ /\A([-_\@a-zA-Z0-9\.%\/= ]+)\z/; $CONFIG{gpg} = $1; diff --git a/cli/icevault.1 b/cli/icevault.1 index 88c0902..4138348 100644 --- a/cli/icevault.1 +++ b/cli/icevault.1 @@ -101,7 +101,7 @@ echo '*.gpg diff=gpg' >"${XDG_DATA_HOME:-$HOME/.local/share}/icevault/.gitattrib \fBicevault git\fR add .gitattributes \fBicevault git\fR commit \-m 'Add an attribute for .gpg binary files.' \fBicevault git\fR config diff.gpg.binary true -\fBicevault git\fR config diff.gpg.textconv 'gpg \-o \- \-\-decrypt \-\-' +\fBicevault git\fR config diff.gpg.textconv 'gpg \-qo \- \-\-decrypt \-\-' .RE .fi @@ -207,7 +207,7 @@ option). .I gpg The \fIgpg\fR(1) command to use. Users of GnuPG 1.4.x will probably want to add the \fB--use-agent\fR and possibly \fB--batch\fR options. -(Default: \(lqgpg\(rq.) +(Default: \(lqgpg \-\-quiet\(rq.) .TP .I keyid -- cgit v1.2.3 From 249aa3187108af883344c635564145c27961c167 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Tue, 31 Mar 2015 22:14:52 +0200 Subject: Don't try to autocomplete incorrect commands. --- cli/bash-completion.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/bash-completion.sh b/cli/bash-completion.sh index 5ae1c63..f3175b9 100644 --- a/cli/bash-completion.sh +++ b/cli/bash-completion.sh @@ -21,7 +21,7 @@ _icevault() { COMPREPLY=() declare -a files - local cmd firstopt=2 opts nargs + local cmd firstopt=2 opts nargs=0 if [ $cword -eq 1 -a -z "$cur" ] || [[ "${words[1]}" =~ :// ]] || [[ "${words[1]}" =~ ^- ]]; then firstopt=1 cmd=fill # default command -- cgit v1.2.3 From 25d8c5b95ca2277cef3e86b3d0cf18c08d9eadda Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Tue, 31 Mar 2015 22:23:51 +0200 Subject: oops --- cli/icevault | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/icevault b/cli/icevault index 7bbc37f..d679b04 100755 --- a/cli/icevault +++ b/cli/icevault @@ -169,7 +169,7 @@ sub loadConfig() { error "Insecure C<%s>", $CONFIG{pwgen} unless $CONFIG{pwgen} =~ /\A([-_\@a-zA-Z0-9\.%\/= ]+)\z/; $CONFIG{pwgen} = $1; - $CONFIG{gpg} //= qw/gpg --quiet/; + $CONFIG{gpg} //= 'gpg --quiet'; $CONFIG{gpg} =~ s#\A~/#$ENV{HOME}#; error "Insecure C<%s>", $CONFIG{gpg} unless $CONFIG{gpg} =~ /\A([-_\@a-zA-Z0-9\.%\/= ]+)\z/; $CONFIG{gpg} = $1; -- cgit v1.2.3 From d98c3455f55d872ff2fd3983da981eb8c578d93b Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Tue, 31 Mar 2015 23:52:54 +0200 Subject: Consider "invisible" forms with visible fields. A form can have a width/height of 0 even though its fields are visible in the browser. E.g., http://www.lenson.com document.forms[0].getBoundingClientRect(); { bottom: 196, height: 0, left: 37, right: 208, top: 196, width: 171, x: 37, y: 196 } document.forms[0][0].getBoundingClientRect(); { bottom: 448, height: 22, left: 60, right: 196, top: 426, width: 136, x: 60, y: 426 } document.forms[0][1].getBoundingClientRect(); { bottom: 495, height: 22, left: 60, right: 196, top: 473, width: 136, x: 60, y: 473 } --- xul-ext/chrome/content/icevault.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/xul-ext/chrome/content/icevault.js b/xul-ext/chrome/content/icevault.js index 1eae3e1..d84c5b7 100644 --- a/xul-ext/chrome/content/icevault.js +++ b/xul-ext/chrome/content/icevault.js @@ -91,12 +91,12 @@ var icevault = (function() { // refresh the form list let forms = [], getForms = []; for (let i = 0; i < state.document.forms.length; i++) { - let form = state.document.forms[i], rect = form.getBoundingClientRect(); - if (rect.width == 0 || rect.height == 0) - continue; // invisible form + let form = state.document.forms[i]; let fields = [], getFields = []; for (let i = 0; i < form.elements.length; i++) { - let field = form.elements[i]; + let field = form.elements[i], rect = field.getBoundingClientRect(); + if (rect.width == 0 || rect.height == 0) + continue; // invisible field if (["text","email","password"].indexOf(field.type) > -1) { // store only these kinds of fields let getField = { name: field.name, type: field.type, value: field.value }; -- cgit v1.2.3 From 6da9213a31752e2908a8b721cb88b6cd190d60f5 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Wed, 1 Apr 2015 00:18:51 +0200 Subject: typo --- cli/icevault | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cli/icevault b/cli/icevault index d679b04..4848cd7 100755 --- a/cli/icevault +++ b/cli/icevault @@ -865,9 +865,9 @@ elsif ($COMMAND eq 'import') { } elsif ($COMMAND eq 'fill') { - getopts( 'force|f' => "-f, --force \tDon't ask before updating the form" - , 'show-passwords|p=s' => "-p, --show-passwords\tDon't redact passwords" - , 'socket|s=s' => "-s, --socket=PATH \tSpecifiy the path to the Icevault socket" + getopts( 'force|f' => "-f, --force \tDon't ask before updating the form" + , 'show-passwords|p' => "-p, --show-passwords\tDon't redact passwords" + , 'socket|s=s' => "-s, --socket=PATH \tSpecifiy the path to the Icevault socket" ); usage() unless $#ARGV == 0; @@ -1017,7 +1017,7 @@ elsif ($COMMAND eq 'fill') { } elsif ($COMMAND eq 'dump') { - getopts('show-passwords|p=s' => "-p, --show-passwords\tDon't redact passwords"); + getopts('show-passwords|p' => "-p, --show-passwords\tDon't redact passwords"); usage() unless $#ARGV == 0; my $id = shift; -- cgit v1.2.3 From 95f2edde30d77fa71b72ae728cbbd3b5b292d511 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Wed, 1 Apr 2015 10:26:19 +0200 Subject: Don't add trailing '/' when completing. --- cli/bash-completion.sh | 3 +-- cli/icevault | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/cli/bash-completion.sh b/cli/bash-completion.sh index f3175b9..4bb01b9 100644 --- a/cli/bash-completion.sh +++ b/cli/bash-completion.sh @@ -91,7 +91,7 @@ _icevault() { local trim= if [ "$cmd" = fill -a \( $OPTIND -eq $cword -o $(( $OPTIND + 1)) -eq $cword \) -a -z "$cur" ]; then - cur="$(icevault _geturi ${socket:+--socket="$socket"})"/ # get URI from webpage + cur="$( icevault _geturi ${socket:+--socket="$socket"} 2>/dev/null )"/ # get URI from webpage elif [ -z "$cmd" -a "$cword" -eq 1 -a "$cur" ]; then # autocomplete command COMPREPLY+=( $(compgen -W "$commands" -- "$cur") ) @@ -117,7 +117,6 @@ _icevault() { uri=$( echo "${uri#$trim}" | sed "s/[][\\{}*?~<>;'\"|&()\!$\` \t]/\\\\&/g" ) COMPREPLY+=( "$uri" ) done < <(icevault _complete -0 "$cur" 2>/dev/null) - [ "${#COMPREPLY[@]}" -eq 1 -a "${COMPREPLY[0]: -1:1}" = / ] && compopt -o nospace return 0 fi } diff --git a/cli/icevault b/cli/icevault index 4848cd7..034bb9d 100755 --- a/cli/icevault +++ b/cli/icevault @@ -732,9 +732,9 @@ if ($COMMAND eq '_complete') { if (all { /\A\Q$s\E:\/\/\Q$h\E\// } @matches) { # common host: list all ids } elsif (all { /\A\Q$s\E:\/\// } @matches) { # common scheme: list only hosts - s#/[^\P{Print}\/]+\z#/# foreach @matches; + s#/[^\P{Print}\/]+\z## foreach @matches; } else { # no common scheme: list only schemes - s#://[^\P{Graph}\/]+/[^\P{Print}\/]+\z#://# foreach @matches; + s#://[^\P{Graph}\/]+/[^\P{Print}\/]+\z## foreach @matches; } my %matches = map {($_ => 1)} grep defined, @matches; -- cgit v1.2.3 From 6db424f4a8fc0586237157dd9cc5d0ca350535b2 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Tue, 1 Sep 2015 02:33:54 +0200 Subject: Use groff's mailto markup. --- cli/icevault.1 | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cli/icevault.1 b/cli/icevault.1 index 4138348..a0f0946 100644 --- a/cli/icevault.1 +++ b/cli/icevault.1 @@ -280,6 +280,9 @@ is a non-empty form, take the first one found. Otherwise (if all forms are empty), take the first form in the list. .SH AUTHOR -Guilhem Moulin +Written by Guilhem Moulin +.MT guilhem@fripost.org +.ME . + .SH SEE ALSO \fBgpg\fR(1) -- cgit v1.2.3