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.
|