Rename try to command-line tool, cotp.c, and fix another off-by-one.
[easyotp.git] / libotp.c
blob0d10cee69f41bfcdae228fb4fd38934353d78162
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 char *s, *end, *b64_ct;
247 char pad_name[PAD_NAME_LENGTH];
249 msg = malloc(sizeof(MESSAGE));
250 if (!msg) {
251 perror("malloc");
252 exit(EX_UNAVAILABLE);
255 /** Format <v0.7 (pad name is unspecified; use default):
256 * MARKER_BEGIN + offset + comma
257 * base64'd data
258 * MARKER_END
260 * Format >=0.7:
261 * MARKER_BEGIN + offset + comma + pad_name + comma
262 * base64'd data
263 * MARKER_END
266 at = 0;
268 /* Locate where the message begins. */
269 s = strstr(input, MARKER_BEGIN);
270 if (!s) {
271 fprintf(stderr, "unpackage: input |%s| lacks beginning marker %s\n",
272 input, MARKER_BEGIN);
273 exit(EX_DATAERR);
276 /* ...and ends. */
277 end = strstr(input, MARKER_END);
278 if (!end) {
279 fprintf(stderr, "unpackage: input |%s| lacks ending marker %s\n",
280 input, MARKER_END);
281 exit(EX_DATAERR);
284 s += strlen(MARKER_BEGIN);
285 msg->offset = strtoul(s, &s, 10);
287 /* Move after mandatory comma. */
288 if (s[0] != ',') {
289 fprintf(stderr, "unpackage: missing comma after offset, at |%s|, input |%s|\n",
290 s, input);
291 exit(EX_DATAERR);
293 ++s;
295 memset(pad_name, 0, PAD_NAME_LENGTH);
297 /* Includes pad name? */
298 if (s[0] != '\n' && s[0] != '\r') {
299 unsigned int i;
301 i = 0;
302 /* v0.7+ message, includes pad name */
303 while(s[0] != '\n' && s[0] != '\r' && s[0] != ',' && s < end) {
304 pad_name[i] = s[0];
305 ++s;
306 ++i;
307 if (i > PAD_NAME_LENGTH) {
308 fprintf(stderr, "unpackage: pad name length > maximum %d, in input |%s|\n",
309 PAD_NAME_LENGTH, input);
310 exit(EX_DATAERR);
315 msg->pad = find_pad(pad_name);
316 if (!msg->pad) {
317 fprintf(stderr, "No such pad by name '%s'\n", pad_name);
318 exit(EX_DATAERR);
321 /* Go to next line */
322 while((s[0] == '\n' || s[0] == '\r' || s[0] == ',') && (s < end))
323 ++s;
325 /* Extract base64 data from end of message. */
326 b64_ct = strdup(s);
327 b64_ct[end - s] = 0;
329 /* Decode base64 */
330 msg->cipher_text = malloc(strlen(b64_ct));
331 msg->length = base64_decode(b64_ct, msg->cipher_text);
332 free(b64_ct);
334 return msg;
337 /** Apply Vernam cipher (XOR) with given input and text.
339 * @param input What to XOR with the pad, either ciphertext to decrypt or plaintext to encrypt.
341 * @param length Length of input, in bytes.
343 * @param pad Pad to XOR 'input' with offset.
345 * @param offset Offset within pad to start XOR at.
347 * @return Pad XOR'd with input. Caller must free.
348 * The return value is the same length as 'length'. However, it is null-terminated
349 * in an extra byte for convenience. There may be embedded nulls.
351 static char *xor_with_pad(char *input, unsigned long length, PAD *pad, unsigned long offset)
353 char *pad_data, *out;
354 unsigned int i;
356 /* Seek to area of pad we're using. */
357 if (fseek(pad->fp, offset, SEEK_SET) < 0) {
358 fprintf(stderr, "failed to seek in pad %s to %ld\n",
359 pad->name, offset);
360 perror("fseek");
361 exit(EXIT_FAILURE);
364 /* Read pad. */
365 pad_data = malloc(length);
366 if (!pad_data) {
367 perror("malloc pad data");
368 exit(EX_UNAVAILABLE);
370 if (fread(pad_data, length, 1, pad->fp) < 1) {
371 fprintf(stderr, "read pad %s, offset %ld, length %ld failed",
372 pad->name, offset, length);
373 exit(EXIT_FAILURE);
376 /* Apply XOR to give output. */
377 out = malloc(length + 1);
378 if (!out) {
379 perror("malloc output buffer");
380 exit(EX_UNAVAILABLE);
383 for (i = 0; i < length; ++i) {
384 out[i] = pad_data[i] ^ input[i];
386 free(pad_data);
388 /* Null-terminate output for convenience when using text-only messages.
389 * The output is still of length msg->length; use msg->length when decrypting
390 * binary messages. */
391 out[length] = 0;
393 return out;
397 /** Decrypt a packaged message and return plaintext.
399 * @param out Pointer to pointer which is set to the output. Caller must free.
400 * There is an extra null at the end of this buffer, not part of the output,
401 * but there may be embedded nulls if binary data is encoded. If this is expected,
402 * use the return value:
404 * @return Length of message, in bytes.
406 unsigned int otp_decrypt(char *input, char **out)
408 MESSAGE *msg;
409 unsigned int length;
411 msg = unpackage(input);
413 if (msg->offset < read_offset(msg->pad)) {
414 fprintf(stderr, "** warning: this is an old message! possible replay attack: %ld < %ld\n",
415 msg->offset, read_offset(msg->pad));
418 length = msg->length;
419 *out = xor_with_pad(msg->cipher_text, msg->length, msg->pad, msg->offset);
420 free_message(msg);
422 return length;
425 /** Package up a message for transport.
427 * @return Packaged message. Caller frees.
429 char *package(MESSAGE *msg, int *out_length)
431 char *out;
432 char *b64_data;
434 /* Space for markers, and 4/3 of plaintext since base64'd. */
435 *out_length = strlen(MARKER_BEGIN) + 1 + OFFSET_SIZE + 1 +
436 1 + msg->length * 4 / 3 + 1 + strlen(MARKER_END) + 1 + 1 + 1;
437 out = malloc(*out_length);
438 if (!out) {
439 perror("malloc otp_decrypt output");
440 exit(EX_DATAERR);
443 base64_encode(msg->cipher_text, msg->length, &b64_data);
445 snprintf(out, *out_length, "%s%ld,%s,\n"
446 "%s\n"
447 "%s\n",
448 MARKER_BEGIN,
449 msg->offset,
450 msg->pad->name,
451 b64_data,
452 MARKER_END);
454 return out;
457 /** Encrypt and package a message.
459 * @return Encrypted, packaged message. Caller must free.
461 char *otp_encrypt(char *input, unsigned int length, char *to, unsigned int *out_length)
463 MESSAGE msg;
464 char *out;
466 msg.pad = find_pad(to);
467 if (!msg.pad) {
468 fprintf(stderr, "otp_encrypt: no such pad %s\n", to);
469 exit(EX_DATAERR);
472 /* Use current offset. */
473 msg.offset = read_offset(msg.pad);
474 msg.length = length;
475 msg.cipher_text = xor_with_pad(input, length, msg.pad, msg.offset);
477 /* Advance pad. */
478 write_offset(msg.pad, msg.offset + msg.length);
480 out = package(&msg, out_length);
482 return out;