integrating GPG with deniable steganography

Andrew Marlow apm6674 at apmsoftwareltd.alkazar.co.uk
Sat Mar 17 15:31:01 CET 2001


I have written an encode and decode program as an attempt to
integrate GPG with deniable steganography. Please let me know
what you think. Perhaps this can become part of the official
GPG code. I am quite happy for the code to be GPL'd.

The scenario is the prisoners problem but with a passive
warden. Let us introduce the players: we have Bob and Alice, who
know each others public keys and who have agreed beforehand that
they will use GPG and my encode/decode software, and we have
Wendy the Warden, who is a passive warden but also knows about
GPG and my encode/decode software. Bob, Alice and Wendy live in
the UK. This means that Wendy may serve an RIP decryption notice
on Alice or Bob, forcing them to decrypt the messages. Failure
to comply results in a jail sentence of 2 years. If they do comply
then they may not inform the other that the communication channel
has been compromised otherwise the sentence is increased to 5 years.

The encode program takes a file containing GPG ASCII-armoured
text and a file containing chaff, which it just considers to be
a series of words. It rewrites the chaff, separating words with
a single space to encode a zero and two spaces to encode a one.
The GPG data is the bit stream to be encoded, minus the header
and trailer. When the GPG data is exhausted, the remaining chaff
is encoded with random data using the standard linear congruential
algorithm (drand48).

The encoded data contains 3 special bytes at the start before
the GPG data is encoded. The first byte is currently not used
and is set to zero. However, this may be used in future versions
of the encode/decode software so putting in now allows future
versions to be backwards compatible. The next 2 bytes are the
lo-byte and hi-byte of a data length, encoded in network byte
order. This is so that the decode software knows how long the
message is. Network byte order is used so that a message can be
decoded on a machine with a different architecture to that one
that encoded it.

Suppose Bob sends Alice a GPG-steg'd message. Wendy intercepts
it and runs the decode program. She then serves Alice with
an RIP decryption notice. Alice refuses. Her argument is that
Wendy has recovered random data because the message she received
was not concealing another message via the encode/decode programs.
Alice says that the actual text sent is what Bob intended Alice
to see as the message and that it is perfectly innocent.
Alice points out that some words are separated by single spaces and
some by two spaces throughout the document on an apparantly
random basis. If Wendy was to run the decode program on *any* text
that contains words that are spaced irregularly, text that resembles
a GPG-encoded message would result. Bob and Alice have to be careful
that the chaff files they use look like innocent meaningful
communication. People that know each other generally don't sent
garbage to each other via email.

Another approach I considered but have not followed through yet,
is to do with how the end of the message is dealt with. 

Having a byte count is simple but does allow Wendy to recover
the GPG'd text. She may have a hard time persauding anyone in
court that it is a GPG'd message, but she does have the GPG'd
text non-the-less. If Wendy can force Alice to release the key
by other means then the communication path is still compromised.
This raises two issues that the software does not deal with at
the moment:

1: Session keys. If Wendy can force the key from Alice somehow
   then  all is lost.  The  best that Alice can do  is  send a 
   message to Bob saying "I revoke my public key but I am not
   going to say why". This is the standard way to avoid the
   tipping-off offence of RIP.  Perhaps a better version of this
   software would use session keys but I haven't had time to
   come up with a way yet.

2: Easy recovery of GPG'd text. I did consider denoting the
   end of the message by a pair of words separated by three
   spaces, and to randomly and infrequently insert three spaces
   both before and after the GPG'd text was encoded. This means
   that a large amount of chaff would hold several messages,
   only one of which would be the real message. Bob and Alice
   are ok because they will simply extract all the possibilities
   and use their private keys until a message was revealed.
   This makes life difficult for Wendy because she has to
   recover all possibilities and say in court that she thinks
   that one of them is a secret message. All the messages would
   look similar and Bob and Alice would claim that it is
   random data and that Wendy is paranoid.

Well, what do people think? I do not use GPG openly now
because of RIP. I have been using SNOW (steganography that
encodes data by appending whitespace onto the end of lines read
from a chaff file) but SNOW has disadvantages in that it is not
deniable and it uses symmetric key encryption.

Regards,

