1 /* SPDX-License-Identifier: LGPL-2.1-or-later */
5 #include "creds-util.h"
6 #include "dirent-util.h"
9 #include "format-table.h"
10 #include "hexdecoct.h"
13 #include "main-func.h"
14 #include "memory-util.h"
15 #include "missing_magic.h"
17 #include "parse-argument.h"
18 #include "pretty-print.h"
19 #include "process-util.h"
20 #include "stat-util.h"
21 #include "string-table.h"
22 #include "terminal-util.h"
23 #include "tpm2-util.h"
26 typedef enum TranscodeMode
{
33 _TRANSCODE_INVALID
= -EINVAL
,
36 static JsonFormatFlags arg_json_format_flags
= JSON_FORMAT_OFF
;
37 static PagerFlags arg_pager_flags
= 0;
38 static bool arg_legend
= true;
39 static bool arg_system
= false;
40 static TranscodeMode arg_transcode
= TRANSCODE_OFF
;
41 static int arg_newline
= -1;
42 static sd_id128_t arg_with_key
= SD_ID128_NULL
;
43 static const char *arg_tpm2_device
= NULL
;
44 static uint32_t arg_tpm2_pcr_mask
= UINT32_MAX
;
45 static const char *arg_name
= NULL
;
46 static bool arg_name_any
= false;
47 static usec_t arg_timestamp
= USEC_INFINITY
;
48 static usec_t arg_not_after
= USEC_INFINITY
;
49 static bool arg_pretty
= false;
51 static const char* transcode_mode_table
[_TRANSCODE_MAX
] = {
52 [TRANSCODE_OFF
] = "off",
53 [TRANSCODE_BASE64
] = "base64",
54 [TRANSCODE_UNBASE64
] = "unbase64",
55 [TRANSCODE_HEX
] = "hex",
56 [TRANSCODE_UNHEX
] = "unhex",
59 DEFINE_PRIVATE_STRING_TABLE_LOOKUP_FROM_STRING(transcode_mode
, TranscodeMode
);
61 static int open_credential_directory(DIR **ret
) {
62 _cleanup_free_
char *j
= NULL
;
68 _cleanup_free_
char *cd
= NULL
;
70 r
= getenv_for_pid(1, "CREDENTIALS_DIRECTORY", &cd
);
76 if (!path_is_absolute(cd
) || !path_is_normalized(cd
))
79 j
= path_join("/proc/1/root", cd
);
85 r
= get_credentials_dir(&p
);
98 static int verb_list(int argc
, char **argv
, void *userdata
) {
99 _cleanup_(table_unrefp
) Table
*t
= NULL
;
100 _cleanup_(closedirp
) DIR *d
= NULL
;
103 r
= open_credential_directory(&d
);
105 return log_error_errno(r
, "No credentials received. (i.e. $CREDENTIALS_PATH not set or pointing to empty directory.)");
107 return log_error_errno(r
, "Failed to open credentials directory: %m");
109 t
= table_new("name", "secure", "size");
113 (void) table_set_align_percent(t
, table_get_cell(t
, 0, 2), 100);
116 _cleanup_close_
int fd
= -1;
117 const char *secure
, *secure_color
= NULL
;
122 de
= readdir_no_dot(d
);
127 return log_error_errno(errno
, "Failed to read credentials directory: %m");
130 if (!IN_SET(de
->d_type
, DT_REG
, DT_UNKNOWN
))
133 if (!credential_name_valid(de
->d_name
))
136 fd
= openat(dirfd(d
), de
->d_name
, O_PATH
|O_CLOEXEC
|O_NOFOLLOW
);
138 if (errno
== ENOENT
) /* Vanished by now? */
141 return log_error_errno(errno
, "Failed to open credential '%s': %m", de
->d_name
);
144 if (fstat(fd
, &st
) < 0)
145 return log_error_errno(errno
, "Failed to stat credential '%s': %m", de
->d_name
);
147 if (!S_ISREG(st
.st_mode
))
150 if ((st
.st_mode
& 0377) != 0) {
151 secure
= "insecure"; /* Anything that is accessible more than read-only to its owner is insecure */
152 secure_color
= ansi_highlight_red();
154 r
= fd_is_fs_type(fd
, RAMFS_MAGIC
);
156 return log_error_errno(r
, "Failed to determine backing file system of '%s': %m", de
->d_name
);
158 secure
= r
? "secure" : "weak"; /* ramfs is not swappable, hence "secure", everything else is "weak" */
159 secure_color
= r
? ansi_highlight_green() : ansi_highlight_yellow4();
164 TABLE_STRING
, de
->d_name
,
165 TABLE_STRING
, secure
,
166 TABLE_SET_COLOR
, secure_color
,
167 TABLE_SIZE
, (uint64_t) st
.st_size
);
169 return table_log_add_error(r
);
172 if ((arg_json_format_flags
& JSON_FORMAT_OFF
) && table_get_rows(t
) <= 1) {
173 log_info("No credentials");
177 return table_print_with_pager(t
, arg_json_format_flags
, arg_pager_flags
, arg_legend
);
180 static int transcode(
184 size_t *ret_output_size
) {
191 assert(ret_output_size
);
193 switch (arg_transcode
) {
195 case TRANSCODE_BASE64
: {
199 l
= base64mem_full(input
, input_size
, 79, &buf
);
204 *ret_output_size
= l
;
208 case TRANSCODE_UNBASE64
:
209 r
= unbase64mem_full(input
, input_size
, true, ret_output
, ret_output_size
);
210 if (r
== -EPIPE
) /* Uneven number of chars */
215 case TRANSCODE_HEX
: {
218 buf
= hexmem(input
, input_size
);
223 *ret_output_size
= input_size
* 2;
227 case TRANSCODE_UNHEX
:
228 r
= unhexmem_full(input
, input_size
, true, ret_output
, ret_output_size
);
229 if (r
== -EPIPE
) /* Uneven number of chars */
235 assert_not_reached("Unexpected transcoding mode");
239 static int print_newline(FILE *f
, const char *data
, size_t l
) {
243 assert(data
|| l
== 0);
245 /* If turned off explicitly, don't print newline */
246 if (arg_newline
== 0)
249 /* If data already has newline, don't print either */
250 if (l
> 0 && data
[l
-1] == '\n')
253 /* Don't bother unless this is a tty */
255 if (fd
>= 0 && isatty(fd
) <= 0)
258 if (fputc('\n', f
) != '\n')
259 return log_error_errno(errno
, "Failed to write trailing newline: %m");
264 static int write_blob(FILE *f
, const void *data
, size_t size
) {
265 _cleanup_(erase_and_freep
) void *transcoded
= NULL
;
268 if (arg_transcode
== TRANSCODE_OFF
&&
269 arg_json_format_flags
!= JSON_FORMAT_OFF
) {
271 _cleanup_(erase_and_freep
) char *suffixed
= NULL
;
272 _cleanup_(json_variant_unrefp
) JsonVariant
*v
= NULL
;
274 if (memchr(data
, 0, size
))
275 return log_error_errno(SYNTHETIC_ERRNO(EBADMSG
), "Credential data contains embedded NUL, can't parse as JSON.");
277 suffixed
= memdup_suffix0(data
, size
);
281 r
= json_parse(suffixed
, JSON_PARSE_SENSITIVE
, &v
, NULL
, NULL
);
283 return log_error_errno(r
, "Failed to parse JSON: %m");
285 json_variant_dump(v
, arg_json_format_flags
, f
, NULL
);
289 if (arg_transcode
!= TRANSCODE_OFF
) {
290 r
= transcode(data
, size
, &transcoded
, &size
);
292 return log_error_errno(r
, "Failed to transcode data: %m");
297 if (fwrite(data
, 1, size
, f
) != size
)
298 return log_error_errno(errno
, "Failed to write credential data: %m");
300 r
= print_newline(f
, data
, size
);
305 return log_error_errno(errno
, "Failed to flush output: %m");
310 static int verb_cat(int argc
, char **argv
, void *userdata
) {
311 _cleanup_(closedirp
) DIR *d
= NULL
;
315 r
= open_credential_directory(&d
);
317 return log_error_errno(r
, "No credentials passed.");
319 return log_error_errno(r
, "Failed to open credentials directory: %m");
321 STRV_FOREACH(cn
, strv_skip(argv
, 1)) {
322 _cleanup_(erase_and_freep
) void *data
= NULL
;
325 if (!credential_name_valid(*cn
)) {
326 log_error("Credential name '%s' is not valid.", *cn
);
332 r
= read_full_file_full(
334 UINT64_MAX
, SIZE_MAX
,
335 READ_FULL_FILE_SECURE
|READ_FULL_FILE_WARN_WORLD_READABLE
,
337 (char**) &data
, &size
);
339 log_error_errno(r
, "Failed to read credential '%s': %m", *cn
);
345 r
= write_blob(stdout
, data
, size
);
353 static int verb_encrypt(int argc
, char **argv
, void *userdata
) {
354 _cleanup_free_
char *base64_buf
= NULL
, *fname
= NULL
;
355 _cleanup_(erase_and_freep
) char *plaintext
= NULL
;
356 const char *input_path
, *output_path
, *name
;
357 _cleanup_free_
void *output
= NULL
;
358 size_t plaintext_size
, output_size
;
365 input_path
= (isempty(argv
[1]) || streq(argv
[1], "-")) ? NULL
: argv
[1];
368 r
= read_full_file_full(AT_FDCWD
, input_path
, UINT64_MAX
, CREDENTIAL_SIZE_MAX
, READ_FULL_FILE_SECURE
|READ_FULL_FILE_FAIL_WHEN_LARGER
, NULL
, &plaintext
, &plaintext_size
);
370 r
= read_full_stream_full(stdin
, NULL
, UINT64_MAX
, CREDENTIAL_SIZE_MAX
, READ_FULL_FILE_SECURE
|READ_FULL_FILE_FAIL_WHEN_LARGER
, &plaintext
, &plaintext_size
);
372 return log_error_errno(r
, "Plaintext too long for credential (allowed size: %zu).", (size_t) CREDENTIAL_SIZE_MAX
);
374 return log_error_errno(r
, "Failed to read plaintext: %m");
376 output_path
= (isempty(argv
[2]) || streq(argv
[2], "-")) ? NULL
: argv
[2];
382 else if (output_path
) {
383 r
= path_extract_filename(output_path
, &fname
);
385 return log_error_errno(r
, "Failed to extract filename from '%s': %m", output_path
);
386 if (r
== O_DIRECTORY
)
387 return log_error_errno(SYNTHETIC_ERRNO(EISDIR
), "Path '%s' refers to directory, refusing.", output_path
);
391 log_warning("No credential name specified, not embedding credential name in encrypted data. (Disable this warning with --name=)");
395 timestamp
= arg_timestamp
!= USEC_INFINITY
? arg_timestamp
: now(CLOCK_REALTIME
);
397 if (arg_not_after
!= USEC_INFINITY
&& arg_not_after
< timestamp
)
398 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
), "Credential is invalidated before it is valid.");
400 r
= encrypt_credential_and_warn(
407 plaintext
, plaintext_size
,
408 &output
, &output_size
);
412 base64_size
= base64mem_full(output
, output_size
, arg_pretty
? 69 : 79, &base64_buf
);
417 _cleanup_free_
char *escaped
= NULL
, *indented
= NULL
, *j
= NULL
;
420 escaped
= cescape(name
);
425 indented
= strreplace(base64_buf
, "\n", " \\\n ");
429 j
= strjoin("SetCredentialEncrypted=", name
, ": \\\n ", indented
, "\n");
433 free_and_replace(base64_buf
, j
);
437 r
= write_string_file(output_path
, base64_buf
, WRITE_STRING_FILE_ATOMIC
|WRITE_STRING_FILE_CREATE
);
439 r
= write_string_stream(stdout
, base64_buf
, 0);
441 return log_error_errno(r
, "Failed to write result: %m");
446 static int verb_decrypt(int argc
, char **argv
, void *userdata
) {
447 _cleanup_(erase_and_freep
) void *plaintext
= NULL
;
448 _cleanup_free_
char *input
= NULL
, *fname
= NULL
;
449 _cleanup_fclose_
FILE *output_file
= NULL
;
450 const char *input_path
, *output_path
, *name
;
451 size_t input_size
, plaintext_size
;
456 assert(IN_SET(argc
, 2, 3));
458 input_path
= (isempty(argv
[1]) || streq(argv
[1], "-")) ? NULL
: argv
[1];
461 r
= read_full_file_full(AT_FDCWD
, argv
[1], UINT64_MAX
, CREDENTIAL_ENCRYPTED_SIZE_MAX
, READ_FULL_FILE_UNBASE64
|READ_FULL_FILE_FAIL_WHEN_LARGER
, NULL
, &input
, &input_size
);
463 r
= read_full_stream_full(stdin
, NULL
, UINT64_MAX
, CREDENTIAL_ENCRYPTED_SIZE_MAX
, READ_FULL_FILE_UNBASE64
|READ_FULL_FILE_FAIL_WHEN_LARGER
, &input
, &input_size
);
465 return log_error_errno(r
, "Data too long for encrypted credential (allowed size: %zu).", (size_t) CREDENTIAL_ENCRYPTED_SIZE_MAX
);
467 return log_error_errno(r
, "Failed to read encrypted credential data: %m");
469 output_path
= (argc
< 3 || isempty(argv
[2]) || streq(argv
[2], "-")) ? NULL
: argv
[2];
475 else if (input_path
) {
476 r
= path_extract_filename(input_path
, &fname
);
478 return log_error_errno(r
, "Failed to extract filename from '%s': %m", input_path
);
479 if (r
== O_DIRECTORY
)
480 return log_error_errno(SYNTHETIC_ERRNO(EISDIR
), "Path '%s' refers to directory, refusing.", input_path
);
484 log_warning("No credential name specified, not validating credential name embedded in encrypted data. (Disable this warning with --name=.)");
488 timestamp
= arg_timestamp
!= USEC_INFINITY
? arg_timestamp
: now(CLOCK_REALTIME
);
490 r
= decrypt_credential_and_warn(
495 &plaintext
, &plaintext_size
);
500 output_file
= fopen(output_path
, "we");
502 return log_error_errno(errno
, "Failed to create output file '%s': %m", output_path
);
508 r
= write_blob(f
, plaintext
, plaintext_size
);
515 static int verb_setup(int argc
, char **argv
, void *userdata
) {
519 r
= get_credential_host_secret(CREDENTIAL_SECRET_GENERATE
|CREDENTIAL_SECRET_WARN_NOT_ENCRYPTED
, NULL
, &size
);
521 return log_error_errno(r
, "Failed to setup credentials host key: %m");
523 log_info("%zu byte credentials host key set up.", size
);
528 static int verb_help(int argc
, char **argv
, void *userdata
) {
529 _cleanup_free_
char *link
= NULL
;
532 r
= terminal_urlify_man("systemd-creds", "1", &link
);
536 printf("%1$s [OPTIONS...] COMMAND ...\n"
537 "\n%5$sDisplay and Process Credentials.%6$s\n"
538 "\n%3$sCommands:%4$s\n"
539 " list Show installed and available versions\n"
540 " cat CREDENTIAL... Show specified credentials\n"
541 " setup Generate credentials host key, if not existing yet\n"
542 " encrypt INPUT OUTPUT Encrypt plaintext credential file and write to\n"
543 " ciphertext credential file\n"
544 " decrypt INPUT [OUTPUT] Decrypt ciphertext credential file and write to\n"
545 " plaintext credential file\n"
546 " -h --help Show this help\n"
547 " --version Show package version\n"
548 "\n%3$sOptions:%4$s\n"
549 " --no-pager Do not pipe output into a pager\n"
550 " --no-legend Do not show the headers and footers\n"
551 " --json=pretty|short|off\n"
552 " Generate JSON output\n"
553 " --system Show credentials passed to system\n"
554 " --transcode=base64|unbase64|hex|unhex\n"
555 " Transcode credential data\n"
556 " --newline=auto|yes|no\n"
557 " Suffix output with newline\n"
558 " -p --pretty Output as SetCredentialEncrypted= line\n"
559 " --name=NAME Override filename included in encrypted credential\n"
560 " --timestamp=TIME Include specified timestamp in encrypted credential\n"
561 " --not-after=TIME Include specified invalidation time in encrypted\n"
563 " --with-key=host|tpm2|host+tpm2|auto\n"
564 " Which keys to encrypt with\n"
565 " -H Shortcut for --with-key=host\n"
566 " -T Shortcut for --with-key=tpm2\n"
567 " --tpm2-device=PATH\n"
568 " Pick TPM2 device\n"
569 " --tpm2-pcrs=PCR1+PCR2+PCR3+…\n"
570 " Specify TPM2 PCRs to seal against\n"
571 "\nSee the %2$s for details.\n"
572 , program_invocation_short_name
574 , ansi_underline(), ansi_normal()
575 , ansi_highlight(), ansi_normal()
581 static int parse_argv(int argc
, char *argv
[]) {
599 static const struct option options
[] = {
600 { "help", no_argument
, NULL
, 'h' },
601 { "version", no_argument
, NULL
, ARG_VERSION
},
602 { "no-pager", no_argument
, NULL
, ARG_NO_PAGER
},
603 { "no-legend", no_argument
, NULL
, ARG_NO_LEGEND
},
604 { "json", required_argument
, NULL
, ARG_JSON
},
605 { "system", no_argument
, NULL
, ARG_SYSTEM
},
606 { "transcode", required_argument
, NULL
, ARG_TRANSCODE
},
607 { "newline", required_argument
, NULL
, ARG_NEWLINE
},
608 { "pretty", no_argument
, NULL
, 'p' },
609 { "with-key", required_argument
, NULL
, ARG_WITH_KEY
},
610 { "tpm2-device", required_argument
, NULL
, ARG_TPM2_DEVICE
},
611 { "tpm2-pcrs", required_argument
, NULL
, ARG_TPM2_PCRS
},
612 { "name", required_argument
, NULL
, ARG_NAME
},
613 { "timestamp", required_argument
, NULL
, ARG_TIMESTAMP
},
614 { "not-after", required_argument
, NULL
, ARG_NOT_AFTER
},
623 while ((c
= getopt_long(argc
, argv
, "hHTp", options
, NULL
)) >= 0) {
628 return verb_help(0, NULL
, NULL
);
634 arg_pager_flags
|= PAGER_DISABLE
;
642 r
= parse_json_argument(optarg
, &arg_json_format_flags
);
653 if (parse_boolean(optarg
) == 0) /* If specified as "false", turn transcoding off */
654 arg_transcode
= TRANSCODE_OFF
;
658 m
= transcode_mode_from_string(optarg
);
660 return log_error_errno(m
, "Failed to parse transcode mode: %m");
668 if (isempty(optarg
) || streq(optarg
, "auto"))
673 r
= parse_boolean_argument("--newline=", optarg
, &b
);
686 if (isempty(optarg
) || streq(optarg
, "auto"))
687 arg_with_key
= SD_ID128_NULL
;
688 else if (streq(optarg
, "host"))
689 arg_with_key
= CRED_AES256_GCM_BY_HOST
;
690 else if (streq(optarg
, "tpm2"))
691 arg_with_key
= CRED_AES256_GCM_BY_TPM2_HMAC
;
692 else if (STR_IN_SET(optarg
, "host+tpm2", "tpm2+host"))
693 arg_with_key
= CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC
;
695 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
), "Unknown key type: %s", optarg
);
700 arg_with_key
= CRED_AES256_GCM_BY_HOST
;
704 arg_with_key
= CRED_AES256_GCM_BY_TPM2_HMAC
;
707 case ARG_TPM2_DEVICE
: {
708 _cleanup_free_
char *device
= NULL
;
710 if (streq(optarg
, "list"))
711 return tpm2_list_devices();
713 if (!streq(optarg
, "auto")) {
714 device
= strdup(optarg
);
719 arg_tpm2_device
= TAKE_PTR(device
);
723 case ARG_TPM2_PCRS
: {
726 if (isempty(optarg
)) {
727 arg_tpm2_pcr_mask
= 0;
731 r
= tpm2_parse_pcrs(optarg
, &mask
);
735 if (arg_tpm2_pcr_mask
== UINT32_MAX
)
736 arg_tpm2_pcr_mask
= mask
;
738 arg_tpm2_pcr_mask
|= mask
;
744 if (isempty(optarg
)) {
750 if (!credential_name_valid(optarg
))
751 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
), "Invalid credential name: %s", optarg
);
754 arg_name_any
= false;
758 r
= parse_timestamp(optarg
, &arg_timestamp
);
760 return log_error_errno(r
, "Failed to parse timestamp: %s", optarg
);
765 r
= parse_timestamp(optarg
, &arg_not_after
);
767 return log_error_errno(r
, "Failed to parse --not-after= timestamp: %s", optarg
);
775 assert_not_reached("Unhandled option");
779 if (arg_tpm2_pcr_mask
== UINT32_MAX
)
780 arg_tpm2_pcr_mask
= TPM2_PCR_MASK_DEFAULT
;
785 static int creds_main(int argc
, char *argv
[]) {
787 static const Verb verbs
[] = {
788 { "list", VERB_ANY
, 1, VERB_DEFAULT
, verb_list
},
789 { "cat", 2, VERB_ANY
, 0, verb_cat
},
790 { "encrypt", 3, 3, 0, verb_encrypt
},
791 { "decrypt", 2, 3, 0, verb_decrypt
},
792 { "setup", VERB_ANY
, 1, 0, verb_setup
},
793 { "help", VERB_ANY
, 1, 0, verb_help
},
797 return dispatch_verb(argc
, argv
, verbs
, NULL
);
800 static int run(int argc
, char *argv
[]) {
805 r
= parse_argv(argc
, argv
);
809 return creds_main(argc
, argv
);
812 DEFINE_MAIN_FUNCTION_WITH_POSITIVE_FAILURE(run
);