What it does is to write out a small preamble, which consists of some bootstrapping data, and then the main data, which consists of the encrypted state. Both of these are written to the same file on disk.
Yenta's actual persistent state is a variable-length string of bytes, called D. [We do not compute a MAC of D; perhaps we should if we can. This would provide some protection against an attack that changes bit(s) of ciphertext (hence trashing the plaintext), but it would require somehow either precomputing a checksum, or computing one on the fly as data is written out. Both are somewhat inconvenient.]
When Yenta first starts up, it asks the user for a passphrase, P. This passphrase does not change unless the user manually changes it. Yenta immediately computes the SHA-1 hash of the passphrase, PSHA, and throws away P.
It then writes out the following data:
Yenta then prompts the user for the passphrase, P, and computes PSHA, as above.
It then reads the encrypted session key, KP, from the preamble, and decrypts it, using the first 128 bits of PSHA as the key. This regenerates the true session key, K.
Now that K is known, Yenta continues reading, now in the encrypted portion of the file, and reads the first 128 bits from it, which should be V1V2---the two concatenated copies of V. If V1 does not match V2, then K must be incorrect. For K to be incorrect, we must have incorrectly decrypted KP, which implies that PSHA is wrong. The only way this could happen is if the user mistyped the passphrase, so we prompt again, and repeat.
Assuming that the verifier matches, we now have a correct session key, so we supply that to the decrypting ePTOB and read the rest of the file, which converts DK back to D.
It appears, as usual, that the primary vulnerabilities are (a) insecure process address space, and (b) the user picking a poor passphrase.
The most obvious defense is to write a temporary copy of the statefile, ensure that it is correct, and then atomically rename it over the old copy. This means that a crash in the middle of the write will not corrupt the existing statefile. But how do we know that the tempfile was, in fact, written correctly?
SCM does not signal any errors in any of its stream-writing functions, because it fails to check the return values of any of the underlying C calls. This means that, if the disk fills up, "write", "display", and friends will merrily attempt to fill the disk to full and bursting, and will continue dumping data overboard even after the disk is full, all without signalling any errors.
Even if we check at the beginning and the end whether the disk is full (by writing a sacrificial file and seeing if we get the bytes back when we read it), consider what happens if the disk momentarily fills in the middle of saving state, then unfills. This could easily happen if something writes a tempfile at the wrong moment. In this case, SCM will dump n bytes on the floor, then keep going. We won't have detected the failure. Even rereading the file may fail to detect it, if the dropped bytes were inside a string constant. One possible solution is to force-output after every single character, then stat the file and see if its length has incremented, or, alternatively, to write and read a sacrificial file after each character of real output. Either of these approaches is (a) extremely difficult to implement (since we write output in larger chunks, and through an encrypting stream as well), and (b) horribly inefficient, probably slowing down checkpointing by at least two orders of magnitude if not more.
To avoid this, we run a verification function over the data written, every time it is written. This function does the work of reading and checking the contents of the preamble against the running Yenta (e.g., encryption protocol version, the browser cert and browser private key, etc), and then computes the SHA-1 hash of the entire encrypted portion of the file, e.g., of the data portion in the discussion above. This is then compared with an identical hash, computed seconds earlier when the data was written to disk. If anything is wrong with the preamble or if the hashes do not match, then something is wrong with the data we just wrote; a single byte missing or even a single bit trashed will be evident.
In this case, we do not rename our obviously-corrupt tempfile over the last successfully-saved statefile. Instead, we delete it again, since it may be contributing to a disk-full condition and is bad in any event. In addition, we set a variable so the user interface knows that something is wrong, and can tell the user, who can presumably attempt to fix whatever is preventing us from successfully writing the statefile.
Note that this gives us no protection over having the statefile trashed after we have checkpointed. If Yenta is still running, the damage will be undone at the next checkpoint, since the old file will simply be thrown away unread. However, if Yenta was not running when the file was trashed, Yenta will simply fail to be able to correctly read the entire thing. (Chances are overwhelming that any corruption of the file will yield garbage after decryption that "read" will complain about, and Yenta will be unable to finish loading its variables.) In this case, the user will have no choice but to restore the file from backup. This is the expected case anyway if files are being trashed at random in the filesystem.
Note also that Yenta's support applications, which write plaintext statefiles and do not save state using encryption, do not do this checking. They save very little irrecoverable state in normal operation; the big exception is the statistics logger, which will simply have its data truncated, losing log entries that arrive while the disk is full and possibly leaving a corrupted last entry. This is not considered a serious problem. Furthermore, this state is not being saved in a statefile at all, but is being explicitly written to a a separate logfile.
Lenny Foner Last modified: Thu Apr 29 16:22:27 EDT 1999