Andrew Marlow.

Here is the code, as two programs apmenc and apmdec.
To build them issue the commands:

gcc -o apmenc apmenc.c
gcc -o apmdec apmdec.c

--------------------------- apmenc.c ----------------------
/*
----------------------------------------------------------------------
This program encodes GPG ASCII armoured text using a chaff file.
The GPG text is considered to be a bistream that is superimposed
onto the chaff file using inter-word spacing, one space for a zero
and two spaces for a one. 

The end of data has to be marked somehow. The approach this program
takes is to encode the length in characters of the message. A character
is encoded as 7-bit ASCII, hence if the message length is n characters
and is encoded in 16 bits (2 bytes) this gives a maximum permitted
message length of 2^16 = 64Kbytes.

If the chaff file has not been exhausted by the time n characters
have been encoded, then the remaining words have either one or two
spaces inbetween on a random basis. This is aid deniability if an
RIP notice is served on the recipient.

This program is coded in C rather than C++ for portability reasons.
It is intended to release it under the GPL and to serve the widest
possible community.

The command line usage is:

apmenc <GPGfile> <chaffFile>

The encoded text is written to stdout.
 */

#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <netinet/in.h>

#ifndef TRUE
#define TRUE 1
#endif
#ifndef FALSE
#define FALSE 0
#endif

const char* errorText()
{
#ifdef SUNOS
    extern char* sys_errlist[];
#endif
    return sys_errlist[errno];
}

void giveUsage(const char* programName)
{
    fprintf(stderr, "Usage: %s <GPGfile> <chaffFile>\n", programName);
}

static char buffer[1024]; /* a GPG line will never be as big as this */

void consumeAfterHeader(FILE* fptr, const char* filename)
{
    /* We need to gobble any version, comment and blank lines */

    if (!fgets(buffer, sizeof(buffer), fptr))
    {
        fprintf(stderr, "Error: Premature end of file for %s.\n", filename);
        exit(1);
    }
    if (strncmp(buffer, "Version:", 8) != 0)
    {
        fprintf(stderr, "Error: Version string not found where expected in %s.\n", filename);
        exit(1);
    }

    /* Read comment line */

    if (!fgets(buffer, sizeof(buffer), fptr))
    {
        fprintf(stderr, "Error: Premature end of file for %s.\n", filename);
        exit(1);
    }
    if (strncmp(buffer, "Comment:", 8) != 0)
    {
        fprintf(stderr, "Error: Comment string not found where expected in %s.\n", filename);
        exit(1);
    }

    if (!fgets(buffer, sizeof(buffer), fptr))
    {
        fprintf(stderr, "Error: Premature end of file for %s.\n", filename);
        exit(1);
    }
    if (buffer[strlen(buffer)-1] == '\n')
        buffer[strlen(buffer)-1] = '\0';
    if (strlen(buffer) > 0)
    {
        fprintf(stderr, "Error: Data found where blank line expected in %s.\n", filename);
        exit(1);
    }
}

void gobbleHeader(FILE* fptr, const char* filename)
{
    int foundHeader;

    foundHeader = FALSE;
    while (!foundHeader && fgets(buffer, sizeof(buffer), fptr))
    {
        if (buffer[strlen(buffer)-1] == '\n')
            buffer[strlen(buffer)-1] = '\0';
        if (strcmp(buffer, "-----BEGIN PGP MESSAGE-----") == 0)
        {
            foundHeader = TRUE;
            consumeAfterHeader(fptr, filename);
        }
    }
}

int gpgByteCount(const char* filename)
{
    int byteCount;
    FILE* fptr;
    int foundHeader;
    char buffer[1024]; /* a GPG line will never be as big as this */

    byteCount = 0;
    fptr = fopen(filename, "r");
    if (fptr == NULL)
    {
        fprintf(stderr, "Error: Failed to open %s for reading (%s)\n", filename, errorText());
        exit(1);
    }

    foundHeader = FALSE;
    while (fgets(buffer, sizeof(buffer), fptr))
    {
        if (buffer[strlen(buffer)-1] == '\n')
            buffer[strlen(buffer)-1] = '\0';

        if (strcmp(buffer, "-----END PGP MESSAGE-----") == 0)
        {
            break;
        }

        if (foundHeader)
        {
            byteCount += strlen(buffer) + 1;
        }

        if (strcmp(buffer, "-----BEGIN PGP MESSAGE-----") == 0)
        {
            foundHeader = TRUE;
            consumeAfterHeader(fptr, filename);
        }
    }

    fclose(fptr);

    if (!foundHeader)
    {
        fprintf(stderr, "Error: Could not find PGP header in file %s.\n", filename);
        exit(1);
    }

    return byteCount;
}

