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