#!/usr/bin/perl
#
# decrypt - make available the stuff locked up in my cfs "cdata" directory
#
# Usage: see output of -help option
#
# Author: Jerry Sweet <cfs-tools@akamail.com>
#
# No warranty.  User of this program agrees to hold author(s) harmless.
#
# $Id: decrypt,v 1.1 2002/07/10 23:26:40 jsweet Rel $

sub gripe {
    #
    # Call this when you want to print a warning message.
    #

    local(@msg) = @_;

    print(STDERR "${prog}: ", @msg, "\n");
}

sub barf {
    #
    # Call this when you want to bail out with an error message.
    #

    local(@msg) = @_;

    &gripe(@msg);
    exit 1;
}

sub dbarf {
    #
    # Call this if you want to print an error message that would
    # indicate a fatal condition if we weren't in -debug mode.
    #

    local(@msg) = @_;

    if ($debug) {
	&gripe(@msg);
    }
    else {
	&barf(@msg);
    }
}

sub help {
    #
    # This is called when the user specifies the -help option on the
    # command line.
    #

    if ('$Revision: 1.1 $' =~ /revision:\s+(\S+)/i) {
        $sw_version = $1; 
    }
    else {
        $sw_version = '(unknown)';
    }

    print <<"xxEndHelpxx";
${prog} - make available the stuff locked up in my cfs "cdata" directory
Version: $sw_version
Usage: ${prog} [-cat=file] [-debug] [-help] [-init] [-shell] [-unique] 
                 [-verbose] [-] [dir]
Options:
  -cat=file      temporarily decrypt dir long enough to cat file to stdout
  -debug         print out what is going to be done without doing it
  -help          print this help message and exit
  -init          initialize encrypted directory (default: "notes")
  -shell         decrypt dir and cd to it, only until interactive shell exits
  -unique        use a unique cleartext directory name (default: \$LOGNAME-dir)
  -verbose       print out what is going to be done before doing it
  -              read key from stdin (default: read /dev/tty, non-echoing)
  dir            directory to decrypt (default: "notes", aka \~/cdata/notes)

Examples:

  1) Create a new ~/cdata/notes private directory:
        decrypt -init
     (You'll be prompted thrice for a key.  Remember it for future reference.)

  2) Make available the contents of the ~/cdata/notes private directory:
        decrypt
     (You'll again be prompted for a key.  Use the key with which you
     created the private directory previously.)

  3) Create a different private directory named "pw" (~/cdata/pw):
        decrypt -init pw
     (You'll be prompted thrice for a key.  Remember it for future reference.)

  4) Search for a pattern in private file ~/cdata/pw/passwords:
        decrypt -cat=passwords -unique pw | grep -i pattern
     (You'll be prompted for a key, as usual.)

No warranty.  User of this program agrees to hold author(s) harmless.
xxEndHelpxx
}

sub get_command_line_options {
    #
    # This checks the command line options that we know about
    # and bails if we get one that we don't know about.
    # We accept MH-style single-dash word options or GNU-style 
    # double-dash ("--") word options.
    #

    local($_);

    while ($#ARGV >= 0) {
	$_ = shift @ARGV;

	if ($no_more_options) {
	    push(@args, $_);
	    next;
	}
	else {
	    if (/^-{1,2}$/) {
		$use_stdin = '-- ';
		next;
            }

	    if (/^-{1,2}(c|ca|cat)$/) {
		++$do_catfile; 
		next;
	    }

	    if (/^-{1,2}(c|ca|cat)=/) {
		++$do_catfile;
		$catfile = $';
		next;
	    }

	    if (/^-{1,2}(d|de|deb|debu|debug)$/) {
		++$debug;
		next;
	    }

	    if (/^-{1,2}(h|he|hel|help)$/) {
		&help;
		exit 1;
	    }

	    if (/^-{1,2}(i|in|ini|init)$/) {
		++$do_init;
		next;
	    }

	    if (/^-{1,2}(s|sh|she|shel|shell)$/) {
		++$do_shell;
		next;
	    }

	    if (/^-{1,2}(u|un|uni|uniq|uniqu|unique)$/) {
		++$unique;
		next;
	    }

	    if (/^-{1,2}(v|ve|ver|verb|verbo|verbos|verbose)$/) {
		++$verbose;
		next;
	    }

	    if (/^[^\-]/) {
		push(@args, $_);
		next;
	    }

	    if (/^-{1,2}$/) {
		++$no_more_options;
		next;
	    }
	}

	&barf("unrecognized command line option \"$_\" - for help use -help");
    } 
}

sub wait_for_attach {
    #
    # Wait for the attached cleartext directory to become available.
    # Tell the user what's going on and print some dots to pass the time.
    #

    local($verbosely) = @_;

    if (! $verbosely) {
	return 1 if $debug;

	for ($n = 0; $n < $time_to_wait && ! -d $attached_dir; $n++) {
	    sleep(1);
	}

	return -d $attached_dir;
    }

    $wait_msg = "Waiting for cfsd to attach $attached_dir";

    $prefix_length = length($wait_msg);
    if ($prefix_length > 64) {
	$prefix_length = 8; 
	$prefix_too_long++;
    }

    $newline_dots_prefix = "\n" . ' ' x $prefix_length;
    $dots_width = 72 - $prefix_length;
    $dots_printed = 0;

    if (! $debug) {
    	for ($n = 0; $n < $time_to_wait && ! -d $attached_dir; $n++) {
 	    sleep(1);
    
 	    if ($n > 5) {
 		if ($n == 6) {
 		    print($wait_msg);
 		    $save_flush = $|;
 		    $| = 1;
 		    if ($prefix_too_long) {
 			print($newline_dots_prefix);
 		    }
 		}
 		print(".");
 		$dots_printed++;
 		if (($dots_printed % $dots_width) == 0) {
 		    print($newline_dots_prefix);
 		}
 	    }
    	}
    
    	if ($dots_printed > 0) {
 	    print("\n");
 	    $| = $save_flush;
    	}   
    }

    if (! $debug && ! -d $attached_dir) {
	$exit_status = 1;
	$return_status = 0;

	print("Cfsd still hasn't attached $encrypted_dir to $attached_dir.\n",
	      "If and when cfsd gets around to it:\n"); 
    }
    else {
	print("Files are now available as cleartext in ${attached_dir}/.\n");
	$return_status = 1;
    }

    print("- Idle timeout is $timeout minutes.\n",
	  "- You can end your session with this command:\n",
	  "\n",
	  "      cdetach $attach_name\n\n");

    return $return_status;
}

sub cat_file_and_detach {
    #
    # This is called when the -cat=file option is applied on the
    # command line.
    #

    return unless &wait_for_attach(0);

    if ($debug || $verbose) {
        print("\tcd ${attached_dir}\n",
	      "\t$cat_prog ${catfile}\n",
	      "\tcd\n",
	      "\t$cdetach_prog ${attach_name}\n");

	return if $debug;
    }

    if (! $debug && ! chdir($attached_dir)) {
	&gripe("problem changing directory to \"${attached_dir}\" - $!");
    }

    $st = system("$cat_prog $catfile");
    $exit_status = $exit_status || ($st >> 8) != 0;
    
    if (! $debug && ! chdir) {
	&gripe("problem changing directory to home directory - $!")
    }

    $st = system("$cdetach_prog $attach_name");
    $exit_status = $exit_status || ($st >> 8) != 0;
}

sub run_interactive_shell_and_detach {
    #
    # This is called when the -shell option is given on the
    # command line.  This changes our working directory to
    # the private cleartext directory and drops us into an
    # what we expect to be an interactive shell, e.g. /bin/bash.
    # After the user exits the sub-shell, we apply the CFS
    # tool "cdetach" automatically.
    #

    return unless &wait_for_attach(1);

    print("Starting shell $ENV{SHELL} in \"${attached_dir}\"...\n",
	  "  Note - \"cdetach ${attach_name}\" will be applied\n",
	  "  automatically upon exit.\n\n");

    if ($debug || $verbose) {
	print("\tcd ${attached_dir}\n",
	      "\t$ENV{SHELL}\n");
    }

    if (! $debug) {
	if (! chdir($attached_dir)) {
	    &gripe("problem changing directory to \"${attached_dir}\" - $!");
	}

	$st = system("$ENV{SHELL}");
	$exit_status = $exit_status || ($st >> 8) != 0;
    }
    
    if ($debug || $verbose) {
	print("\tcd\n",
	      "\t$cdetach_prog $attach_name\n");
    }

    if (! $debug) {
	if (! chdir) {
	    &gripe("problem changing directory to home directory - $!");
	}

	if (-d $attached_dir) {
	    $st = system("$cdetach_prog $attach_name");
	    $exit_status = $exit_status || ($st >> 8) != 0;
	}
	else {
	    print("\"${attached_dir}\" already cdetached.\n");
	}
    }
}

sub create_encrypted_dir {
    #
    # This is called when the -init option is specified on the
    # command line.  This creates the specified private directory,
    # or ~/cdata/notes by default.  To do this, we apply the
    # CFS tool "cmkdir", which prompts twice for a Key.
    #

    local($st);

    return unless $do_init;

    #
    # Check for things that we need.
    #

    foreach $prog (($cmkdir_prog, $mkdir_prog)) {
	if (! -x $prog) {
	    &dbarf("$prog - required program not found for -init.  ",
		   "Apply mkdir and cmkdir by hand, or modify \"${prog}\".");
	}
    }

    #
    # Create the private directory's parent directory if necessary by
    # using "mkdir -p".
    #

    print("Creating private directory ${encrypted_dir_parent}/",
	  "${attach_suffix}...\n"); 

    if (! -d $encrypted_dir_parent) {
	if ($debug || $verbose) {
	    print("\t$mkdir_prog -p ${encrypted_dir_parent}\n");
	}

	if (! $debug) {
	    $st = system("$mkdir_prog -p $encrypted_dir_parent");
	    if ($st >> 8) {
		&barf("problem creating parent directory ",
		      "$encrypted_dir_parent - $mkdir_prog exit status ", 
		      $st >> 8);
	    }
	}
    }

    #
    # Visit the private directory's parent directory and check to see
    # whether the private directory already exists.
    #

    if ($debug || $verbose) {
	print("\tcd ${encrypted_dir_parent}\n");
    }

    if (! $debug) {
	if (! chdir($encrypted_dir_parent)) {
	    &barf("problem changing directory to $encrypted_dir_parent - $!");
	}
    }

    if (-e $attach_suffix) {
	&gripe("cannot initialize \"${attach_suffix}\" - ",
	       "it already exists.");
	return;
    }

    # Invoke the CFS tool "cmkdir" to create the private directory.
    #
    # Improvements needed: if this had been written in Expect, we could've
    # used "stty" in order to turn off echoing ourselves prior to reading
    # the Key only once (or maybe twice) for -init.
    # Prompting three times for the Key is kinda lame (the third time being
    # when we later invoke "cattach").
    # Instead, this script could be modified to use the CPAN IO::stty module
    # in order to get non-echoing input a la getpass(3).  Doing that,
    # however, narrows the audience of this script to those who can
    # download and install CPAN modules for themselves.
    # 

    if ($debug || $verbose) {
	print("\t$cmkdir_prog ${attach_suffix}\n");
    }

    if (! $debug) {
	$st = system("$cmkdir_prog $attach_suffix");
	if ($st >> 8) {
	    &barf("problem initializing ciphertext subdirectory ",
		  "$attach_suffix - $cmkdir_prog exit status ", $st >> 8); 
	}
    }

    #
    # Tell the user in detail about what we just did.
    #

    if (-d $encrypted_dir || $debug) {
	print("The encrypted directory \"${encrypted_dir}\"\n",
	      "has been created.  Next we'll try to attach it.\n",
	      "Once the attachment is complete, you can put your cleartext\n",
	      "files into \"${attached_dir}\".\n",
	      "\n",
	      "Enter your Key once more for the attach process.\n\n");
    }
    else {
	&barf("hmmm...$encrypted_dir wasn't created as expected.")
	    unless $debug;
    }
}

sub check_for_required_stuff {
    #
    # Here we attempt to ensure that various required programs,
    # whose paths are specified in the Customization Section below,
    # do indeed exist.
    #

    local($no_nfs_check);

    # Check for the CFS program "cattach".

    if ( ! -x $cattach_prog ) {
	&dbarf("\"${cattach_prog}\" - required program not found.");
    }
    
    # Check for the CFS program "cdetach".

    if (($do_catfile || $do_shell) && ! -x $cdetach_prog) {
	&dbarf("\"${cdetach_prog}\" - required program not found.");
    }
    
    # Check that we have a defined value for $attach_prefix.

    if (! defined($attach_prefix)) {
	&dbarf("attachment prefix not defined - normally this is ",
	       "defined as your user id (from environment variable ",
	       "\$LOGNAME)");
    }
    
    # Check that we have a "pidof" program (or custom equivalent).

    if (! -x $pidof_prog) {
	&gripe("\"${pidof_prog}\" - desired program not found.");
	++$no_nfs_check;
    }

    # Check that we have a running NFS rpc.mountd (or equivalent).

    return if ($no_nfs_check && ! $debug);

    if ($debug || $verbose) {
	print("\t$pidof_prog \"${mountd_process_name}\"\n");
    }

    if (! $debug) {
	local($mountd_pid);

	($mountd_pid = `$pidof_prog "$mountd_process_name"`) =~ s/\s+$//;
    
	if (! $mountd_pid) {
	    &gripe("NFS mount daemon \"${mountd_process_name}\~ does not",
		   " appear to be running - this may not work.");
	}
    }   
}


#
# Main
#

# ========================================================================
# Customization section
#

#
# Idle timeout (in minutes) for cattach.
#
$timeout = 10;

#
# Top-level mount point for cattach-ed cleartext directories.
#
$cfs_root = '/mnt/crypt';

#
# Location of cattach program.
#
$cattach_prog = '/usr/bin/cattach';

#
# Location of cdetach program.
#
$cdetach_prog = '/usr/bin/cdetach';

