Cover V02, I06
Article
Listing 1
Listing 2

nov93.tar


Building a Secure Journal/Logging Utility with Encryption

Leor Zolman

I don't usually "recycle" ideas from my past writings in other publications, but for this article I decided to make an exception. Back in the June, 1991 issue of The C Users Journal I presented a C program named j, a personal journal utility (as in diary, not the computer science sense of "journal"). That program provided an easy-to-use interface for the creation of dated, sequential entries in cumulative ASCII text files, with support for multiple subject categories. While I created it specifically with personal journal writing in mind, one user later pointed out how the program could be equally useful as a software or system logging mechanism. Such a utility allows a system administrator (or team of administrators) to easily create log entries documenting any information deemed worthy of recording.

The j program turned into one of those perpetual projects for me. When I sat down to make entries in my electronic journal, about as often as not I'd first take some time to modify how j worked; often that left me no time to actually make a journal entry.

I chose C as the original implementation language for j because I was writing for CUJ and wanted to stress portability (in this case, between DOS and UNIX on the source code level). Eventually, I decided to abandon the DOS version and focus on the UNIX implementation in order to take advantage of UNIX's encryption utilities. Without the requirement for DOS support, it became apparent that j is an application better suited to implementation in shell script rather than in C. After all, most of the "work" is done by external programs. All j does is check the environment and command line for controlling parameters, initialize/concatenate files, and call up the text editor and encryption programs as needed. After loading up the original C version with features and struggling to make them all work together, I found that a total rewrite in shell script resulted in a much more coherent piece of code. That shell version is what I present in this article.

Basic Use

The j script has two links, named j and jsee. When invoked as j, it adds new journal text to the current cumulative monthly text file under the invoking user's $HOME/.Journ directory. The jsee link is used to examine and/or modify either the current cumulative text file, or any cumulative text file created by j.

Setting aside the encryption features for now, here is a description of what j does. The first time it is invoked by a user, j checks whether or not the $HOME/.Journ directory exists, and creates it if necessary. Then, j checks if a name argument was given on the command line. If not, j assumes the user wants to add some text to the current default cumulative monthly text file named $HOME/.Journ/yy-mm.ext, where yy is the current two-digit year, mm is the current two-digit month, and ext is an extension dependent upon encryption status (described later). If a name argument was given on the command line, that name is interpreted as the name of a subdirectory of the $HOME/.Journ directory in which the cumulative text file resides (or is to reside). In that case, the full pathname of the cumulative text file to use becomes: $HOME/.Journ/subdir-name/yy-mm.ext.

Once j has figured out which cumulative text file to use, it creates a text file containing a header line with the current date and encryption status, then starts up the text editor to let the user add text to the file to form a complete journal entry. When the user exits the editor, j checks to see if the file has been modified (e.g., the user has saved the work.) If not, no further action is taken -- j assumes the user has decided to abort the journal entry for whatever reason. If the file has changed, then the entire contents are appended to the cumulative text file identified above.

To bring up a previous journal entry, you use jsee. When invoked with no arguments, jsee brings up your text editor with the contents of the current default cumulative monthly journal file, $HOME/.Journ/yy-mm.ext. You may then examine the file's contents and/or edit the file as desired. jsee accepts an optional subdirectory name command-line parameter, similar to j. Unlike j, however, jsee also accepts a straight pathname of a cumulative journal text file, minus the extension, as an argument. If such a name is supplied to jsee, the appropriate encryption extension is tacked on and the result is treated as the pathname of the journal file you wish to examine. For example, the command:

jsee 93-07

would bring up the contents of a file named ./93-07.ext, where ext is an extension dependent upon encryption status. On the other hand, the command:

jsee project1

would bring up the file $HOME/.Journ/project1/yy-mm.ext, where yy-mm are the current year/month, and ext is dependent upon encryption status.

For everyday use as a personal journal manager, the operation of j/jsee couldn't be simpler. To create a new entry, just enter the command:

j

To review the current month's entries, enter:

jsee

Encryption Support

