#!/usr/bin/perl -w
###############################################################################
#
# This script encrypts some data such that the original content can be
# retrieved by different combinations of several user's passwords.
#
# Example: There's Alice, Bob and Eve. Each of them gets a password.
# Then let's define the groups as pairs of any two: (Alice, Bob), (Bob, Eve),
# (Alice, Eve). The decryption then works if you know the all user passwords of
# any group. In this case you need the passwords of two of them.
#
# Groups can be defined arbitrarily in terms of users and size (as long as > 0).
# It is NOT checked if a groups is repeated. The user order does not matter but
# group repetition with a different order is also not checked.
#
# This is achieved by:
# 1. Generate one password per user
# 2. Encrypt the data file with a master key
# 3. Encrypt the master key with the group password.
#    The group password is the sorted concatenation of the respective
#    user passwords.
# 4. Pack all the file together in a tar archive.
# 5. Keep track of checksums and user passwords and show required information.
#
# For this, the following tools are required (and hence dependencies):
# * perl
# * mktemp
# * pwgen
# * gpg
# * tar
#
# Due to entropy limits on most systems, the script should not be run too often.
# Make sure your groups are sound before running the program. If you need to
# experiment, temporarily set the $KEY_LEN_MASTER variable to a lower value.
#
# Run without parameters or '-h' to see the syntax.
#
# This script is part of the keysafe toolchain.
#
################################################################################
#
# Copyright (c) 2014, Matthias Baumgartner
# All rights reserved.
# 
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#     * Redistributions of source code must retain the above copyright
#       notice, this list of conditions and the following disclaimer.
#     * Redistributions in binary form must reproduce the above copyright
#       notice, this list of conditions and the following disclaimer in the
#       documentation and/or other materials provided with the distribution.
#     * Neither the name of the keysafe nor the
#       names of its contributors may be used to endorse or promote products
#       derived from this software without specific prior written permission.
# 
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL MATTHIAS BAUMGARTNER BE LIABLE FOR ANY
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
# 
###############################################################################

use warnings;

# Config vars
my $KEY_LEN_MASTER = 2048;
my $KEY_LEN_USER = 20;
my $CIPHER_ALGO_MASTER = 'AES';
my $CIPHER_ALGO_GROUP = 'AES';
my $ALGO_CHECKSUM = 'sha256';
my $DST_DATA = 'data.gpg';
my $DST_GRP  = 'group_';
my $DEBUG = 0;

###############################################################################

# Get the command-line params
my $syntax = "syntax: encrypt.pl </pth/to/source> </pth/to/destination> \"<Groups as: User1 User2; User2 User3; User1; User1 User2 User3>\"\n";
my $src = shift || die("ERROR: Please specify a source.\n$syntax");
if ($src eq '-h' or $src eq '--help')
{
    print "$syntax";
    print "Group definition: Each group is a whitespace-seperated list of user names. Several groups are seperated from each other by semicolon.\n";
    die;
}
my $dst = shift || die("ERROR: Please specify a destination.\n$syntax");
my $opts_groups = shift || die("ERROR: Please specify groups.\n$syntax");
@groups = split('\s*;\s*', $opts_groups);
die("ERROR: Please specify groups.\n$syntax") unless $#groups > 0;

###############################################################################

# Initialization
my $dst_tmp = `mktemp -d`;
$dst_tmp =~ s/\n//;
print "Temporary destination: $dst_tmp\n" if $DEBUG;

# Generate random master key
my $key_master;
if ($DEBUG)
{
    $key_master = `gpg --armor --gen-random 0 ${KEY_LEN_MASTER}` || die("Key generation failed");
}
else
{
    $key_master = `gpg --armor --gen-random 1 ${KEY_LEN_MASTER}` || die("Key generation failed");
}
$key_master =~ s/\n//;
print "Master key: $key_master\n" if $DEBUG;