#
# Location of cmkdir program.
#
$cmkdir_prog = '/usr/bin/cmkdir';

#
# Location of "cat" program.
#
$cat_prog = '/bin/cat';

#
# Location of "mkdir" program.
#
$mkdir_prog = '/bin/mkdir';

#
# Name of "rpc.mountd" (or equivalent) program for which to check
# in order to verify that the necessary NFS functionality is
# available.
#
$mountd_process_name = 'rpc.mountd';

#
# Location of "pidof" program.  (This finds the process ID of a running
# program.)
#
$pidof_prog = '/sbin/pidof';

#
# Location of source cyphertext "root" directory containing
# other directories to be cattach-ed.
#
$cipher_root = "$ENV{HOME}/cdata";

#
# Default subdirectory (under $cipher_root) to be cattach-ed.
#
$default_cipher_source = "notes";

#
# Name of attachment point (e.g. your login id, $ENV{LOGNAME}).
#
$attach_prefix = "$ENV{LOGNAME}";

#
# Time to wait until the cattach completes.
#
$time_to_wait = 60;

#
# ========================================================================

# Get the base name by which this program was invoked.
($prog = $0) =~ s|.*/||;

# Redirect the output of print() to STDERR.
select(STDERR);   

# Check the command line for options.
&get_command_line_options;

#
# To improve privacy, files and directories created by this program
# will have read/write permission only for the user who invoked this
# program.
#
umask(077);

#
# If real problems are encountered, we want to exit with a non-zero
# exit status.
#
$exit_status = 0;

#
# Check for required programs.
#
&check_for_required_stuff;

#
# Figure out where the encrypted source directory is supposed to be
# and whether it exists.
#

if ($#args >= 0) {
    if ($do_catfile && ! $catfile) {
	$catfile = shift @args;

	if (! $catfile) {
	    &barf("missing argument for -cat");
	}
    }

    $source_dir = shift @args;

    # If the argument specifies an absolute path, use that as-is.
    # Otherwise, look for it under the relative "root" $cipher_root.

    if ($source_dir =~ m|^/|) {
	$encrypted_dir = $source_dir;
    }
    else {
	$encrypted_dir = "${cipher_root}/$source_dir";
    }
}
else {
    $encrypted_dir = "${cipher_root}/$default_cipher_source";
}


#
# Figure out what the suffix of the attachment point should be
# and the parent directory of the encrypted source directory.
#

if ($encrypted_dir =~ m|[^/]+$|) {
    $attach_suffix = $&;
    $encrypted_dir_parent = $`;
    if (length($encrypted_dir_parent) > 1) {
	$encrypted_dir_parent =~ s|/+$||;
    }
}
else {
    &barf("unable to derive source or parent directory names from ",
	  "\"${encrypted_dir}\"");
}

#
# Figure out the name of the attachment point.
#

if ($unique) {
    $attach_name = sprintf("%d.%d.%d", $$, time, int(rand 100000000));
}
else {
    $attach_name = "${attach_prefix}-${attach_suffix}";
}

$attached_dir = "${cfs_root}/${attach_name}";

#
# If the user threw the -init switch, then try to create the
# encyphered source directory before doing anything else.
#

if ($do_init) {
    &create_encrypted_dir;
}

#
# Ensure that the encyphered source directory exists.
#

if (! -d $encrypted_dir) {
    if (! $do_init) {
	&dbarf("The encyphered directory \"${encrypted_dir}\" ",
	       "doesn't appear to exist.  Use -init, perhaps?");
    }
}

#
# Check to see whether the attachment point already exists.
#

if (-d $attached_dir) {
    &barf("The cleartext directory $attached_dir is already available.")
	unless $do_catfile;
}

#
# Change directory to the encrypted source directory parent.
#

if ($debug || $verbose) {
    print("\tcd ${encrypted_dir_parent}\n");
}

if (! $debug && ! chdir($encrypted_dir_parent)) {
    &barf("problem changing directory to \"${encrypted_dir_parent}\" - $!");
}

#
# Invoke cattach and check its status.
#

if ($debug || $verbose) {
    print("\t$cattach_prog -l -i $timeout ${use_stdin}$attach_suffix ",
	  "${attach_name}\n");
    $cattach_success = $debug;
}

if (! $debug) {
    $st = system("$cattach_prog -l -i $timeout ${use_stdin}$attach_suffix " . 
		 $attach_name);
    $cattach_success = ($st >> 8) == 0;
}   

#
# Wait for the attachment process to complete.
#

if ($cattach_success) {
    if ($do_catfile) {
	&cat_file_and_detach;
    }
    elsif ($do_shell) {
	&run_interactive_shell_and_detach;
    }
    else {
	&wait_for_attach(1);
    }

}
else {
    $exit_status = 1;
}

exit $exit_status;
