diff options
-rw-r--r-- | Changelog | 5 | ||||
-rwxr-xr-x | bin/raps2 | 22 | ||||
-rw-r--r-- | lib/App/Raps2.pm | 418 | ||||
-rw-r--r-- | lib/App/Raps2/Password.pm | 227 | ||||
-rw-r--r-- | lib/App/Raps2/UI.pm | 208 | ||||
-rw-r--r-- | t/20-app-raps2-password.t | 2 |
6 files changed, 455 insertions, 427 deletions
@@ -1,3 +1,8 @@ +git HEAD + + * Rename App::Raps2::Password's crypt method to bcrypt to avoid name + confusion with Perl's builtin function + App::Raps2 0.3 - Wed May 18 2011 * Terminal input is now read via Term::ReadLine (included in perl core) @@ -9,21 +9,21 @@ use 5.010; use App::Raps2; my $raps2 = App::Raps2->new(); -my ($action, @args) = @ARGV; +my ( $action, @args ) = @ARGV; -my $VERSION = '0.3'; +our $VERSION = '0.3'; $raps2->sanity_check(); $raps2->load_config(); given ($action) { - when ('add') { $raps2->cmd_add(@args) } - when ('del') { $raps2->cmd_remove(@args) } - when ('dump') { $raps2->cmd_dump(@args) } - when ('edit') { $raps2->cmd_edit(@args) } - when ('get') { $raps2->cmd_get(@args) } - when ('info') { $raps2->cmd_info(@args) } - when ('list') { $raps2->cmd_list(@args) } + when ('add') { $raps2->cmd_add(@args) } + when ('del') { $raps2->cmd_remove(@args) } + when ('dump') { $raps2->cmd_dump(@args) } + when ('edit') { $raps2->cmd_edit(@args) } + when ('get') { $raps2->cmd_get(@args) } + when ('info') { $raps2->cmd_info(@args) } + when ('list') { $raps2->cmd_list(@args) } when ('version') { say "raps2 version ${VERSION}" } } @@ -92,6 +92,10 @@ List all saved accounts with their respective Logins and URLs =back +=head1 OPTIONS + +None. + =head1 EXIT STATUS zero on success, non-zero otherwise. diff --git a/lib/App/Raps2.pm b/lib/App/Raps2.pm index 0f1fdc4..4a204aa 100644 --- a/lib/App/Raps2.pm +++ b/lib/App/Raps2.pm @@ -1,32 +1,5 @@ package App::Raps2; -=head1 NAME - -App::Raps2 - A Password safe - -=head1 SYNOPSIS - - use App::Raps2; - - my $raps2 = App::Raps2->new(); - my ($action, @args) = @ARGV; - - $raps2->sanity_check(); - $raps2->load_config(); - - given ($action) { - when ('add') { $raps2->cmd_add(@args) } - when ('dump') { $raps2->cmd_dump(@args) } - when ('get') { $raps2->cmd_get(@args) } - when ('info') { $raps2->cmd_info(@args) } - } - -=head1 DESCRIPTION - -B<App::Raps2> is the backend for B<raps2>, a simple commandline password safe. - -=cut - use strict; use warnings; use 5.010; @@ -40,28 +13,8 @@ use File::Slurp qw(read_dir slurp write_file); our $VERSION = '0.3'; -=head1 METHODS - -=over - -=item $raps2 = App::Raps2->new(I<%conf>) - -Returns a new B<App::Raps2> object. - -Accepted configuration parameters are: - -=over - -=item B<cost> => I<int> - -B<cost> of key setup, passed on to App::Raps2::Password(3pm). - -=back - -=cut - sub new { - my ($obj, %conf) = @_; + my ( $obj, %conf ) = @_; my $ref = {}; $ref->{xdg_conf} = config_home('raps2'); @@ -71,24 +24,17 @@ sub new { $ref->{default} = \%conf; - return bless($ref, $obj); + return bless( $ref, $obj ); } -=item $raps2->file_to_hash(I<$file>) - -Reads $file (lines with key/value separated by whitespace) and returns a hash -with its key/value pairs. - -=cut - sub file_to_hash { - my ($self, $file) = @_; + my ( $self, $file ) = @_; my %ret; - for my $line (slurp($file)) { - my ($key, $value) = split(qr{\s+}, $line); + for my $line ( slurp($file) ) { + my ( $key, $value ) = split( qr{ \s+ }x, $line ); - if (not ($key and $value)) { + if ( not( $key and $value ) ) { next; } @@ -97,63 +43,44 @@ sub file_to_hash { return %ret; } -=item $raps2->sanity_check() - -Create working directories (~/.config/raps2 and ~/.local/share/raps2, or the -respective XDG environment variable contents), if they don't exist yet. - -Calls B<create_config> if no raps2 config was found. - -=cut - sub sanity_check { my ($self) = @_; - make_path($self->{xdg_conf}); - make_path($self->{xdg_data}); + make_path( $self->{xdg_conf} ); + make_path( $self->{xdg_data} ); - if (not -e $self->{xdg_conf} . '/password') { + if ( not -e $self->{xdg_conf} . '/password' ) { $self->create_config(); } return; } -=item $raps2->get_master_password() - -Asks the user for the master passphrase. - -=cut - sub get_master_password { my ($self) = @_; - my $pass = $self->ui->read_pw('Master Password', 0); + my $pass = $self->ui->read_pw( 'Master Password', 0 ); $self->{pass} = App::Raps2::Password->new( - cost => $self->{default}->{cost}, - salt => $self->{master_salt}, + cost => $self->{default}->{cost}, + salt => $self->{master_salt}, passphrase => $pass, ); - $self->{pass}->verify($self->{master_hash}); -} - -=item $raps2->create_config() - -Creates a default config and asks the user to set a master password. + $self->{pass}->verify( $self->{master_hash} ); -=cut + return; +} sub create_config { my ($self) = @_; my $cost = 12; - my $pass = $self->ui->read_pw('Master Password', 1); + my $pass = $self->ui->read_pw( 'Master Password', 1 ); $self->{pass} = App::Raps2::Password->new( - cost => $cost, + cost => $cost, passphrase => $pass, ); - my $hash = $self->pw->crypt(); + my $hash = $self->pw->bcrypt(); my $salt = $self->pw->salt(); write_file( @@ -162,61 +89,47 @@ sub create_config { "salt ${salt}\n", "hash ${hash}\n", ); -} - -=item $raps2->load_config() -Load config - -=cut + return; +} sub load_config { my ($self) = @_; - my %cfg = $self->file_to_hash($self->{xdg_conf} . '/password'); + my %cfg = $self->file_to_hash( $self->{xdg_conf} . '/password' ); $self->{master_hash} = $cfg{hash}; $self->{master_salt} = $cfg{salt}; $self->{default}->{cost} //= $cfg{cost}; -} - -=item $raps2->pw() - -Returns the App::Raps2::Password(3pm) object. -=cut + return; +} sub pw { my ($self) = @_; - if (defined $self->{pass}) { + if ( defined $self->{pass} ) { return $self->{pass}; } else { - confess('No App::Raps2::Password object, did you call get_master_password?'); + confess( + 'No App::Raps2::Password object, did you call get_master_password?' + ); } -} - -=item $raps2->ui() - -Returns the App::Raps2::UI(3pm) object. -=cut + return; +} sub ui { my ($self) = @_; + return $self->{ui}; } -=item $raps2->cmd_add(I<$name>) - -Adds a new password file called $name. - -=cut - sub cmd_add { - my ($self, $name) = @_; + my ( $self, $name ) = @_; + my $pwfile = $self->{xdg_data} . "/${name}"; - if (-e $pwfile) { + if ( -e $pwfile ) { confess('Password file already exists'); } @@ -225,39 +138,34 @@ sub cmd_add { my $salt = $self->pw->create_salt(); my $url = $self->ui->read_line('URL'); my $login = $self->ui->read_line('Login'); - my $pass = $self->ui->read_pw('Password', 1); + my $pass = $self->ui->read_pw( 'Password', 1 ); my $extra = $self->ui->read_multiline('Additional content'); $self->pw->salt($salt); - my $pass_hash = $self->pw->encrypt($pass); + my $pass_hash = $self->pw->encrypt($pass); my $extra_hash = ( - $extra ? - $self->pw->encrypt($extra) : - q{} + $extra + ? $self->pw->encrypt($extra) + : q{} ); - write_file( - $pwfile, - "url ${url}\n", + $pwfile, "url ${url}\n", "login ${login}\n", "salt ${salt}\n", "hash ${pass_hash}\n", "extra ${extra_hash}\n", ); -} - -=item $raps2->cmd_dump(I<$account>) -Dumps the content of I<account> - -=cut + return; +} sub cmd_dump { - my ($self, $name) = @_; + my ( $self, $name ) = @_; + my $pwfile = $self->{xdg_data} . "/${name}"; - if (not -e $pwfile) { + if ( not -e $pwfile ) { confess('Password file does not exist'); } @@ -265,45 +173,42 @@ sub cmd_dump { $self->get_master_password(); - $self->pw->salt($key{salt}); + $self->pw->salt( $key{salt} ); $self->ui()->output( - ['URL', $key{url}], - ['Login', $key{login}], - ['Password', $self->pw->decrypt($key{hash})], + [ 'URL', $key{url} ], + [ 'Login', $key{login} ], + [ 'Password', $self->pw->decrypt( $key{hash} ) ], ); - if ($key{extra}) { - print $self->pw->decrypt($key{extra}); + if ( $key{extra} ) { + print $self->pw->decrypt( $key{extra} ); } -} - -=item $raps2->cmd_edit(I<$acount>) -Edit I<account>. - -=cut + return; +} sub cmd_edit { - my ($self, $name) = @_; + my ( $self, $name ) = @_; + my $pwfile = $self->{xdg_data} . "/${name}"; my $pass_hash; - if (not -e $pwfile) { + if ( not -e $pwfile ) { confess('Password file does not exist'); } my %key = $self->file_to_hash($pwfile); $self->get_master_password(); - $self->pw->salt($key{salt}); + $self->pw->salt( $key{salt} ); - my $salt = $key{salt}; - my $url = $self->ui->read_line('URL', $key{url}); - my $login = $self->ui->read_line('Login', $key{login}); - my $pass = $self->ui->read_pw('New password (empty to keep old)', 1); + my $salt = $key{salt}; + my $url = $self->ui->read_line( 'URL', $key{url} ); + my $login = $self->ui->read_line( 'Login', $key{login} ); + my $pass = $self->ui->read_pw( 'New password (empty to keep old)', 1 ); my $extra = $key{extra} // q{}; - if (length($pass)) { + if ( length($pass) ) { $pass_hash = $self->pw->encrypt($pass); } else { @@ -311,26 +216,22 @@ sub cmd_edit { } write_file( - $pwfile, - "url ${url}\n", + $pwfile, "url ${url}\n", "login ${login}\n", "salt ${salt}\n", "hash ${pass_hash}\n", "extra ${extra}\n", ); -} - -=item $raps2->cmd_get(I<$name>) -Puts the password saved in $name into the X clipboard. - -=cut + return; +} sub cmd_get { - my ($self, $name) = @_; + my ( $self, $name ) = @_; + my $pwfile = $self->{xdg_data} . "/${name}"; - if (not -e $pwfile) { + if ( not -e $pwfile ) { confess('Password file does not exist'); } @@ -338,79 +239,190 @@ sub cmd_get { $self->get_master_password(); - $self->pw->salt($key{salt}); + $self->pw->salt( $key{salt} ); - $self->ui()->to_clipboard($self->pw->decrypt($key{hash})); + $self->ui()->to_clipboard( $self->pw->decrypt( $key{hash} ) ); - if ($key{extra}) { - print $self->pw->decrypt($key{extra}) + if ( $key{extra} ) { + print $self->pw->decrypt( $key{extra} ); } -} - -=item $raps2->cmd_info(I<$name>) -Prints unencrypted information about $name. - -=cut + return; +} sub cmd_info { - my ($self, $name) = @_; + my ( $self, $name ) = @_; + my $pwfile = $self->{xdg_data} . "/${name}"; - if (not -e $pwfile) { + if ( not -e $pwfile ) { confess('Password file does not exist'); } my %key = $self->file_to_hash($pwfile); - $self->ui()->output( - ['URL', $key{url}], - ['Login', $key{login}], - ); -} + $self->ui()->output( [ 'URL', $key{url} ], [ 'Login', $key{login} ], ); -=item $raps2->cmd_list() - -Lists all saved passwords and their logins and urls - -=cut + return; +} sub cmd_list { my ($self) = @_; - my @files = read_dir($self->{xdg_data}); - for my $file (sort @files) { - my %key = $self->file_to_hash($self->{xdg_data} . "/${file}"); + my @files = read_dir( $self->{xdg_data} ); + + for my $file ( sort @files ) { + my %key = $self->file_to_hash( $self->{xdg_data} . "/${file}" ); $self->ui->list( - ['Account', $file], - ['Login', $key{login}], - ['URL', $key{url}], + [ 'Account', $file ], + [ 'Login', $key{login} ], + [ 'URL', $key{url} ], ); } -} - -=item $raps2->cmd_remove(I<$name>) - -Remove (unlink) the account I<name>. -=cut + return; +} sub cmd_remove { - my ($self, $name) = @_; + my ( $self, $name ) = @_; + my $pwfile = $self->{xdg_data} . "/${name}"; - if (-e $pwfile) { + if ( -e $pwfile ) { unlink($pwfile); } else { say STDERR 'File did not exist, so could not be removed'; } + + return; } +1; + +__END__ + +=head1 NAME + +App::Raps2 - A Password safe + +=head1 SYNOPSIS + + use App::Raps2; + + my $raps2 = App::Raps2->new(); + my ($action, @args) = @ARGV; + + $raps2->sanity_check(); + $raps2->load_config(); + + given ($action) { + when ('add') { $raps2->cmd_add(@args) } + when ('dump') { $raps2->cmd_dump(@args) } + when ('get') { $raps2->cmd_get(@args) } + when ('info') { $raps2->cmd_info(@args) } + } + +=head1 DESCRIPTION + +B<App::Raps2> is the backend for B<raps2>, a simple commandline password safe. + +=head1 VERSION + +This manual documents App::Raps2 version 0.3 + +=head1 METHODS + +=over + +=item $raps2 = App::Raps2->new(I<%conf>) + +Returns a new B<App::Raps2> object. + +Accepted configuration parameters are: + +=over + +=item B<cost> => I<int> + +B<cost> of key setup, passed on to App::Raps2::Password(3pm). + =back +=item $raps2->file_to_hash(I<$file>) + +Reads $file (lines with key/value separated by whitespace) and returns a hash +with its key/value pairs. + +=item $raps2->sanity_check() + +Create working directories (~/.config/raps2 and ~/.local/share/raps2, or the +respective XDG environment variable contents), if they don't exist yet. + +Calls B<create_config> if no raps2 config was found. + +=item $raps2->get_master_password() + +Asks the user for the master passphrase. + +=item $raps2->create_config() + +Creates a default config and asks the user to set a master password. + +=item $raps2->load_config() + +Load config + +=item $raps2->pw() + +Returns the App::Raps2::Password(3pm) object. + +=item $raps2->ui() + +Returns the App::Raps2::UI(3pm) object. + +=item $raps2->cmd_add(I<$name>) + +Adds a new password file called $name. + +=item $raps2->cmd_dump(I<$account>) + +Dumps the content of I<account> + +=item $raps2->cmd_edit(I<$acount>) + +Edit I<account>. + +=item $raps2->cmd_get(I<$name>) + +Puts the password saved in $name into the X clipboard. + +=item $raps2->cmd_info(I<$name>) + +Prints unencrypted information about $name. + +=item $raps2->cmd_list() + +Lists all saved passwords and their logins and urls + +=item $raps2->cmd_remove(I<$name>) + +Remove (unlink) the account I<name>. + +=back + +=head1 DIAGNOSTICS + +If anything goes wrong, B<App::Raps2> will die with a backtrace (using +B<confess> from Carp(3pm)). + =head1 DEPENDENCIES -L<App::Raps2::Password>, L<App::Raps2::UI>, L<File::BaseDir>, L<File::Slurp>. +App::Raps2::Password(3pm), App::Raps2::UI(3pm), File::BaseDir(3pm), +File::Slurp(3pm). + +=head1 BUGS AND LIMITATIONS + +Unknown. =head1 AUTHOR diff --git a/lib/App/Raps2/Password.pm b/lib/App/Raps2/Password.pm index 1e33f58..1a3ab63 100644 --- a/lib/App/Raps2/Password.pm +++ b/lib/App/Raps2/Password.pm @@ -11,6 +11,103 @@ use Crypt::Eksblowfish::Bcrypt qw(bcrypt_hash en_base64 de_base64); our $VERSION = '0.3'; +sub new { + my ( $obj, %conf ) = @_; + + $conf{cost} //= 12; + + if ( not defined $conf{salt} ) { + $conf{salt} = create_salt(); + } + + if ( length( $conf{salt} ) != 16 ) { + confess('incorrect salt length'); + } + + if ( not( defined $conf{passphrase} and length $conf{passphrase} ) ) { + confess('no passphrase given'); + } + + my $ref = \%conf; + + return bless( $ref, $obj ); +} + +sub create_salt { + my ($self) = @_; + my $salt = q{}; + + for ( 1 .. 16 ) { + $salt .= chr( 0x21 + int( rand(90) ) ); + } + + return $salt; +} + +sub salt { + my ( $self, $salt ) = @_; + + if ( defined $salt ) { + if ( length($salt) != 16 ) { + confess('incorrect salt length'); + } + + $self->{salt} = $salt; + } + + return $self->{salt}; +} + +sub encrypt { + my ( $self, $in ) = @_; + + my $eksblowfish = Crypt::Eksblowfish->new( $self->{cost}, $self->{salt}, + $self->{passphrase}, ); + my $cbc = Crypt::CBC->new( -cipher => $eksblowfish ); + + return $cbc->encrypt_hex($in); +} + +sub decrypt { + my ( $self, $in ) = @_; + + my $eksblowfish = Crypt::Eksblowfish->new( $self->{cost}, $self->{salt}, + $self->{passphrase}, ); + my $cbc = Crypt::CBC->new( -cipher => $eksblowfish ); + + return $cbc->decrypt_hex($in); +} + +sub bcrypt { + my ($self) = @_; + + return en_base64( + bcrypt_hash( + { + key_nul => 1, + cost => $self->{cost}, + salt => $self->{salt}, + }, + $self->{passphrase}, + ) + ); +} + +sub verify { + my ( $self, $testhash ) = @_; + + my $myhash = $self->bcrypt(); + + if ( $testhash eq $myhash ) { + return 1; + } + confess('Passwords did not match'); +} + +1; + +__END__ + =head1 NAME App::Raps2::Password - Password class for App::Raps2 @@ -23,7 +120,7 @@ App::Raps2::Password - Password class for App::Raps2 passphrase => 'secret', ); - my $oneway_hash = $raps2->crypt(); + my $oneway_hash = $raps2->bcrypt(); $raps2->verify($oneway_hash); my $twoway_hash = $raps2->encrypt('data'); @@ -34,6 +131,10 @@ App::Raps2::Password - Password class for App::Raps2 This manual documents B<App::Raps2::Password> version 0.3 +=head1 DESCRIPTION + +App::Raps2::Pasword is a wrapper around Crypt::Eksblowfish. + =head1 METHODS =over @@ -60,153 +161,47 @@ generates its own. =back -=cut - -sub new { - my ($obj, %conf) = @_; - - $conf{cost} //= 12; - - if (not defined $conf{salt}) { - $conf{salt} = create_salt(); - } - - if (length($conf{salt}) != 16) { - confess('incorrect salt length'); - } - - if (not (defined $conf{passphrase} and length $conf{passphrase})) { - confess('no passphrase given'); - } - - my $ref = \%conf; - - return bless($ref, $obj); -} - =item $pass->create_salt() Returns a new 16-byte salt. Contains only printable characters. -=cut - -sub create_salt { - my ($self) = @_; - my $salt = q{}; - - for (1 .. 16) { - $salt .= chr(0x21 + int(rand(90))); - } - - return $salt; -} - =item $pass->salt([I<salt>]) Returns the currently used salt and optionally changes it to I<salt>. -=cut - -sub salt { - my ($self, $salt) = @_; - - if (defined $salt) { - if (length($salt) != 16) { - confess('incorrect salt length'); - } - - $self->{salt} = $salt; - } - - return $self->{salt}; -} - =item $pass->encrypt(I<data>) Encrypts I<data> with the passphrase saved in the object, returns the corresponding hexadecimal hash (as string). -=cut - -sub encrypt { - my ($self, $in) = @_; - - my $eksblowfish = Crypt::Eksblowfish->new( - $self->{cost}, - $self->{salt}, - $self->{passphrase}, - ); - my $cbc = Crypt::CBC->new(-cipher => $eksblowfish); - - return $cbc->encrypt_hex($in); -} - =item $pass->decrypt(I<hexstr>) Decrypts I<hexstr> (as created by B<encrypt>), returns its original content. -=cut - -sub decrypt { - my ($self, $in) = @_; - - my $eksblowfish = Crypt::Eksblowfish->new( - $self->{cost}, - $self->{salt}, - $self->{passphrase}, - ); - my $cbc = Crypt::CBC->new(-cipher => $eksblowfish); - - return $cbc->decrypt_hex($in); -} - -=item $pass->crypt() +=item $pass->bcrypt() Return a base64 bcrypt hash of the password, salted with the salt. -=cut - -sub crypt { - my ($self) = @_; - - return en_base64( - bcrypt_hash({ - key_nul => 1, - cost => $self->{cost}, - salt => $self->{salt}, - }, - $self->{passphrase}, - )); -} - =item $pass->verify(I<hash>) Verify a hash as returned by B<crypt>. Returns true if it matches, dies if it doesn't. -=cut - -sub verify { - my ($self, $testhash) = @_; - - my $myhash = $self->crypt(); +=back - if ($testhash eq $myhash) { - return 1; - } - confess('Passwords did not match'); -} +=head1 DIAGNOSTICS -1; +When anything goes wrong, App::Raps2::Password will use Carp(3pm)'s B<confess> +method to die with a backtrace. -__END__ +=head1 DEPENDENCIES -=back +Crypt::CBC(3pm), Crypt::Eksblowfish(3pm). -=head1 DEPENDENCIES +=head1 BUGS AND LIMITATIONS -B<Crypt::CBC>, B<Crypt::Eksblowfish>. +Unknown. =head1 SEE ALSO diff --git a/lib/App/Raps2/UI.pm b/lib/App/Raps2/UI.pm index 4d7f3d0..b588222 100644 --- a/lib/App/Raps2/UI.pm +++ b/lib/App/Raps2/UI.pm @@ -10,113 +10,61 @@ use Term::ReadLine; our $VERSION = '0.3'; -=head1 NAME - -App::Raps2::UI - App::Raps2 User Interface - -=head1 SYNOPSIS - - my $ui = App::Raps2::UI->new(); - - my $input = $ui->read_line('Say something'); - - my $password = $ui->read_pw('New password', 1); - - $ui->to_clipboard('stuff!'); - -=head1 VERSION - -This manual documents B<App::Raps2::UI> version 0.3 - -=head1 METHODS - -=over - -=item $ui = App::Raps2::UI->new() - -Returns a new App::Raps2::UI object. - -=cut - sub new { my ($obj) = @_; - my $ref = {}; - $ref->{term_readline} = Term::ReadLine->new('App::Raps2'); - return bless($ref, $obj); -} -=item $ui->list(I<\@item1>, I<\@item2>, I<\@item3>) + my $ref = {}; -Print the list items neatly formatted to stdout. Each I<item> looks like B<[> -I<key>, I<value> B<]>. When B<list> is called for the first time, it will -print the keys as well as the values. + $ref->{term_readline} = Term::ReadLine->new('App::Raps2'); -=cut + return bless( $ref, $obj ); +} sub list { - my ($self, @list) = @_; + my ( $self, @list ) = @_; + my $format = "%-20s %-20s %s\n"; - if (not $self->{list}->{header}) { - printf($format, map { $_->[0] } @list); + if ( not $self->{list}->{header} ) { + printf( $format, map { $_->[0] } @list ); $self->{list}->{header} = 1; } - printf($format, map { $_->[1] // q{} } @list); -} - -=item $ui->read_line(I<$question>, [I<$prefill>]) - -Print "I<question>: " to stdout and wait for the user to input text followed -by a newline. I<prefill> sets the default content of the answer field. - -Returns the user's reply, excluding the newline. + printf( $format, map { $_->[1] // q{} } @list ); -=cut + return; +} sub read_line { - my ($self, $str, $pre) = @_; + my ( $self, $str, $pre ) = @_; - my $input = $self->{term_readline}->readline("${str}: ", $pre); + my $input = $self->{term_readline}->readline( "${str}: ", $pre ); return $input; } -=item $ui->read_multiline(I<$message>) - -Like B<read_line>, but repeats I<message> each time the user hits return. -Input is terminated by EOF (Ctrl+D). Returns a string concatenation of all -lines (including newlines). - -=cut - sub read_multiline { - my ($self, $str) = @_; + my ( $self, $str ) = @_; + my $in; say "${str} (^D to quit)"; - while (my $line = $self->read_line('multiline')) { + while ( my $line = $self->read_line('multiline') ) { $in .= "${line}\n"; } + return $in; } -=item $ui->read_pw(I<$message>, I<$verify>) - -Prompt the user for a password. I<message> is displayed, the user's input is -noch echoed. If I<verify> is set, the user has to enter the same input twice, -otherwise B<read_pw> dies. Returns the input. - -=cut - sub read_pw { - my ($self, $str, $verify) = @_; - my ($in1, $in2); + my ( $self, $str, $verify ) = @_; + my ( $in1, $in2 ); my $term = POSIX::Termios->new(); + $term->getattr(0); - $term->setlflag($term->getlflag() & ~POSIX::ECHO); - $term->setattr(0, POSIX::TCSANOW); + $term->setlflag( $term->getlflag() & ~POSIX::ECHO ); + $term->setattr( 0, POSIX::TCSANOW ); print "${str}: "; $in1 = readline(STDIN); @@ -128,49 +76,39 @@ sub read_pw { print "\n"; } - $term->setlflag($term->getlflag() | POSIX::ECHO); - $term->setattr(0, POSIX::TCSANOW); + $term->setlflag( $term->getlflag() | POSIX::ECHO ); + $term->setattr( 0, POSIX::TCSANOW ); - if ($verify and $in1 ne $in2) { + if ( $verify and $in1 ne $in2 ) { confess('Input lines did not match'); } chomp $in1; + return $in1; } -=item $ui->to_clipboard(I<$string>) +sub to_clipboard { + my ( $self, $str ) = @_; -Place I<string> in the primary X Clipboard. + open( my $clipboard, q{|-}, 'xclip -l 1' ) + or confess("Failed to execute xclip -l 1: $!"); -=cut + print $clipboard $str; -sub to_clipboard { - my ($self, $str) = @_; + close($clipboard) + or confess("Failed to close pipe to xclip: $!"); - open(my $clipboard, '|-', 'xclip -l 1'); - print $clipboard $str; - close($clipboard); return; } -=item $ui->output(I<\@pair>, I<...>) - -I<pair> consinsts of B<[> I<key>, I<value> B<]>. For each I<pair>, prints -" key : value" to stdout. - -=cut - sub output { - my ($self, @out) = @_; + my ( $self, @out ) = @_; for my $pair (@out) { - printf( - "%-8s : %s\n", - $pair->[0], - $pair->[1] // q{}, - ); + printf( "%-8s : %s\n", $pair->[0], $pair->[1] // q{}, ); } + return; } @@ -178,12 +116,86 @@ sub output { __END__ +=head1 NAME + +App::Raps2::UI - App::Raps2 User Interface + +=head1 SYNOPSIS + + my $ui = App::Raps2::UI->new(); + + my $input = $ui->read_line('Say something'); + + my $password = $ui->read_pw('New password', 1); + + $ui->to_clipboard('stuff!'); + +=head1 VERSION + +This manual documents B<App::Raps2::UI> version 0.3 + +=head1 DESCRIPTION + +App::Raps2::UI is used by App::Raps2 to interface with the user, i.e. do input +and output on the terminal. + +=head1 METHODS + +=over + +=item $ui = App::Raps2::UI->new() + +Returns a new App::Raps2::UI object. + +=item $ui->list(I<\@item1>, I<\@item2>, I<\@item3>) + +Print the list items neatly formatted to stdout. Each I<item> looks like B<[> +I<key>, I<value> B<]>. When B<list> is called for the first time, it will +print the keys as well as the values. + +=item $ui->read_line(I<$question>, [I<$prefill>]) + +Print "I<question>: " to stdout and wait for the user to input text followed +by a newline. I<prefill> sets the default content of the answer field. + +Returns the user's reply, excluding the newline. + +=item $ui->read_multiline(I<$message>) + +Like B<read_line>, but repeats I<message> each time the user hits return. +Input is terminated by EOF (Ctrl+D). Returns a string concatenation of all +lines (including newlines). + +=item $ui->read_pw(I<$message>, I<$verify>) + +Prompt the user for a password. I<message> is displayed, the user's input is +noch echoed. If I<verify> is set, the user has to enter the same input twice, +otherwise B<read_pw> dies. Returns the input. + +=item $ui->to_clipboard(I<$string>) + +Place I<string> in the primary X Clipboard. + +=item $ui->output(I<\@pair>, I<...>) + +I<pair> consinsts of B<[> I<key>, I<value> B<]>. For each I<pair>, prints +" key : value" to stdout. + =back +=head1 DIAGNOSTICS + +When App::Raps2::UI encounters an error, it uses Carp(3pm)'s B<confess> +function to die with a backtrace. + =head1 DEPENDENCIES This module requires B<Term::ReadLine> and the B<xclip> executable. +=head1 BUGS AND LIMITATIONS + +Unknown. + =head1 SEE ALSO App::Raps2(3pm). diff --git a/t/20-app-raps2-password.t b/t/20-app-raps2-password.t index 5faa7e9..a8f56e8 100644 --- a/t/20-app-raps2-password.t +++ b/t/20-app-raps2-password.t @@ -82,7 +82,7 @@ like( 'verify: does not verify invalid hash' ); -ok($pw->verify($pw->crypt('truth')), 'crypt->verify okay'); +ok($pw->verify($pw->bcrypt('truth')), 'bcrypt->verify okay'); is($pw->salt(), $salt, 'salt() returns current salt'); |