clivepass: added --show option.
[clive-utils.git] / clivepass
blob12adc267b4d82343ff3125c6ac01d55c58d9dd33
1 #!/usr/bin/env perl
2 # -*- coding: ascii -*-
3 ###########################################################################
4 # clivepass, the login password utility for clive
5 # Copyright (C) 2008 Toni Gundogdu.
7 # This file is part of clive-utils.
9 # clivepass is free software: you can redistribute it and/or modify
10 # it under the terms of the GNU General Public License as published by
11 # the Free Software Foundation, either version 3 of the License, or
12 # (at your option) any later version.
14 # clivepass is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 # GNU General Public License for more details.
19 # You should have received a copy of the GNU General Public License
20 # along with clivepass. If not, see <http://www.gnu.org/licenses/>.
21 ###########################################################################
23 # Keep it simple.
25 use warnings;
26 use strict;
28 binmode(STDOUT, ":utf8");
30 use Crypt::PasswdMD5;
31 use Crypt::Twofish2;
32 use Config::Tiny;
33 use Crypt::Salt;
34 # Core modules:
35 use Digest::MD5 qw(md5_hex);
36 use MIME::Base64;
37 use Getopt::Long qw(:config bundling);
38 use File::Spec;
39 use File::Find;
40 use File::Path;
41 use Pod::Usage;
42 use Encode;
44 my $VERSION = "2.0beta3";
45 my $CONFIGDIR = $ENV{CLIVEPASS_CONFIGDIR}
46 || File::Spec->catfile($ENV{HOME}, ".config/clivepass");
47 my $PASSWDFILE = File::Spec->catfile($CONFIGDIR, "passwd");
49 my %opts;
50 GetOptions(\%opts,
51 'create|c', 'add|a=s', 'get|g=s', 'edit|e=s', 'delete|d=s',
52 'show|s', 'manual|m', 'help|h', 'version|v' => \&print_version,
53 ) or pod2usage(1);
55 pod2usage(-exitstatus => 0, -verbose => 1) if $opts{help};
56 pod2usage(-exitstatus => 0, -verbose => 2) if $opts{manual};
58 main();
60 sub main {
61 mkpath( [$CONFIGDIR], 1, 0700 );
63 if ( $opts{add} ) { add_login(); }
64 elsif ( $opts{create} ) { create_passwd(); }
65 elsif ( $opts{edit} ) { edit_login(); }
66 elsif ( $opts{delete} ) { delete_login(); }
67 elsif ( $opts{get} ) { get_login(); }
68 elsif ( $opts{show} ) { show_logins(); }
69 else {
70 pod2usage(-exitstatus => 0, -verbose => 1);
74 sub create_passwd {
75 if ( -e $PASSWDFILE ) {
76 print "WARN: $PASSWDFILE exists already.\n"
77 . "WARN: You are about to overwrite the existing file.\n"
78 . "WARN: Hit ctrl-c now if that's not your intention.\n";
80 print "Creating $PASSWDFILE.\n";
82 my ($phrase, $again);
83 $phrase = getpass("Enter passphrase: ") while ( ! $phrase );
84 $again = getpass("Enter passphrase again: ") while ( ! $again );
86 print STDERR "[error] passphrases did not match\n" and exit
87 unless $phrase eq $again;
89 my $passwd = Config::Tiny->new;
90 $passwd->{_}->{phrase} = unix_md5_crypt($phrase, salt(8));
91 $passwd->write($PASSWDFILE);
93 return ( passwd => $passwd, phrase => $phrase );
96 sub verify_phrase {
97 my ($phrase_hash) = @_;
99 print STDERR "[error] $PASSWDFILE: phrase hash not found\n" and exit
100 unless $phrase_hash;
102 my $phrase;
103 $phrase = getpass("Enter passphrase: ") while ( ! $phrase );
105 if ( unix_md5_crypt($phrase, $phrase_hash) ne $phrase_hash ) {
106 print STDERR "[error] invalid passphrase\n";
107 exit;
109 return $phrase;
112 sub get_key {
113 my ($dupl_user) = @_;
115 print STDERR "[error] $PASSWDFILE does not exist, use --create\n"
116 and exit if ( ! -e $PASSWDFILE );
118 my $passwd = Config::Tiny->read($PASSWDFILE);
120 if ( $dupl_user ) {
121 my ($id, $pwd) = lookup_login($passwd, $dupl_user);
122 print STDERR qq/[error] login with the "$dupl_user" /
123 . "username exists already\n" and exit if $pwd;
126 my $phrase = verify_phrase($passwd->{_}->{phrase});
127 my $key = md5_hex($phrase);
129 return ($key, $passwd);
133 sub getpass {
134 if ( -t STDOUT ) {
135 system "stty -echo";
136 print shift;
138 chomp(my $passwd = <STDIN>);
140 if ( -t STDOUT ) {
141 print "\n";
142 system "stty echo";
144 return $passwd;
147 sub new_login {
148 my ($key, $passwd, $user) = @_;
150 my ($pwd, $again);
151 $pwd = getpass("Enter password for $user: ") while ( ! $pwd );
152 $again = getpass("Re-enter the password: ") while ( ! $again );
154 print STDERR "[error] passwords did not match\n" and exit
155 unless $pwd eq $again;
157 my $c = Crypt::Twofish2->new($key, Crypt::Twofish2::MODE_CBC);
159 $passwd->{login}->{$user} =
160 encode_base64( $c->encrypt( pack('a16',$pwd) ) );
162 $passwd->write($PASSWDFILE);
165 sub add_login {
166 my ($key, $passwd) = get_key($opts{add});
167 new_login($key, $passwd, $opts{add});
170 sub edit_login {
171 my ($key, $passwd) = get_key();
172 my ($id, $pwd) = lookup_login($passwd, $opts{edit});
174 print STDERR qq/[error] no such login with the "$opts{edit}" /
175 . "username exists\n" and exit unless $pwd;
177 print "WARN: Changing password for the login "
178 . qq/with the username "$id".\n/;
180 new_login($key, $passwd, $id);
183 sub get_login {
184 my ($key, $passwd) = get_key();
185 my ($id, $pwd) = lookup_login($passwd, $opts{get});
187 print STDERR qq/[error] no such login with the "$opts{get}" /
188 . "username exists\n" and exit unless $pwd;
190 my $c = Crypt::Twofish2->new($key, Crypt::Twofish2::MODE_CBC);
191 print "login: " .$opts{get}."=". $c->decrypt(decode_base64($pwd)) ."\n";
194 sub show_logins {
195 my $passwd = Config::Tiny->read($PASSWDFILE);
196 foreach ( $passwd->{login} ) {
197 while ( my ($id, $pwd) = each(%{$_}) ) {
198 printf "%20s = %-32s\n", $id, $pwd;
203 sub delete_login {
204 my ($key, $passwd) = get_key();
205 my ($id, $pwd) = lookup_login($passwd, $opts{delete});
207 print STDERR qq/[error] no such login with the "$opts{delete}" /
208 . "username exists\n" and exit unless $pwd;
210 print "WARN: About to delete the login with the username "
211 . qq/"$id".\n/ ."> Confirm delete (y/N): ";
213 chomp(my $confirm = <STDIN>);
214 exit unless $confirm eq "y";
216 delete $passwd->{login}->{$id};
217 $passwd->write($PASSWDFILE);
220 sub lookup_login {
221 my ($passwd, $user) = @_;
223 foreach ( $passwd->{login} ) {
224 while ( my ($id, $pwd) = each(%{$_}) ) {
225 if ( $id eq $user ) {
226 return ($id,$pwd);
232 sub print_version {
233 my $perl_v = sprintf "%vd", $^V;
234 print
235 "clivepass version $VERSION. Copyright (C) 2008 Toni Gundogdu.
237 Perl: $perl_v ($^O)
238 Modules:
239 * Crypt::PasswdMD5/$Crypt::PasswdMD5::VERSION\t* Crypt::Twofish2/$Crypt::Twofish2::VERSION
240 * Crypt::Salt/$Crypt::Salt::VERSION\t\t* Config::Tiny/$Config::Tiny::VERSION
242 Core modules:
243 * Getopt::Long/$Getopt::Long::VERSION\t\t* Encode/$Encode::VERSION
244 * File::Spec/$File::Spec::VERSION\t\t* File::Find/$File::Find::VERSION
245 * Pod::Usage/$Pod::Usage::VERSION\t\t* Digest::MD5/$Digest::MD5::VERSION
246 * MIME::Base64/$MIME::Base64::VERSION
248 This program comes with ABSOLUTELY NO WARRANTY. You may redistribute copies of
249 clivepass under the terms of the GNU General Public License as published by the
250 Free Software Foundation, either version 3 of the License, or (at your option)
251 any later version. You should have received a copy of the General Public License
252 along with this program. If not, see http://www.gnu.org/licenses/.
253 "; exit;
256 __END__
258 =head1 NAME
260 clivepass - the login password utility for clive
262 =head1 SYNOPSIS
264 clivepass [option]...
266 =head1 DESCRIPTION
268 clivepass is an utility that can be used to create and change passwords
269 for websites used by L<clive(1)>. The passwords are encrypted and saved
270 along with the username information. Access is restricted by using a
271 global passphrase.
273 Historically, a similar utility, clive-passwd, was part of clive 1.x but
274 was written in Python instead of Perl.
276 =head1 OPTIONS
278 B<Basic Options>
280 =over 4
282 =item B<-h --help>
284 Show help and exit.
286 =item B<-v --version>
288 Show version and exit.
290 =item B<-c --create>
292 Create a new passwd file. See also L</FILES>.
294 =item B<-a --add=>I<username>
296 Add a new login with the I<username>.
298 =item B<-e --edit=>I<username>
300 Change login password for I<username>.
302 =item B<-g --get=>I<username>
304 Print the decrypted login password for the I<username>
305 to STDOUT.
307 =item B<-s --show>
309 Show all saved login usernames with encrypted passwords.
311 =item B<-d --delete=>I<username>
313 Delete the login with the I<username>.
315 =back
317 =head1 EXAMPLES
319 =over 4
321 =item % clivepass -a myusername
323 =item % clivepass -g myusername
325 =back
327 =head1 FILES
329 By default, clivepass searches the ~/.config/clivepass directory for the
330 password file. The B<CLIVEPASS_CONFIGDIR> environment variable can be used
331 to override this behaviour.
333 =over 4
335 =item ~/.config/clivepass/passwd
337 Password file. Contains the salted passphrase hash and login usernames and
338 encrypted passwords.
340 =head1 SEE ALSO
342 L<clive(1)> L<clivescan(1)> L<clivefeed(1)>
344 =head1 OTHER
346 Project: http://googlecode.com/p/clive-utils/
348 A clive-utils development repository can be obtained from:
350 % git clone git://repo.or.cz/clive-utils.git
352 Patches welcome.
354 =head1 AUTHOR
356 Written by Toni Gundogdu <legatvs@gmail.com>
358 =cut