/* * (C) 2014-2024 by Christian Hesse <mail@eworm.de> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. * */ #define _GNU_SOURCE #include <fcntl.h> #include <getopt.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/random.h> #include <sys/stat.h> #include <termios.h> #include <unistd.h> #include <systemd/sd-daemon.h> #include <iniparser/iniparser.h> #include <keyutils.h> #include <yubikey.h> #include <ykpers-1/ykdef.h> #include <ykpers-1/ykcore.h> #include <libcryptsetup.h> #include "../config.h" #include "../version.h" #define PROGNAME "ykfde" /* Yubikey supports write of 64 byte challenge to slot, returns * HMAC-SHA1 response. * * Lengths are defined in ykpers-1/ykdef.h: * SHA1_MAX_BLOCK_SIZE 64 * SHA1_DIGEST_SIZE 20 * * For passphrase we use hex encoded digest, that is twice the * length of binary digest. */ #define CHALLENGELEN SHA1_MAX_BLOCK_SIZE #define RESPONSELEN SHA1_MAX_BLOCK_SIZE #define PASSPHRASELEN SHA1_DIGEST_SIZE * 2 #define MAX2FLEN CHALLENGELEN / 2 const static char optstring[] = "hn:Ns:SV"; const static struct option options_long[] = { /* name has_arg flag val */ { "help", no_argument, NULL, 'h' }, { "2nd-factor", required_argument, NULL, 's' }, { "ask-2nd-factor", no_argument, NULL, 'S' }, { "new-2nd-factor", required_argument, NULL, 'n' }, { "ask-new-2nd-factor", no_argument, NULL, 'N' }, { "version", no_argument, NULL, 'V' }, { 0, 0, 0, 0 } }; char * ask_secret(const char * text) { struct termios tp, tp_save; char * factor = NULL; size_t len; ssize_t readlen; bool onTerminal = false; /* get terminal properties */ if (tcgetattr(STDIN_FILENO, &tp) == 0) { onTerminal = true; tp_save = tp; /* disable echo on terminal */ tp.c_lflag &= ~ECHO; if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &tp) < 0) { fprintf(stderr, "Failed setting terminal attributes.\n"); return NULL; } printf("Please give %s:", text); } readlen = getline(&factor, &len, stdin); factor[readlen - 1] = '\0'; if (onTerminal == true) { putchar('\n'); /* restore terminal */ if (tcsetattr(STDIN_FILENO, TCSANOW, &tp_save) < 0) { fprintf(stderr, "Failed to restore terminal attributes.\n"); free(factor); return NULL; } } return factor; } int main(int argc, char **argv) { unsigned int version = 0, help = 0, challenge_int[CHALLENGELEN]; char challenge_old[CHALLENGELEN + 1], challenge_new[CHALLENGELEN + 1], response_old[RESPONSELEN], response_new[RESPONSELEN], passphrase_old[PASSPHRASELEN + 1], passphrase_new[PASSPHRASELEN + 1]; const char * tmp; char challengefilename[sizeof(CHALLENGEDIR) + 11 /* "/challenge-" */ + 10 /* unsigned int in char */ + 1], challengefiletmpname[sizeof(CHALLENGEDIR) + 11 /* "/challenge-" */ + 10 /* unsigned int in char */ + 7 /* -XXXXXX */ + 1]; int challengefile = 0, challengefiletmp = 0; int i; size_t len; int8_t rc = EXIT_FAILURE; /* cryptsetup */ const char * device_name; int8_t luks_slot = -1; struct crypt_device *cryptdevice; crypt_status_info cryptstatus; crypt_keyslot_info cryptkeyslot; char * passphrase = NULL; /* keyutils */ key_serial_t key = -1; void * payload = NULL; char * second_factor = NULL, * new_2nd_factor = NULL, * new_2nd_factor_verify = NULL; /* yubikey */ YK_KEY * yk; uint8_t yk_slot = SLOT_CHAL_HMAC2; unsigned int serial = 0; /* iniparser */ dictionary * ini; char section_ykslot[10 /* unsigned int in char */ + 1 + sizeof(CONFYKSLOT) + 1]; char section_luksslot[10 + 1 + sizeof(CONFLUKSSLOT) + 1]; /* get command line options */ while ((i = getopt_long(argc, argv, optstring, options_long, NULL)) != -1) switch (i) { case 'h': help++; break; case 'n': case 'N': if (new_2nd_factor != NULL) { fprintf(stderr, "We already have a new second factor. Did you specify it twice?\n"); goto out10; } if (optarg == NULL) { /* N */ if ((new_2nd_factor = ask_secret("new second factor")) == NULL) goto out10; if ((new_2nd_factor_verify = ask_secret("new second factor for verification")) == NULL) goto out10; if (strcmp(new_2nd_factor, new_2nd_factor_verify) != 0) { fprintf(stderr, "Verification failed, given strings do not match.\n"); goto out10; } } else { /* n */ new_2nd_factor = strdup(optarg); memset(optarg, '*', strlen(optarg)); } break; case 's': case 'S': if (second_factor != NULL) { fprintf(stderr, "We already have a second factor. Did you specify it twice?\n"); goto out10; } if (optarg == NULL) { /* S */ second_factor = ask_secret("current second factor"); } else { /* s */ second_factor = strdup(optarg); memset(optarg, '*', strlen(optarg)); } break; case 'V': version++; break; } if (version > 0) printf("%s: %s v%s (compiled: " __DATE__ ", " __TIME__ ")\n", argv[0], PROGNAME, VERSION); if (help > 0) fprintf(stderr, "usage: %s [-h|--help] [-n|--new-2nd-factor <new-2nd-factor>] [-N|--ask-new-2nd-factor]\n" " [-s|--2nd-factor <2nd-factor>] [-S|--ask-2nd-factor] [-V|--version]\n", argv[0]); if (version > 0 || help > 0) return EXIT_SUCCESS; /* initialize static buffers */ memset(challenge_int, 0, CHALLENGELEN * sizeof(unsigned int)); memset(challenge_old, 0, CHALLENGELEN + 1); memset(challenge_new, 0, CHALLENGELEN + 1); memset(response_old, 0, RESPONSELEN); memset(response_new, 0, RESPONSELEN); memset(passphrase_old, 0, PASSPHRASELEN + 1); memset(passphrase_new, 0, PASSPHRASELEN + 1); if ((ini = iniparser_load(CONFIGFILE)) == NULL) { fprintf(stderr, "Could not parse configuration file.\n"); goto out10; } if ((device_name = iniparser_getstring(ini, "general:" CONFDEVNAME, NULL)) == NULL) { /* read from crypttab? */ /* get device from currently open devices? */ fprintf(stderr, "Could not read LUKS device from configuration file.\n"); goto out20; } /* init and open first Yubikey */ if (yk_init() == 0) { perror("yk_init() failed"); goto out20; } if ((yk = yk_open_first_key()) == NULL) { fprintf(stderr, "No Yubikey available.\n"); goto out30; } /* read the serial number from key */ if (yk_get_serial(yk, 0, 0, &serial) == 0) { perror("yk_get_serial() failed"); goto out40; } /* get the yk slot */ sprintf(section_ykslot, "%d:" CONFYKSLOT, serial); yk_slot = iniparser_getint(ini, "general:" CONFYKSLOT, yk_slot); yk_slot = iniparser_getint(ini, section_ykslot, yk_slot); switch (yk_slot) { case 1: case SLOT_CHAL_HMAC1: yk_slot = SLOT_CHAL_HMAC1; break; case 2: case SLOT_CHAL_HMAC2: default: yk_slot = SLOT_CHAL_HMAC2; break; } /* get the luks slot */ sprintf(section_luksslot, "%d:" CONFLUKSSLOT, serial); luks_slot = iniparser_getint(ini, section_luksslot, luks_slot); if (luks_slot < 0) { fprintf(stderr, "Please set LUKS key slot for Yubikey with serial %d!\n" "Add something like this to " CONFIGFILE ":\n\n" "[%d]\nluks slot = 1\n", serial, serial); goto out40; } /* try to get a second factor */ if (iniparser_getboolean(ini, "general:" CONF2NDFACTOR, 0) > 0 && second_factor == NULL && new_2nd_factor == NULL) { if (sd_notify(0, "READY=0\nSTATUS=Detecting systemd...") == 0) fprintf(stderr, "Not running from systemd, you may have to give\n" "second factor manually if required.\n"); else if ((key = keyctl_search(KEY_SPEC_USER_KEYRING, "user", "ykfde-2f", 0)) < 0) /* get second factor from key store */ fprintf(stderr, "Failed requesting key. That's ok if you do not use\n" "second factor. Give it manually if required.\n"); /* if we have a key id we have a key - so this should succeed */ if (key > -1) { if (keyctl_read_alloc(key, &payload) < 0) { perror("Failed reading payload from key"); goto out40; } second_factor = payload; } } /* use an empty string if second_factor is still NULL */ if (second_factor == NULL) second_factor = strdup(""); /* warn when second factor is not enabled in config */ if (iniparser_getboolean(ini, "general:" CONF2NDFACTOR, 0) == 0 && ((second_factor != NULL && *second_factor != 0) || (new_2nd_factor != NULL && *new_2nd_factor != 0))) fprintf(stderr, "Warning: Processing second factor, but not enabled in config!\n"); /* get random number - try random first, fall back to urandom We generate an array of unsigned int, the use modulo to limit to printable ASCII characters (32 to 127). */ if ((len = getrandom(challenge_int, CHALLENGELEN * sizeof(unsigned int), GRND_RANDOM|GRND_NONBLOCK)) != CHALLENGELEN * sizeof(unsigned int)) len += getrandom((void *)((size_t)challenge_int + len), CHALLENGELEN * sizeof(unsigned int) - len, 0); for (i = 0; i < CHALLENGELEN; i++) challenge_new[i] = (challenge_int[i] % (127 - 32)) + 32; /* these are the filenames for challenge * we need this for reading and writing */ sprintf(challengefilename, CHALLENGEDIR "/challenge-%d", serial); sprintf(challengefiletmpname, CHALLENGEDIR "/challenge-%d-XXXXXX", serial); /* write new challenge to file */ if ((challengefiletmp = mkstemp(challengefiletmpname)) < 0) { fprintf(stderr, "Could not open file %s for writing.\n", challengefiletmpname); goto out40; } if (write(challengefiletmp, challenge_new, CHALLENGELEN) < 0) { fprintf(stderr, "Failed to write challenge to file.\n"); goto out50; } if (fsync(challengefiletmp) < 0) { fprintf(stderr, "Failed to sync file to disk.\n"); goto out50; } challengefiletmp = close(challengefiletmp); /* now that the new challenge has been written to file... * add second factor to new challenge */ tmp = new_2nd_factor ? new_2nd_factor : second_factor; len = strlen(tmp); memcpy(challenge_new, tmp, len < MAX2FLEN ? len : MAX2FLEN); /* do challenge/response and encode to hex */ if (yk_challenge_response(yk, yk_slot, true, CHALLENGELEN, (unsigned char *) challenge_new, RESPONSELEN, (unsigned char *) response_new) == 0) { perror("yk_challenge_response() failed"); goto out50; } yubikey_hex_encode((char *) passphrase_new, (char *) response_new, SHA1_DIGEST_SIZE); /* get status of crypt device * We expect this to be active (or busy). It is the actual root device, no? */ cryptstatus = crypt_status(cryptdevice, device_name); if (cryptstatus != CRYPT_ACTIVE && cryptstatus != CRYPT_BUSY) { fprintf(stderr, "Device %s is invalid or inactive.\n", device_name); goto out50; } /* initialize crypt device */ if (crypt_init_by_name(&cryptdevice, device_name) < 0) { fprintf(stderr, "Device %s failed to initialize.\n", device_name); goto out60; } cryptkeyslot = crypt_keyslot_status(cryptdevice, luks_slot); if (cryptkeyslot == CRYPT_SLOT_INVALID) { fprintf(stderr, "Key slot %d is invalid.\n", luks_slot); goto out60; } else if (cryptkeyslot == CRYPT_SLOT_ACTIVE || cryptkeyslot == CRYPT_SLOT_ACTIVE_LAST) { /* read challenge from file */ if ((challengefile = open(challengefilename, O_RDONLY)) < 0) { perror("Failed opening challenge file for reading"); goto out60; } if (read(challengefile, challenge_old, CHALLENGELEN) < 0) { perror("Failed reading challenge from file"); goto out60; } challengefile = close(challengefile); /* finished reading challenge */ /* copy the second factor */ len = strlen(second_factor); memcpy(challenge_old, second_factor, len < MAX2FLEN ? len : MAX2FLEN); /* do challenge/response and encode to hex */ if (yk_challenge_response(yk, yk_slot, true, CHALLENGELEN, (unsigned char *) challenge_old, RESPONSELEN, (unsigned char *) response_old) == 0) { perror("yk_challenge_response() failed"); goto out60; } yubikey_hex_encode((char *) passphrase_old, (char *) response_old, SHA1_DIGEST_SIZE); if (crypt_keyslot_change_by_passphrase(cryptdevice, luks_slot, luks_slot, passphrase_old, PASSPHRASELEN, passphrase_new, PASSPHRASELEN) < 0) { fprintf(stderr, "Could not update passphrase for key slot %d.\n", luks_slot); goto out60; } if (renameat2(AT_FDCWD, challengefiletmpname, AT_FDCWD, challengefilename, RENAME_EXCHANGE) < 0) { fprintf(stderr, "Failed to rename (exchange) challenge files.\n"); goto out60; } if (unlink(challengefiletmpname) < 0) { fprintf(stderr, "Failed to delete old challenge file.\n"); goto out60; } } else { /* ck == CRYPT_SLOT_INACTIVE */ if ((passphrase = ask_secret("existing LUKS passphrase")) == NULL) goto out60; if (crypt_keyslot_add_by_passphrase(cryptdevice, luks_slot, passphrase, strlen(passphrase), passphrase_new, PASSPHRASELEN) < 0) { fprintf(stderr, "Could not add passphrase for key slot %d.\n", luks_slot); goto out60; } if (rename(challengefiletmpname, challengefilename) < 0) { fprintf(stderr, "Failed to rename new challenge file.\n"); goto out60; } } sd_notify(0, "READY=1\nSTATUS=All done."); rc = EXIT_SUCCESS; out60: /* free crypt context */ crypt_free(cryptdevice); out50: /* close the challenge file */ if (challengefile) close(challengefile); if (challengefiletmp) close(challengefiletmp); if (access(challengefiletmpname, F_OK) == 0) unlink(challengefiletmpname); out40: /* close Yubikey */ if (yk_close_key(yk) == 0) perror("yk_close_key() failed"); out30: /* release Yubikey */ if (yk_release() == 0) perror("yk_release() failed"); out20: /* free iniparser dictionary */ iniparser_freedict(ini); out10: /* wipe response (cleartext password!) from memory */ /* This is statically allocated and always save to wipe! */ memset(challenge_int, 0, CHALLENGELEN * sizeof(unsigned int)); memset(challenge_old, 0, CHALLENGELEN + 1); memset(challenge_new, 0, CHALLENGELEN + 1); memset(response_old, 0, RESPONSELEN); memset(response_new, 0, RESPONSELEN); memset(passphrase_old, 0, PASSPHRASELEN + 1); memset(passphrase_new, 0, PASSPHRASELEN + 1); free(passphrase); free(new_2nd_factor_verify); free(new_2nd_factor); free(second_factor); return rc; } // vim: set syntax=c: