1 /*
2 Copyright 2020 Northern.tech AS
3
4 This file is part of CFEngine 3 - written and maintained by Northern.tech AS.
5
6 This program is free software; you can redistribute it and/or modify it
7 under the terms of the GNU General Public License as published by the
8 Free Software Foundation; version 3.
9
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
14
15 You should have received a copy of the GNU General Public License
16 along with this program; if not, write to the Free Software
17 Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA
18
19 To the extent this program is licensed as part of the Enterprise
20 versions of CFEngine, the applicable Commercial Open Source License
21 (COSL) may apply to this file if you as a licensee so wish it. See
22 included file COSL.txt.
23 */
24
25 /*
26 cf-secret.c
27
28 Copyright (C) 2017 cfengineers.net
29
30 Written and maintained by Jon Henrik Bjornstad <jonhenrik@cfengineers.net>
31
32 Licensed under the Apache License, Version 2.0 (the "License");
33 you may not use this file except in compliance with the License.
34 You may obtain a copy of the License at
35
36 http://www.apache.org/licenses/LICENSE-2.0
37
38 Unless required by applicable law or agreed to in writing, software
39 distributed under the License is distributed on an "AS IS" BASIS,
40 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
41 See the License for the specific language governing permissions and
42 limitations under the License.
43
44 */
45
46 #include <platform.h>
47 #include <openssl/err.h>
48
49 #include <lastseen.h>
50 #include <crypto.h>
51 #include <generic_agent.h> /* GenericAgentSetDefaultDigest */
52 #include <writer.h>
53 #include <man.h>
54 #include <conversion.h>
55 #include <hash.h>
56 #include <known_dirs.h>
57 #include <string_lib.h>
58 #include <file_lib.h>
59 #include <sequence.h>
60 #include <string_sequence.h>
61 #include <unistd.h>
62
63 #define BUFSIZE 1024
64
65 #define MAX_HEADER_LEN 256 /* "Key[126]: Value[128]" */
66 #define MAX_HEADER_KEY_LEN 126
67 #define MAX_HEADER_VAL_LEN 128
68
69 typedef enum {
70 HOST_RSA_KEY_PRIVATE,
71 HOST_RSA_KEY_PUBLIC,
72 } HostRSAKeyType;
73
74 /* see README.md for details about the format */
75 typedef enum {
76 CF_SECRET_FORMAT_V_1_0,
77 } CFKeyCryptFormatVersion;
78
79 static const char passphrase[] = "Cfengine passphrase";
80
81 //*******************************************************************
82 // DOCUMENTATION / GETOPT CONSTS:
83 //*******************************************************************
84
85 static const char *const CF_SECRET_SHORT_DESCRIPTION =
86 "cf-secret: Use CFEngine cryptographic keys to encrypt and decrypt files";
87
88 static const char *const CF_SECRET_MANPAGE_LONG_DESCRIPTION =
89 "cf-secret offers a simple way to encrypt or decrypt files using keys "
90 "generated by cf-key. CFEngine uses asymmetric cryptography, and "
91 "cf-secret allows you to encrypt a file using a public key file. "
92 "The encrypted file can only be decrypted on the host with the "
93 "corresponding private key. Original author: Jon Henrik Bjornstad "
94 "<jonhenrik@cfengineers.net>";
95
96 static const struct option OPTIONS[] =
97 {
98 {"help", no_argument, 0, 'h'},
99 {"manpage", no_argument, 0, 'M'},
100 {"debug", no_argument, 0, 'd'},
101 {"verbose", no_argument, 0, 'v'},
102 {"log-level", required_argument, 0, 'g'},
103 {"inform", no_argument, 0, 'I'},
104 {"key", required_argument, 0, 'k'},
105 {"host", required_argument, 0, 'H'},
106 {"output", required_argument, 0, 'o'},
107 {NULL, 0, 0, '\0'}
108 };
109
110 static const char *const HINTS[] =
111 {
112 "Print the help message",
113 "Print the man page",
114 "Enable debugging output",
115 "Enable verbose output",
116 "Specify how detailed logs should be. Possible values: 'error', 'warning', 'notice', 'info', 'verbose', 'debug'",
117 "Enable basic information output",
118 "Comma-separated list of key files to use (one of -k/-H options is required for encryption)",
119 "Comma-separated list of hosts to encrypt/decrypt for (defaults to 'localhost' for decryption)",
120 "Output file (required)",
121 NULL
122 };
123
124 static const Description COMMANDS[] =
125 {
126 {"encrypt", "Encrypt data for one or more hosts/keys", "cf-secret encrypt -k/-H KEY/HOST -o OUTPUT INPUT"},
127 {"decrypt", "Decrypt data", "cf-secret decrypt [-k/-H KEY/HOST] -o OUTPUT INPUT"},
128 {"print-headers", "Print headers from an encrypted file", "cf-secret print-headers ENCRYPTED_FILE"},
129 {NULL, NULL, NULL}
130 };
131
GetIPAddress(struct sockaddr * sa)132 static inline void *GetIPAddress(struct sockaddr *sa)
133 {
134 assert(sa != NULL);
135 if (sa->sa_family == AF_INET)
136 {
137 return &(((struct sockaddr_in*)sa)->sin_addr);
138 }
139 return &(((struct sockaddr_in6*)sa)->sin6_addr);
140 }
141
142 /**
143 * Get path of the RSA key for the given host.
144 */
GetHostRSAKey(const char * host,HostRSAKeyType type)145 static char *GetHostRSAKey(const char *host, HostRSAKeyType type)
146 {
147 const char *key_ext = NULL;
148 if (type == HOST_RSA_KEY_PRIVATE)
149 {
150 key_ext = ".priv";
151 }
152 else
153 {
154 key_ext = ".pub";
155 }
156
157 struct addrinfo *result;
158 int error = getaddrinfo(host, NULL, NULL, &result);
159 if (error != 0)
160 {
161 Log(LOG_LEVEL_ERR, "Failed to get IP from host (getaddrinfo: %s)",
162 gai_strerror(error));
163 return NULL;
164 }
165
166 char *buffer = malloc(BUFSIZE);
167 char hash[CF_HOSTKEY_STRING_SIZE];
168 char ipaddress[64];
169 bool found = false;
170 for (struct addrinfo *res = result; !found && (res != NULL); res = res->ai_next)
171 {
172 inet_ntop(res->ai_family,
173 GetIPAddress((struct sockaddr *) res->ai_addr),
174 ipaddress, sizeof(ipaddress));
175 if (StringStartsWith(ipaddress, "127.") || StringEqual(ipaddress, "::1"))
176 {
177 Log(LOG_LEVEL_VERBOSE, "Using localhost%s key", key_ext);
178 found = true;
179 snprintf(buffer, BUFSIZE, "%s/ppkeys/localhost%s", WORKDIR, key_ext);
180 return buffer;
181 }
182 found = Address2Hostkey(hash, sizeof(hash), ipaddress);
183 }
184 if (found)
185 {
186 Log(LOG_LEVEL_DEBUG, "Found host '%s' for address '%s'", hash, ipaddress);
187 snprintf(buffer, BUFSIZE, "%s/ppkeys/root-%s%s", WORKDIR, hash, key_ext);
188 freeaddrinfo(result);
189 return buffer;
190 }
191 else
192 {
193 Log(LOG_LEVEL_DEBUG, "Searching key by IP");
194 for (struct addrinfo *res = result; res != NULL; res = res->ai_next)
195 {
196 inet_ntop(res->ai_family,
197 GetIPAddress((struct sockaddr *) res->ai_addr),
198 ipaddress, sizeof(ipaddress));
199 snprintf(buffer, BUFSIZE, "%s/ppkeys/root-%s%s", WORKDIR, ipaddress, key_ext);
200 if (access(buffer, F_OK) == 0)
201 {
202 Log(LOG_LEVEL_DEBUG, "Found matching key: '%s'", buffer);
203 freeaddrinfo(result);
204 return buffer;
205 }
206 }
207 }
208 return NULL;
209 }
210
ReadPrivateKey(const char * privkey_path)211 static RSA *ReadPrivateKey(const char *privkey_path)
212 {
213 FILE *fp = safe_fopen(privkey_path,"r");
214
215 if (fp == NULL)
216 {
217 Log(LOG_LEVEL_ERR, "Could not open private key '%s'", privkey_path);
218 return NULL;
219 }
220 RSA *privkey = PEM_read_RSAPrivateKey(fp, (RSA **) NULL, NULL, (void *) passphrase);
221 if (privkey == NULL)
222 {
223 unsigned long err = ERR_get_error();
224 Log(LOG_LEVEL_ERR, "Could not read private key '%s': %s",
225 privkey_path, ERR_reason_error_string(err));
226 }
227 fclose(fp);
228 return privkey;
229 }
230
ReadPublicKey(const char * pubkey_path)231 static RSA *ReadPublicKey(const char *pubkey_path)
232 {
233 FILE *fp = safe_fopen(pubkey_path, "r");
234
235 if (fp == NULL)
236 {
237 Log(LOG_LEVEL_ERR, "Could not open public key '%s'", pubkey_path);
238 return NULL;
239 }
240
241 RSA *pubkey = PEM_read_RSAPublicKey(fp, NULL, NULL, (void *) passphrase);
242 if (pubkey == NULL)
243 {
244 unsigned long err = ERR_get_error();
245 Log(LOG_LEVEL_ERR, "Could not read public key '%s': %s",
246 pubkey_path, ERR_reason_error_string(err));
247 }
248 fclose(fp);
249 return pubkey;
250 }
251
OpenInputOutput(const char * path,const char * mode)252 static FILE *OpenInputOutput(const char *path, const char *mode)
253 {
254 assert(path != NULL);
255 assert(mode != NULL);
256 if (StringEqual(path, "-"))
257 {
258 if (*mode == 'r')
259 {
260 return stdin;
261 }
262 else
263 {
264 return stdout;
265 }
266 }
267 return safe_fopen(path, mode);
268 }
269
RSAEncrypt(Seq * rsa_keys,const char * input_path,const char * output_path)270 static bool RSAEncrypt(Seq *rsa_keys, const char *input_path, const char *output_path)
271 {
272 assert((rsa_keys != NULL) && (SeqLength(rsa_keys) > 0));
273
274 FILE *input_file = OpenInputOutput(input_path, "r");
275 if (input_file == NULL)
276 {
277 Log(LOG_LEVEL_ERR, "Could not open input file '%s'", input_path);
278 return false;
279 }
280
281 FILE *output_file = OpenInputOutput(output_path, "w");
282 if (output_file == NULL)
283 {
284 Log(LOG_LEVEL_ERR, "Could not create or open output file '%s'", output_path);
285 fclose(input_file);
286 return false;
287 }
288
289 const size_t n_keys = SeqLength(rsa_keys);
290 Seq *evp_keys = SeqNew(n_keys, EVP_PKEY_free);
291 for (size_t i = 0; i < n_keys; i++)
292 {
293 RSA *rsa_key = SeqAt(rsa_keys, i);
294 EVP_PKEY *evp_key = EVP_PKEY_new();
295 if (EVP_PKEY_set1_RSA(evp_key, rsa_key) == 0)
296 {
297 Log(LOG_LEVEL_ERR, "Failed to initialize encryption context");
298 SeqDestroy(evp_keys);
299 fclose(input_file);
300 fclose(output_file);
301 return false;
302 }
303 SeqAppend(evp_keys, evp_key);
304 }
305
306 bool success = true;
307
308 const EVP_CIPHER *cipher = EVP_aes_256_cbc();
309 EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
310 const int key_size = EVP_PKEY_size((EVP_PKEY*) SeqAt(evp_keys, 0));
311
312 /* This sequence and the 'enc_key_sizes' array are both populated by the
313 * EVP_SealInit() call below. */
314 Seq *enc_keys = SeqNew(n_keys, free);
315 for (size_t i = 0; i < n_keys; i++)
316 {
317 SeqAppend(enc_keys, xmalloc(key_size));
318 }
319 int enc_key_sizes[n_keys];
320
321 const int iv_size = EVP_CIPHER_iv_length(cipher);
322 unsigned char iv[iv_size];
323
324 const int block_size = EVP_CIPHER_block_size(cipher);
325 char plaintext[block_size], ciphertext[2 * block_size];
326 int ct_len;
327
328 int ret = EVP_SealInit(ctx, cipher,
329 (unsigned char**) SeqGetData(enc_keys), enc_key_sizes, iv,
330 (EVP_PKEY**) SeqGetData(evp_keys), n_keys);
331 if (ret == 0)
332 {
333 Log(LOG_LEVEL_ERR, "Failed to initialize encryption context");
334 success = false;
335 goto cleanup;
336 }
337
338 /* newline and NUL-byte => +2 */
339 Log(LOG_LEVEL_VERBOSE, "Writing headers");
340 char header[MAX_HEADER_LEN + 2] = "Version: 1.0\n";
341 size_t header_len = strlen(header);
342 ssize_t n_written = FullWrite(fileno(output_file), header, header_len);
343 if (n_written != header_len)
344 {
345 Log(LOG_LEVEL_ERR, "Failed to write header to the output file '%s'", output_path);
346 success = false;
347 goto cleanup;
348 }
349 Log(LOG_LEVEL_VERBOSE, "Writing Encrypted-for headers");
350 for (size_t i = 0; i < n_keys; i++)
351 {
352 char *key_digest = GetPubkeyDigest(SeqAt(rsa_keys, i));
353 header_len = snprintf(header, MAX_HEADER_LEN + 2, "Encrypted-for: %s\n", key_digest);
354 free(key_digest);
355 assert(header_len <= (MAX_HEADER_LEN + 2));
356 n_written = FullWrite(fileno(output_file), header, header_len);
357 if (n_written != header_len)
358 {
359 Log(LOG_LEVEL_ERR, "Failed to write header to the output file '%s'", output_path);
360 success = false;
361 goto cleanup;
362 }
363 }
364 n_written = FullWrite(fileno(output_file), "\n", 1);
365 if (n_written != 1)
366 {
367 Log(LOG_LEVEL_ERR, "Failed to write header to the output file '%s'", output_path);
368 success = false;
369 goto cleanup;
370 }
371 Log(LOG_LEVEL_VERBOSE, "Writing IV");
372 n_written = FullWrite(fileno(output_file), iv, iv_size);
373 if (n_written != iv_size)
374 {
375 Log(LOG_LEVEL_ERR, "Failed to write IV to the output file '%s'", output_path);
376 success = false;
377 goto cleanup;
378 }
379
380 Log(LOG_LEVEL_VERBOSE, "Writing keys");
381 for (size_t i = 0; i < n_keys; i++)
382 {
383 const char *enc_key = SeqAt(enc_keys, i);
384 n_written = FullWrite(fileno(output_file), enc_key, enc_key_sizes[i]);
385 if (n_written != enc_key_sizes[i])
386 {
387 Log(LOG_LEVEL_ERR, "Failed to write key to the output file '%s'", output_path);
388 success = false;
389 goto cleanup;
390 }
391 }
392
393 size_t processed = 0;
394 while (success && !feof(input_file))
395 {
396 ssize_t n_read = ReadFileStreamToBuffer(input_file, block_size, plaintext);
397 if (n_read == FILE_ERROR_READ)
398 {
399 Log(LOG_LEVEL_ERR, "Could not read file '%s'", input_path);
400 success = false;
401 break;
402 }
403 ret = EVP_SealUpdate(ctx, ciphertext, &ct_len, plaintext, n_read);
404 if (ret == 0)
405 {
406 Log(LOG_LEVEL_ERR, "Failed to encrypt data: %s",
407 ERR_error_string(ERR_get_error(), NULL));
408 success = false;
409 break;
410 }
411 n_written = FullWrite(fileno(output_file), ciphertext, ct_len);
412 if (n_written < 0)
413 {
414 Log(LOG_LEVEL_ERR, "Could not write file '%s'", output_path);
415 success = false;
416 break;
417 }
418 processed += n_read;
419 Log(LOG_LEVEL_VERBOSE, "%zu bytes processed", processed);
420 }
421 Log(LOG_LEVEL_VERBOSE, "Finalizing");
422 ret = EVP_SealFinal(ctx, ciphertext, &ct_len);
423 if (ret == 0)
424 {
425 Log(LOG_LEVEL_ERR, "Failed to encrypt data: %s",
426 ERR_error_string(ERR_get_error(), NULL));
427 success = false;
428 }
429 if (ct_len > 0)
430 {
431 n_written = FullWrite(fileno(output_file), ciphertext, ct_len);
432 if (n_written < 0)
433 {
434 Log(LOG_LEVEL_ERR, "Could not write file '%s'", output_path);
435 success = false;
436 }
437 }
438 OPENSSL_cleanse(plaintext, block_size);
439
440 cleanup:
441 fclose(input_file);
442 fclose(output_file);
443 SeqDestroy(evp_keys);
444 SeqDestroy(enc_keys);
445 EVP_CIPHER_CTX_free(ctx);
446 return success;
447 }
448
CheckHeader(const char * key,const char * value)449 static inline bool CheckHeader(const char *key, const char *value)
450 {
451 if (StringEqual(key, "Version"))
452 {
453 if (!StringEqual(value, "1.0"))
454 {
455 Log(LOG_LEVEL_ERR, "Unsupported file format version: '%s'", value);
456 return false;
457 }
458 else
459 {
460 return true;
461 }
462 }
463 else if (StringEqual(key, "Encrypted-for"))
464 {
465 /* TODO: do some verification that 'value' is valid hash digest? */
466 return true;
467 }
468 else
469 {
470 Log(LOG_LEVEL_ERR, "Unsupported header: '%s'", key);
471 return false;
472 }
473 }
474
475 /**
476 * Read from #input_file into #buffer at most #max_chars or until #delimiter is
477 * encountered. If #stop_on_space, also stop when ' ' is read. #buffer is
478 * NUL-terminated when this function returns.
479 *
480 * @warning #buffer has to be at least #max_chars+1 big to accommodate for the
481 * terminating NUL byte.
482 */
ReadUntilDelim(FILE * input_file,char * buffer,size_t max_chars,char delimiter,bool stop_on_space)483 static inline bool ReadUntilDelim(FILE *input_file, char *buffer, size_t max_chars, char delimiter, bool stop_on_space)
484 {
485 bool done = false;
486 size_t i = 0;
487 int c = fgetc(input_file);
488 while (!done && (i < max_chars))
489 {
490 if (c == EOF)
491 {
492 done = true;
493 }
494 else if (c == delimiter)
495 {
496 done = true;
497 ungetc(c, input_file);
498 }
499 else if (stop_on_space && (c == ' '))
500 {
501 done = true;
502 ungetc(c, input_file);
503 }
504 else
505 {
506 buffer[i] = (char) c;
507 i++;
508 if (i < max_chars)
509 {
510 c = fgetc(input_file);
511 }
512 }
513 }
514 buffer[i] = '\0';
515
516 bool ran_out = ((i > max_chars) || (c == EOF));
517 return !ran_out;
518 }
519
SkipSpaces(FILE * input_file)520 static inline void SkipSpaces(FILE *input_file)
521 {
522 int c = ' ';
523 while (!feof(input_file) && (c == ' '))
524 {
525 c = fgetc(input_file);
526 }
527 if (c != ' ')
528 {
529 ungetc(c, input_file);
530 }
531 }
532
ParseHeader(FILE * input_file,char * key,char * value)533 static inline bool ParseHeader(FILE *input_file, char *key, char *value)
534 {
535 bool ok = ReadUntilDelim(input_file, key, MAX_HEADER_KEY_LEN, ':', true);
536 if (ok)
537 {
538 SkipSpaces(input_file);
539 ok = (fgetc(input_file) == ':');
540 SkipSpaces(input_file);
541 ok = ReadUntilDelim(input_file, value, MAX_HEADER_VAL_LEN, '\n', false);
542 }
543 ok = (fgetc(input_file) == '\n');
544 return ok;
545 }
546
ParseHeaders(FILE * input_file,RSA * privkey,size_t * enc_key_pos,size_t * n_enc_keys)547 static bool ParseHeaders(FILE *input_file, RSA *privkey, size_t *enc_key_pos, size_t *n_enc_keys)
548 {
549 assert(enc_key_pos != NULL);
550 assert(n_enc_keys != NULL);
551
552 /* Make sure these are always set by this function. */
553 *enc_key_pos = 0;
554 *n_enc_keys = 0;
555
556 /* Actually works for a private RSA key too because it contains the public
557 * key. */
558 char *key_digest = GetPubkeyDigest(privkey);
559 bool version_specified = false;
560 bool found_matching_digest = false;
561 size_t n_enc_for_headers = 0;
562
563 char key[MAX_HEADER_KEY_LEN + 1];
564 char value[MAX_HEADER_VAL_LEN + 1];
565
566 while (ParseHeader(input_file, key, value))
567 {
568 Log(LOG_LEVEL_DEBUG, "Parsed header '%s: %s'", key, value);
569 if (!CheckHeader(key, value))
570 {
571 free(key_digest);
572 return false;
573 }
574
575 if (StringEqual(key, "Version"))
576 {
577 version_specified = true;
578 }
579 else if (StringEqual(key, "Encrypted-for"))
580 {
581 Log(LOG_LEVEL_DEBUG, "Encrypted for '%s'", value);
582 if (StringEqual(value, key_digest))
583 {
584 found_matching_digest = true;
585 *enc_key_pos = n_enc_for_headers;
586 }
587 n_enc_for_headers++;
588 }
589
590 /* headers are supposed to be terminated by a blank line */
591 int next = fgetc(input_file);
592 if (next == '\n')
593 {
594 if (!version_specified)
595 {
596 Log(LOG_LEVEL_ERR, "File format version not specified");
597 }
598 if (!found_matching_digest)
599 {
600 Log(LOG_LEVEL_ERR, "File not encrypted for host '%s'", key_digest);
601 }
602 *n_enc_keys = n_enc_for_headers;
603 free(key_digest);
604 return (version_specified && found_matching_digest);
605 }
606 else if (next == EOF)
607 {
608 Log(LOG_LEVEL_ERR, "Failed to parse headers from");
609 free(key_digest);
610 return false;
611 }
612 else
613 {
614 /* keep trying */
615 ungetc(next, input_file);
616 }
617 }
618 Log(LOG_LEVEL_ERR, "Failed to parse headers");
619 free(key_digest);
620 return false;
621 }
622
RSADecrypt(RSA * privkey,const char * input_path,const char * output_path)623 static bool RSADecrypt(RSA *privkey, const char *input_path, const char *output_path)
624 {
625 FILE *input_file = OpenInputOutput(input_path, "r");
626 if (input_file == NULL)
627 {
628 Log(LOG_LEVEL_ERR, "Cannot open input file '%s'", input_path);
629 return false;
630 }
631
632 FILE *output_file = OpenInputOutput(output_path, "w");
633 if (output_file == NULL)
634 {
635 Log(LOG_LEVEL_ERR, "Cannot open output file '%s'", output_path);
636 fclose(input_file);
637 return false;
638 }
639
640 EVP_PKEY *evp_key = EVP_PKEY_new();
641 if (EVP_PKEY_set1_RSA(evp_key, privkey) == 0)
642 {
643 Log(LOG_LEVEL_ERR, "Failed to initialize decryption context");
644 fclose(input_file);
645 fclose(output_file);
646 return false;
647 }
648
649 bool success = true;
650
651 Log(LOG_LEVEL_VERBOSE, "Parsing headers");
652 size_t our_key_pos;
653 size_t n_enc_keys;
654 if (!ParseHeaders(input_file, privkey, &our_key_pos, &n_enc_keys))
655 {
656 fclose(input_file);
657 fclose(output_file);
658 return false;
659 }
660 Log(LOG_LEVEL_DEBUG, "Parsed %zu keys", n_enc_keys);
661
662 const EVP_CIPHER *cipher = EVP_aes_256_cbc();
663 EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
664
665 const int iv_size = EVP_CIPHER_iv_length(cipher);
666 unsigned char iv[iv_size];
667
668 const int key_size = EVP_PKEY_size(evp_key);
669 unsigned char ek[key_size];
670 unsigned char dev_null[key_size];
671
672 const int block_size = EVP_CIPHER_block_size(cipher);
673
674 char plaintext[block_size], ciphertext[2 * block_size];
675 int pt_len;
676
677 ssize_t n_read = ReadFileStreamToBuffer(input_file, iv_size, iv);
678 if (n_read != iv_size)
679 {
680 Log(LOG_LEVEL_ERR, "Failed to read the IV from '%s'", input_path);
681 goto cleanup;
682 }
683
684 /* just skip the keys that are not ours */
685 size_t nth_key = 0;
686 for (; nth_key < our_key_pos; nth_key++)
687 {
688 n_read = ReadFileStreamToBuffer(input_file, key_size, dev_null);
689 if (n_read != key_size)
690 {
691 Log(LOG_LEVEL_ERR, "Failed to read the key from '%s'", input_path);
692 goto cleanup;
693 }
694 Log(LOG_LEVEL_DEBUG, "Skipping key");
695 }
696 /* read our key */
697 Log(LOG_LEVEL_DEBUG, "Reading key");
698 n_read = ReadFileStreamToBuffer(input_file, key_size, ek);
699 if (n_read != key_size)
700 {
701 Log(LOG_LEVEL_ERR, "Failed to read the key from '%s'", input_path);
702 goto cleanup;
703 }
704 nth_key++;
705 /* skip the remaining keys */
706 for (; nth_key < n_enc_keys; nth_key++)
707 {
708 n_read = ReadFileStreamToBuffer(input_file, key_size, dev_null);
709 if (n_read != key_size)
710 {
711 Log(LOG_LEVEL_ERR, "Failed to read the key from '%s'", input_path);
712 goto cleanup;
713 }
714 Log(LOG_LEVEL_DEBUG, "Skipping key");
715 }
716
717 int ret = EVP_OpenInit(ctx, cipher, ek, key_size, iv, evp_key);
718 if (ret == 0)
719 {
720 char *key_digest = GetPubkeyDigest(privkey);
721 Log(LOG_LEVEL_ERR, "Failed to decrypt contents using key '%s'", key_digest);
722 free(key_digest);
723 success = false;
724 goto cleanup;
725 }
726
727 Log(LOG_LEVEL_VERBOSE, "Decrypting data");
728 size_t processed = 0;
729 ssize_t n_written;
730 while (success && !feof(input_file))
731 {
732 n_read = ReadFileStreamToBuffer(input_file, block_size, ciphertext);
733 if (n_read == FILE_ERROR_READ)
734 {
735 Log(LOG_LEVEL_ERR, "Could not read file '%s'", input_path);
736 success = false;
737 break;
738 }
739 ret = EVP_OpenUpdate(ctx, plaintext, &pt_len, ciphertext, n_read);
740 if (ret == 0)
741 {
742 Log(LOG_LEVEL_ERR, "Failed to decrypt data: %s",
743 ERR_error_string(ERR_get_error(), NULL));
744 success = false;
745 break;
746 }
747 n_written = FullWrite(fileno(output_file), plaintext, pt_len);
748 if (n_written < 0)
749 {
750 Log(LOG_LEVEL_ERR, "Could not write file '%s'", output_path);
751 success = false;
752 break;
753 }
754 processed += n_read;
755 Log(LOG_LEVEL_VERBOSE, "%zu bytes processed", processed);
756 }
757 Log(LOG_LEVEL_VERBOSE, "Finalizing");
758 ret = EVP_OpenFinal(ctx, plaintext, &pt_len);
759 if (ret == 0)
760 {
761 Log(LOG_LEVEL_ERR, "Failed to decrypt data: %s",
762 ERR_error_string(ERR_get_error(), NULL));
763 success = false;
764 }
765 if (pt_len > 0)
766 {
767 n_written = FullWrite(fileno(output_file), plaintext, pt_len);
768 if (n_written < 0)
769 {
770 Log(LOG_LEVEL_ERR, "Could not write file '%s'", output_path);
771 success = false;
772 }
773 }
774 OPENSSL_cleanse(plaintext, block_size);
775
776 cleanup:
777 fclose(input_file);
778 fclose(output_file);
779 EVP_PKEY_free(evp_key);
780 EVP_CIPHER_CTX_free(ctx);
781 return success;
782 }
783
LoadPublicKeys(Seq * key_paths)784 static Seq *LoadPublicKeys(Seq *key_paths)
785 {
786 const size_t n_keys = SeqLength(key_paths);
787 Seq *pub_keys = SeqNew(n_keys, RSA_free);
788 for (size_t i = 0; i < n_keys; i++)
789 {
790 const char *key_path = SeqAt(key_paths, i);
791 Log(LOG_LEVEL_VERBOSE, "Reading key '%s'", key_path);
792 RSA *pubkey = ReadPublicKey(key_path);
793 if (pubkey == NULL)
794 {
795 SeqDestroy(pub_keys);
796 return NULL;
797 }
798 SeqAppend(pub_keys, pubkey);
799 }
800 return pub_keys;
801 }
802
CFKeyCryptHelp()803 static void CFKeyCryptHelp()
804 {
805 Writer *w = FileWriter(stdout);
806 WriterWriteHelp(w, "cf-secret", OPTIONS, HINTS, COMMANDS, true, true);
807 FileWriterDetach(w);
808 }
809
CFKeyCryptMan()810 void CFKeyCryptMan()
811 {
812 Writer *out = FileWriter(stdout);
813 ManPageWrite(out, "cf-secret", time(NULL),
814 CF_SECRET_SHORT_DESCRIPTION,
815 CF_SECRET_MANPAGE_LONG_DESCRIPTION,
816 OPTIONS, HINTS, COMMANDS, true, true);
817 FileWriterDetach(out);
818 }
819
main(int argc,char * argv[])820 int main(int argc, char *argv[])
821 {
822 if (argc < 2)
823 {
824 CFKeyCryptHelp();
825 exit(EXIT_FAILURE);
826 }
827
828 opterr = 0;
829 char *key_path_arg = NULL;
830 char *input_path = NULL;
831 char *output_path = NULL;
832 char *host_arg = NULL;
833 bool encrypt = false;
834 bool decrypt = false;
835 bool print_headers = false;
836
837 size_t offset = 0;
838 if (StringEqual(argv[1], "encrypt"))
839 {
840 encrypt = true;
841 offset++;
842 }
843 else if (StringEqual(argv[1], "decrypt"))
844 {
845 offset++;
846 decrypt = true;
847 }
848 else if (StringEqual(argv[1], "print-headers"))
849 {
850 print_headers = true;
851 offset++;
852 }
853
854 int c = 0;
855 while ((c = getopt_long(argc - offset, argv + offset, "hMedk:o:H:", OPTIONS, NULL)) != -1)
856 {
857 switch (c)
858 {
859 case 'h':
860 CFKeyCryptHelp();
861 exit(EXIT_SUCCESS);
862 break;
863 case 'M':
864 CFKeyCryptMan();
865 exit(EXIT_SUCCESS);
866 break;
867 case 'd':
868 LogSetGlobalLevel(LOG_LEVEL_DEBUG);
869 Log(LOG_LEVEL_DEBUG, "Debug log level enabled");
870 break;
871 case 'v':
872 LogSetGlobalLevel(LOG_LEVEL_VERBOSE);
873 Log(LOG_LEVEL_VERBOSE, "Verbose log level enabled");
874 break;
875 case 'I':
876 LogSetGlobalLevel(LOG_LEVEL_INFO);
877 Log(LOG_LEVEL_INFO, "Inform log level enabled");
878 break;
879 case 'g':
880 LogSetGlobalLevelArgOrExit(optarg);
881 break;
882 case 'k':
883 key_path_arg = optarg;
884 break;
885 case 'o':
886 output_path = optarg;
887 break;
888 case 'H':
889 host_arg = optarg;
890 break;
891 default:
892 Log(LOG_LEVEL_ERR, "Unknown option '-%c'", optopt);
893 CFKeyCryptHelp();
894 exit(EXIT_FAILURE);
895 }
896 }
897
898 if (!(decrypt || encrypt || print_headers))
899 {
900 printf("Command required. Specify either 'encrypt', 'decrypt' or 'print-headers'\n");
901 CFKeyCryptHelp();
902 exit(EXIT_FAILURE);
903 }
904
905 /* Increment 'optind' because of command being argv[0]. */
906 optind++;
907 input_path = argv[optind];
908
909 /* Some more unexpected arguments? */
910 if (argc > (optind + offset))
911 {
912 Log(LOG_LEVEL_ERR, "Unexpected non-option argument: '%s'", argv[optind + 1]);
913 exit(EXIT_FAILURE);
914 }
915
916 if (print_headers)
917 {
918 FILE *input_file = OpenInputOutput(input_path, "r");
919 char key[MAX_HEADER_KEY_LEN + 1];
920 char value[MAX_HEADER_VAL_LEN + 1];
921
922 bool done = false;
923 while (!done && ParseHeader(input_file, key, value))
924 {
925 Log(LOG_LEVEL_DEBUG, "Parsed header '%s: %s'", key, value);
926 if (!CheckHeader(key, value))
927 {
928 fclose(input_file);
929 exit(EXIT_FAILURE);
930 }
931 printf("%s: %s\n", key, value);
932
933 /* headers are supposed to be terminated by a blank line */
934 int next = fgetc(input_file);
935 if (next == '\n')
936 {
937 done = true;
938 }
939 else
940 {
941 ungetc(next, input_file);
942 }
943 }
944 fclose(input_file);
945 exit(EXIT_SUCCESS);
946 }
947
948 if (decrypt && (host_arg == NULL) && (key_path_arg == NULL))
949 {
950 /* Decryption requires a private key which is usually only available for
951 * the local host. Let's just default to localhost if no other specific
952 * host/key is given for decryption. */
953 Log(LOG_LEVEL_VERBOSE, "Using the localhost private key for decryption");
954 host_arg = "localhost";
955 }
956
957 if ((host_arg != NULL) && (key_path_arg != NULL))
958 {
959 Log(LOG_LEVEL_ERR,
960 "--host/-H is used to specify a public key and cannot be used with --key/-k");
961 exit(EXIT_FAILURE);
962 }
963
964 if (input_path == NULL)
965 {
966 Log(LOG_LEVEL_ERR, "No input file specified (Use -h for help)");
967 exit(EXIT_FAILURE);
968 }
969 if (output_path == NULL)
970 {
971 Log(LOG_LEVEL_ERR, "No output file specified (Use -h for help)");
972 exit(EXIT_FAILURE);
973 }
974
975 CryptoInitialize();
976 GenericAgentSetDefaultDigest(&CF_DEFAULT_DIGEST, &CF_DEFAULT_DIGEST_LEN);
977
978 Seq *key_paths;
979 if (key_path_arg != NULL)
980 {
981 Log(LOG_LEVEL_DEBUG, "-k/--key given: '%s'", key_path_arg);
982 key_paths = SeqStringFromString(key_path_arg, ',');
983 }
984 else
985 {
986 key_paths = SeqNew(16, free);
987 }
988
989 if (host_arg != NULL)
990 {
991 Log(LOG_LEVEL_DEBUG, "-H/--host given: '%s'", host_arg);
992 Seq *hosts = SeqStringFromString(host_arg, ',');
993 const size_t n_hosts = SeqLength(hosts);
994 for (size_t i = 0; i < n_hosts; i++)
995 {
996 HostRSAKeyType key_type = encrypt ? HOST_RSA_KEY_PUBLIC : HOST_RSA_KEY_PRIVATE;
997 char *host = SeqAt(hosts, i);
998 char *host_key_path = GetHostRSAKey(host, key_type);
999 if (!host_key_path)
1000 {
1001 Log(LOG_LEVEL_ERR, "Unable to locate key for host '%s'", host);
1002 SeqDestroy(hosts);
1003 SeqDestroy(key_paths);
1004 exit(EXIT_FAILURE);
1005 }
1006 SeqAppend(key_paths, host_key_path);
1007 }
1008 SeqDestroy(hosts);
1009 }
1010 assert ((key_paths != NULL) && (SeqLength(key_paths) > 0));
1011
1012 // Encrypt or decrypt
1013 bool success;
1014 if (encrypt)
1015 {
1016 Log(LOG_LEVEL_DEBUG, "Encrypting");
1017 Seq *pub_keys = LoadPublicKeys(key_paths);
1018 SeqDestroy(key_paths);
1019 if (pub_keys == NULL)
1020 {
1021 Log(LOG_LEVEL_ERR, "Failed to load public key(s)");
1022 exit(EXIT_FAILURE);
1023 }
1024
1025 success = RSAEncrypt(pub_keys, input_path, output_path);
1026 SeqDestroy(pub_keys);
1027 if (!success)
1028 {
1029 Log(LOG_LEVEL_ERR, "Encryption failed");
1030 exit(EXIT_FAILURE);
1031 }
1032 }
1033 else if (decrypt)
1034 {
1035 Log(LOG_LEVEL_DEBUG, "Decrypting");
1036 const size_t n_keys = SeqLength(key_paths);
1037 if (n_keys > 1)
1038 {
1039 Log(LOG_LEVEL_ERR, "--decrypt requires only one key/host to be specified");
1040 SeqDestroy(key_paths);
1041 exit(EXIT_FAILURE);
1042 }
1043 RSA *private_key = ReadPrivateKey((char *) SeqAt(key_paths, 0));
1044 SeqDestroy(key_paths);
1045 success = RSADecrypt(private_key, input_path, output_path);
1046 RSA_free(private_key);
1047 if (!success)
1048 {
1049 Log(LOG_LEVEL_ERR, "Decryption failed");
1050 exit(EXIT_FAILURE);
1051 }
1052 }
1053 else
1054 {
1055 ProgrammingError("Unexpected error in cf-secret");
1056 exit(EXIT_FAILURE);
1057 }
1058
1059 return 0;
1060 }
1061