#!/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 Data::Serializer;
#cpanm Crypt::CBC
#cpanm Crypt::Blowfish
use Term::ReadPassword::Win32 qw(read_password);
$Term::ReadPassword::Win32::SUPPRESS_NEWLINE = 1;

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";
        # Term::ReadPassword::Win32 seems to mess up, so that the diamonds
        # don't whait anymore for an ENTER, so I use read_password instead
        read_password();
        print STDERR "\n";
    }
}

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

# to avoid any problems with the reading of passwords
# we first and foremost slurp anything from stdin
my $data_stdin = '';
if ( defined $ARGV[0] and $ARGV[0] eq '-' ) {
    my @datalist_stdin = <STDIN>;
    $data_stdin .= $_ foreach (@datalist_stdin);
}

# read password
print STDERR 'Password: ';
my $password = read_password();
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    => {},
                  );

# user wants to write
if ( defined $ARGV[0] ) {
    print STDERR 'Password confirmation: ';
    my $password_retyped = read_password();
    print STDERR "\n";
    die 'Password missmatch' if ($password ne $password_retyped);

    $encrypt_mode = 1;
    my $filename = $ARGV[0];

    # read either from slurped STDIN or the filename
    my $data;
    if ($filename eq '-') {
        $data = $data_stdin;
    }
    else {
        $data = read_file($filename);
    }

    $new_state =$obj->serialize($data);
}
# user just wants to read
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.008

=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

Or you might want to encrypt output from another program.
You can do so, using a dash.

 cat secret | perl unding -

Decrypt and display content stored in unding.

 ./unding

In all cases you must enter a password.

=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<Write to the DATA section in Perl|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 *

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__

