#!/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
}

######################################################
# --fatinit / 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;
    }
}

#################
# --init option #
#################

if( defined $ARGV[0] and $ARGV[0] eq '--init') {
        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 0;
}

#########################################
# 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";
        if($^O eq 'MSWin32') {
        	open(TTY, "CON") or die $!;
        }
        else {
        	open(TTY, "</dev/tty") or die $!;
        }
        ReadLine(0, *TTY);
        close(TTY);
    }
}

###############################
# 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: ';
#https://www.perlmonks.org/?node_id=570552
if($^O eq 'MSWin32') {
	open(TTY, "CON") or die $!;
}
else {
	open(TTY, "</dev/tty") or die $!;
}
ReadMode('noecho', *TTY)  unless ($^O eq 'MSWin32');
my $password = ReadLine(0, *TTY);
ReadMode('restore', *TTY) unless ($^O eq 'MSWin32');
close(TTY);
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: ';
	#https://www.perlmonks.org/?node_id=570552
	if($^O eq 'MSWin32') {
		open(TTY, "CON") or die $!;
	}
	else {
		open(TTY, "</dev/tty") or die $!;
	}
    ReadMode('noecho', *TTY)  unless ($^O eq 'MSWin32');
    my $password_retyped = ReadLine(0, *TTY);
    ReadMode('restore', *TTY) unless ($^O eq 'MSWin32');
    close(TTY);
    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.011

=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.
You can explicitly do this via the C<--init> option:

 unding --init

I<(The script will do this automatically, if it is in writing context and doesn't have the permissions necessary.)>

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.

=item *

Under Windows your password input is visible!
As soon there is a technical solution, of how to hide the password on Windows, it will be implemented.
Patches welcome.

=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__