# Encrypt source file with the master key
my $cmd_enc_data = "gpg --batch --symmetric --cipher-algo ${CIPHER_ALGO_MASTER} --passphrase-fd 0 --output ${dst_tmp}/${DST_DATA} ${src}";
print "\$ $cmd_enc_data\n" if $DEBUG;
open(FD_DATA, "|-", $cmd_enc_data);
print FD_DATA $key_master;
close FD_DATA || die("Encryption failed");

# Go through groups, create slot and create users on the fly
my $idx_group = 0;
my @tmp_groups;
my %users;
foreach my $ref_groups (@groups)
{
    $idx_group += 1;
    print "Groups: $ref_groups\n" if $DEBUG;
    @grp_users = split('\s+', $ref_groups);

    # Add users on the fly
    my @keys_users;
    foreach my $user (@grp_users)
    {
        # Create user password, if not done yet
        unless(exists $users{$user})
        {
            $pw = `pwgen -B ${KEY_LEN_USER}`;
            $pw =~ s/\n//;
            # Remove de/en ambiguous chars from the passwords
            $pw =~ s/y/k/i;
            $pw =~ s/z/l/i;
            $users{$user}{'password'} = $pw;
            $users{$user}{'groups'} = [];
            print "password for $user: $pw\n" if $DEBUG;
        }

        # Add to group password
        push(@keys_users, $users{$user}{'password'});

        # Add to user-view
        push($users{$user}{'groups'}, join(', ', @grp_users));
    }

    # Sort passwords
    @keys_users = sort @keys_users;
    my $key_group = join('', @keys_users);

    print "Group password: $key_group\n" if $DEBUG;

    # Encrypt master key with group password
    my $tmp_grp = "$DST_GRP$idx_group";
    push(@tmp_groups, $tmp_grp);
    my $cmd_enc_group = "gpg --batch --symmetric --cipher-algo ${CIPHER_ALGO_GROUP} --passphrase-fd 0 --output ${dst_tmp}/${tmp_grp}";
    print "\$ $cmd_enc_group\n" if $DEBUG;
    open(FD_GRP, "|-", $cmd_enc_group);
    print FD_GRP $key_group . "\n";
    print FD_GRP $key_master;
    close FD_GRP or die("Encryption failed");
}

# Get list of all relevant files
my $all_files = ${DST_DATA} . ' ' . join(' ', @tmp_groups);
print "All files: $all_files\n" if $DEBUG;

# Pack all encrypted data and write to target file
my $cmd_tar = "tar -czf ${dst} -C ${dst_tmp} ${all_files}";
print "\$ $cmd_tar\n" if $DEBUG;
system($cmd_tar);
die("Packing failed") if ($? != 0);

# Get the destination's checksum
my $cmd_checksum = "gpg --print-md ${ALGO_CHECKSUM} ${dst}";
print "\$ $cmd_checksum\n" if $DEBUG;
my $checksum = `$cmd_checksum`;
$checksum =~ s/[\r\n\s]+/ /g;
$checksum =~ s/^.*:\s*//;
print "Checksum: $checksum\n" if $DEBUG;

# Remove all temporary files
unlink "$dst_tmp/$_" foreach @tmp_groups;
unlink "$dst_tmp/$DST_DATA";
rmdir $dst_tmp;

###############################################################################

# Pretty print user sheets
foreach $user (keys %users)
{
    # FIXME: Print partners without repeating current user
    # FIXME: Print partners on new lines, with identation
    # FIXME: Printable format (PDF)
    # FIXME: Print password as QR-code
    my $password = $users{$user}{'password'};
    my @partners = join("; ", @{$users{$user}{'groups'}});
    print
"
-------------------------------------------------------------------------------

User name      : $user
User password  : $password
File checksum  : $checksum
Valid combos   : @partners

You MUST enter the password by yourself, in presence of your partners and in
a secure environment (bypassers, trusted hardware, no internet connection).

-------------------------------------------------------------------------------

";
}



## EOF ##
