#!/usr/bin/env perl
use strict;
use warnings;
# Autor: Boris Däppen, 2018
# No guarantee given, use at own risk and will

# PODNAME: unding
# ABSTRACT: dark magic, encrypted wallet

use v5.6.0;

use Cwd qw(getcwd abs_path);
use File::Copy;
use File::Path 'remove_tree';
use File::Slurp;
use Term::ReadKey;
use Data::Serializer;
#cpanm Crypt::CBC
#cpanm Crypt::Blowfish

my  $DATA_ptr;
our $old_state; # what was in DATA, when script started
our $new_state; # what should be in DATA, after script ends
our $encrypt_mode = 0;

INIT {
    $DATA_ptr = tell DATA;        # where is DATA?
    $old_state = join "", <DATA>; # slurp
}

##########################################
# handle special case for App::FatPacker #
##########################################

# print this file, fatpacked to STDOUT
if( defined $ARGV[0] and $ARGV[0] eq '--fatinit') {

    # try to load module App::FatPacker
    my $fatpack_loaded = eval {
          require App::FatPacker;
          App::FatPacker->import();
          1;
    };

    if($fatpack_loaded) {
        my @options = ('pack', $0);
        # fatpack this very file and exit
        App::FatPacker->new()->run_script(\@options);
        # delete caching directory
        remove_tree('fatlib');
        exit 0;
    }
    else {
        # inform user, that this option needs App::FatPacker installed
        print STDERR "You need to install App::FatPacker to use this feature\n";
        exit 1;
    }
}

#########################################
# see if help needs to be given to user #
#########################################

my $help_message = <<'END_HELP';

  Encrypt a file. Content will be stored in unding.
  Attention: File will remain on disk!

      unding /path/to/file

  Decrypt and display content stored in unding.

      unding

END_HELP

# evaluate if help should be shown
if (      # decrypt (no arg given) but nothing stored
          (not defined $ARGV[0] and (length($old_state) < 2))
          # encrypt (arg given) but --help instead of path
       or (defined $ARGV[0] and $ARGV[0] =~ /^(-h|--help)/)
   ) {
     print STDERR $help_message;
     exit 1;
}

###########################
# check write permissions #
###########################

# evualuate if everything is fine for writing, if needed
if ( defined $ARGV[0] ) {
    if (not -w $0) {
        print STDERR "Write permissons on script needed.\n";
        my $cwd = getcwd;
        my $exe = abs_path($0);
        copy ($exe, $cwd);
        print STDERR "Copied $exe to $cwd for you.\n";
        print STDERR "Use your new local copy.\n";
        exit 1;
    }
    if (not length($old_state) < 2) {
        print STDERR "Existing data will be overwritten.\n";
        print STDERR "CTRL+C to abort, ENTER to continue";
        ReadLine(0);
    }
}

###############################
# process normal user request #
###############################

# read password, without any prompt
print STDERR 'Password: ';
ReadMode('noecho');
my $password = ReadLine(0);
ReadMode('restore');
print STDERR "\n";

my $obj = Data::Serializer->new(
                   serializer => 'Data::Dumper',
                   digester   => 'SHA-256',
                   cipher     => 'Blowfish',
                   secret     => $password,
                   portable   => '1',
                   compress   => '0',
             serializer_token => '1',
                   options    => {},
                  );

if ( defined $ARGV[0] ) {
    print STDERR 'Password confirmation: ';
    ReadMode('noecho');
    my $password_retyped = ReadLine(0);
    ReadMode('restore');
    print STDERR "\n";
    die 'Password missmatch' if ($password ne $password_retyped);
    $encrypt_mode = 1;
    my $filename = $ARGV[0];
    my $data = read_file($filename);
    $new_state =$obj->serialize($data);
}
else {
    my $data =$obj->deserialize($old_state);
    if (defined $data) {
        print $data;
    }
    else {
        print STDERR "Wrong password?\n";
    }
}

END {
    if ($encrypt_mode) {
        open DATA, '+<', $0;   # $0 is the name of this script
        seek DATA, $DATA_ptr, 0;
        print DATA $new_state;
        truncate DATA, tell DATA;  # in case new data is shorter than old data
        close DATA;
    }
}

=pod

=encoding UTF-8

=head1 NAME

unding - dark magic, encrypted wallet

=head1 VERSION

version 0.007

=head1 SYNOPSIS

This is an executable script, not a library.
If you are a first time user, you might want to consult the L</SETUP> section in this document first.

Encrypt a file. Content will be stored in unding.
Attention: File will remain on disk!

 ./unding /path/to/file

Decrypt and display content stored in unding.

 ./unding

In both cases you must enter a password, but no prompt will tell you so.

=head1 SETUP

Since this script needs write access on it self (yes!), it's best you copy it to your local directory before using.
It will do this automatically for you, if it is in writing context and doesn't have the permissions necessary.
But if you want to do it manually, this is how you do it in C<bash>:

 cp $(which unding) .

If you want to take this script with you (e.g. on a flash drive) and run it on different machines (e.g. for looking at your encrypted secrets), you might also want to take  all module dependencies with you.
You can create a copy of this script which includes all needed modules inside itself.
You need to install L<App::FatPacker> and then just run something similar to the following (here with redirection of C<stdout>):

 /usr/bin/unding --fatinit > my_packed_unding

You might also want to use the C<fatpack> interface of L<App::FatPacker> manually though.

=head1 MOTIVATION

B<Why «dark magic»?> The script uses a technique which making use of, is higly disencouraged by intelligent programmers:
L<https://stackoverflow.com/questions/41061214/write-to-the-data-section-in-perl>

B<Why the name «unding»?> C<unding> derives from the German I<Un-Ding>, meaning the negation of a thing, a I<Not-Thing> (nothing).
The negation can be meant pejoratively, relating to the I<dark magic> invoced.
But also descriptively, because the script transforms a I<thing> (text) into a I<nothing> (cypher text).

B<So what's in for me?> The I<dark magic> offers you your encrypted secrets together with the code to decrypt it in one single file.
All you need to de- and encrypt is this file and a Perl environment.
No separation of ciphertext and cryptologic.

=head1 WARNINGS

=over

=item *

This is an early release. Use it at your own risk.

=item *

While encryption, the script does not check if it overwrites existing encrypted data.

=item *

While encryption, the script does not check if the password is the same as before. Decrypt once to be sure you made no typos.

=item *

After encryption, the script does not delete the original file. You have to do this yourself. This may be considered a feature, since it might prevent loss of data.

=back

=head1 AUTHOR

Boris Däppen <bdaeppen.perl@gmail.com>

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2018 by Boris Däppen.

This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.

=cut

__DATA__

