I was learning about getopt
to parse command line options and I decided to write this small XOR cipher program for files.
It acts as a "UNIX filter" (I guess) since default input and output are stdin
and stdout
. It can generate a random key file from /dev/urandom
if the provided one doesn't exists (size of this file can also be specified).
I split the thing in three files and here they are:
fxor.h
#ifndef FXOR_HEADER
#define FXOR_HEADER
#include <stdio.h>
#include <stdbool.h>
#define URANDOM "/dev/urandom"
/*
* fxor: applies XOR cipher to bytes from in file using bytes from key file as
* key, writes result to out file and returns true.
* If key file is smaller than in file, key file will restart from
* beginning each time EOF is reached.
*
* in and key files are assumed to be opened in read mode, out file in
* write mode. in, out, key can't be the same file, otherwise behaviour
* is undefined.
*
* If an error occurs while writing to out, returns false.
*/
bool fxor(FILE *in, FILE *out, FILE *key);
/*
* fwrite_urandom: writes nbytes bytes to out file from URANDOM and returns
* true. out file is assumed to be opened in write mode.
* If URANDOM can't be opened or an error occurs while writing
* to out, returns false.
*/
bool fwrite_urandom(FILE *out, size_t nbytes);
#endif
fxor.c
#include "fxor.h"
bool fxor(FILE *in, FILE *out, FILE *key)
{
unsigned char in_byte;
unsigned char key_byte;
int c;
while ((c = fgetc(in)) != EOF) {
in_byte = c;
if ((c = fgetc(key)) == EOF) {
rewind(key);
c = fgetc(key);
}
key_byte = c;
if (fputc(in_byte ^ key_byte, out) == EOF) {
return false;
}
}
return true;
}
bool fwrite_urandom(FILE *out, size_t nbytes)
{
FILE *urandom;
if ((urandom = fopen(URANDOM, "rb")) == NULL) {
return false;
}
while (nbytes-- > 0) {
if (fputc(fgetc(urandom), out) == EOF) {
fclose(urandom);
return false;
}
}
fclose(urandom);
return true;
}
main.c
#include <stdio.h>
#include <errno.h>
#include <getopt.h>
#include <ctype.h>
#include <stdint.h>
#include "fxor.h"
#define DEFAULT_KEY_LENGTH 65536
/*
* usage: print an usage message.
*/
static void usage(const char *prog_name);
/*
* atost: convert str to a size_t value and returns it.
* No check for overflow is made, if str contains an invalid size_t
* value, SIZE_MAX will be returned. str can start with '+'. If str is
* an empty string 0 will be returned.
*/
static size_t atost(const char *str);
/*
* open_or_gen_key: if key_filename exists, it will be opened in read mode and
* the pointer returned, otherwise, if it doesn't exists, it
* will be created, its size will be key_length bytes and it
* will be filled with bytes from URANDOM. A pointer to it
* will be returned after reopening the file in read mode.
* If an error occurs, returns NULL.
*/
static FILE *open_or_gen_key(const char *key_filename, size_t key_length);
/*
* fclose_all: close three file it gets as input. It is safe to pass NULL
* pointer as file, no operation on it will be performed.
*/
static void fclose_all(FILE *in, FILE *out, FILE *key);
static void usage(const char *prog_name)
{
printf("usage: %s -k key [-l key_length] [-i input] [-o output] [-h]\n\n"
" -k key key filename, required. If doesn't exists, a file\n"
" filled with bytes from %s will be created and\n"
" used for xor operation, default size will be %d bytes,\n"
" you can specify a different one with -l option.\n\n"
" -l key_length optional size in bytes for key file if has to be generated,\n"
" must be >= 0. If -k file exists, -l will not be used.\n\n"
" -i input input file, default standard input.\n\n"
" -o output output file, default standard output.\n\n"
" -h show this message and exit.\n",
prog_name, URANDOM, DEFAULT_KEY_LENGTH);
}
static size_t atost(const char *str)
{
size_t value = 0;
for (size_t i = (str[0] == '+') ? 1 : 0;
str[i] != '\0';
i++) {
if ( ! isdigit(str[i])) {
return SIZE_MAX;
}
value = value * 10 + (str[i] - '0');
}
return value;
}
static FILE *open_or_gen_key(const char *key_filename, size_t key_length)
{
FILE *key;
if ((key = fopen(key_filename, "rb")) == NULL && errno == ENOENT) {
if ((key = fopen(key_filename, "wb")) == NULL) {
return NULL;
}
if ( ! fwrite_urandom(key, key_length)) {
fclose(key);
return NULL;
}
key = freopen(key_filename, "rb", key);
}
return key;
}
static void fclose_all(FILE *in, FILE *out, FILE *key)
{
if (in != NULL) {
fclose(in);
}
if (out != NULL) {
fclose(out);
}
if (key != NULL) {
fclose(key);
}
}
int main(int argc, char **argv)
{
char *in_filename, *out_filename, *key_filename, *key_length_str;
in_filename = out_filename = key_filename = key_length_str = NULL;
int o;
while ((o = getopt(argc, argv, "k:l:i:o:h")) != -1) {
switch (o) {
case 'k':
key_filename = optarg;
break;
case 'l':
key_length_str = optarg;
break;
case 'i':
in_filename = optarg;
break;
case 'o':
out_filename = optarg;
break;
case 'h':
usage(argv[0]);
return 0;
default:
return 1;
}
}
if (key_filename == NULL) {
fprintf(stderr, "%s: option is required -- 'k'\n", argv[0]);
return 1;
}
FILE *in = stdin;
FILE *out = stdout;
FILE *key = NULL;
size_t key_length = DEFAULT_KEY_LENGTH;
if (key_length_str != NULL && (key_length = atost(key_length_str)) == SIZE_MAX) {
fprintf(stderr, "%s: invalid argument for option -- 'l'\n", argv[0]);
return 1;
}
if (in_filename != NULL && (in = fopen(in_filename, "rb")) == NULL) {
perror(in_filename);
return 1;
}
if (out_filename != NULL && (out = fopen(out_filename, "wb")) == NULL) {
perror(out_filename);
fclose_all(in, out, key);
return 1;
}
if ((key = open_or_gen_key(key_filename, key_length)) == NULL) {
perror(key_filename);
fclose_all(in, out, key);
return 1;
}
if ( ! fxor(in, out, key)) {
fprintf(stderr, "%s: error while applying xor\n", argv[0]);
fclose_all(in, out, key);
return 1;
}
fclose_all(in, out, key);
}
Example:
$ cc -std=c11 -Wall -Wextra -Werror -o fxor fxor.c main.c
$ l
fxor* fxor.c fxor.h main.c
$ echo "Hello fxor!" | ./fxor -k hello.key -l 16 -o hello.xor
$ ll | grep "hello*"
-rw-r--r-- 1 marco marco 16 Nov 18 16:16 hello.key
-rw-r--r-- 1 marco marco 12 Nov 18 16:16 hello.xor
$ ./fxor -i hello.xor -k hello.key
Hello fxor!
$ xxd -b hello.key
00000000: 10000010 01110111 01110010 01110001 01110100 10011111 .wrqt.
00000006: 00111111 10110001 10110001 10011011 10111010 00111011 ?....;
0000000c: 01110010 01001000 11100110 11001111 rH..
$ xxd -b hello.xor
00000000: 11001010 00010010 00011110 00011101 00011011 10111111 ......
00000006: 01011001 11001001 11011110 11101001 10011011 00110001 Y....1
$ ./fxor -i hello.xor -k hello.key | xxd -b
00000000: 01001000 01100101 01101100 01101100 01101111 00100000 Hello
00000006: 01100110 01111000 01101111 01110010 00100001 00001010 fxor!.
$
I'd like to get some feedback about style and advice about dealing with files and errors: how can I improve the "if cascade" made of "if fopen
fails, print an error, close things opened before and return 1
"?
Also, I bet that opening a device just to get random bytes isn't the best thing to do. Do you have any suggestion on that?