To make my journal entries as secure as possible, I decided to take advantage of UNIX's crypt command to implement automatic encryption/decryption of journal text. This turned into a bigger Pandora's box than I ever could have imagined, due primarily to the inconsistency of crypt features across UNIX platforms. My primary work with j/jsee has been under SCO XENIX and SCO UNIX (two products from the same vendor), yet the crypts from just these two platforms contain a surprising number of dissimilarities that I've had to build in support for. And the crypt command I've experimented with on yet a third Unix platform, Encore UMAX, differs slightly from both the others.

Vanilla crypt

First, I'll to take a look at the common, standard usage of the crypt command. In its basic form, crypt encrypts the plain text provided on its standard input stream and sends the encrypted text to its standard output. The user can specify the encryption key as the only option on the command line. Thus, the command:

crypt foobar <file.1 >file.2

creates a file named file.2 having exactly the same length as the existing file file.1 and containing the text from file.1 encrypted with the key "foobar". To decrypt file.2, the command to use is:

crypt foobar <file.2 >whatever

After execution, a file named whatever should contain exactly the same plain text as was originally contained in file.1.

If the encryption key is omitted, then crypt prompts the user for the key before performing the encryption. This is probably the most secure way of using crypt; however, the way j/jsee works often requires multiple runs of crypt within a single process pipeline, and using the "no key" format is incompatible with such a usage. The only way to avoid the pipeline would be to create temporary files, and that kind of approach opens its own can of security worms.

Up to this point, all versions of crypt I've used display the same behavior, and if this was all the functionality I expected out of crypt, then there wouldn't be any problem. However, invoking crypt with the key on the command line is highly insecure under XENIX, and only marginally secure (sort of like "only a little bit" pregnant) under UNIX. Under XENIX, anyone can list the system process table with full detail using the command

ps -ef

during the entire time that the crypt process is executing. This would reveal the entire command line used to start up crypt -- including the actual encryption key specified by the crypt user. Using SCO's XENIX-specific version of crypt, I've found no way around this "hole".

The UNIX version is a little more careful to wipe out any trace of the encryption key entered on the command line. It accomplishes this by taking advantage of a new feature of crypt specific to the UNIX version: the -k option. In this version, specifying -k in place of a literal encryption key causes crypt to read the actual key out of an environment variable named CrYpTkEy. The assumption in this case is that you've defined and exported CrYpTkEy before running crypt.

The advantage of getting the key from an environment variable is that there is no simple way for any user, including the super-user, to sneak looks into another user's environment. When supplied with a literal encryption key, the UNIX crypt can quickly stuff the string into the required environment variable and then exec a new crypt with -k to overlay the original crypt process and perform the encryption. There would still be a short window of opportunity when someone running ps -ef might be able to see the encryption key, but a user would have to be real fast and lucky to catch a glimpse through that window. Of more concern would be whether or not process accounting is enabled on your system; if so, the system administrator might have access to a record of the original crypt command line with the explicit encryption key spelled out.

The best way I've found to take advantage of the -k option is to provide a way of prompting the user for an encryption key that leaves no revealing process accounting trails for nosy administrators to follow (not that any of us self-respecting administrators would ever stoop that low, of course...), and then to initialize the CrYpTkEy environment variable for use by subsequent crypt invocations. Under XENIX, where -k isn't supported, I've made the interface similar for at least the convenience, if not the security, of the user.

Configuring j/jsee

The greatest complexity of j/jsee is a result of providing the flexibility to adapt to both the differing forms of the crypt command and the varying security needs of users. While you might sense a bit of "overkill" in the range of configuration options, the end result is that once you've set it up to fit your needs, the program will be both easy to use and about as secure as you need it to be.

The comments at the start of the script (Listing 1) provide a complete reference to internal configuration variables and sensitivity to external environment variables. There are three areas involved: encryption control (whether or not to encrypt), getting the encryption key, and supplying that key to crypt as necessary.

Encryption Control

To determine whether or not the user wants the journal text to be encrypted, j/jsee examines its command line, the JCRYPT external environment variable, and the ASK_CRYPT configuration variable.

