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