char* getNextWord(FILE* fptr)
{
    static int foundEof = FALSE;
    static char word[1024]; /* We have to pray there won't be a bigger word than this */
    int len;
    int c;

    if (foundEof)
        return NULL;

    word[0] = '\0';
    len = 0;

    /* skip leading whitespace */

    while (c = fgetc(fptr), c != EOF && isspace(c))
        ;

    if (c == EOF)
        return NULL; /* this means end of file */

    word[len++] = c;
    while (c = fgetc(fptr), c != EOF && !isspace(c))
    {
        word[len++] = c;
    }
    word[len] = '\0';

    if (c == EOF)
        foundEof = TRUE;

    return word;
}

void encodeByte(int c, int bitCount, int lineLimit, FILE* fptr, const char* filename,  int* len)
{
    int i;
    char* word;

    static int powers_of_2 [] = {
                                    0x01
                                  , 0x02
                                  , 0x04
                                  , 0x08
                                  , 0x10
                                  , 0x20
                                  , 0x40
                                  , 0x80
                                };

    if (*len >= lineLimit)
    {
        printf("\n");
        *len = 0;
        word = getNextWord(fptr);
        if (word == NULL)
        {
            fprintf(stderr, "Error: chaff file %s is not big enough.\n", filename);
            exit(1);
        }

        *len += strlen(word);
        printf("%s", word);
    }

    for (i = 0; i < bitCount; i++)
    {
        word = getNextWord(fptr);
        if (word == NULL)
        {
            fprintf(stderr, "Error: chaff file %s is not big enough.\n", filename);
            exit(1);
        }

        *len += strlen(word) + 1;
        if (*len >= lineLimit)
        {
            printf("\n");
            printf("%s", word);

            word = getNextWord(fptr);
            if (word == NULL)
            {
                fprintf(stderr, "Error: chaff file %s is not big enough.\n", filename);
                exit(1);
            }
            *len = strlen(word);
        }

        if ((c & powers_of_2[i]) == powers_of_2[i])
        {
            printf (" ");
            *len = *len + 1;
        }
        printf(" %s", word);
    }
}

int getByte(FILE* fptr)
{
    int c;
    static int pos = -1;

    if (pos >= strlen(buffer))
        pos = -1;

    if (pos == -1)
    {
        if (!(fgets(buffer, sizeof(buffer), fptr)))
            return EOF;
        pos = 0;
        if (strcmp(buffer, "-----END PGP MESSAGE-----") == 0)
            return EOF;
    }
    return buffer[pos++];
}

int main (int argc, char* argv[])
{
    static int lineLimit = 80;
    short sBytes;
    short nsBytes;
    int i;
    long t;
    int c;
    int len;
    int byteCount;
    FILE* fptr;
    FILE* gpgFptr;
    char* word;

    if (argc != 3)
    {
        giveUsage(argv[0]);
        exit(0);
    }

    byteCount = gpgByteCount(argv[1]);
    sBytes = (short)byteCount;
    nsBytes = htons(sBytes);
    if (byteCount > sBytes)
    {
        fprintf(stderr, "Error: GPG message is too long to be encoded.\n");
        exit(1);
    }

    gpgFptr = fopen(argv[1], "r");
    if (gpgFptr == NULL)
    {
        fprintf(stderr, "Error: Failed to open %s (%s)\n", argv[1], errorText());
        exit(1);
    }

    fptr = fopen(argv[2], "r");
    if (fptr == NULL)
    {
        fprintf(stderr, "Error: Failed to open %s (%s)\n", argv[2], errorText());
        exit(1);
    }

    word = getNextWord(fptr);
    if (word == NULL)
    {
        fprintf(stderr, "Error: chaff file %s is not big enough.\n", argv[2]);
        exit(1);
    }
    len = strlen(word);
    printf("%s", word);

    /* 
        The first 8 bits is a special byte.
        It is currently unused but is reserved for
        future expansion. We may put a version indicator
        in here later perhaps.
    */

    encodeByte(0, 8, lineLimit, fptr, argv[2], &len);

    /* Encode the length of the message */

    c = nsBytes & 0x00FF;
    encodeByte(c, 8, lineLimit, fptr, argv[2], &len);
    c = ((nsBytes & 0xFF00) >> 8) & 0xFF;
    encodeByte(c, 8, lineLimit, fptr, argv[2], &len);

    /* Gobble the header again */

    gobbleHeader(gpgFptr, argv[1]);

    /* Encode each byte in the GPG ASCII armoured text */

    while (c = getByte(gpgFptr), c != EOF)
    {
        encodeByte(c, 7, lineLimit, fptr, argv[2], &len);
    }

    fclose(gpgFptr);

    /* Now we output the rest of the chaff with random spacing. */

    t = (long)time((time_t*)0);
    srand48(t);

    while (word = getNextWord(fptr), word != NULL)
    {
        if (len >= lineLimit)
        {
            printf("\n");
            len = 0;
            len += strlen(word);
            printf("%s", word);
            continue;
        }

        if (drand48() >= 0.5)
        {
            printf (" ");
            len++;
        }
        len += strlen(word) + 1;
        printf(" %s", word);
    }

    printf("\n");

    fclose(fptr);

    return 0;
}

--------------------------- apmdec.c ----------------------
/*
This reconstructs the GPG ASCII armoured text from a chaff
file that has the bitstream encoded via inter-word spacing.
 */

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <netinet/in.h>

#ifndef TRUE
#define TRUE 1
#endif
#ifndef FALSE
#define FALSE 0
#endif

const char* errorText()
{
#ifdef SUNOS
    extern char* sys_errlist[];
#endif
    return sys_errlist[errno];
}

int getByte(FILE* fptr, int bitCount)
{
    int i;
    int c;
    int endOfWord;
    int bit;
    int byte;

    byte = 0;
    for (i = 0; i < bitCount; i++)
    {
        bit = 0;
        endOfWord = FALSE;
        while (!endOfWord && ((c = fgetc(fptr)) != EOF))
        {
            if (c == ' ')
            {
                endOfWord = TRUE;
                c = fgetc(fptr);
                if (c == ' ')
                {
                    bit = 1;
                }
            }
        }

        if (c == EOF)
        {
            fprintf(stderr, "Error: Ran out of encoded text.\n");
            exit(1);
        }
        byte |= ((bit << i) & 0xFF);
    }
    return byte;
}

void giveUsage(const char* programName)
{
    fprintf(stderr, "Usage: %s <chaffFile>\n", programName);
}

int main (int argc, char* argv[])
{
    unsigned int i;
    int c;
    int c1;
    int c2;
    int nsByteCount;
    int byteCount;

    FILE* fptr;

    if (argc != 2)
    {
        giveUsage(argv[0]);
        exit(0);
    }

    fptr = fopen(argv[1], "r");
    if (fptr == NULL)
    {
        fprintf(stderr, "Error: Failed to open %s for reading (%s)\n", argv[1], errorText());
        exit(1);
    }

    printf ("-----BEGIN PGP MESSAGE-----\n"
            "Version: GnuPG v1.0.4 (GNU/Linux)\n"
            "\n");

    /* Get the special byte (currently unused) */

    c1 = getByte(fptr, 8);

    /* Get the length of the message */

    c1 = getByte(fptr, 8);
    c2 = getByte(fptr, 8);

    nsByteCount = ((c2 << 8) & 0xFF00) | (c1 & 0xFF);

    byteCount = ntohs(nsByteCount);
    for (i = 0; i < byteCount; i++)
    {
        c = getByte(fptr, 7);
        printf("%c", c);
    }

    fclose(fptr);

    printf ("-----END PGP MESSAGE-----\n");
    return 0;
}

/*------------end of code -----------------*/




More information about the Gnupg-devel mailing list