Detect negative return values from base64_decode().
[easyotp.git] / libotp.c
blob6e659308734ffaff9879938c736aa0a5ea82ff5a
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>
12 #include "libotp.h"
13 #include "base64.h"
15 PAD *pads = NULL;
17 /** Show a list of all loaded pads. */
18 void show_pads()
20 PAD *p;
22 for (p = pads; p; p = p->next) {
23 printf("Pad: %s: %s\n", p->name, p->local_filename);
27 /** Open a companion offset file for given pad. Caller must close.
29 * @param mode Either "rt" or "wt", for reading or writing.
31 FILE *open_offset_file(PAD *p, char *mode)
33 FILE *ofp;
34 char *offset_filename;
35 size_t filename_length;
37 /* Offset is stored in a separate file; pad plus OFFSET_FILE_NAME_EXTENSION. */
38 filename_length = strlen(p->local_filename) + strlen(OFFSET_FILE_EXTENSION) + 1;
39 offset_filename = malloc(filename_length);
40 if (!offset_filename) {
41 perror("malloc");
42 exit(EX_UNAVAILABLE);
44 snprintf(offset_filename, filename_length, "%s" OFFSET_FILE_EXTENSION, p->local_filename);
46 /* Read offset from file. */
47 ofp = fopen(offset_filename, mode);
48 if (!ofp) {
49 fprintf(stderr, "opening offset file %s failed\n", offset_filename);
50 perror("fopen");
51 free(offset_filename);
52 exit(EX_IOERR);
54 free(offset_filename);
56 return ofp;
59 /** Read the pad offset of a given pad. */
60 unsigned long read_offset(PAD *p)
62 FILE *ofp;
63 char buffer[OFFSET_SIZE];
64 unsigned long offset;
66 ofp = open_offset_file(p, "rt");
68 memset(buffer, 0, OFFSET_SIZE);
69 if (fread(buffer, 1, OFFSET_SIZE - 1, ofp) < 1) {
70 fprintf(stderr, "could not read offset file for %s\n", p->local_filename);
71 exit(EX_IOERR);
74 if (fclose(ofp) != 0) {
75 fprintf(stderr, "error closing offset file for reading for %s\n", p->local_filename);
76 perror("fclose");
77 exit(EX_IOERR);
80 /* We finally got it! */
81 offset = strtoul(buffer, NULL, 10);
83 return offset;
86 /** Write the pad offset to the given pad. */
87 void write_offset(PAD *p, unsigned long offset)
89 FILE *ofp;
90 char buffer[OFFSET_SIZE];
92 ofp = open_offset_file(p, "wt");
94 memset(buffer, 0, OFFSET_SIZE);
95 snprintf(buffer, OFFSET_SIZE - 1, "%ld", offset);
97 if (fwrite(buffer, strlen(buffer), 1, ofp) != 1) {
98 fprintf(stderr, "write error saving offset %ld for %s\n", offset, p->local_filename);
99 exit(EX_IOERR);
103 if (fclose(ofp) != 0) {
104 fprintf(stderr, "error closing offset file for writing for %s\n", p->local_filename);
105 perror("fclose");
106 exit(EX_IOERR);
110 /** Load a pad file from disk, adding to 'pads' global. */
111 void load_pad(char *local_filename, char *pad_name)
113 FILE *fp;
114 PAD *new_pad;
116 fp = fopen("/Volumes/Not Backed Up/otp/otp-dazzlement", "rb");
117 if (!fp) {
118 perror("fopen");
119 exit(EXIT_FAILURE);
122 new_pad = malloc(sizeof(PAD));
123 if (!new_pad) {
124 perror("malloc");
125 exit(EX_UNAVAILABLE);
128 new_pad->local_filename = strdup(local_filename);
129 new_pad->name = strdup(pad_name);
130 new_pad->fp = fp;
131 new_pad->next = NULL;
133 /* Add to linked list. */
134 if (!pads) {
135 pads = new_pad;
136 } else {
137 PAD *p, *tail;
139 /* Find tail */
140 for (p = pads; p; p = p->next)
141 tail = p;
142 tail->next = new_pad;
146 /** Read a line from a file, up to max characters.
147 * Does not place \n in line string.
149 * Based on http://www.eskimo.com/~scs/cclass/notes/sx6c.html
151 * @return Line length (0 for empty), or EOF for end-of-file.
153 static int getline(FILE *fp, char *line, unsigned int max)
155 int i, c;
157 i = 0;
158 max -= 1;
160 while ((c = fgetc(fp)) != EOF) {
161 if (c == '\n')
162 break;
163 if (i < max)
164 line[i++] = c;
168 if (c == EOF && i == 0)
169 return EOF;
171 line[i] = '\0';
172 return i;
175 void load_config(char *config_filename)
177 FILE *cfg;
178 char line[MAX_CONFIG_LINE];
180 cfg = fopen(config_filename, "rt");
181 if (!cfg) {
182 fprintf(stderr, "failed to open configuration file %s\n", config_filename);
183 perror("fopen config file");
184 exit(EX_IOERR);
187 while(getline(cfg, line, MAX_CONFIG_LINE) != EOF) {
188 char *pad_name, *pad_filename;
190 /* Line is: (pad name)=(local filename) */
191 pad_name = strtok(line, "=");
192 if (!pad_name)
193 continue;
194 pad_filename = strtok(NULL, "=");
196 /* TODO: separate send and recv filename, separated with : */
198 load_pad(pad_filename, pad_name);
202 /** Find pad with given name. */
203 PAD *find_pad(char *pad_name)
205 PAD *p;
207 /* Null or blank pad = default (first) pad. */
208 if (!pad_name || !strcmp(pad_name, ""))
209 return pads;
211 for (p = pads; p; p = p->next) {
212 if (!strcmp(p->name, pad_name))
213 return p;
216 return NULL;
219 /** Close all pads and free allocated memory. */
220 void free_pads()
222 PAD *p, *next;
224 for (p = pads; p; p = next) {
225 free(p->name);
226 free(p->local_filename);
227 fclose(p->fp);
228 next = p->next;
229 free(p);
233 void free_message(MESSAGE *msg)
235 free(msg->cipher_text);
236 free(msg);
239 /** Unpackage a message packaged for transport.
241 * Caller must free_message(). */
242 MESSAGE *unpackage(char *input)
244 MESSAGE *msg;
245 unsigned int at;
246 int b64_ret;
247 char *s, *end, *b64_ct;
248 char pad_name[PAD_NAME_LENGTH];
250 msg = malloc(sizeof(MESSAGE));
251 if (!msg) {
252 perror("malloc");
253 exit(EX_UNAVAILABLE);
256 /** Format <v0.7 (pad name is unspecified; use default):
257 * MARKER_BEGIN + offset + comma
258 * base64'd data
259 * MARKER_END
261 * Format >=0.7:
262 * MARKER_BEGIN + offset + comma + pad_name + comma
263 * base64'd data
264 * MARKER_END
267 at = 0;
269 /* Locate where the message begins. */
270 s = strstr(input, MARKER_BEGIN);
271 if (!s) {
272 fprintf(stderr, "unpackage: input |%s| lacks beginning marker %s\n",
273 input, MARKER_BEGIN);
274 exit(EX_DATAERR);
277 /* ...and ends. */
278 end = strstr(input, MARKER_END);
279 if (!end) {
280 fprintf(stderr, "unpackage: input |%s| lacks ending marker %s\n",
281 input, MARKER_END);
282 exit(EX_DATAERR);
285 s += strlen(MARKER_BEGIN);
286 msg->offset = strtoul(s, &s, 10);
288 /* Move after mandatory comma. */
289 if (s[0] != ',') {
290 fprintf(stderr, "unpackage: missing comma after offset, at |%s|, input |%s|\n",
291 s, input);
292 exit(EX_DATAERR);
294 ++s;
296 memset(pad_name, 0, PAD_NAME_LENGTH);
298 /* Includes pad name? */
299 if (s[0] != '\n' && s[0] != '\r') {
300 unsigned int i;
302 i = 0;
303 /* v0.7+ message, includes pad name */
304 while(s[0] != '\n' && s[0] != '\r' && s[0] != ',' && s < end) {
305 pad_name[i] = s[0];
306 ++s;
307 ++i;
308 if (i > PAD_NAME_LENGTH) {
309 fprintf(stderr, "unpackage: pad name length > maximum %d, in input |%s|\n",
310 PAD_NAME_LENGTH, input);
311 exit(EX_DATAERR);
316 msg->pad = find_pad(pad_name);
317 if (!msg->pad) {
318 fprintf(stderr, "No such pad by name '%s'\n", pad_name);
319 exit(EX_DATAERR);
322 /* Go to next line */
323 while((s[0] == '\n' || s[0] == '\r' || s[0] == ',') && (s < end))
324 ++s;
326 /* Extract base64 data from end of message. */
327 b64_ct = strdup(s);
328 b64_ct[end - s] = 0;
330 printf("unpackage: b64 length: %d\n", (unsigned int)strlen(b64_ct));
331 /* Decode base64 */
332 msg->cipher_text = malloc(strlen(b64_ct));
334 /* Decode base64. This may return -1 if it fails, so assign to a
335 * signed variable. */
336 b64_ret = base64_decode(b64_ct, msg->cipher_text);
338 if (b64_ret < 0) {
339 fprintf(stderr, "unpackage: failed to base64 decode |%s|\n", b64_ct);
340 exit(EX_DATAERR);
343 msg->length = b64_ret;
345 free(b64_ct);
347 return msg;
350 /** Apply Vernam cipher (XOR) with given input and text.
352 * @param input What to XOR with the pad, either ciphertext to decrypt or plaintext to encrypt.
354 * @param length Length of input, in bytes.
356 * @param pad Pad to XOR 'input' with offset.
358 * @param offset Offset within pad to start XOR at.
360 * @return Pad XOR'd with input. Caller must free.
361 * The return value is the same length as 'length'. However, it is null-terminated
362 * in an extra byte for convenience. There may be embedded nulls.
364 static char *xor_with_pad(char *input, unsigned long length, PAD *pad, unsigned long offset)
366 char *pad_data, *out;
367 unsigned int i;
369 /* Seek to area of pad we're using. */
370 if (fseek(pad->fp, offset, SEEK_SET) < 0) {
371 fprintf(stderr, "failed to seek in pad %s to %ld\n",
372 pad->name, offset);
373 perror("fseek");
374 exit(EXIT_FAILURE);
377 /* Read pad. */
378 pad_data = malloc(length);
379 if (!pad_data) {
380 perror("malloc pad data");
381 exit(EX_UNAVAILABLE);
383 if (fread(pad_data, length, 1, pad->fp) < 1) {
384 fprintf(stderr, "read pad %s, offset %ld, length %ld failed",
385 pad->name, offset, length);
386 exit(EXIT_FAILURE);
389 /* Apply XOR to give output. */
390 out = malloc(length + 1);
391 if (!out) {
392 perror("malloc output buffer");
393 exit(EX_UNAVAILABLE);
396 for (i = 0; i < length; ++i) {
397 out[i] = pad_data[i] ^ input[i];
399 free(pad_data);
401 /* Null-terminate output for convenience when using text-only messages.
402 * The output is still of length msg->length; use msg->length when decrypting
403 * binary messages. */
404 out[length] = 0;
406 return out;
410 /** Decrypt a packaged message and return plaintext.
412 * @param out Pointer to pointer which is set to the output. Caller must free.
413 * There is an extra null at the end of this buffer, not part of the output,
414 * but there may be embedded nulls if binary data is encoded. If this is expected,
415 * use the return value:
417 * @return Length of message, in bytes.
419 unsigned int otp_decrypt(char *input, char **out)
421 MESSAGE *msg;
422 unsigned int length;
424 msg = unpackage(input);
426 if (msg->offset < read_offset(msg->pad)) {
427 fprintf(stderr, "** warning: this is an old message! possible replay attack: %ld < %ld\n",
428 msg->offset, read_offset(msg->pad));
431 printf("otp_decrypt: length=%d\n", (int)msg->length);
433 length = msg->length;
434 *out = xor_with_pad(msg->cipher_text, msg->length, msg->pad, msg->offset);
435 free_message(msg);
437 return length;
440 /** Package up a message for transport.
442 * @return Packaged message. Caller frees.
444 char *package(MESSAGE *msg, unsigned int *out_length)
446 char *out;
447 char *b64_data;
449 /* Space for markers, and 4/3 of plaintext since base64'd. */
450 *out_length = strlen(MARKER_BEGIN) + 1 + OFFSET_SIZE + 1 +
451 1 + msg->length * 4 / 3 + 1 + strlen(MARKER_END) + 1 + 1 + 1;
452 out = malloc(*out_length);
453 if (!out) {
454 perror("malloc otp_decrypt output");
455 exit(EX_DATAERR);
458 base64_encode(msg->cipher_text, msg->length, &b64_data);
460 snprintf(out, *out_length, "%s%ld,%s,\n"
461 "%s\n"
462 "%s\n",
463 MARKER_BEGIN,
464 msg->offset,
465 msg->pad->name,
466 b64_data,
467 MARKER_END);
469 return out;
472 /** Encrypt and package a message.
474 * @return Encrypted, packaged message. Caller must free.
476 char *otp_encrypt(char *input, unsigned int length, char *to, unsigned int *out_length)
478 MESSAGE msg;
479 char *out;
480 char *input_padded;
481 unsigned int length_padded;
483 msg.pad = find_pad(to);
484 if (!msg.pad) {
485 fprintf(stderr, "otp_encrypt: no such pad %s\n", to);
486 exit(EX_DATAERR);
489 /* Round up to nearest multiple of PADDING_MULTIPLE. Mitigates analysis
490 * based on message length. */
491 length_padded = length + (PADDING_MULTIPLE - length % PADDING_MULTIPLE);
492 input_padded = malloc(length_padded);
493 if (!input_padded) {
494 perror("malloc input padded");
495 exit(EXIT_FAILURE);
498 /* input_padded is input, padded with 0s. */
499 memset(input_padded, 0, length_padded);
500 strncpy(input_padded, input, length);
502 /* Use current offset. */
503 msg.offset = read_offset(msg.pad);
504 msg.length = length_padded;
505 msg.cipher_text = xor_with_pad(input_padded, length_padded, msg.pad, msg.offset);
507 /* Advance pad. */
508 write_offset(msg.pad, msg.offset + msg.length);
510 out = package(&msg, out_length);
512 return out;