diff options
Diffstat (limited to 'lib/App')
-rw-r--r-- | lib/App/Raps2.pm | 193 | ||||
-rw-r--r-- | lib/App/Raps2/Password.pm | 98 | ||||
-rw-r--r-- | lib/App/Raps2/UI.pm | 94 |
3 files changed, 385 insertions, 0 deletions
diff --git a/lib/App/Raps2.pm b/lib/App/Raps2.pm new file mode 100644 index 0000000..4746e5a --- /dev/null +++ b/lib/App/Raps2.pm @@ -0,0 +1,193 @@ +package App::Raps2; + + + + +use strict; +use warnings; +use autodie; +use 5.010; + +use base 'Exporter'; + +use App::Raps2::Password; +use App::Raps2::UI; +use File::Path qw(make_path); +use File::Slurp qw(slurp write_file); + +our @EXPORT_OK = (); +our $VERSION = '0.1'; + +sub create_salt { + my $salt = q{}; + + for (1 .. 16) { + $salt .= chr(0x21 + int(rand(90))); + } + + return $salt; +} + +sub file_to_hash { + my ($file) = @_; + my %ret; + + for my $line (slurp($file)) { + my ($key, $value) = split(qr{\s+}, $line); + + if (not ($key and $value)) { + next; + } + + $ret{$key} = $value; + } + return %ret; +} + +sub new { + my ($obj, %conf) = @_; + my $ref = {}; + + $ref->{'xdg_conf'} = $ENV{'XDG_CONFIG_HOME'} // "$ENV{HOME}/.config/raps2"; + $ref->{'xdg_data'} = $ENV{'XDG_DATA_HOME'} // + "$ENV{HOME}/.local/share/raps2"; + + $ref->{'ui'} = App::Raps2::UI->new(); + + $ref->{'default'} = \%conf; + + return bless($ref, $obj); +} + +sub sanity_check { + my ($self) = @_; + + make_path($self->{'xdg_conf'}); + make_path($self->{'xdg_data'}); + + if (not -e $self->{'xdg_conf'} . '/password') { + $self->create_config(); + } + + return; +} + +sub get_master_password { + my ($self) = @_; + my $pass = $self->{'ui'}->read_pw('Master Password', 0); + + $self->{'pass'} = App::Raps2::Password->new( + cost => $self->{'default'}->{'cost'}, + salt => $self->{'master_salt'}, + passphrase => $pass, + ); + + if (not $self->{'pass'}->verify($self->{'master_hash'})) { + return undef; + } +} + +sub create_config { + my ($self) = @_; + my $cost = 12; + my $salt = create_salt(); + my $pass = $self->{'ui'}->read_pw('Master Password', 1); + + $self->{'pass'} = App::Raps2::Password->new( + cost => $cost, + salt => $salt, + passphrase => $pass, + ); + my $hash = $self->{'pass'}->crypt(); + + write_file( + $self->{'xdg_conf'} . '/password', + "cost ${cost}\n", + "salt ${salt}\n", + "hash ${hash}\n", + ); +} + +sub load_config { + my ($self) = @_; + my %cfg = file_to_hash($self->{'xdg_conf'} . '/password'); + $self->{'master_hash'} = $cfg{'hash'}; + $self->{'master_salt'} = $cfg{'salt'}; + $self->{'default'}->{'cost'} //= $cfg{'cost'}; +} + +sub cmd_add { + my ($self, $name) = @_; + my $pwfile = $self->{'xdg_data'} . "/${name}"; + my $ui = $self->{'ui'}; + + if (-e $pwfile) { + return undef; + } + + $self->get_master_password(); + + my $salt = create_salt(); + my $url = $ui->read_line('URL'); + my $login = $ui->read_line('Login'); + my $pass = $ui->read_pw('Password', 1); + my $extra = $ui->read_multiline('Additional content'); + + $self->{'pass'}->salt($salt); + my $pass_hash = $self->{'pass'}->encrypt($pass); + my $extra_hash = ( + $extra ? + $self->{'pass'}->encrypt($extra) : + q{} + ); + + + write_file( + $pwfile, + "url ${url}\n", + "login ${login}\n", + "salt ${salt}\n", + "hash ${pass_hash}\n", + "extra ${extra_hash}\n", + ); +} + +sub cmd_dump { + my ($self, $name) = @_; + my $pwfile = $self->{'xdg_data'} . "/${name}"; + + if (not -e $pwfile) { + return undef; + } + + my %key = file_to_hash($pwfile); + + $self->get_master_password(); + + $self->{'pass'}->salt($key{'salt'}); + + $self->{'ui'}->output( + ['URL', $key{'url'}], + ['Login', $key{'login'}], + ['Password', $self->{'pass'}->decrypt($key{'hash'})], + ); + if ($key{'extra'}) { + say $self->{'pass'}->decrypt($key{'extra'}); + } +} + + +sub cmd_info { + my ($self, $name) = @_; + my $pwfile = $self->{'xdg_data'} . "/${name}"; + + if (not -e $pwfile) { + return undef; + } + + my %key = file_to_hash($pwfile); + $self->{'ui'}->output( + ['URL', $key{'url'}], + ['Login', $key{'login'}], + ); +} diff --git a/lib/App/Raps2/Password.pm b/lib/App/Raps2/Password.pm new file mode 100644 index 0000000..2ac1a51 --- /dev/null +++ b/lib/App/Raps2/Password.pm @@ -0,0 +1,98 @@ +package App::Raps2::Password; + + + + +use strict; +use warnings; +use autodie; +use 5.010; + +use base 'Exporter'; + +use Crypt::CBC; +use Crypt::Eksblowfish; +use Crypt::Eksblowfish::Bcrypt qw(bcrypt_hash en_base64 de_base64); + +our @EXPORT_OK = (); +our $VERSION = '0.1'; + +sub new { + my ($obj, %conf) = @_; + + $conf{'cost'} //= 12; + + if (not (defined $conf{'salt'} and length($conf{'salt'}) == 16)) { + return undef; + } + + if (not (defined $conf{'passphrase'} and length $conf{'passphrase'})) { + return undef; + } + + my $ref = \%conf; + + return bless($ref, $obj); +} + +sub salt { + my ($self, $salt) = @_; + + if (not (defined $salt and length($salt) == 16)) { + return undef; + } + + $self->{'salt'} = $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 crypt { + 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->crypt(); + + if ($testhash eq $myhash) { + return 1; + } + return undef; +} + +1; diff --git a/lib/App/Raps2/UI.pm b/lib/App/Raps2/UI.pm new file mode 100644 index 0000000..43abf29 --- /dev/null +++ b/lib/App/Raps2/UI.pm @@ -0,0 +1,94 @@ +package App::Raps2::UI; + +use strict; +use warnings; +use autodie; +use 5.010; + +use base 'Exporter'; + +use POSIX; + +our @EXPORT_OK = (); +our $VERSION = '0.1'; + +sub new { + my ($obj) = @_; + my $ref = {}; + return bless($ref, $obj); +} + +sub read_line { + my ($self, $str) = @_; + + print "${str}: "; + my $input = readline(STDIN); + + chomp $input; + return $input; +} + +sub read_multiline { + my ($self, $str) = @_; + my $in; + + say "${str} (^D to quit)"; + + while (my $line = <STDIN>) { + $in .= $line; + } + return $in; +} + +sub read_pw { + 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); + + print "${str}: "; + $in1 = readline(STDIN); + print "\n"; + + if ($verify) { + print 'Verify: '; + $in2 = readline(STDIN); + print "\n"; + } + + $term->setlflag($term->getlflag() | POSIX::ECHO); + $term->setattr(0, POSIX::TCSANOW); + + if ($verify and $in1 ne $in2) { + return undef; + } + + chomp $in1; + return $in1; +} + +sub to_clipboard { + my ($self, $str) = @_; + + open(my $clipboard, '|-', 'xclip -l 1'); + print $clipboard $str; + close($clipboard); + return; +} + +sub output { + my ($self, @out) = @_; + + for my $pair (@out) { + printf( + "%-8s : %s\n", + @{$pair}, + ); + } + return; +} + +1; |