Fix off-by-one error in null termination of decrypted messages.
[easyotp.git] / libotp.c
blob2db7ed634892852fc95c5993cb81b484d8ed1ad0
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);
96 printf("buffer=%s\n", buffer);
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 : */
197 printf("pad=|%s|, filename=|%s|\n", pad_name, pad_filename);
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;
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 printf("offset=%ld\n", msg->offset);
290 /* Move after mandatory comma. */
291 if (s[0] != ',') {
292 fprintf(stderr, "unpackage: missing comma after offset, at |%s|, input |%s|\n",
293 s, input);
294 exit(EX_DATAERR);
296 ++s;
298 memset(pad_name, 0, PAD_NAME_LENGTH);
300 /* Includes pad name? */
301 if (s[0] != '\n' && s[0] != '\r') {
302 unsigned int i;
304 i = 0;
305 /* v0.7+ message, includes pad name */
306 while(s[0] != '\n' && s[0] != '\r' && s[0] != ',' && s < end) {
307 pad_name[i] = s[0];
308 ++s;
309 ++i;
310 if (i > PAD_NAME_LENGTH) {
311 fprintf(stderr, "unpackage: pad name length > maximum %d, in input |%s|\n",
312 PAD_NAME_LENGTH, input);
313 exit(EX_DATAERR);
318 printf("Pad name: |%s|\n", pad_name);
320 msg->pad = find_pad(pad_name);
321 if (!msg->pad) {
322 fprintf(stderr, "No such pad by name '%s'\n", pad_name);
323 exit(EX_DATAERR);
326 /* Go to next line */
327 while((s[0] == '\n' || s[0] == '\r' || s[0] == ',') && (s < end))
328 ++s;
330 printf("s=%s\n", s);
332 /* Extract base64 data from end of message. */
333 b64_ct = strdup(s);
334 b64_ct[end - s] = 0;
336 printf("b64_data=<%s>\n", b64_ct);
338 /* Decode base64 */
339 msg->cipher_text = malloc(strlen(b64_ct));
340 msg->length = base64_decode(b64_ct, msg->cipher_text);
341 free(b64_ct);
343 printf("decoded to %ld bytes\n", msg->length);
345 return msg;
348 /** Decrypt a packaged message and return plaintext.
350 * @param out Pointer to pointer which is set to the output. Caller must free.
351 * There is an extra null at the end of this buffer, not part of the output,
352 * but there may be embedded nulls if binary data is encoded. If this is expected,
353 * use the return value:
355 * @return Length of message, in bytes.
357 unsigned int otp_decrypt(char *input, char **out)
359 MESSAGE *msg;
360 unsigned int length;
362 msg = unpackage(input);
364 if (msg->offset < read_offset(msg->pad)) {
365 fprintf(stderr, "** warning: this is an old message! possible replay attack: %ld < %ld\n",
366 msg->offset, read_offset(msg->pad));
369 length = msg->length;
370 *out = otp_decrypt_msg(msg);
371 free_message(msg);
373 return length;
376 /** Deccrypt an encrypted message.
378 * @return Decrypted message, of length msg->length. Caller frees.
380 char *otp_decrypt_msg(MESSAGE *msg)
382 char *pad_data, *out;
383 unsigned int i;
385 /* Seek to area of pad we're using. */
386 if (fseek(msg->pad->fp, msg->offset, SEEK_SET) < 0) {
387 fprintf(stderr, "failed to seek in pad %s to %ld\n",
388 msg->pad->name, msg->offset);
389 perror("fseek");
390 exit(EXIT_FAILURE);
393 /* Read pad. */
394 pad_data = malloc(msg->length);
395 if (!pad_data) {
396 perror("malloc pad data");
397 exit(EX_UNAVAILABLE);
399 if (fread(pad_data, msg->length, 1, msg->pad->fp) < 1) {
400 fprintf(stderr, "read pad %s, offset %ld, length %ld failed",
401 msg->pad->name, msg->offset, msg->length);
402 exit(EXIT_FAILURE);
405 /* Apply XOR to give output. */
406 out = malloc(msg->length + 1);
407 if (!out) {
408 perror("malloc output buffer");
409 exit(EX_UNAVAILABLE);
412 for (i = 0; i < msg->length; ++i) {
413 out[i] = pad_data[i] ^ msg->cipher_text[i];
416 /* Null-terminate output for convenience when using text-only messages.
417 * The output is still of length msg->length; use msg->length when decrypting
418 * binary messages. */
419 out[msg->length] = 0;
421 free(pad_data);
423 return out;