Integrate pad rewriting into cli.
[easyotp.git] / libotp.c
blobdefe403113284cad53cced5c89434ba870ce435d
1 /** Practical One-time Pad Library
3 * Created:20080514
4 * By Jeff Connelly
5 */
7 #include <stdio.h>
8 #include <stdlib.h>
9 #include <sysexits.h>
10 #include <string.h>
11 #include <ctype.h>
13 #include "libotp.h"
14 #include "base64.h"
16 PAD *pads = NULL;
18 /** Show a list of all loaded pads. */
19 void show_pads()
21 PAD *p;
23 for (p = pads; p; p = p->next) {
24 printf("Pad: %s: %s\n", p->name, p->local_filename);
28 /** Open a companion offset file for given pad. Caller must close.
30 * @param mode Either "rt" or "wt", for reading or writing.
32 FILE *open_offset_file(PAD *p, char *mode)
34 FILE *ofp;
35 char *offset_filename;
36 size_t filename_length;
38 /* Offset is stored in a separate file; pad plus OFFSET_FILE_NAME_EXTENSION. */
39 filename_length = strlen(p->local_filename) + strlen(OFFSET_FILE_EXTENSION) + 1;
40 offset_filename = malloc(filename_length);
41 if (!offset_filename) {
42 perror("malloc");
43 exit(EX_UNAVAILABLE);
45 snprintf(offset_filename, filename_length, "%s" OFFSET_FILE_EXTENSION, p->local_filename);
47 /* Read offset from file. */
48 ofp = fopen(offset_filename, mode);
49 if (!ofp) {
50 fprintf(stderr, "opening offset file %s failed\n", offset_filename);
51 perror("fopen");
52 free(offset_filename);
53 exit(EX_IOERR);
55 free(offset_filename);
57 return ofp;
60 /** Read the pad offset of a given pad. */
61 unsigned long read_offset(PAD *p)
63 FILE *ofp;
64 char buffer[OFFSET_SIZE];
65 unsigned long offset;
67 ofp = open_offset_file(p, "rt");
69 memset(buffer, 0, OFFSET_SIZE);
70 if (fread(buffer, 1, OFFSET_SIZE - 1, ofp) < 1) {
71 fprintf(stderr, "could not read offset file for %s\n", p->local_filename);
72 exit(EX_IOERR);
75 if (fclose(ofp) != 0) {
76 fprintf(stderr, "error closing offset file for reading for %s\n", p->local_filename);
77 perror("fclose");
78 exit(EX_IOERR);
81 /* We finally got it! */
82 offset = strtoul(buffer, NULL, 10);
84 return offset;
87 /** Write the pad offset to the given pad. */
88 void write_offset(PAD *p, unsigned long offset)
90 FILE *ofp;
91 char buffer[OFFSET_SIZE];
93 ofp = open_offset_file(p, "wt");
95 memset(buffer, 0, OFFSET_SIZE);
96 snprintf(buffer, OFFSET_SIZE - 1, "%ld", offset);
98 if (fwrite(buffer, strlen(buffer), 1, ofp) != 1) {
99 fprintf(stderr, "write error saving offset %ld for %s\n", offset, p->local_filename);
100 exit(EX_IOERR);
104 if (fclose(ofp) != 0) {
105 fprintf(stderr, "error closing offset file for writing for %s\n", p->local_filename);
106 perror("fclose");
107 exit(EX_IOERR);
111 /** Load a pad file from disk, adding to 'pads' global. */
112 void load_pad(char *local_filename, char *pad_name)
114 FILE *fp;
115 PAD *new_pad;
117 fp = fopen(local_filename, "rb+");
118 if (!fp) {
119 perror("fopen");
120 exit(EXIT_FAILURE);
123 new_pad = malloc(sizeof(PAD));
124 if (!new_pad) {
125 perror("malloc");
126 exit(EX_UNAVAILABLE);
129 new_pad->local_filename = strdup(local_filename);
130 new_pad->name = strdup(pad_name);
131 new_pad->fp = fp;
132 new_pad->next = NULL;
134 /* Add to linked list. */
135 if (!pads) {
136 pads = new_pad;
137 } else {
138 PAD *p, *tail;
140 /* Find tail */
141 for (p = pads; p; p = p->next)
142 tail = p;
143 tail->next = new_pad;
147 /** Read a line from a file, up to max characters.
148 * Does not place \n in line string.
150 * Based on http://www.eskimo.com/~scs/cclass/notes/sx6c.html
152 * @return Line length (0 for empty), or EOF for end-of-file.
154 static int getline(FILE *fp, char *line, unsigned int max)
156 int i, c;
158 i = 0;
159 max -= 1;
161 while ((c = fgetc(fp)) != EOF) {
162 if (c == '\n')
163 break;
164 if (i < max)
165 line[i++] = c;
169 if (c == EOF && i == 0)
170 return EOF;
172 line[i] = '\0';
173 return i;
176 void load_config(char *config_filename)
178 FILE *cfg;
179 char line[MAX_CONFIG_LINE];
181 cfg = fopen(config_filename, "rt");
182 if (!cfg) {
183 fprintf(stderr, "failed to open configuration file %s\n", config_filename);
184 perror("fopen config file");
185 exit(EX_IOERR);
188 while(getline(cfg, line, MAX_CONFIG_LINE) != EOF) {
189 char *pad_name, *pad_filename;
191 /* Line is: (pad name)=(local filename) */
192 pad_name = strtok(line, "=");
193 if (!pad_name)
194 continue;
195 pad_filename = strtok(NULL, "=");
197 /* TODO: separate send and recv filename, separated with : */
199 load_pad(pad_filename, pad_name);
203 /** Find pad with given name. */
204 PAD *find_pad(char *pad_name)
206 PAD *p;
208 /* Null or blank pad = default (first) pad. */
209 if (!pad_name || !strcmp(pad_name, ""))
210 return pads;
212 for (p = pads; p; p = p->next) {
213 if (!strcmp(p->name, pad_name))
214 return p;
217 return NULL;
220 /** Close all pads and free allocated memory. */
221 void free_pads()
223 PAD *p, *next;
225 for (p = pads; p; p = next) {
226 free(p->name);
227 free(p->local_filename);
228 fclose(p->fp);
229 next = p->next;
230 free(p);
234 void free_message(MESSAGE *msg)
236 free(msg->cipher_text);
237 free(msg);
240 /** Unpackage a message packaged for transport.
242 * Caller must free_message(). */
243 MESSAGE *unpackage(char *input)
245 MESSAGE *msg;
246 unsigned int at, i, j;
247 int b64_ret;
248 char *s, *end, *b64_ct;
249 char pad_name[PAD_NAME_LENGTH];
251 msg = malloc(sizeof(MESSAGE));
252 if (!msg) {
253 perror("malloc");
254 exit(EX_UNAVAILABLE);
257 /** Format <v0.7 (pad name is unspecified; use default):
258 * MARKER_BEGIN + offset + comma
259 * base64'd data
260 * MARKER_END
262 * Format >=0.7:
263 * MARKER_BEGIN + offset + comma + pad_name + comma
264 * base64'd data
265 * MARKER_END
268 at = 0;
270 /* Locate where the message begins. */
271 s = strstr(input, MARKER_BEGIN);
272 if (!s) {
273 fprintf(stderr, "unpackage: input |%s| lacks beginning marker %s\n",
274 input, MARKER_BEGIN);
275 exit(EX_DATAERR);
278 /* ...and ends. */
279 end = strstr(input, MARKER_END);
280 if (!end) {
281 fprintf(stderr, "unpackage: input |%s| lacks ending marker %s\n",
282 input, MARKER_END);
283 exit(EX_DATAERR);
286 s += strlen(MARKER_BEGIN);
287 msg->offset = strtoul(s, &s, 10);
289 /* Move after mandatory comma. */
290 if (s[0] != ',') {
291 fprintf(stderr, "unpackage: missing comma after offset, at |%s|, input |%s|\n",
292 s, input);
293 exit(EX_DATAERR);
295 ++s;
297 memset(pad_name, 0, PAD_NAME_LENGTH);
299 /* Includes pad name? */
300 if (s[0] != '\n' && s[0] != '\r') {
301 unsigned int i;
303 i = 0;
304 /* v0.7+ message, includes pad name */
305 while(s[0] != '\n' && s[0] != '\r' && s[0] != ',' && s < end) {
306 pad_name[i] = s[0];
307 ++s;
308 ++i;
309 if (i > PAD_NAME_LENGTH) {
310 fprintf(stderr, "unpackage: pad name length > maximum %d, in input |%s|\n",
311 PAD_NAME_LENGTH, input);
312 exit(EX_DATAERR);
317 msg->pad = find_pad(pad_name);
318 if (!msg->pad) {
319 fprintf(stderr, "No such pad by name '%s'\n", pad_name);
320 exit(EX_DATAERR);
323 /* Go to next line */
324 while((s[0] == '\n' || s[0] == '\r' || s[0] == ',') && (s < end))
325 ++s;
327 /* Extract base64 data from end of message, removing whitespace. */
328 b64_ct = malloc(end - s + 4);
329 if (!b64_ct) {
330 perror("malloc b64_ct");
331 exit(EXIT_FAILURE);
334 i = j = 0;
337 if (!isspace(s[i]))
338 b64_ct[j++] = s[i];
339 } while(i++ < end - s);
340 b64_ct[i - 1] = '\0';
341 b64_ct[i ] = '\0';
342 b64_ct[i + 1] = '\0';
343 b64_ct[i + 2] = '\0';
346 /* Add base64 padding, if needed */
347 i = strlen(b64_ct) % 4;
348 while(i--)
349 b64_ct[strlen(b64_ct)] = '=';
351 b64_ct[strlen(b64_ct) + 1] = '\0';
353 /* Decode base64 */
354 msg->cipher_text = malloc(strlen(b64_ct));
356 /* Decode base64. This may return -1 if it fails, so assign to a
357 * signed variable. */
358 b64_ret = base64_decode(b64_ct, msg->cipher_text);
360 if (b64_ret < 0) {
361 fprintf(stderr, "unpackage: failed to base64 decode |%s|\n", b64_ct);
362 exit(EX_DATAERR);
365 msg->length = b64_ret;
367 free(b64_ct);
369 return msg;
372 /** Apply Vernam cipher (XOR) with given input and text.
374 * @param input What to XOR with the pad, either ciphertext to decrypt or plaintext to encrypt.
376 * @param length Length of input, in bytes.
378 * @param pad Pad to XOR 'input' with offset.
380 * @param offset Offset within pad to start XOR at.
382 * @return Pad XOR'd with input. Caller must free.
383 * The return value is the same length as 'length'. However, it is null-terminated
384 * in an extra byte for convenience. There may be embedded nulls.
386 static char *xor_with_pad(char *input, unsigned long length, PAD *pad, unsigned long offset)
388 char *pad_data, *out;
389 unsigned int i;
391 /* Seek to area of pad we're using. */
392 if (fseek(pad->fp, offset, SEEK_SET) < 0) {
393 fprintf(stderr, "failed to seek in pad %s to %ld\n",
394 pad->name, offset);
395 perror("fseek");
396 exit(EXIT_FAILURE);
399 /* Read pad. */
400 pad_data = malloc(length);
401 if (!pad_data) {
402 perror("malloc pad data");
403 exit(EX_UNAVAILABLE);
405 if (fread(pad_data, length, 1, pad->fp) < 1) {
406 fprintf(stderr, "read pad %s, offset %ld, length %ld failed",
407 pad->name, offset, length);
408 exit(EXIT_FAILURE);
411 /* Apply XOR to give output. */
412 out = malloc(length + 1);
413 if (!out) {
414 perror("malloc output buffer");
415 exit(EX_UNAVAILABLE);
418 for (i = 0; i < length; ++i) {
419 out[i] = pad_data[i] ^ input[i];
421 free(pad_data);
423 /* Null-terminate output for convenience when using text-only messages.
424 * The output is still of length msg->length; use msg->length when decrypting
425 * binary messages. */
426 out[length] = 0;
428 return out;
432 /** Decrypt a packaged message and return plaintext.
434 * @param out Pointer to pointer which is set to the output. Caller must free.
435 * There is an extra null at the end of this buffer, not part of the output,
436 * but there may be embedded nulls if binary data is encoded. If this is expected,
437 * use the return value:
439 * @return Length of message, in bytes.
441 unsigned int otp_decrypt(char *input, char **out)
443 MESSAGE *msg;
444 unsigned int length;
446 msg = unpackage(input);
448 if (msg->offset < read_offset(msg->pad)) {
449 #ifdef WARN_REPLAY
450 fprintf(stderr, "** warning: this is an old message! possible replay attack: %ld < %ld\n",
451 msg->offset, read_offset(msg->pad));
452 #endif
455 length = msg->length;
456 *out = xor_with_pad(msg->cipher_text, msg->length, msg->pad, msg->offset);
457 free_message(msg);
459 return length;
462 /** Replace part of the pad corresponding to an encrypted message
463 * so that it encrypts to something else.
465 * @param input A packaged, encrypted message.
466 * @param with What to make 'input' decrypt to by changing the pad.
468 unsigned int otp_replace(char *input, char *with)
470 unsigned int length;
471 int i;
472 char c;
474 MESSAGE *msg;
475 msg = unpackage(input);
477 if (fseek(msg->pad->fp, msg->offset, SEEK_SET) < 0) {
478 perror("fseek");
479 exit(EXIT_FAILURE);
482 for (i = 0; i < msg->length; ++i) {
483 char with_c;
485 /* What to make the message decrypt to. */
486 if (i < strlen(with))
487 with_c = with[i];
488 else
489 with_c = '\0';
491 c = msg->cipher_text[i] ^ with_c;
493 fputc(c, msg->pad->fp);
496 return length;
499 /** Package up a message for transport.
501 * @return Packaged message. Caller frees.
503 char *package(MESSAGE *msg, unsigned int *out_length)
505 char *out;
506 char *b64_data;
508 /* Space for markers, and 4/3 of plaintext since base64'd. */
509 *out_length = strlen(MARKER_BEGIN) + 1 + OFFSET_SIZE + 1 +
510 1 + msg->length * 4 / 3 + 1 + strlen(MARKER_END) + 1 + 1 + 1;
511 out = malloc(*out_length);
512 if (!out) {
513 perror("malloc otp_decrypt output");
514 exit(EX_DATAERR);
517 base64_encode(msg->cipher_text, msg->length, &b64_data);
519 snprintf(out, *out_length, "%s%ld,%s,\n"
520 "%s\n"
521 "%s\n",
522 MARKER_BEGIN,
523 msg->offset,
524 msg->pad->name,
525 b64_data,
526 MARKER_END);
528 return out;
531 /** Encrypt and package a message.
533 * @return Encrypted, packaged message. Caller must free.
535 char *otp_encrypt(char *input, unsigned int length, char *to, unsigned int *out_length)
537 MESSAGE msg;
538 char *out;
539 char *input_padded;
540 unsigned int length_padded;
542 msg.pad = find_pad(to);
543 if (!msg.pad) {
544 fprintf(stderr, "otp_encrypt: no such pad %s\n", to);
545 exit(EX_DATAERR);
548 /* Round up to nearest multiple of PADDING_MULTIPLE. Mitigates analysis
549 * based on message length. */
550 length_padded = length + (PADDING_MULTIPLE - length % PADDING_MULTIPLE);
551 input_padded = malloc(length_padded);
552 if (!input_padded) {
553 perror("malloc input padded");
554 exit(EXIT_FAILURE);
557 /* input_padded is input, padded with 0s. */
558 memset(input_padded, 0, length_padded);
559 strncpy(input_padded, input, length);
561 /* Use current offset. */
562 msg.offset = read_offset(msg.pad);
563 msg.length = length_padded;
564 msg.cipher_text = xor_with_pad(input_padded, length_padded, msg.pad, msg.offset);
566 /* Advance pad. */
567 write_offset(msg.pad, msg.offset + msg.length);
569 out = package(&msg, out_length);
571 return out;