JCRYPT, the external variable, can be set to Y or N. If Y, j/jsee uses encryption unless (a) the - option appears on the command line, or (b) ASK_CRYPT is Y and the user answers "no" to the encryption prompt.

  • If JCRYPT is N, then encryption is disabled.

  • If the ASK_CRYPT configuration variable is set to Y, then j/jsee prompts the user to verify encryption when JCRYPT is set to Y. When JCRYPT is N, ASK_CRYPT has no effect.

  • When JCRYPT is undefined and no command-line options appear, encryption is disabled. If -e or -ekey is used on the command line, encryption is enabled.

  • If - appears on the command line, encryption is always disabled, regardless of any other configuration parameters.

    Whenever encryption is in effect, the extension on the cumulative journal text files manipulated during that session is .txe. Without encryption, the extension is .txt. Forcing different extensions based on encryption status helps prevent the inadvertent mixing of plain and encrypted text within the same file. Even though j/jsee always checks encrypted journal files for a signature line at the beginning to avoid encryption key conflicts, the distinct extension names let users determine whether or not journal files are encrypted without having to examine file contents.

    Getting the Encryption Key

    SCO UNIX's crypt uses an environment variable named CrYpTkEy to store the encryption key in the user's environment (for use with -k), while the Encore's crypt uses CRYPTKEY for the same purpose. To avoid having to do a conditional global replace as part of the j/jsee configuration process, I've created a configuration variable named CRYPT_KEY_VAR that lets you change just one line and forget it.

    When running j/jsee, the user is responsible for initializing the variable named by CRYPT_KEY_VAR if the local crypt supports -k (and the use of that feature is desired). Although the user can insert an assignment statement into his or her startup file to perform the initialization, that is not a good idea from a security standpoint. It would be better if there were no "hard copy" of the encryption key anywhere on the system. Alternatively, the user can enter the shell assignment explicitly before using j/jsee, e.g.,

    $ CrYpTkEy=cowabunga_dude

    In this case, the startup profile should include the export statement

    export CrYpTkEy

    so the user does not have to type it in every time. Since typing out the word CrYpTkEy can get pretty annoying, I've written a shell function named key (Listing 2) for inclusion in the startup profile. This function prompts the user for a new encryption key, reads the new key value without echoing the characters to the screen, and exports the variable.

    Screen echo is disabled during key input, so a user interrupt during the read statement would cause a return to the system prompt with screen echo still disabled. For key to be robust, it needs to respond intelligently to a user pressing the INTR key during encryption key input. key deals with this issue by defining a shell variable named TRAPPED upon receipt of a user interrupt. After the input line has been read, key tests this variable and takes appropriate action if an interrupt had indeed been processed.

    Passing the Encryption Key to crypt

    j/jsee needs to know whether or not your particular version of crypt supports the -k option. You supply this information via the internal configuration variable CRYPT_HAS_K. If set to N, then j/jsee is obliged to supply the literal encryption key on the crypt command line every time it invokes crypt.

    If CRYPT_HAS_K is set to Y, then j/jsee makes sure the environment variable named by CRYPT_KEY_VAR is set to the encryption key and the -k option is used with crypt.

    Note: even if you use the key function with a version of crypt that does not support -k, the encryption key is still stored in the environment variable named by CRYPT_KEY_VAR; in this case, j/jsee simply gets the key value out of the environment variable and explicitly includes it on the crypt command line when needed. If you should someday move to a system where -k is supported, you'd reconfigure CRYPT_HAS_K to Y and notice no change at all in any aspect of j/jsee's user interface.

    Command-Line Processing

    I didn't want to restrict the order in which things have to appear on the j/jsee command line. The format I chose for the encryption options (-e, taking an optional argument; -, having meaning, etc.), however, precluded use of the standard getopts mechanism for command-line parsing. To allow the encryption option to appear either before or after the journal-file or journal-dir arguments, I basically check for all permutations via an exhaustive brute-force approach. Since my format supports a maximum of two distinct command-line arguments, it is not prohibitively difficult to check for the five possible permutations (no args, encryption option only, name option only, or both in either order). Lines 287-320 figure out what the user is trying to say on the command line.

    j/jsee Shell Functions

    Lines 159-266 contain several shell functions used internally within j/jsee. Here are the descriptions of these functions:

    usage: Displays a usage summary.

    ed_invoke: Invokes the user's text editor on the named plain-text ASCII file. If the editor is microEMACS or Epsilon, then ed_invoke uses some editor-specific techniques to enhance the user interface. Before editing the file, ed_invoke does an ls -l on the file and saves the output. After the editing session, another ls -l is run and the result is compared to the first one. If they are identical, then a message indicating that fact is displayed and j/jsee terminates.

    get_key: Assigns the encryption key to the environment variable key, obtaining the value either indirectly through CRYPT_KEY_VAR or through direct keyboard input from the user.

    test_k: If -k is supported, "moves" the encryption key out of the key variable and into the appropriate environment variable supported by the local crypt command, then sets key to -k for use on the crypt command line.

    ask: A general purpose yes/no utility function.

    Code Walk-Through

    Execution of j/jsee begins at line 269 with a banner message, and lines 271-285 create the master journal directory, if necessary.

    Then, lines 287-320 process the command-line arguments, initializing the following environment variables:

    OPTS: the option string specified, if any

    name_given: Y if a filename or subdirectory name was specified, else N

    jdir: the pathname of the directory containing the target journal file

    jfile: the filename of the target journal file.

    Lines 326-354 process the encryption options, initializing the following environment variables:

    crypt: Y if encrypting, else N

    key: if -k is not supported, this is set to the literal value of the encryption key. If -k is supported, then this becomes -k and the external environment variable named by CRYPT_KEY_VAR is set to the encryption key value

    EXT: set to txe if encryption is in effect, else to txt.

    Lines 356-372 perform some additional processing to determine the cumulative journal text filename when jsee is invoked. This is basically to support jsee's ability to accept a journal filename as an argument.

    Lines 383-398 check for the existence of any named subdirectory supplied on the j command line, and create it if necessary. jsee does not support subdirectory creation, since its purpose is to examine existing journal text files only.

    Lines 400-409 complete the specification of the journal text file by adding an appropriate extension onto the pathname, if required. If running as jsee, then a filename argument would already have been processed into a directory and filename (with appropriate extension) by the code in lines 362-369.

    Lines 417-436 create a brand new cumulative monthly text file. This happens whenever it is time for the first entry of a new month, in the main $JOURN_DIR directory or any of its subdirectories.

    If the cumulative journal file already exists and encryption is in effect, then lines 437-445 check encryption key synchronization. Since appending text onto an encrypted data file necessarily involves decrypting the original file, appending the new text to the plain text of the original file, and encrypting the entire resulting file, there is a potential for disaster if a different key is used during the append process than was used to encrypt the earlier data. If j didn't check to make sure that the current encryption key successfully decrypts the existing cumulative text file, the result of misspelling an encryption key would be total loss of all previous cumulative monthly data. Whenever a new cumulative file is initialized, a signature line containing the word "Journal" is written at the beginning of the file. Thereafter, any time you run j/jsee to create a new entry or examine an existing cumulative file, lines 438-444 decrypt that file with your currently selected encryption key and make sure the word Journal is there before letting you do anything else.

    With all these preliminaries out of the way, j/jsee can finally do its real work. If the command was invoked as jsee, then lines 447-464 execute the jsee logic to update the specified cumulative journal file. If the command was invoked as j, then lines 466-489 create a skeletal journal entry, let the user edit it, and then append the edited text onto the selected cumulative journal file.

    Last Thoughts

    There are some subtle "gotchas" involved in using crypt portably, and I've tried to cover the most relevant issues in the j/jsee showcase. You may find j/jsee useful in its own right as a journal and/or logging utility, or you can extract whichever pieces you may need to help construct your own encryption-related applications. I'll probably keep on tinkering with this code for years -- if you're curious about what it might evolve into after another year or two, just e-mail me then and I'll be glad to send you back the latest (greatest?) version.

    About the Author

    Leor Zolman wrote BDS C, the first C compiler targeted exclusively for personal computers. Leor is currently an instructor on UNIX topics for Boston University's Corporate Education Center, a regular contributor to The C Users Journal and Sys Admin magazines, and "Tech Tips" editor for Windows/DOS Developer's Journal. His first book, Illustrated C, was recently published by R&D Publications, Inc. He may be contacted at 74 Marblehead St., North Reading, MA 01864, or on Usenet/Internet as: leor@bdsoft.com.


     



  •