#!/usr/bin/perl -w use strict; use Curses; use Curses::Widgets; use Curses::Widgets::TextField; use Curses::Widgets::ButtonSet; use Curses::Widgets::ListBox; use Curses::Widgets::TextMemo; use Curses::Widgets::ComboBox; my $Version = 0.4; my ($mwh, $actions, $filename, $ok_edit, $info, $info1, $filelist, $s_or_r, $recipient, $sign_menu); # Widget handles my ($loop_control, $first_action, $maxy, $maxx, $fname, $output, $infile, $options, $srchoice, $sign_choice, $r_name, $list_of_keys, $short_id, $pure_id); # Various Scalars my @files; # List of files in current directory my @keys; # List of public keys (colon format) my @keylist; # Fields in colon-separated key list my @key_id_list; # Key IDs and Names for widget display my @id_list; # 8-digit hex key ID as param for '--edit-key' # Get list of files in current dir, then put a space as first item # in file list for use as an 'empty' selection. @files = sort(qx(find . -maxdepth 1 -type f \! -name '.*' -print)); unshift(@files, ' '); # Instantiate new Curses object and initialize the Curses screen $mwh = new Curses; noecho(); halfdelay(5); $mwh->keypad(1); curs_set(0); # Check that size of terminal window is OK $mwh->getmaxyx($maxy, $maxx); if ($maxx < 80 or $maxy < 24) { $mwh->erase(); endwin(); die ("Terminal is $maxx x $maxy; must be at least 80 x 24; aborting...\n"); } # Create the widgets that are used for all actions; others when needed $actions = action_widget(); $filelist = filelist(); $filename = file_in(); $info = info_win(); $ok_edit = ok_or_edit(); # Retrieve value from 'actions' widget and select appropriate routine, # then blank 'actions' widget to give a clear screen for other widgets $actions->execute($mwh); $first_action = $actions->getField('VALUE'); erase_widget($actions, $mwh); if ($first_action == 0) { # Action is 'Verify' $loop_control = 1; while ($loop_control) { # 'while' loop allows 'OK/Edit' option # Use 'list_files' subroutine to select a filename $fname = list_files($filelist, $filename, $info, $mwh); # Set selected name in 'filename' widget window $filename->setField(VALUE => $fname); # Display 'filename' widget and accept or edit final filename $filename->execute($mwh); $fname = $filename->getField('VALUE'); # Set 'verify' option for gpg $options = "--verify "; # Use the retrieved values to show gpg command to be executed, then # display OK/Edit button for final action $loop_control = final_phase($options, $fname, $info, $ok_edit, $mwh, $first_action); } # End of while loop } elsif ($first_action == 1) { # Action is 'Encrypt' # Instantiate special widgets $s_or_r = s_or_r(); $recipient = param_in(); $loop_control = 1; while ($loop_control) { # Ensure that '$options' has null value to begin loop $options = ''; # Use 'list_files' subroutine to select a filename $fname = list_files($filelist, $filename, $info, $mwh); # Set selected name in 'filename' widget window $filename->setField(VALUE => $fname); $filename->execute($mwh); # Get final filename and test for wildcards $fname = $filename->getField('VALUE'); if ($fname =~ /[\*\?]/) { # Filename operand contains wildcards $options .= "--encrypt-files "; } else { # Determine if file is text; if so use ASCII armor output my $ftype = qx(file -b $fname); if ($ftype =~ /text/) { $options = "-a "; } $options .= "--encrypt "; } # Display 'recipient choice' menu $s_or_r->execute($mwh); # Get chosen recipient; display 'recipient ID' widget if needed $srchoice = $s_or_r->getField('VALUE'); if ($srchoice) { # Display 'recipient' box for Recipient ID $recipient->execute($mwh); $r_name = $recipient->getField('VALUE'); $options .= "-r $r_name "; } else { $options .= "--default-recipient-self "; } # Use the retrieved values to show gpg command to be executed, then # display OK/Edit button for final action $loop_control = final_phase($options, $fname, $info, $ok_edit, $mwh, $first_action); # Clear old widgets if "Edit" was selected if ($loop_control) { erase_widget($s_or_r, $mwh); erase_widget($recipient, $mwh); } } # End of while loop } elsif ($first_action == 2) { # Action is 'Decrypt' $loop_control = 1; while ($loop_control) { # Ensure that '$options' has null value to begin loop $options = ''; # Use 'list_files' subroutine to select a filename $fname = list_files($filelist, $filename, $info, $mwh); # Set selected name in 'filename' widget window $filename->setField(VALUE => $fname); $filename->execute($mwh); # Get final filename and test for wildcards $fname = $filename->getField('VALUE'); if ($fname =~ /[\*\?]/) { $options = "--decrypt-files "; } # Use the retrieved values to show gpg command to be executed, then # display OK/Edit button for final action $loop_control = final_phase($options, $fname, $info, $ok_edit, $mwh, $first_action); } # End of while loop } elsif ($first_action == 3) { # Action is 'Sign' # Instantiate special widgets $sign_menu = sign_menu(); $recipient = param_in(); $s_or_r = s_or_r(); $loop_control = 1; while ($loop_control) { # Ensure that '$options' has null value to begin loop $options = ''; # Use 'list_files' subroutine to select a filename $fname = list_files($filelist, $filename, $info, $mwh); # Set selected name in 'filename' widget window $filename->setField(VALUE => $fname); $filename->execute($mwh); # Display 'sign_menu' for types of signatures and get a choice $sign_menu->execute($mwh); $sign_choice = $sign_menu->getField('VALUE'); # Note selected filename for use later $fname = $filename->getField('VALUE'); # Determine if file is ASCII to use ASCII armor output my $ftype = qx(file -b $fname); if ($ftype =~ /text/) { $options = "-a "; } # Set 'options' for type of sig chosen if ($sign_choice == 0) { $options .= "--clearsign "; } elsif ($sign_choice == 1) { $options .= "--detach-sig "; } elsif ($sign_choice == 2) { $options .= "--sign "; } elsif ($sign_choice == 3) { $options .= "--sign --encrypt "; # Call routines to select a recipient; set options accordingly $s_or_r->execute($mwh); $srchoice = $s_or_r->getField('VALUE'); if ($srchoice == 1) { $recipient->execute($mwh); $r_name = $recipient->getField('VALUE'); $options .= "-r $r_name "; } } else { # Indicates program logic error $mwh->erase(); endwin(); die "Program Logic Error\n"; } # Use the retrieved values to show gpg command to be executed, then # display OK/Edit button for final action $loop_control = final_phase($options, $fname, $info, $ok_edit, $mwh, $first_action); if ($loop_control) { # Erase widgets to clear screen for editing erase_widget($s_or_r, $mwh); erase_widget($recipient, $mwh); erase_widget($sign_menu, $mwh); } } # End of 'while' loop } elsif ($first_action == 4) { # List Keys # Instantiate widget for input of Key ID string $recipient = param_in(); $loop_control = 1; while ($loop_control) { # Action is 'List Keys' # Modify caption and width of 'recipient' widget and display $recipient->setField('CAPTION' => 'Key ID String/blank=All'); $recipient->execute($mwh); # Erase 'recipient' widget for now erase_widget($recipient, $mwh); # Get key IDs to list; store in the 'fname' var $fname = $recipient->getField('VALUE'); # Add a 'less' pipe if all keys are to be listed if ($fname eq " " or $fname eq "") { $fname = "|less"; } # Set 'list-keys' option $options = "--list-keys "; # Use the retrieved values to show gpg command to be executed, then # display OK/Edit button for final action $loop_control = final_phase($options, $fname, $info, $ok_edit, $mwh, $first_action); } # End of while loop } elsif ($first_action == 5) { # Edit Keys # Make a list of all public keys in colon format for ease of parsing @keys = qx(gpg --with-colons --list-keys); # Strip out all data except name and ID fields from 'pub' records foreach (@keys) { @keylist = split(/:/); if ($keylist[0] eq "pub") { # Form 8-didit ID from full ID $short_id = substr($keylist[4],8,9); my $sublist = $short_id . " " . $keylist[9]; push (@key_id_list, $sublist ); } } # Instantiate scrolling widget to show Key IDs $list_of_keys = key_list(); $loop_control = 1; while ($loop_control) { $list_of_keys->execute($mwh); # Erase 'list_of_keys' widget after selection of ID erase_widget($list_of_keys, $mwh); # Parse ID line to get 8-digit Key ID @id_list = split(/ /, $list_of_keys->getField('VALUE')); $options = "--edit-key "; $fname = $id_list[0]; $loop_control = final_phase($options, $fname, $info, $ok_edit, $mwh, $first_action); erase_widget($info, $mwh); } # End of 'while' loop } else { # Indicates Program Error $mwh->erase(); endwin(); die "Button Selection/Program Error\n"; } # Clean up Curses stuff $mwh->erase(); endwin(); # Make clean screen for start of gpg command system("clear"); # Leave the perl process and execute the gpg command in terminal exec("gpg $options $fname"); die "An 'impossible' logic error has occurred. A bug report would be a Good Thing.\n"; # For insurance # Subroutine definitions follow # Erase widget and reset window background to black # Usage: erase_widget($widget_handle, $window_handle) sub erase_widget { my($w_h, $mwh) = @_; $w_h->_init($mwh); bkgd($mwh, COLOR_BLACK); return; } # Instantiate main menu sub action_widget { my $act = Curses::Widgets::ButtonSet->new({ LENGTH => 15, VALUE => 0, INPUTFUNC => \&scankey, FOREGROUND => 'blue', BACKGROUND => 'white', BORDER => 1, FOCUSSWITCH => "\t\n", HORIZONTAL => 0, X => 1, Y => 1, LABELS => [('Verify','Encrypt','Decrypt','Sign','List Keys','Edit Keys')], }); return($act); } # Instantiate menu for type of sig wanted sub sign_menu { my $act = Curses::Widgets::ButtonSet->new({ LENGTH => 15, VALUE => 0, INPUTFUNC => \&scankey, FOREGROUND => 'blue', BACKGROUND => 'white', BORDER => 1, FOCUSSWITCH => "\t\n", HORIZONTAL => 0, X => 1, Y => 1, LABELS => [('Clear Sign','Detached Sig','Sign','Sign/Encrypt')], }); return($act); } # Instantiate menu for the usual 'OK/Edit' choice buttons sub ok_or_edit { my $okq = Curses::Widgets::ButtonSet->new({ LENGTH => 15, VALUE => 0, FOREGROUND => 'red', BACKGROUND => 'white', BORDER => 0, FOCUSSWITCH => "\n", HORIZONTAL => 1, PADDING => 5, X => 22, Y => 22, LABELS => [('OK','Edit Input')], }); return($okq); } # Instantiate menu to show/edit final file name sub file_in { my $file_in = Curses::Widgets::TextField->new({ X => 22, Y => 1, FOREGROUND => 'blue', BACKGROUND => 'white', MAXLENGTH => $maxx, COLUMNS => 34, CAPTION => 'File Selected for Action' }); return($file_in); } # Instantiate menu for Recipient ID or Key ID sub param_in { my $param_in = Curses::Widgets::TextField->new({ X => 52, Y => 8, FOREGROUND => 'blue', BACKGROUND => 'white', MAXLENGTH => $maxx, COLUMNS => 24, CAPTION => 'Recipient ID' }); return($param_in); } # Instantiate 'info' widget to show operation status and gpg command sub info_win { my $info_win = Curses::Widgets::TextMemo->new({ CAPTION => 'Progress...', COLUMNS => $maxx-4, LINES => 4, VALUE => '', FOREGROUND => 'blue', BACKGROUND => 'white', BORDER => 1, FOCUSSWITCH => "\t", CURSORPOS => 0, TEXTSTART => 0, X => 1, Y => 12, READONLY => 0, }); return($info_win); } # Instantiate 'info1' widget for extra 'Edit Key' help sub info1_win { my $info1_win = Curses::Widgets::TextMemo->new({ CAPTION => '', COLUMNS => $maxx-4, LINES => 1, VALUE => 'Enter "help" at "Command>" prompt to see "edit-key" cmds', FOREGROUND => 'white', BACKGROUND => 'blue', BORDER => 1, FOCUSSWITCH => "\t", CURSORPOS => 0, TEXTSTART => 0, X => 1, Y => 18, READONLY => 0, }); return($info1_win); } # Instantiate menu to select encrypt 'to self' or 'to named recipient' sub s_or_r { my $act = Curses::Widgets::ButtonSet->new({ LENGTH => 16, VALUE => 0, INPUTFUNC => \&scankey, FOREGROUND => 'blue', BACKGROUND => 'white', BORDER => 1, FOCUSSWITCH => "\t\n", HORIZONTAL => 0, X => 60, Y => 1, LABELS => [('to Self', 'to Named ID')], }); return($act); } # Instantiate scrolling widget to show files in current dir sub filelist { my $cb = Curses::Widgets::ComboBox->new({ CAPTION => 'Select the File', CAPTIONCOL => 'blue', COLUMNS => 72, MAXLENGTH => 80, MASK => undef, VALUE => 'Down Arrow for List', INPUTFUNC => \&scankey, FOREGROUND => 'blue', BACKGROUND => 'white', BORDER => 1, BORDERCOL => 'blue', FOCUSSWITCH => "\n\t", CURSORPOS => 0, TEXTSTART => 0, PASSWORD => 0, X => 1, Y => 1, READONLY => 0, LISTITEMS => \@files, }); return $cb; } # Subroutine displays list of files in current dir; returns name selected # Invoke as list_files($filelist, $filename, $info, $mwh) sub list_files { my ($filelist, $filename, $info, $mwh) = @_; my $file_sel; # Display the filelist widget $filelist->execute($mwh); # Get file selected $file_sel = $filelist->getField('VALUE'); # Erase filelist widget and redraw the others erase_widget($filelist, $mwh); $filename->draw($mwh, 0); $info->setField(VALUE => '...Waiting for remainder of input'); $info->draw($mwh, 0); # Return the selected filename return($file_sel); } # Subroutine to show command to be executed and offer OK/Edit button # for final action # Invoke as final_phase($options, $fname, $info, $ok_edit, $mwh, $first_action) sub final_phase { # Use the retrieved values to show command to be executed my($options, $fname, $info, $ok_edit, $mwh, $first_action) = @_; $info->setField('CAPTION' => ' >>> The gpg Command Will Be...'); if ($first_action == 5 ) { # 'Edit Key' requires different message my $output = "gpg " . $options . $fname . "\nChoose OK or Edit; hit 'Return'\n"; $info->setField('VALUE' => $output); $info->draw($mwh, 0); # Instantiate and draw extra widget for added help with 'Edit-Key' action $info1 = info1_win(); $info1->draw($mwh,0); } else { my $output = "\ngpg " . $options . $fname . "\nChoose OK or Edit; hit 'Return'"; $info->setField('VALUE' => $output); $info->draw($mwh, 0); } # Display OK/Edit button for final action $ok_edit->execute($mwh); return($ok_edit->getField('VALUE')); } sub key_list { my $cb = Curses::Widgets::ComboBox->new({ CAPTION => 'Select a Key ID to Edit', CAPTIONCOL => 'blue', COLUMNS => 72, MAXLENGTH => 80, MASK => undef, VALUE => 'Hit Down-Arrow to See Key ID List', INPUTFUNC => \&scankey, FOREGROUND => 'blue', BACKGROUND => 'white', BORDER => 1, BORDERCOL => 'blue', FOCUSSWITCH => "\n\t", CURSORPOS => 0, TEXTSTART => 0, PASSWORD => 0, X => 1, Y => 1, READONLY => 0, LISTITEMS => \@key_id_list }); return $cb; } =pod =head1 NAME 'mygpg' - a curses interface for the execution of routine gpg commands. =head1 DESCRIPTION This script uses a curses interface in Q-and-A style to construct a gpg command line for common operations. There is no need to remember the syntax of gpg commands or options. =head1 README 'mygpg' uses a curses interface in Q-and-A style to construct a gpg command line for common operations. There is no need to remember the syntax of gpg commands or options. The 'gpg' command that is run when the script exits uses the gpg defaults found in the gpg configuration file, normally '~/.gnupg/gpg.conf'. This includes all options except 'armor' and 'default-recipient-self', as explained below. The use of ASCII armor for the output is controlled by the type of file being encrypted or signed. The file type is determined by the Unix 'file' command: text files produce armored output whereas non-text files produce the default output type defined in the gpg configuration file. During 'encrypt' operations, selecting 'Self' in the 'Recipient' widget forces the use of the '--default-recipient-self' param on the gpg command line. The file selection widgets are scrolled with the up- and down-arrow keys. To select a choice, hit 'Return.' To pass from one widget to the next, hit 'Return' or 'Tab.' 'mygpg' operates on the file(s) in the current working directory, and saves output there as well. The text in the 'filename' widget box can be edited to include wildcards so that multiple files can be encrypted or signed. =head1 PREREQUISITES =over =item strict =item Curses =item Curses::Widgets =item Curses::Widgets::TextField =item Curses::Widgets::ButtonSet =item Curses::Widgets::ListBox =item Curses::Widgets::TextMemo =item Curses::Widgets::ComboBox =item And, of course, 'gpg'. =back =head1 SCRIPT CATEGORIES UNIX : System_administration =head1 AUTHOR / COPYRIGHT Howard L. Arons, hlarons@CPAN.org Copyright (c) 2005 by Howard L. Arons. All Rights Reserved. This script is free software; you can redistribute it and/or modify it under the same terms as Perl itself. If you have suggestions for improvement, please e-mail me. If you make improvements, kindly send me a copy of your changes. Thanks. =cut