+/* -*-c-*-
+ *
+ * Common code for testing encryption modes
+ *
+ * (c) 2018 Straylight/Edgeware
+ */
+
+/*----- Licensing notice --------------------------------------------------*
+ *
+ * This file is part of Catacomb.
+ *
+ * Catacomb is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Library General Public License as published
+ * by the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * Catacomb is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Library General Public License for more details.
+ *
+ * You should have received a copy of the GNU Library General Public
+ * License along with Catacomb. If not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
+ * USA.
+ */
+
+/*----- Header files ------------------------------------------------------*/
+
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <unistd.h>
+
+#include <mLib/alloc.h>
+#include <mLib/bits.h>
+#include <mLib/dstr.h>
+#include <mLib/quis.h>
+#include <mLib/report.h>
+
+#include "modes-test.h"
+
+/*----- The reference data ------------------------------------------------*/
+
+#ifdef SMALL_TEST
+static const octet text[] = "A small piece of text for testing encryption.";
+#else
+#define STORY "\
+Once upon a time there were a beautiful princess, a slightly nutty wizard,\n\
+and a watermelon. Now, the watermelon had decided that it probably wasn't\n\
+going to get very far with the princess unless it did something pretty\n\
+drastic. So it asked the wizard to turn it into a handsome prince.\n\
+\n\
+At least, this is the way that the wizard viewed the situation. He might\n\
+have just hallucinated it all; those mushrooms had looked ever so nice.\n\
+\n\
+Back to the point. The watermelon had expressed its desire not to be a\n\
+watermelon any more. And the wizard was probably tripping something quite\n\
+powerful. He hunted around a bit for his staff, and mumbled something\n\
+that film directors would think of as sounding appropriately arcane and\n\
+mystical (but was, in fact, just the ingredients list for an ancient\n\
+remedy for athlete's foot) and *pop*. Cooked watermelon. Yuk.\n\
+\n\
+Later in the year, the princess tripped over the hem of her dress, fell\n\
+down a spiral staircase, and died. The king ordered dressmakers to attach\n\
+safety warnings to long dresses.\n\
+\n\
+And the wizard? Who cares?\n\
+"
+static const octet text[] = STORY STORY;
+#endif
+
+#define TEXTSZ (sizeof(text))
+
+static const octet key[] = "Penguins rule OK, rhubarb cauliflower",
+ iv[] = "EdgewareCatacomb, parsley, sage, rosemary and thyme";
+
+/*----- Static variables --------------------------------------------------*/
+
+/* Encryption buffers, for ciphertext, recovered plaintext, and consistency
+ * reference.
+ */
+static octet ct[TEXTSZ], pt[TEXTSZ], ref[TEXTSZ];
+
+/* A resizeable buffer for verifying regression data. */
+static octet *t = 0; size_t tsz = 0;
+
+/*----- Diagnostic utilities ----------------------------------------------*/
+
+/* Print the @sz@-byte buffer @p@, beginning at offset @off@ within some
+ * larger buffer, marking block boundaries every @blksz@ bytes.
+ */
+static void hexdump(const octet *p, size_t sz, size_t off, size_t blksz)
+{
+ const octet *q = p + sz;
+ for (sz = 0; p < q; p++, sz++) {
+ printf("%02x", *p);
+ if ((off + sz + 1)%blksz == 0) putchar(':');
+ }
+}
+
+/* Print the buffer @p@, labelling it as @what@, splitting it into three
+ * pieces of sizes @sz0@, @sz1@, and @sz2@ respectively. Block boundaries
+ * every @blksz@ bytes are shown consistency, independent of the split
+ * positions.
+ */
+static void dump_split(const char *what, size_t blksz, const octet *p,
+ size_t sz0, size_t sz1, size_t sz2)
+{
+ printf("\t%-16s = ", what);
+ hexdump(p, sz0, 0, blksz);
+ if (sz1) { printf(", "); hexdump(p + sz0, sz1, sz0, blksz); }
+ if (sz2) { printf(", "); hexdump(p + sz0 + sz1, sz2, sz0 + sz1, blksz); }
+ fputc('\n', stdout);
+}
+
+/*----- Regression-data utilities -----------------------------------------*/
+
+/* Regression modes. We can @CHECK@ existing data, @RECORD@ new data, or
+ * @IGNORE@ the regression testing entirely.
+ */
+enum { IGNORE, RECORD, CHECK };
+
+/* Read or write regression data from/to @fp@ according to @rmode@. The data
+ * item is described as @what@ in diagnostic messages, and consists of @sz@
+ * bytes beginning at @p@.
+ *
+ * If @rmode@ is @IGNORE@, then this function does nothing; if @rmode@ is
+ * @RECORD@, then it writes @p@ to the output file with some framing; and if
+ * @rmode@ is @CHECK@ then it checks that the next chunk of data from the
+ * file matches @p@.
+ *
+ * Returns zero if all is well or @-1@ on a mismatch; I/O errors are fatal.
+ *
+ * Framing is trivial and consists of a 4-byte big-endian non-inclusive
+ * length prepended to each buffer. No padding is written to maintain
+ * alignment.
+ */
+static int regress_data(int rmode, FILE *fp, const char *what,
+ const void *p, size_t sz)
+{
+ octet b[4];
+ size_t psz;
+
+ switch (rmode) {
+ case IGNORE:
+ return (0);
+ case RECORD:
+ STORE32(b, sz);
+ if (!fwrite(b, 4, 1, fp) || !fwrite(p, sz, 1, fp))
+ die(1, "failed to write %s: %s", what, strerror(errno));
+ return (0);
+ case CHECK:
+ if (!fread(b, 4, 1, fp))
+ die(1, "failed to read %s length: %s", what,
+ ferror(fp) ? strerror(errno) : "unexpected eof");
+ psz = LOAD32(b);
+ if (psz != sz)
+ die(1, "incorrect %s length (%lu /= %lu; sync failure?)",
+ what, (unsigned long)psz, (unsigned long)sz);
+ if (tsz < sz) { xfree(t); t = xmalloc(sz); tsz = sz; }
+ if (!fread(t, sz, 1, fp))
+ die(1, "failed to read %s: %s", what,
+ ferror(fp) ? strerror(errno) : "unexpected eof");
+ if (memcmp(p, t, sz) != 0) return (-1);
+ return (0);
+ default:
+ abort();
+ }
+}
+
+/* Read or write framing data from/to @fp@ according to @rmode@. The framing
+ * item is describd as @what@ in diagnostic messages, and consists of @sz@
+ * bytes beginning at @p@.
+ *
+ * Framing data is used to verify that a recorded regression-data file is
+ * still appropriate for use. A fatal error is reported on any kind of
+ * failure.
+ */
+static void regress_framing(int rmode, FILE *fp, const char *what,
+ const void *p, size_t sz)
+{
+ if (regress_data(rmode, fp, what, p, sz))
+ die(1, "regression framing mismatch for %s (bug, or wrong file)", what);
+}
+
+/* Read or write crypto data from/to @fp@ according to @rmode@. The data
+ * item is describd as @what@ in diagnostic messages, and consists of the
+ * bytes beginning at @p@. For the purposes of diagnostics, this buffer has
+ * been notionally split into three pieces, with sizes @sz0@, @sz1@, and
+ * @sz2@, respectively.
+ *
+ * If al is well, return zero. If the crypto data doesn't match the recorded
+ * regression data, then report the mismatch, showing the way in which the
+ * buffer is split, and return -1. I/O errors are fatal.
+ */
+static int regress_crypto(int rmode, FILE *fp, const char *what, size_t blksz,
+ const void *p, size_t sz0, size_t sz1, size_t sz2)
+{
+ int rc;
+
+ rc = regress_data(rmode, fp, what, p, sz0 + sz1 + sz2);
+ if (rc) {
+ printf("\nRegression mismatch (split = %lu/%lu/%lu)\n",
+ (unsigned long)sz0, (unsigned long)sz1, (unsigned long)sz2);
+ dump_split("plaintext", blksz, text, sz0, sz1, sz2);
+ dump_split("expected ct", blksz, t, sz0, sz1, sz2);
+ dump_split("computed ct", blksz, p, sz0, sz1, sz2);
+ fputc('\n', stdout);
+ }
+ return (rc);
+}
+
+/*----- Selecting fragment sizes ------------------------------------------*/
+
+/* Return codes from @step@. */
+enum { STEP, LIMIT, RESET };
+
+/* Update @*sz_inout@ the next largest suitable fragment size, up to a
+ * maximum of @max@.
+ *
+ * If the new size is still smaller than the maximum, then return @STEP@. If
+ * the size is maximal, then return @LIMIT@. If the size was previously
+ * maximal already, then return @RESET@.
+ *
+ * The sizes here are selected powers of two, and powers of two plus or minus
+ * 1, with the objective of testing how internal buffering is affected when
+ * the cursor is misaligned and realigned with block boundaries.
+ */
+static int step(size_t *sz_inout, size_t max)
+{
+ size_t i;
+
+ static size_t steps[] = { 1, 7, 8, 9, 15, 16, 17,
+ 63, 64, 65, 255, 256, 257 };
+
+ if (*sz_inout == max) return (RESET);
+ for (i = 0; i < N(steps); i++)
+ if (steps[i] > *sz_inout) {
+ if (steps[i] < max) { *sz_inout = steps[i]; return (STEP); }
+ else break;
+ }
+ *sz_inout = max; return (LIMIT);
+}
+
+/*----- Main code ---------------------------------------------------------*/
+
+/* --- @test_encmode@ --- *
+ *
+ * Arguments: @const char *name@ = name of the encryption scheme; used to
+ * find the regression-test filename
+ * @size_t ksz@ = key size to use, or zero for `don't care'
+ * @size_t blksz@ = block size
+ * @size_t minsz@ = smallest acceptable buffer size, or 1
+ * @unsigned f@ = various additional flags
+ * @setupfn *setup@ = key-setup function
+ * @resetfn *reset@ = state-reset function
+ * @encfn *enc@ = encryption function
+ * @decfn *dec@ = decryption function
+ * @int argc@ = number of command-line arguments
+ * @char *argv@ = pointer to command-line argument vector
+ *
+ * Returns: Zero on success, nonzero to report failure.
+ *
+ * Use: Tests an encryption mode which doesn't have any more formal
+ * test vectors.
+ *
+ * The @name@ is used firstly in diagnostic output and secondly
+ * to form the default filename to use for regression-test data,
+ * as `.../symm/t/modes/NAME.regress'.
+ *
+ * The key size @ksz@ is simply passed on back to the @setup@
+ * function, unless the caller passes in zero, in which case
+ * @test_encmode@ chooses a key size for itself.
+ *
+ * The block size @blksz@ is used in failure reports, to draw
+ * attention to the block structure in the various buffers,
+ * which may assist with diagnosis. It's also used to determine
+ * when to apply a consistency check: see below regarding the
+ * @TEMF_REFALIGN@ flag.
+ *
+ * The minimum buffer size @minsz@ expresses a limitation on the
+ * provided @enc@ and @dec@ functions, that they don't work on
+ * inputs smaller than @minsz@; accordingly, @test_encmode@ will
+ * not test such small sizes. This should be 1 if the mode has
+ * no limitation.
+ *
+ * The flags @f@ influence testing in various ways explained
+ * below.
+ *
+ * The caller-provided functions are assumed to act on some
+ * global but hidden state,
+ *
+ * * @setup@ is (currently, at least) called only once, with
+ * the key @k@ and its chosen size @ksz@.
+ *
+ * * @reset@ is called at the start of each encryption or
+ * decryption operation, to program in the initialization
+ * vector to use. Currently, the same IV is used in all of
+ * the tests, but this might not always be the case.
+ *
+ * * @enc@ is called to encrypt a source buffer @s@ and write
+ * the ciphertext to a destination @d@; @sz@ is the common
+ * size of these buffers.
+ *
+ * * @dec@ is called to decrypt a source buffer @s@ and write
+ * the recovered plaintext to a destination @d@; @sz@ is the
+ * common size of these buffers.
+ *
+ * Finally, @int argc@ and @char *argv@ are the command-line
+ * arguments provided to @main@; @test_encmode@ parses these and
+ * alters its behaviour accordingly.
+ *
+ * Currently, @test_encmode@'s tests are built around a single,
+ * fairly large, fixed message. In each test step, the message
+ * is split into a number of fragments which are encrypted and
+ * decrypted in turn.
+ *
+ * The following tests are performed.
+ *
+ * * The fundamental `round-trip' test, which verifies that
+ * the message can be encrypted and then decrypted
+ * successfully, if the same fragment boundaries are used in
+ * both cases.
+ *
+ * * A `consistency' test. Some modes, such as CFB, OFB, and
+ * counter, are `resumable': encryption operations are
+ * insensitive to the position of fragment boundaries, so a
+ * single message can be broken into fragments without
+ * affecting the result. If @TEMF_REFALIGN@ is clear then
+ * the mode under test is verified to have this property.
+ * If @TEMF_REFALIGN' is set, a weaker property is verified:
+ * that encryption is insensitive to the position of
+ * /block-aligned/ fragment boundaries only.
+ *
+ * * A `regression' test, which verifies that the code
+ * produces the same ciphertext as a previous version. By
+ * setting command-line arguments appropriately, a test
+ * program can be told to record ciphertexts in a (binary)
+ * data file. Usually, instead, the program will read the
+ * recorded ciphertexts back and verify that it produces the
+ * same data. For resumable modes, it's only necessary to
+ * record single ciphertext, since all the other ciphertexts
+ * must be equal by consistency; otherwise all non-block-
+ * aligned splits are recorded separately.
+ */
+
+int test_encmode(const char *name,
+ size_t ksz, size_t blksz, size_t minsz, unsigned f,
+ setupfn *setup, resetfn *reset, encfn *enc, encfn *dec,
+ int argc, char *argv[])
+{
+ int ok = 1, refp = 0, i;
+ size_t sz0, sz1, sz2;
+ const char spinner[] = "/-\\|";
+ int rmode = CHECK, spin = isatty(STDOUT_FILENO) ? 0 : -1;
+ int regr;
+ const char *rname = 0, *p;
+ FILE *fp;
+ dstr d = DSTR_INIT;
+
+ ego(argv[0]);
+
+ /* Parse the command-line options. */
+ p = 0; i = 1;
+ for (;;) {
+
+ /* Read the next argument. */
+ if (!p || !*p) {
+ if (i >= argc) break;
+ p = argv[i++];
+ if (strcmp(p, "--") == 0) break;
+ if (p[0] != '-' || p[1] == 0) { i--; break; }
+ p++;
+ }
+
+ /* Interpret an option. */
+ switch (*p++) {
+ case 'h':
+ printf("%s test driver\n"
+ "Usage: %s [-i] [-o|-f FILENAME]\n", QUIS, QUIS);
+ exit(0);
+ case 'i':
+ rmode = IGNORE;
+ break;
+ case 'o':
+ if (!*p) {
+ if (i >= argc) die(1, "option `-o' expects an argument");
+ p = argv[i++];
+ }
+ rmode = RECORD; rname = p; p = 0;
+ break;
+ case 'f':
+ if (!*p) {
+ if (i >= argc) die(1, "option `-f' expects an argument");
+ p = argv[i++];
+ }
+ rmode = CHECK; rname = p; p = 0;
+ break;
+ default:
+ die(1, "option `-%c' unknown", p[-1]);
+ }
+ }
+
+ /* Check there's nothing else left. */
+ if (i < argc) die(1, "trailing junk on command line");
+
+ /* Open the regression-data file. */
+ if (rmode == IGNORE)
+ fp = 0;
+ else {
+ if (!rname) {
+ DRESET(&d); dstr_putf(&d, SRCDIR"/t/modes/%s.regress", name);
+ rname = xstrdup(d.buf);
+ }
+ fp = fopen(rname, rmode == RECORD ? "wb" : "rb");
+ if (!fp)
+ die(1, "failed to open `%s' for %s: %s", rname,
+ rmode == RECORD ? "writing" : "reading", strerror(errno));
+ }
+
+ /* Write a header describing the file, to trap misuse for the wrong mode,
+ * and changes in the text.
+ */
+ DRESET(&d);
+ dstr_putf(&d, "mode=%s, text=%lu", name, (unsigned long)TEXTSZ);
+ regress_framing(rmode, fp, "header", d.buf, d.len);
+
+ /* Start things up. */
+ printf("%s: ", name);
+ setup(key, ksz ? ksz: sizeof(key));
+
+ /* Work through various sizes of up to three fragments. The middle
+ * fragment is the important one, since it can be misaligned or not at
+ * either end.
+ */
+ sz0 = sz1 = minsz;
+ for (;;) {
+
+ /* If output is to a terminal then display a spinner to keep humans
+ * amused.
+ */
+ if (spin >= 0) {
+ printf("\r%s: [%c]\b\b", name, spinner[spin]); fflush(stdout);
+ spin = (spin + 1)&3;
+ }
+
+ /* Prepare for the test. */
+ sz2 = TEXTSZ - sz1 - sz0;
+ ok = 1;
+
+ /* Encrypt the three fragments. */
+ reset(iv);
+ enc(text, ct, sz0);
+ if (sz1) {
+ memcpy(ct + sz0, text + sz0, sz1);
+ enc(ct + sz0, ct + sz0, sz1);
+ }
+ if (sz2)
+ enc(text + sz0 + sz1, ct + sz0 + sz1, sz2);
+
+ /* Try to check consistency. We can't do this if (a) the mode is
+ * non-resumable and the fragments sizes are misaligned, or (b) this is
+ * our first pass through and we don't have a consistency reference yet.
+ *
+ * Also, decide whether to deploy the regression test, which we do if and
+ * only if we can't compare against the consistency reference.
+ */
+ regr = 0;
+ if ((f&TEMF_REFALIGN) && (sz0%blksz || sz1%blksz)) regr = 1;
+ else if (!refp) { memcpy(ref, ct, TEXTSZ); regr = 1; refp = 1; }
+ else if (memcmp(ref, ct, TEXTSZ) != 0) {
+ ok = 0;
+ printf("\nConsistency failure (split = %lu/%lu/%lu)\n",
+ (unsigned long)sz0, (unsigned long)sz1, (unsigned long)sz2);
+ dump_split("plaintext", blksz, text, sz0, sz1, sz2);
+ dump_split("reference", blksz, ref, sz0, sz1, sz2);
+ dump_split("ciphertext", blksz, ct, sz0, sz1, sz2);
+ fputc('\n', stdout);
+ }
+
+ /* If we need the regression test then do that. Write a framing record
+ * to avoid confusion if the policy changes.
+ */
+ if (regr) {
+ DRESET(&d);
+ dstr_putf(&d, "split = %lu/%lu/%lu",
+ (unsigned long)sz0, (unsigned long)sz1, (unsigned long)sz2);
+ regress_framing(rmode, fp, "split", d.buf, d.len);
+ if (regress_crypto(rmode, fp, "regress", blksz, ct, sz0, sz1, sz2))
+ ok = 0;
+ }
+
+ /* Finally, decrypt and check that the round-trip works. */
+ reset(iv);
+ dec(ct, pt, sz0);
+ if (sz1) {
+ memcpy(pt + sz0, ct + sz0, sz1);
+ dec(pt + sz0, pt + sz0, sz1);
+ }
+ if (sz2)
+ dec(ct + sz0 + sz1, pt + sz0 + sz1, sz2);
+ if (memcmp(text, pt, TEXTSZ) != 0) {
+ ok = 0;
+ printf("\nRound-trip failure (split = %lu/%lu/%lu)\n",
+ (unsigned long)sz0, (unsigned long)sz1, (unsigned long)sz2);
+ dump_split("plaintext", blksz, text, sz0, sz1, sz2);
+ dump_split("ciphertext", blksz, ct, sz0, sz1, sz2);
+ dump_split("recovered", blksz, pt, sz0, sz1, sz2);
+ fputc('\n', stdout);
+ }
+
+ /* Update the fragment sizes. */
+ if (!sz1) break;
+ if (step(&sz1, TEXTSZ - sz0) == RESET) {
+ if (step(&sz0, TEXTSZ) == LIMIT) sz1 = 0;
+ else sz1 = minsz;
+ }
+ }
+
+ /* Close the regression data file. */
+ if (fp && (ferror(fp) || fclose(fp)))
+ die(1, "error closing `%s': %s", rname, strerror(errno));
+
+ /* Finish off the eyecandy spinner. */
+ if (spin >= 0) printf("\r%s: [%c] ", name, ok ? '*' : 'X');
+
+ /* Summarize the test result. */
+ if (ok) printf("ok\n");
+ else printf("failed\n");
+
+ /* And we're done. */
+ dstr_destroy(&d);
+ return (!ok);
+}
+
+/*----- That's all, folks -------------------------------------------------*/