int main(int argc, char *argv[]) { uint8_t buf[SECRET_BITS/8 + SCRATCHCODES*BYTES_PER_SCRATCHCODE]; static const char hotp[] = "\" HOTP_COUNTER 1\n"; static const char totp[] = "\" TOTP_AUTH\n"; static const char disallow[] = "\" DISALLOW_REUSE\n"; static const char window[] = "\" WINDOW_SIZE 17\n"; static const char ratelimit[] = "\" RATE_LIMIT 3 30\n"; char secret[(SECRET_BITS + BITS_PER_BASE32_CHAR-1)/BITS_PER_BASE32_CHAR + 1 /* newline */ + sizeof(hotp) + // hotp and totp are mutually exclusive. sizeof(disallow) + sizeof(window) + sizeof(ratelimit) + 5 + // NN MMM (total of five digits) SCRATCHCODE_LENGTH*(SCRATCHCODES + 1 /* newline */) + 1 /* NUL termination character */]; enum { ASK_MODE, HOTP_MODE, TOTP_MODE } mode = ASK_MODE; enum { ASK_REUSE, DISALLOW_REUSE, ALLOW_REUSE } reuse = ASK_REUSE; int force = 0, quiet = 0; int r_limit = 0, r_time = 0; char *secret_fn = NULL; char *label = NULL; int window_size = 0; int idx; for (;;) { static const char optstring[] = "+hctdDfl:qQ:r:R:us:w:W"; static struct option options[] = { { "help", 0, 0, 'h' }, { "counter-based", 0, 0, 'c' }, { "time-based", 0, 0, 't' }, { "disallow-reuse", 0, 0, 'd' }, { "allow-reuse", 0, 0, 'D' }, { "force", 0, 0, 'f' }, { "label", 1, 0, 'l' }, { "quiet", 0, 0, 'q' }, { "qr-mode", 1, 0, 'Q' }, { "rate-limit", 1, 0, 'r' }, { "rate-time", 1, 0, 'R' }, { "no-rate-limit", 0, 0, 'u' }, { "secret", 1, 0, 's' }, { "window-size", 1, 0, 'w' }, { "minimal-window", 0, 0, 'W' }, { 0, 0, 0, 0 } }; idx = -1; int c = getopt_long(argc, argv, optstring, options, &idx); if (c > 0) { for (int i = 0; options[i].name; i++) { if (options[i].val == c) { idx = i; break; } } } else if (c < 0) { break; } if (idx-- <= 0) { // Help (or invalid argument) err: usage(); if (idx < -1) { fprintf(stderr, "Failed to parse command line\n"); _exit(1); } exit(0); } else if (!idx--) { // counter-based if (mode != ASK_MODE) { fprintf(stderr, "Duplicate -c and/or -t option detected\n"); _exit(1); } if (reuse != ASK_REUSE) { reuse_err: fprintf(stderr, "Reuse of tokens is not a meaningful parameter " "when in counter-based mode\n"); _exit(1); } mode = HOTP_MODE; } else if (!idx--) { // time-based if (mode != ASK_MODE) { fprintf(stderr, "Duplicate -c and/or -t option detected\n"); _exit(1); } mode = TOTP_MODE; } else if (!idx--) { // disallow-reuse if (reuse != ASK_REUSE) { fprintf(stderr, "Duplicate -d and/or -D option detected\n"); _exit(1); } if (mode == HOTP_MODE) { goto reuse_err; } reuse = DISALLOW_REUSE; } else if (!idx--) { // allow-reuse if (reuse != ASK_REUSE) { fprintf(stderr, "Duplicate -d and/or -D option detected\n"); _exit(1); } if (mode == HOTP_MODE) { goto reuse_err; } reuse = ALLOW_REUSE; } else if (!idx--) { // force if (force) { fprintf(stderr, "Duplicate -f option detected\n"); _exit(1); } force = 1; } else if (!idx--) { // label if (label) { fprintf(stderr, "Duplicate -l option detected\n"); _exit(1); } label = strdup(optarg); } else if (!idx--) { // quiet if (quiet) { fprintf(stderr, "Duplicate -q option detected\n"); _exit(1); } quiet = 1; } else if (!idx--) { // qr-mode if (qr_mode != QR_UNSET) { fprintf(stderr, "Duplicate -Q option detected\n"); _exit(1); } if (!strcasecmp(optarg, "none")) { qr_mode = QR_NONE; } else if (!strcasecmp(optarg, "ansi")) { qr_mode = QR_ANSI; } else if (!strcasecmp(optarg, "utf8")) { qr_mode = QR_UTF8; } else { fprintf(stderr, "Invalid qr-mode \"%s\"\n", optarg); _exit(1); } } else if (!idx--) { // rate-limit if (r_limit > 0) { fprintf(stderr, "Duplicate -r option detected\n"); _exit(1); } else if (r_limit < 0) { fprintf(stderr, "-u is mutually exclusive with -r\n"); _exit(1); } char *endptr; errno = 0; long l = strtol(optarg, &endptr, 10); if (errno || endptr == optarg || *endptr || l < 1 || l > 10) { fprintf(stderr, "-r requires an argument in the range 1..10\n"); _exit(1); } r_limit = (int)l; } else if (!idx--) { // rate-time if (r_time > 0) { fprintf(stderr, "Duplicate -R option detected\n"); _exit(1); } else if (r_time < 0) { fprintf(stderr, "-u is mutually exclusive with -R\n"); _exit(1); } char *endptr; errno = 0; long l = strtol(optarg, &endptr, 10); if (errno || endptr == optarg || *endptr || l < 15 || l > 600) { fprintf(stderr, "-R requires an argument in the range 15..600\n"); _exit(1); } r_time = (int)l; } else if (!idx--) { // no-rate-limit if (r_limit > 0 || r_time > 0) { fprintf(stderr, "-u is mutually exclusive with -r/-R\n"); _exit(1); } if (r_limit < 0) { fprintf(stderr, "Duplicate -u option detected\n"); _exit(1); } r_limit = r_time = -1; } else if (!idx--) { // secret if (secret_fn) { fprintf(stderr, "Duplicate -s option detected\n"); _exit(1); } if (!*optarg) { fprintf(stderr, "-s must be followed by a filename\n"); _exit(1); } secret_fn = strdup(optarg); if (!secret_fn) { perror("malloc()"); _exit(1); } } else if (!idx--) { // window-size if (window_size) { fprintf(stderr, "Duplicate -w/-W option detected\n"); _exit(1); } char *endptr; errno = 0; long l = strtol(optarg, &endptr, 10); if (errno || endptr == optarg || *endptr || l < 1 || l > 21) { fprintf(stderr, "-w requires an argument in the range 1..21\n"); _exit(1); } window_size = (int)l; } else if (!idx--) { // minimal-window if (window_size) { fprintf(stderr, "Duplicate -w/-W option detected\n"); _exit(1); } window_size = -1; } else { fprintf(stderr, "Error\n"); _exit(1); } } idx = -1; if (optind != argc) { goto err; } if (reuse != ASK_REUSE && mode != TOTP_MODE) { fprintf(stderr, "Must select time-based mode, when using -d or -D\n"); _exit(1); } if ((r_time && !r_limit) || (!r_time && r_limit)) { fprintf(stderr, "Must set -r when setting -R, and vice versa\n"); _exit(1); } if (!label) { uid_t uid = getuid(); const char *user = getUserName(uid); char hostname[128] = { 0 }; if (gethostname(hostname, sizeof(hostname)-1)) { strcpy(hostname, "unix"); } label = strcat(strcat(strcpy(malloc(strlen(user) + strlen(hostname) + 2), user), "@"), hostname); free((char *)user); } int fd = open("/dev/urandom", O_RDONLY); if (fd < 0) { perror("Failed to open \"/dev/urandom\""); return 1; } if (read(fd, buf, sizeof(buf)) != sizeof(buf)) { urandom_failure: perror("Failed to read from \"/dev/urandom\""); return 1; } base32_encode(buf, SECRET_BITS/8, (uint8_t *)secret, sizeof(secret)); int use_totp; if (mode == ASK_MODE) { use_totp = maybe("Do you want authentication tokens to be time-based"); } else { use_totp = mode == TOTP_MODE; } if (!quiet) { displayQRCode(secret, label, use_totp); printf("Your new secret key is: %s\n", secret); printf("Your verification code is %06d\n", generateCode(secret, 0)); printf("Your emergency scratch codes are:\n"); } free(label); strcat(secret, "\n"); if (use_totp) { strcat(secret, totp); } else { strcat(secret, hotp); } for (int i = 0; i < SCRATCHCODES; ++i) { new_scratch_code:; int scratch = 0; for (int j = 0; j < BYTES_PER_SCRATCHCODE; ++j) { scratch = 256*scratch + buf[SECRET_BITS/8 + BYTES_PER_SCRATCHCODE*i + j]; } int modulus = 1; for (int j = 0; j < SCRATCHCODE_LENGTH; j++) { modulus *= 10; } scratch = (scratch & 0x7FFFFFFF) % modulus; if (scratch < modulus/10) { // Make sure that scratch codes are always exactly eight digits. If they // start with a sequence of zeros, just generate a new scratch code. if (read(fd, buf + (SECRET_BITS/8 + BYTES_PER_SCRATCHCODE*i), BYTES_PER_SCRATCHCODE) != BYTES_PER_SCRATCHCODE) { goto urandom_failure; } goto new_scratch_code; } if (!quiet) { printf(" %08d\n", scratch); } snprintf(strrchr(secret, '\000'), sizeof(secret) - strlen(secret), "%08d\n", scratch); } close(fd); if (!secret_fn) { char *home = getenv("HOME"); if (!home || *home != '/') { fprintf(stderr, "Cannot determine home directory\n"); return 1; } secret_fn = malloc(strlen(home) + strlen(SECRET) + 1); if (!secret_fn) { perror("malloc()"); _exit(1); } strcat(strcpy(secret_fn, home), SECRET); } if (!force) { printf("\nDo you want me to update your \"%s\" file (y/n) ", secret_fn); fflush(stdout); char ch; do { ch = getchar(); } while (ch == ' ' || ch == '\r' || ch == '\n'); if (ch != 'y' && ch != 'Y') { exit(0); } } secret_fn = realloc(secret_fn, 2*strlen(secret_fn) + 3); if (!secret_fn) { perror("malloc()"); _exit(1); } char *tmp_fn = strrchr(secret_fn, '\000') + 1; strcat(strcpy(tmp_fn, secret_fn), "~"); // Add optional flags. if (use_totp) { if (reuse == ASK_REUSE) { maybeAddOption("Do you want to disallow multiple uses of the same " "authentication\ntoken? This restricts you to one login " "about every 30s, but it increases\nyour chances to " "notice or even prevent man-in-the-middle attacks", secret, sizeof(secret), disallow); } else if (reuse == DISALLOW_REUSE) { addOption(secret, sizeof(secret), disallow); } if (!window_size) { maybeAddOption("By default, tokens are good for 30 seconds and in order " "to compensate for\npossible time-skew between the " "client and the server, we allow an extra\ntoken before " "and after the current time. If you experience problems " "with poor\ntime synchronization, you can increase the " "window from its default\nsize of 1:30min to about 4min. " "Do you want to do so", secret, sizeof(secret), window); } else { char buf[80]; sprintf(buf, "\" WINDOW_SIZE %d\n", window_size); addOption(secret, sizeof(secret), buf); } } else { if (!window_size) { maybeAddOption("By default, three tokens are valid at any one time. " "This accounts for\ngenerated-but-not-used tokens and " "failed login attempts. In order to\ndecrease the " "likelihood of synchronization problems, this window " "can be\nincreased from its default size of 3 to 17. Do " "you want to do so", secret, sizeof(secret), window); } else { char buf[80]; sprintf(buf, "\" WINDOW_SIZE %d\n", window_size > 0 ? window_size : use_totp ? 3 : 1); addOption(secret, sizeof(secret), buf); } } if (!r_limit && !r_time) { maybeAddOption("If the computer that you are logging into isn't hardened " "against brute-force\nlogin attempts, you can enable " "rate-limiting for the authentication module.\nBy default, " "this limits attackers to no more than 3 login attempts " "every 30s.\nDo you want to enable rate-limiting", secret, sizeof(secret), ratelimit); } else if (r_limit > 0 && r_time > 0) { char buf[80]; sprintf(buf, "\"RATE_LIMIT %d %d\n", r_limit, r_time); addOption(secret, sizeof(secret), buf); } fd = open(tmp_fn, O_WRONLY|O_EXCL|O_CREAT|O_NOFOLLOW|O_TRUNC, 0400); if (fd < 0) { fprintf(stderr, "Failed to create \"%s\" (%s)", secret_fn, strerror(errno)); free(secret_fn); return 1; } if (write(fd, secret, strlen(secret)) != (ssize_t)strlen(secret) || rename(tmp_fn, secret_fn)) { perror("Failed to write new secret"); unlink(secret_fn); close(fd); free(secret_fn); return 1; } free(secret_fn); close(fd); return 0; }
int main(int argc, char *argv[]) { int fd = open("/dev/urandom", O_RDONLY); if (fd < 0) { perror("Failed to open \"/dev/urandom\""); return 1; } uint8_t buf[SECRET_BITS/8 + SCRATCHCODES*BYTES_PER_SCRATCHCODE]; static const char totp[] = "\" TOTP_AUTH\n"; static const char disallow[] = "\" DISALLOW_REUSE\n"; char secret[(SECRET_BITS + BITS_PER_BASE32_CHAR-1)/BITS_PER_BASE32_CHAR + 1 /* newline */ + sizeof(totp) + sizeof(disallow) + SCRATCHCODE_LENGTH*(SCRATCHCODES + 1 /* newline */) + 1 /* NUL termination character */]; if (read(fd, buf, sizeof(buf)) != sizeof(buf)) { urandom_failure: perror("Failed to read from \"/dev/urandom\""); return 1; } base32_encode(buf, SECRET_BITS/8, (uint8_t *)secret, sizeof(secret)); displayQRCode(secret); strcat(secret, "\n"); printf("Your new secret key is: %s", secret); printf("Your verification code is %06d\n", generateCode(secret, 0)); printf("Your emergency scratch codes are:\n"); strcat(secret, totp); for (int i = 0; i < SCRATCHCODES; ++i) { new_scratch_code:; int scratch = 0; for (int j = 0; j < BYTES_PER_SCRATCHCODE; ++j) { scratch = 256*scratch + buf[SECRET_BITS/8 + BYTES_PER_SCRATCHCODE*i + j]; } int modulus = 1; for (int j = 0; j < SCRATCHCODE_LENGTH; j++) { modulus *= 10; } scratch = (scratch & 0x7FFFFFFF) % modulus; if (scratch < modulus/10) { // Make sure that scratch codes are always exactly eight digits. If they // start with a sequence of zeros, just generate a new scratch code. if (read(fd, buf + (SECRET_BITS/8 + BYTES_PER_SCRATCHCODE*i), BYTES_PER_SCRATCHCODE) != BYTES_PER_SCRATCHCODE) { goto urandom_failure; } goto new_scratch_code; } printf(" %08d\n", scratch); snprintf(strrchr(secret, '\000'), sizeof(secret) - strlen(secret), "%08d\n", scratch); } close(fd); printf("\nDo you want me to update your \"~%s\" file (y/n) ", SECRET); fflush(stdout); char ch; do { ch = getchar(); } while (ch == ' ' || ch == '\r' || ch == '\n'); if (ch == 'y' || ch == 'Y') { char *home = getenv("HOME"); if (!home || *home != '/') { fprintf(stderr, "Cannot determine home directory\n"); return 1; } char *fn = calloc(1, 2*(strlen(home) + strlen(SECRET) + 2)); if (!fn) { perror("Cannot allocate space for filename"); return 1; } strcat(strcpy(fn, home), SECRET "~"); char *tmp_fn = strrchr(fn, '\000'); memcpy(tmp_fn, fn, strlen(fn)); tmp_fn[-1] = '\000'; // Add optional flag requesting that the PAM module prevents reuse of // authentication tokens. printf("\nDo you want to disallow multiple uses of the same " "authentication\ntoken? This restricts you to one login about " "every 30s, but it increases\nyour chances to notice or even " "prevent man-in-the-middle attacks (y/n) "); fflush(stdout); do { ch = getchar(); } while (ch == ' ' || ch == '\r' || ch == '\n'); if (ch == 'y' || ch == 'Y') { assert(strlen(secret) + sizeof(disallow) <= sizeof(secret)); char *scratchCodes = strchr(secret, '\n') + 1; memmove(scratchCodes + sizeof(disallow) - 1, scratchCodes, strlen(scratchCodes) + 1); memcpy(scratchCodes, disallow, sizeof(disallow)-1); } fd = open(tmp_fn, O_WRONLY|O_EXCL|O_CREAT|O_NOFOLLOW|O_TRUNC, 0400); if (fd < 0) { perror("Failed to create \"~"SECRET"\""); free(fn); return 1; } if (write(fd, secret, strlen(secret)) != (ssize_t)strlen(secret) || rename(tmp_fn, fn)) { perror("Failed to write new secret"); unlink(fn); close(fd); free(fn); return 1; } free(fn); close(fd); } return 0; }