/*
 * Copyright (C) 2014-2022 Yubico AB - See COPYING
 */

#include <fido.h>
#include <fido/es256.h>
#include <fido/rs256.h>
#include <fido/eddsa.h>
#include <syslog.h>

#include <openssl/ec.h>
#include <openssl/obj_mac.h>

#include <inttypes.h>
#include <limits.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <pwd.h>
#include <errno.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>

#include "b64.h"
#include "debug.h"
#include "util.h"

#define SSH_MAX_SIZE 8192
#define SSH_HEADER "-----BEGIN OPENSSH PRIVATE KEY-----\n"
#define SSH_HEADER_LEN (sizeof(SSH_HEADER) - 1)
#define SSH_TRAILER "-----END OPENSSH PRIVATE KEY-----\n"
#define SSH_TRAILER_LEN (sizeof(SSH_TRAILER) - 1)
#define SSH_AUTH_MAGIC "openssh-key-v1"
#define SSH_AUTH_MAGIC_LEN (sizeof(SSH_AUTH_MAGIC)) // AUTH_MAGIC includes \0
#define SSH_ES256 "sk-ecdsa-sha2-nistp256@openssh.com"
#define SSH_ES256_LEN (sizeof(SSH_ES256) - 1)
#define SSH_ES256_POINT_LEN 65
#define SSH_P256_NAME "nistp256"
#define SSH_P256_NAME_LEN (sizeof(SSH_P256_NAME) - 1)
#define SSH_EDDSA "sk-ssh-ed25519@openssh.com"
#define SSH_EDDSA_LEN (sizeof(SSH_EDDSA) - 1)
#define SSH_EDDSA_POINT_LEN 32
#define SSH_SK_USER_PRESENCE_REQD 0x01
#define SSH_SK_USER_VERIFICATION_REQD 0x04
#define SSH_SK_RESIDENT_KEY 0x20

struct opts {
  fido_opt_t up;
  fido_opt_t uv;
  fido_opt_t pin;
};

struct pk {
  void *ptr;
  int type;
};

static int hex_decode(const char *ascii_hex, unsigned char **blob,
                      size_t *blob_len) {
  *blob = NULL;
  *blob_len = 0;

  if (ascii_hex == NULL || (strlen(ascii_hex) % 2) != 0)
    return (0);

  *blob_len = strlen(ascii_hex) / 2;
  *blob = calloc(1, *blob_len);
  if (*blob == NULL)
    return (0);

  for (size_t i = 0; i < *blob_len; i++) {
    unsigned int c;
    int n = -1;
    int r = sscanf(ascii_hex, "%02x%n", &c, &n);
    if (r != 1 || n != 2 || c > UCHAR_MAX) {
      free(*blob);
      *blob = NULL;
      *blob_len = 0;
      return (0);
    }
    (*blob)[i] = (unsigned char) c;
    ascii_hex += n;
  }

  return (1);
}

static char *normal_b64(const char *websafe_b64) {
  char *b64;
  char *p;
  size_t n;

  n = strlen(websafe_b64);
  if (n > SIZE_MAX - 3)
    return (NULL);

  b64 = calloc(1, n + 3);
  if (b64 == NULL)
    return (NULL);

  memcpy(b64, websafe_b64, n);
  p = b64;

  while ((p = strpbrk(p, "-_")) != NULL) {
    switch (*p) {
      case '-':
        *p++ = '+';
        break;
      case '_':
        *p++ = '/';
        break;
    }
  }

  switch (n % 4) {
    case 1:
      b64[n] = '=';
      break;
    case 2:
    case 3:
      b64[n] = '=';
      b64[n + 1] = '=';
      break;
  }

  return (b64);
}

static int translate_old_format_pubkey(es256_pk_t *es256_pk,
                                       const unsigned char *pk, size_t pk_len) {
  EC_KEY *ec = NULL;
  EC_POINT *q = NULL;
  const EC_GROUP *g = NULL;
  int r = FIDO_ERR_INTERNAL;

  if (es256_pk == NULL)
    goto fail;

  if ((ec = EC_KEY_new_by_curve_name(NID_X9_62_prime256v1)) == NULL ||
      (g = EC_KEY_get0_group(ec)) == NULL)
    goto fail;

  if ((q = EC_POINT_new(g)) == NULL ||
      !EC_POINT_oct2point(g, q, pk, pk_len, NULL) ||
      !EC_KEY_set_public_key(ec, q))
    goto fail;

  r = es256_pk_from_EC_KEY(es256_pk, ec);

fail:
  if (ec != NULL)
    EC_KEY_free(ec);
  if (q != NULL)
    EC_POINT_free(q);

  return r;
}

static int is_resident(const char *kh) { return strcmp(kh, "*") == 0; }

static void reset_device(device_t *device) {
  free(device->keyHandle);
  free(device->publicKey);
  free(device->coseType);
  free(device->attributes);
  memset(device, 0, sizeof(*device));
}

static int parse_native_credential(const cfg_t *cfg, char *s, device_t *cred) {
  const char *delim = ",";
  const char *kh, *pk, *type, *attr;
  char *saveptr = NULL;

  memset(cred, 0, sizeof(*cred));

  if ((kh = strtok_r(s, delim, &saveptr)) == NULL) {
    debug_dbg(cfg, "Missing key handle");
    goto fail;
  }

  if ((pk = strtok_r(NULL, delim, &saveptr)) == NULL) {
    debug_dbg(cfg, "Missing public key");
    goto fail;
  }

  if ((type = strtok_r(NULL, delim, &saveptr)) == NULL) {
    debug_dbg(cfg, "Old format, assume es256 and +presence");
    cred->old_format = 1;
    type = "es256";
    attr = "+presence";
  } else if ((attr = strtok_r(NULL, delim, &saveptr)) == NULL) {
    debug_dbg(cfg, "Empty attributes");
    attr = "";
  }

  cred->keyHandle = cred->old_format ? normal_b64(kh) : strdup(kh);
  if (cred->keyHandle == NULL || (cred->publicKey = strdup(pk)) == NULL ||
      (cred->coseType = strdup(type)) == NULL ||
      (cred->attributes = strdup(attr)) == NULL) {
    debug_dbg(cfg, "Unable to allocate memory for credential components");
    goto fail;
  }

  return 1;

fail:
  reset_device(cred);
  return 0;
}

static int parse_native_format(const cfg_t *cfg, const char *username,
                               FILE *opwfile, device_t *devices,
                               unsigned *n_devs) {

  const char *s_user;
  char *buf = NULL, *s_credential;
  size_t bufsiz = 0;
  ssize_t len;
  unsigned i;
  int r = 0;

  while ((len = getline(&buf, &bufsiz, opwfile)) != -1) {
    char *saveptr = NULL;
    if (len > 0 && buf[len - 1] == '\n')
      buf[len - 1] = '\0';

    debug_dbg(cfg, "Read %zu bytes", len);

    s_user = strtok_r(buf, ":", &saveptr);
    if (s_user && strcmp(username, s_user) == 0) {
      debug_dbg(cfg, "Matched user: %s", s_user);

      // only keep last line for this user
      for (i = 0; i < *n_devs; i++) {
        reset_device(&devices[i]);
      }
      *n_devs = 0;

      i = 0;
      while ((s_credential = strtok_r(NULL, ":", &saveptr))) {
        if ((*n_devs)++ > cfg->max_devs - 1) {
          *n_devs = cfg->max_devs;
          debug_dbg(cfg,
                    "Found more than %d devices, ignoring the remaining ones",
                    cfg->max_devs);
          break;
        }

        if (!parse_native_credential(cfg, s_credential, &devices[i])) {
          debug_dbg(cfg, "Failed to parse credential");
          goto fail;
        }

        debug_dbg(cfg, "KeyHandle for device number %u: %s", i + 1,
                  devices[i].keyHandle);
        debug_dbg(cfg, "publicKey for device number %u: %s", i + 1,
                  devices[i].publicKey);
        debug_dbg(cfg, "COSE type for device number %u: %s", i + 1,
                  devices[i].coseType);
        debug_dbg(cfg, "Attributes for device number %u: %s", i + 1,
                  devices[i].attributes);
        i++;
      }
    }
  }

  if (!feof(opwfile)) {
    debug_dbg(cfg, "authfile parsing ended before eof (%d)", errno);
    goto fail;
  }

  r = 1;
fail:
  free(buf);
  return r;
}

static int load_ssh_key(const cfg_t *cfg, char **out, FILE *opwfile,
                        size_t opwfile_size) {
  size_t buf_size;
  char *buf = NULL;
  char *cp = NULL;
  int r = 0;
  int ch;

  *out = NULL;

  if (opwfile_size < SSH_HEADER_LEN + SSH_TRAILER_LEN) {
    debug_dbg(cfg, "Malformed SSH key (length)");
    goto fail;
  }

  buf_size = opwfile_size > SSH_MAX_SIZE ? SSH_MAX_SIZE : opwfile_size;
  if ((cp = buf = calloc(1, buf_size)) == NULL) {
    debug_dbg(cfg, "Failed to allocate buffer for SSH key");
    goto fail;
  }

  // NOTE(adma): +1 for \0
  if (fgets(buf, SSH_HEADER_LEN + 1, opwfile) == NULL ||
      strlen(buf) != SSH_HEADER_LEN ||
      strncmp(buf, SSH_HEADER, SSH_HEADER_LEN) != 0) {
    debug_dbg(cfg, "Malformed SSH key (header)");
    goto fail;
  }

  while (opwfile_size > 0 && buf_size > 1) {
    ch = fgetc(opwfile);
    if (ch == EOF) {
      debug_dbg(cfg, "Unexpected authfile termination");
      goto fail;
    }

    opwfile_size--;

    if (ch != '\n' && ch != '\r') {
      *cp = (char) ch;
      buf_size--;
      if (ch == '-') {
        // NOTE(adma): no +1 here since we already read one '-'
        if (buf_size < SSH_TRAILER_LEN ||
            fgets(cp + 1, SSH_TRAILER_LEN, opwfile) == NULL ||
            strlen(cp) != SSH_TRAILER_LEN ||
            strncmp(cp, SSH_TRAILER, SSH_TRAILER_LEN) != 0) {
          debug_dbg(cfg, "Malformed SSH key (trailer)");
          goto fail;
        }

        r = 1;
        *(cp) = '\0';
        break;
      } else {
        cp++;
      }
    }
  }

fail:
  if (r != 1) {
    free(buf);
    buf = NULL;
  }

  *out = buf;

  return r;
}

static int ssh_get(const unsigned char **buf, size_t *size, unsigned char *dst,
                   size_t len) {
  if (*size < len)
    return 0;
  if (dst != NULL)
    memcpy(dst, *buf, len);
  *buf += len;
  *size -= len;
  return 1;
}

static int ssh_get_u8(const unsigned char **buf, size_t *size, uint8_t *val) {
  return ssh_get(buf, size, val, sizeof(*val));
}

static int ssh_get_u32(const unsigned char **buf, size_t *size, uint32_t *val) {
  if (!ssh_get(buf, size, (unsigned char *) val, sizeof(*val)))
    return 0;
  if (val != NULL)
    *val = ntohl(*val);
  return 1;
}

static int ssh_get_string_ref(const unsigned char **buf, size_t *size,
                              const unsigned char **ref, size_t *lenp) {
  uint32_t len;

  if (!ssh_get_u32(buf, size, &len))
    return 0;
  if (!ssh_get(buf, size, NULL, len))
    return 0;
  if (ref != NULL)
    *ref = *buf - len;
  if (lenp != NULL)
    *lenp = len;
  return 1;
}

static int ssh_get_cstring(const unsigned char **buf, size_t *size, char **str,
                           size_t *lenp) {
  const unsigned char *ref;
  size_t len;

  if (!ssh_get_string_ref(buf, size, &ref, &len))
    return 0;
  if (str != NULL) {
    if (len > SIZE_MAX - 1 || (*str = calloc(1, len + 1)) == NULL)
      return 0;
    memcpy(*str, ref, len);
  }
  if (lenp != NULL)
    *lenp = len;
  return 1;
}

static int ssh_log_cstring(const cfg_t *cfg, const unsigned char **buf,
                           size_t *size, const char *name) {
  char *str = NULL;
  size_t len;

  (void) name; // silence compiler warnings if PAM_DEBUG disabled

  if (!ssh_get_cstring(buf, size, &str, &len)) {
    debug_dbg(cfg, "Malformed SSH key (%s)", name);
    return 0;
  }
  debug_dbg(cfg, "%s (%zu) \"%s\"", name, len, str);

  free(str);
  return 1;
}

static int ssh_get_attrs(const cfg_t *cfg, const unsigned char **buf,
                         size_t *size, char **attrs) {
  char tmp[32] = {0};
  uint8_t flags;
  int r;

  // flags
  if (!ssh_get_u8(buf, size, &flags)) {
    debug_dbg(cfg, "Malformed SSH key (flags)");
    return 0;
  }
  debug_dbg(cfg, "flags: %02x", flags);

  r = snprintf(tmp, sizeof(tmp), "%s%s",
               flags & SSH_SK_USER_PRESENCE_REQD ? "+presence" : "",
               flags & SSH_SK_USER_VERIFICATION_REQD ? "+verification" : "");
  if (r < 0 || (size_t) r >= sizeof(tmp)) {
    debug_dbg(cfg, "Unable to prepare flags");
    return 0;
  }

  if ((*attrs = strdup(tmp)) == NULL) {
    debug_dbg(cfg, "Unable to allocate attributes");
    return 0;
  }

  return 1;
}

static int ssh_get_pubkey(const cfg_t *cfg, const unsigned char **buf,
                          size_t *size, char **type_p, char **pubkey_p) {
  char *ssh_type = NULL;
  char *ssh_curve = NULL;
  const unsigned char *blob;
  size_t len;
  int type;
  size_t point_len;
  int ok = 0;

  *type_p = NULL;
  *pubkey_p = NULL;

  // key type
  if (!ssh_get_cstring(buf, size, &ssh_type, &len)) {
    debug_dbg(cfg, "Malformed SSH key (keytype)");
    goto err;
  }

  if (len == SSH_ES256_LEN && memcmp(ssh_type, SSH_ES256, SSH_ES256_LEN) == 0) {
    type = COSE_ES256;
    point_len = SSH_ES256_POINT_LEN;
  } else if (len == SSH_EDDSA_LEN &&
             memcmp(ssh_type, SSH_EDDSA, SSH_EDDSA_LEN) == 0) {
    type = COSE_EDDSA;
    point_len = SSH_EDDSA_POINT_LEN;
  } else {
    debug_dbg(cfg, "Unknown key type %s", ssh_type);
    goto err;
  }

  debug_dbg(cfg, "keytype (%zu) \"%s\"", len, ssh_type);

  if (type == COSE_ES256) {
    // curve name
    if (!ssh_get_cstring(buf, size, &ssh_curve, &len)) {
      debug_dbg(cfg, "Malformed SSH key (curvename)");
      goto err;
    }

    if (len == SSH_P256_NAME_LEN &&
        memcmp(ssh_curve, SSH_P256_NAME, SSH_P256_NAME_LEN) == 0) {
      debug_dbg(cfg, "curvename (%zu) \"%s\"", len, ssh_curve);
    } else {
      debug_dbg(cfg, "Unknown curve %s", ssh_curve);
      goto err;
    }
  }

  // point
  if (!ssh_get_string_ref(buf, size, &blob, &len)) {
    debug_dbg(cfg, "Malformed SSH key (point)");
    goto err;
  }

  if (len != point_len) {
    debug_dbg(cfg, "Invalid point length, should be %zu, found %zu", point_len,
              len);
    goto err;
  }

  if (type == COSE_ES256) {
    // Skip the initial '04'
    if (len < 1) {
      debug_dbg(cfg, "Failed to skip initial '04'");
      goto err;
    }
    blob++;
    len--;
  }

  if (!b64_encode(blob, len, pubkey_p)) {
    debug_dbg(cfg, "Unable to allocate public key");
    goto err;
  }

  if ((*type_p = strdup(cose_string(type))) == NULL) {
    debug_dbg(cfg, "Unable to allocate COSE type");
    goto err;
  }

  ok = 1;
err:
  if (!ok) {
    free(*type_p);
    free(*pubkey_p);
    *type_p = NULL;
    *pubkey_p = NULL;
  }
  free(ssh_type);
  free(ssh_curve);

  return ok;
}

static int parse_ssh_format(const cfg_t *cfg, FILE *opwfile,
                            size_t opwfile_size, device_t *devices,
                            unsigned *n_devs) {
  char *b64 = NULL;
  const unsigned char *decoded;
  unsigned char *decoded_initial = NULL;
  size_t decoded_len;
  const unsigned char *blob;
  uint32_t check1, check2, tmp;
  size_t len;
  int r = 0;

  // The logic below is inspired by
  // how ssh parses its own keys. See sshkey.c
  reset_device(&devices[0]);
  *n_devs = 0;

  if (!load_ssh_key(cfg, &b64, opwfile, opwfile_size) ||
      !b64_decode(b64, (void **) &decoded_initial, &decoded_len)) {
    debug_dbg(cfg, "Unable to decode credential");
    goto out;
  }

  decoded = decoded_initial;

  // magic
  if (decoded_len < SSH_AUTH_MAGIC_LEN ||
      memcmp(decoded, SSH_AUTH_MAGIC, SSH_AUTH_MAGIC_LEN) != 0) {
    debug_dbg(cfg, "Malformed SSH key (magic)");
    goto out;
  }

  decoded += SSH_AUTH_MAGIC_LEN;
  decoded_len -= SSH_AUTH_MAGIC_LEN;

  if (!ssh_log_cstring(cfg, &decoded, &decoded_len, "ciphername") ||
      !ssh_log_cstring(cfg, &decoded, &decoded_len, "kdfname") ||
      !ssh_log_cstring(cfg, &decoded, &decoded_len, "kdfoptions"))
    goto out;

  if (!ssh_get_u32(&decoded, &decoded_len, &tmp)) {
    debug_dbg(cfg, "Malformed SSH key (nkeys)");
    goto out;
  }
  debug_dbg(cfg, "nkeys: %" PRIu32, tmp);
  if (tmp != 1) {
    debug_dbg(cfg, "Multiple keys not supported");
    goto out;
  }

  // public_key (skip)
  if (!ssh_get_string_ref(&decoded, &decoded_len, NULL, NULL)) {
    debug_dbg(cfg, "Malformed SSH key (pubkey)");
    goto out;
  }

  // private key (consume length)
  if (!ssh_get_u32(&decoded, &decoded_len, &tmp) || decoded_len < tmp) {
    debug_dbg(cfg, "Malformed SSH key (pvtkey length)");
    goto out;
  }

  // check1, check2
  if (!ssh_get_u32(&decoded, &decoded_len, &check1) ||
      !ssh_get_u32(&decoded, &decoded_len, &check2)) {
    debug_dbg(cfg, "Malformed SSH key (check1, check2)");
    goto out;
  }

  debug_dbg(cfg, "check1: %" PRIu32, check1);
  debug_dbg(cfg, "check2: %" PRIu32, check2);

  if (check1 != check2) {
    debug_dbg(cfg, "Mismatched check values");
    goto out;
  }

  if (!ssh_get_pubkey(cfg, &decoded, &decoded_len, &devices[0].coseType,
                      &devices[0].publicKey) ||
      !ssh_log_cstring(cfg, &decoded, &decoded_len, "application") ||
      !ssh_get_attrs(cfg, &decoded, &decoded_len, &devices[0].attributes))
    goto out;

  // keyhandle
  if (!ssh_get_string_ref(&decoded, &decoded_len, &blob, &len) ||
      !b64_encode(blob, len, &devices[0].keyHandle)) {
    debug_dbg(cfg, "Malformed SSH key (keyhandle)");
    goto out;
  }

  debug_dbg(cfg, "KeyHandle for device number %u: %s", 1, devices[0].keyHandle);
  debug_dbg(cfg, "publicKey for device number %u: %s", 1, devices[0].publicKey);
  debug_dbg(cfg, "COSE type for device number %u: %s", 1, devices[0].coseType);
  debug_dbg(cfg, "Attributes for device number %u: %s", 1,
            devices[0].attributes);

  // reserved (skip)
  if (!ssh_get_string_ref(&decoded, &decoded_len, NULL, NULL)) {
    debug_dbg(cfg, "Malformed SSH key (reserved)");
    goto out;
  }

  // comment
  if (!ssh_log_cstring(cfg, &decoded, &decoded_len, "comment"))
    goto out;

  // padding
  if (decoded_len >= 255) {
    debug_dbg(cfg, "Malformed SSH key (padding length)");
    goto out;
  }

  for (int i = 1; (unsigned) i <= decoded_len; i++) {
    if (decoded[i - 1] != i) {
      debug_dbg(cfg, "Malformed SSH key (padding)");
      goto out;
    }
  }

  *n_devs = 1;
  r = 1;

out:
  if (r != 1) {
    reset_device(&devices[0]);
    *n_devs = 0;
  }

  free(decoded_initial);
  free(b64);

  return r;
}

int get_devices_from_authfile(const cfg_t *cfg, const char *username,
                              device_t *devices, unsigned *n_devs) {

  int r = PAM_AUTHINFO_UNAVAIL;
  int fd = -1;
  struct stat st;
  struct passwd *pw = NULL, pw_s;
  char buffer[BUFSIZE];
  int gpu_ret;
  FILE *opwfile = NULL;
  size_t opwfile_size;
  unsigned i;

  /* Ensure we never return uninitialized count. */
  *n_devs = 0;

  fd = open(cfg->auth_file, O_RDONLY | O_CLOEXEC | O_NOCTTY);
  if (fd < 0) {
    if (errno == ENOENT && cfg->nouserok) {
      r = PAM_IGNORE;
    }
    debug_dbg(cfg, "Cannot open authentication file: %s", strerror(errno));
    goto err;
  }

  if (fstat(fd, &st) < 0) {
    debug_dbg(cfg, "Cannot stat authentication file: %s", strerror(errno));
    goto err;
  }

  if (!S_ISREG(st.st_mode)) {
    debug_dbg(cfg, "Authentication file is not a regular file");
    goto err;
  }

  if ((st.st_mode & (S_IWGRP | S_IWOTH)) != 0) {
    /* XXX: attempt to prevent two messages to syslog */
    if (cfg->debug_file) {
      debug_dbg(cfg,
                "Permissions %04o for '%s' are too open. Please change the "
                "file mode bits to 0644 or more restrictive. This may become "
                "an error in the future!",
                (unsigned int) st.st_mode & 0777, cfg->auth_file);
    }
#ifndef WITH_FUZZING
    /* XXX: force a message to syslog, regardless of the debug level */
    syslog(LOG_AUTHPRIV | LOG_WARNING,
           "warning(pam_u2f): Permissions %04o for '%s' are too open. Please "
           "change the file mode bits to 0644 or more restrictive. This may "
           "become an error in the future!",
           (unsigned int) st.st_mode & 0777, cfg->auth_file);
#endif
  }

  if (st.st_size < 0) {
    debug_dbg(cfg, "Invalid stat size for %s: %jd", cfg->auth_file,
              (intmax_t) st.st_size);
    goto err;
  }
  opwfile_size = (size_t) st.st_size;

  gpu_ret = getpwuid_r(st.st_uid, &pw_s, buffer, sizeof(buffer), &pw);
  if (gpu_ret != 0 || pw == NULL) {
    debug_dbg(cfg, "Unable to retrieve credentials for uid %u, (%s)", st.st_uid,
              strerror(errno));
    goto err;
  }

  if (strcmp(pw->pw_name, username) != 0 && strcmp(pw->pw_name, "root") != 0) {
    if (strcmp(username, "root") != 0) {
      debug_dbg(cfg,
                "The owner of the authentication file is neither %s nor root",
                username);
    } else {
      debug_dbg(cfg, "The owner of the authentication file is not root");
    }
    goto err;
  }

  opwfile = fdopen(fd, "r");
  if (opwfile == NULL) {
    debug_dbg(cfg, "fdopen: %s", strerror(errno));
    goto err;
  } else {
    fd = -1; /* fd belongs to opwfile */
  }

  if (cfg->sshformat == 0) {
    if (parse_native_format(cfg, username, opwfile, devices, n_devs) != 1) {
      goto err;
    }
  } else {
    if (parse_ssh_format(cfg, opwfile, opwfile_size, devices, n_devs) != 1) {
      goto err;
    }
  }

  debug_dbg(cfg, "Found %d device(s) for user %s", *n_devs, username);
  r = PAM_SUCCESS;

err:
  if (r != PAM_SUCCESS) {
    for (i = 0; i < *n_devs; i++) {
      reset_device(&devices[i]);
    }
    *n_devs = 0;
  } else if (*n_devs == 0) {
    r = cfg->nouserok ? PAM_IGNORE : PAM_USER_UNKNOWN;
  }

  if (opwfile)
    fclose(opwfile);

  if (fd != -1)
    close(fd);

  return r;
}

void free_devices(device_t *devices, const unsigned n_devs) {
  unsigned i;

  if (!devices)
    return;

  for (i = 0; i < n_devs; i++) {
    reset_device(&devices[i]);
  }

  free(devices);
}

static int get_authenticators(const cfg_t *cfg, const fido_dev_info_t *devlist,
                              size_t devlist_len, fido_assert_t *assert,
                              const int rk, fido_dev_t **authlist) {
  const fido_dev_info_t *di = NULL;
  fido_dev_t *dev = NULL;
  int r;
  size_t i;
  size_t j;

  debug_dbg(cfg, "Working with %zu authenticator(s)", devlist_len);

  for (i = 0, j = 0; i < devlist_len; i++) {
    debug_dbg(cfg, "Checking whether key exists in authenticator %zu", i);

    di = fido_dev_info_ptr(devlist, i);
    if (!di) {
      debug_dbg(cfg, "Unable to get device pointer");
      continue;
    }

    debug_dbg(cfg, "Authenticator path: %s", fido_dev_info_path(di));

    dev = fido_dev_new();
    if (!dev) {
      debug_dbg(cfg, "Unable to allocate device type");
      continue;
    }

    r = fido_dev_open(dev, fido_dev_info_path(di));
    if (r != FIDO_OK) {
      debug_dbg(cfg, "Failed to open authenticator: %s (%d)", fido_strerr(r),
                r);
      fido_dev_free(&dev);
      continue;
    }

    if (rk || cfg->nodetect) {
      /* resident credential or nodetect: try all authenticators */
      authlist[j++] = dev;
    } else {
      r = fido_dev_get_assert(dev, assert, NULL);
      if ((!fido_dev_is_fido2(dev) && r == FIDO_ERR_USER_PRESENCE_REQUIRED) ||
          (fido_dev_is_fido2(dev) && r == FIDO_OK)) {
        authlist[j++] = dev;
        debug_dbg(cfg, "Found key in authenticator %zu", i);
        return (1);
      }
      debug_dbg(cfg, "Key not found in authenticator %zu", i);

      fido_dev_close(dev);
      fido_dev_free(&dev);
    }
  }

  if (j != 0)
    return (1);
  else {
    debug_dbg(cfg, "Key not found");
    return (0);
  }
}

static void init_opts(struct opts *opts) {
  opts->up = FIDO_OPT_FALSE;
  opts->uv = FIDO_OPT_OMIT;
  opts->pin = FIDO_OPT_FALSE;
}

static void parse_opts(const cfg_t *cfg, const char *attr, struct opts *opts) {
  if (cfg->userpresence == 1 || strstr(attr, "+presence")) {
    opts->up = FIDO_OPT_TRUE;
  } else if (cfg->userpresence == 0) {
    opts->up = FIDO_OPT_FALSE;
  } else {
    opts->up = FIDO_OPT_OMIT;
  }

  if (cfg->userverification == 1 || strstr(attr, "+verification")) {
    opts->uv = FIDO_OPT_TRUE;
  } else if (cfg->userverification == 0)
    opts->uv = FIDO_OPT_FALSE;
  else {
    opts->uv = FIDO_OPT_OMIT;
  }

  if (cfg->pinverification == 1 || strstr(attr, "+pin")) {
    opts->pin = FIDO_OPT_TRUE;
  } else if (cfg->pinverification == 0) {
    opts->pin = FIDO_OPT_FALSE;
  } else {
    opts->pin = FIDO_OPT_OMIT;
  }
}

static int get_device_opts(fido_dev_t *dev, int *pin, int *uv) {
  fido_cbor_info_t *info = NULL;
  const bool *val;

  *pin = *uv = -1; /* unsupported */

  if (fido_dev_is_fido2(dev)) {
    char *const *ptr;
    size_t len;

    if ((info = fido_cbor_info_new()) == NULL ||
        fido_dev_get_cbor_info(dev, info) != FIDO_OK) {
      fido_cbor_info_free(&info);
      return 0;
    }

    ptr = fido_cbor_info_options_name_ptr(info);
    val = fido_cbor_info_options_value_ptr(info);
    len = fido_cbor_info_options_len(info);
    for (size_t i = 0; i < len; i++) {
      if (strcmp(ptr[i], "clientPin") == 0) {
        *pin = val[i];
      } else if (strcmp(ptr[i], "uv") == 0) {
        *uv = val[i];
      }
    }
  }

  fido_cbor_info_free(&info);
  return 1;
}

static int match_device_opts(fido_dev_t *dev, struct opts *opts) {
  int pin, uv;

  /* FIXME: fido_dev_{supports,has}_{pin,uv} (1.7.0) */
  if (!get_device_opts(dev, &pin, &uv)) {
    return -1;
  }

  if (opts->uv == FIDO_OPT_FALSE && uv < 0) {
    opts->uv = FIDO_OPT_OMIT;
  }

  if ((opts->pin == FIDO_OPT_TRUE && pin != 1) ||
      (opts->uv == FIDO_OPT_TRUE && uv != 1)) {
    return 0;
  }

  return 1;
}

static int set_opts(const cfg_t *cfg, const struct opts *opts,
                    fido_assert_t *assert) {
  if (fido_assert_set_up(assert, opts->up) != FIDO_OK) {
    debug_dbg(cfg, "Failed to set UP");
    return 0;
  }
  if (fido_assert_set_uv(assert, opts->uv) != FIDO_OK) {
    debug_dbg(cfg, "Failed to set UV");
    return 0;
  }

  return 1;
}

static int set_cdh(const cfg_t *cfg, fido_assert_t *assert) {
  unsigned char cdh[32];
  int r;

  if (!random_bytes(cdh, sizeof(cdh))) {
    debug_dbg(cfg, "Failed to generate challenge");
    return 0;
  }

  r = fido_assert_set_clientdata_hash(assert, cdh, sizeof(cdh));
  if (r != FIDO_OK) {
    debug_dbg(cfg, "Unable to set challenge: %s (%d)", fido_strerr(r), r);
    return 0;
  }

  return 1;
}

static fido_assert_t *prepare_assert(const cfg_t *cfg, const device_t *device,
                                     const struct opts *opts) {
  fido_assert_t *assert = NULL;
  unsigned char *buf = NULL;
  size_t buf_len;
  int ok = 0;
  int r;

  if ((assert = fido_assert_new()) == NULL) {
    debug_dbg(cfg, "Unable to allocate assertion");
    goto err;
  }

  if (device->old_format)
    r = fido_assert_set_rp(assert, cfg->appid);
  else
    r = fido_assert_set_rp(assert, cfg->origin);

  if (r != FIDO_OK) {
    debug_dbg(cfg, "Unable to set origin: %s (%d)", fido_strerr(r), r);
    goto err;
  }

  if (is_resident(device->keyHandle)) {
    debug_dbg(cfg, "Credential is resident");
  } else {
    debug_dbg(cfg, "Key handle: %s", device->keyHandle);
    if (!b64_decode(device->keyHandle, (void **) &buf, &buf_len)) {
      debug_dbg(cfg, "Failed to decode key handle");
      goto err;
    }

    r = fido_assert_allow_cred(assert, buf, buf_len);
    if (r != FIDO_OK) {
      debug_dbg(cfg, "Unable to set keyHandle: %s (%d)", fido_strerr(r), r);
      goto err;
    }
  }

  if (!set_opts(cfg, opts, assert)) {
    debug_dbg(cfg, "Failed to set assert options");
    goto err;
  }

  if (!set_cdh(cfg, assert)) {
    debug_dbg(cfg, "Failed to set client data hash");
    goto err;
  }

  ok = 1;

err:
  if (!ok)
    fido_assert_free(&assert);

  free(buf);

  return assert;
}

static void reset_pk(struct pk *pk) {
  if (pk->type == COSE_ES256) {
    es256_pk_free((es256_pk_t **) &pk->ptr);
  } else if (pk->type == COSE_RS256) {
    rs256_pk_free((rs256_pk_t **) &pk->ptr);
  } else if (pk->type == COSE_EDDSA) {
    eddsa_pk_free((eddsa_pk_t **) &pk->ptr);
  }
  memset(pk, 0, sizeof(*pk));
}

int cose_type(const char *str, int *type) {
  if (strcasecmp(str, "es256") == 0) {
    *type = COSE_ES256;
  } else if (strcasecmp(str, "rs256") == 0) {
    *type = COSE_RS256;
  } else if (strcasecmp(str, "eddsa") == 0) {
    *type = COSE_EDDSA;
  } else {
    *type = 0;
    return 0;
  }

  return 1;
}

const char *cose_string(int type) {
  switch (type) {
    case COSE_ES256:
      return "es256";
    case COSE_RS256:
      return "rs256";
    case COSE_EDDSA:
      return "eddsa";
    default:
      return "unknown";
  }
}

static int parse_pk(const cfg_t *cfg, int old, const char *type, const char *pk,
                    struct pk *out) {
  unsigned char *buf = NULL;
  size_t buf_len;
  int ok = 0;
  int r;

  reset_pk(out);

  if (old) {
    if (!hex_decode(pk, &buf, &buf_len)) {
      debug_dbg(cfg, "Failed to decode public key");
      goto err;
    }
  } else {
    if (!b64_decode(pk, (void **) &buf, &buf_len)) {
      debug_dbg(cfg, "Failed to decode public key");
      goto err;
    }
  }

  if (!cose_type(type, &out->type)) {
    debug_dbg(cfg, "Unknown COSE type '%s'", type);
    goto err;
  }

  // For backwards compatibility, failure to pack the public key is not
  // returned as an error.  Instead, it is handled by fido_verify_assert().
  if (out->type == COSE_ES256) {
    if ((out->ptr = es256_pk_new()) == NULL) {
      debug_dbg(cfg, "Failed to allocate ES256 public key");
      goto err;
    }
    if (old) {
      r = translate_old_format_pubkey(out->ptr, buf, buf_len);
    } else {
      r = es256_pk_from_ptr(out->ptr, buf, buf_len);
    }
    if (r != FIDO_OK) {
      debug_dbg(cfg, "Failed to convert ES256 public key");
    }
  } else if (out->type == COSE_RS256) {
    if ((out->ptr = rs256_pk_new()) == NULL) {
      debug_dbg(cfg, "Failed to allocate RS256 public key");
      goto err;
    }
    r = rs256_pk_from_ptr(out->ptr, buf, buf_len);
    if (r != FIDO_OK) {
      debug_dbg(cfg, "Failed to convert RS256 public key");
    }
  } else if (out->type == COSE_EDDSA) {
    if ((out->ptr = eddsa_pk_new()) == NULL) {
      debug_dbg(cfg, "Failed to allocate EDDSA public key");
      goto err;
    }
    r = eddsa_pk_from_ptr(out->ptr, buf, buf_len);
    if (r != FIDO_OK) {
      debug_dbg(cfg, "Failed to convert EDDSA public key");
    }
  } else {
    debug_dbg(cfg, "COSE type '%s' not handled", type);
    goto err;
  }

  ok = 1;
err:
  free(buf);

  return ok;
}

int do_authentication(const cfg_t *cfg, const device_t *devices,
                      const unsigned n_devs, pam_handle_t *pamh) {
  fido_assert_t *assert = NULL;
  fido_dev_info_t *devlist = NULL;
  fido_dev_t **authlist = NULL;
  int cued = 0;
  int r;
  int retval = PAM_AUTH_ERR;
  size_t ndevs = 0;
  size_t ndevs_prev = 0;
  unsigned i = 0;
  struct opts opts;
  struct pk pk;
  char *pin = NULL;

  init_opts(&opts);
#ifndef WITH_FUZZING
  fido_init(cfg->debug ? FIDO_DEBUG : 0);
#else
  fido_init(0);
#endif
  memset(&pk, 0, sizeof(pk));

  devlist = fido_dev_info_new(DEVLIST_LEN);
  if (!devlist) {
    debug_dbg(cfg, "Unable to allocate devlist");
    goto out;
  }

  r = fido_dev_info_manifest(devlist, DEVLIST_LEN, &ndevs);
  if (r != FIDO_OK) {
    debug_dbg(cfg, "Unable to discover device(s), %s (%d)", fido_strerr(r), r);
    goto out;
  }

  ndevs_prev = ndevs;

  debug_dbg(cfg, "Device max index is %zu", ndevs);

  authlist = calloc(DEVLIST_LEN + 1, sizeof(fido_dev_t *));
  if (!authlist) {
    debug_dbg(cfg, "Unable to allocate authenticator list");
    goto out;
  }

  if (cfg->nodetect)
    debug_dbg(cfg, "nodetect option specified, suitable key detection will be "
                   "skipped");

  i = 0;
  while (i < n_devs) {
    debug_dbg(cfg, "Attempting authentication with device number %d", i + 1);

    init_opts(&opts); /* used during authenticator discovery */
    assert = prepare_assert(cfg, &devices[i], &opts);
    if (assert == NULL) {
      debug_dbg(cfg, "Failed to prepare assert");
      goto out;
    }

    if (!parse_pk(cfg, devices[i].old_format, devices[i].coseType,
                  devices[i].publicKey, &pk)) {
      debug_dbg(cfg, "Failed to parse public key");
      goto out;
    }

    if (get_authenticators(cfg, devlist, ndevs, assert,
                           is_resident(devices[i].keyHandle), authlist)) {
      for (size_t j = 0; authlist[j] != NULL; j++) {
        /* options used during authentication */
        parse_opts(cfg, devices[i].attributes, &opts);

        r = match_device_opts(authlist[j], &opts);
        if (r != 1) {
          debug_dbg(cfg, "%s, skipping authenticator",
                    r < 0 ? "Failed to query supported options"
                          : "Unsupported options");
          continue;
        }

        if (!set_opts(cfg, &opts, assert)) {
          debug_dbg(cfg, "Failed to set assert options");
          goto out;
        }

        if (!set_cdh(cfg, assert)) {
          debug_dbg(cfg, "Failed to reset client data hash");
          goto out;
        }

        if (opts.pin == FIDO_OPT_TRUE) {
          pin = converse(pamh, PAM_PROMPT_ECHO_OFF, "Please enter the PIN: ");
          if (pin == NULL) {
            debug_dbg(cfg, "converse() returned NULL");
            goto out;
          }
        }
        if (opts.up == FIDO_OPT_TRUE || opts.uv == FIDO_OPT_TRUE) {
          if (cfg->manual == 0 && cfg->cue && !cued) {
            cued = 1;
            converse(pamh, PAM_TEXT_INFO,
                     cfg->cue_prompt != NULL ? cfg->cue_prompt : DEFAULT_CUE);
          }
        }
        r = fido_dev_get_assert(authlist[j], assert, pin);
        if (pin) {
          explicit_bzero(pin, strlen(pin));
          free(pin);
          pin = NULL;
        }
        if (r == FIDO_OK) {
          if (opts.pin == FIDO_OPT_TRUE || opts.uv == FIDO_OPT_TRUE) {
            r = fido_assert_set_uv(assert, FIDO_OPT_TRUE);
            if (r != FIDO_OK) {
              debug_dbg(cfg, "Failed to set UV");
              goto out;
            }
          }
          r = fido_assert_verify(assert, 0, pk.type, pk.ptr);
          if (r == FIDO_OK) {
            retval = PAM_SUCCESS;
            goto out;
          }
        }
      }
    } else {
      debug_dbg(cfg, "Device for this keyhandle is not present");
    }

    i++;

    fido_dev_info_free(&devlist, ndevs);

    devlist = fido_dev_info_new(DEVLIST_LEN);
    if (!devlist) {
      debug_dbg(cfg, "Unable to allocate devlist");
      goto out;
    }

    r = fido_dev_info_manifest(devlist, DEVLIST_LEN, &ndevs);
    if (r != FIDO_OK) {
      debug_dbg(cfg, "Unable to discover device(s), %s (%d)", fido_strerr(r),
                r);
      goto out;
    }

    if (ndevs > ndevs_prev) {
      debug_dbg(cfg,
                "Devices max_index has changed: %zu (was %zu). Starting over",
                ndevs, ndevs_prev);
      ndevs_prev = ndevs;
      i = 0;
    }

    for (size_t j = 0; authlist[j] != NULL; j++) {
      fido_dev_close(authlist[j]);
      fido_dev_free(&authlist[j]);
    }

    fido_assert_free(&assert);
  }

out:
  reset_pk(&pk);
  fido_assert_free(&assert);
  fido_dev_info_free(&devlist, ndevs);

  if (authlist) {
    for (size_t j = 0; authlist[j] != NULL; j++) {
      fido_dev_close(authlist[j]);
      fido_dev_free(&authlist[j]);
    }
    free(authlist);
  }

  return retval;
}

#define MAX_PROMPT_LEN (1024)

static int manual_get_assert(const cfg_t *cfg, const char *prompt,
                             pam_handle_t *pamh, fido_assert_t *assert) {
  char *b64_cdh = NULL;
  char *b64_rpid = NULL;
  char *b64_authdata = NULL;
  char *b64_sig = NULL;
  unsigned char *authdata = NULL;
  unsigned char *sig = NULL;
  size_t authdata_len;
  size_t sig_len;
  int r;
  int ok = 0;

  b64_cdh = converse(pamh, PAM_PROMPT_ECHO_ON, prompt);
  b64_rpid = converse(pamh, PAM_PROMPT_ECHO_ON, prompt);
  b64_authdata = converse(pamh, PAM_PROMPT_ECHO_ON, prompt);
  b64_sig = converse(pamh, PAM_PROMPT_ECHO_ON, prompt);

  if (!b64_decode(b64_authdata, (void **) &authdata, &authdata_len)) {
    debug_dbg(cfg, "Failed to decode authenticator data");
    goto err;
  }

  if (!b64_decode(b64_sig, (void **) &sig, &sig_len)) {
    debug_dbg(cfg, "Failed to decode signature");
    goto err;
  }

  r = fido_assert_set_count(assert, 1);
  if (r != FIDO_OK) {
    debug_dbg(cfg, "Failed to set signature count of assertion");
    goto err;
  }

  r = fido_assert_set_authdata(assert, 0, authdata, authdata_len);
  if (r != FIDO_OK) {
    debug_dbg(cfg, "Failed to set authdata of assertion");
    goto err;
  }

  r = fido_assert_set_sig(assert, 0, sig, sig_len);
  if (r != FIDO_OK) {
    debug_dbg(cfg, "Failed to set signature of assertion");
    goto err;
  }

  ok = 1;
err:
  free(b64_cdh);
  free(b64_rpid);
  free(b64_authdata);
  free(b64_sig);
  free(authdata);
  free(sig);

  return ok;
}

int do_manual_authentication(const cfg_t *cfg, const device_t *devices,
                             const unsigned n_devs, pam_handle_t *pamh) {
  fido_assert_t *assert[n_devs];
  struct pk pk[n_devs];
  char *b64_challenge = NULL;
  char prompt[MAX_PROMPT_LEN];
  char buf[MAX_PROMPT_LEN];
  int retval = PAM_AUTH_ERR;
  int n;
  int r;
  unsigned i = 0;
  struct opts opts;

  init_opts(&opts);
  memset(assert, 0, sizeof(assert));
  memset(pk, 0, sizeof(pk));

#ifndef WITH_FUZZING
  fido_init(cfg->debug ? FIDO_DEBUG : 0);
#else
  fido_init(0);
#endif

  for (i = 0; i < n_devs; ++i) {
    /* options used during authentication */
    parse_opts(cfg, devices[i].attributes, &opts);
    assert[i] = prepare_assert(cfg, &devices[i], &opts);
    if (assert[i] == NULL) {
      debug_dbg(cfg, "Failed to prepare assert");
      goto out;
    }

    debug_dbg(cfg, "Attempting authentication with device number %d", i + 1);

    if (!parse_pk(cfg, devices[i].old_format, devices[i].coseType,
                  devices[i].publicKey, &pk[i])) {
      debug_dbg(cfg, "Unable to parse public key %u", i);
      goto out;
    }

    if (!b64_encode(fido_assert_clientdata_hash_ptr(assert[i]),
                    fido_assert_clientdata_hash_len(assert[i]),
                    &b64_challenge)) {
      debug_dbg(cfg, "Failed to encode challenge");
      goto out;
    }

    debug_dbg(cfg, "Challenge: %s", b64_challenge);

    n = snprintf(prompt, sizeof(prompt), "Challenge #%u:", i + 1);
    if (n <= 0 || (size_t) n >= sizeof(prompt)) {
      debug_dbg(cfg, "Failed to print challenge prompt");
      goto out;
    }

    converse(pamh, PAM_TEXT_INFO, prompt);

    n = snprintf(buf, sizeof(buf), "%s\n%s\n%s", b64_challenge, cfg->origin,
                 devices[i].keyHandle);
    if (n <= 0 || (size_t) n >= sizeof(buf)) {
      debug_dbg(cfg, "Failed to print fido2-assert input string");
      goto out;
    }

    converse(pamh, PAM_TEXT_INFO, buf);

    free(b64_challenge);
    b64_challenge = NULL;
  }

  converse(pamh, PAM_TEXT_INFO,
           "Please pass the challenge(s) above to fido2-assert, and "
           "paste the results in the prompt below.");

  for (i = 0; i < n_devs; ++i) {
    n = snprintf(prompt, sizeof(prompt), "Response #%u: ", i + 1);
    if (n <= 0 || (size_t) n >= sizeof(prompt)) {
      debug_dbg(cfg, "Failed to print response prompt");
      goto out;
    }

    if (!manual_get_assert(cfg, prompt, pamh, assert[i])) {
      debug_dbg(cfg, "Failed to get assert %u", i);
      goto out;
    }

    r = fido_assert_verify(assert[i], 0, pk[i].type, pk[i].ptr);
    if (r == FIDO_OK) {
      retval = PAM_SUCCESS;
      break;
    }
  }

out:
  for (i = 0; i < n_devs; i++) {
    fido_assert_free(&assert[i]);
    reset_pk(&pk[i]);
  }

  free(b64_challenge);

  return retval;
}

static int _converse(pam_handle_t *pamh, int nargs,
                     const struct pam_message **message,
                     struct pam_response **response) {
  struct pam_conv *conv;
  int retval;

  retval = pam_get_item(pamh, PAM_CONV, (void *) &conv);

  if (retval != PAM_SUCCESS) {
    return retval;
  }

  return conv->conv(nargs, message, response, conv->appdata_ptr);
}

char *converse(pam_handle_t *pamh, int echocode, const char *prompt) {
  const struct pam_message msg = {.msg_style = echocode,
                                  .msg = (char *) (uintptr_t) prompt};
  const struct pam_message *msgs = &msg;
  struct pam_response *resp = NULL;
  int retval = _converse(pamh, 1, &msgs, &resp);
  char *ret = NULL;

  if (retval != PAM_SUCCESS || resp == NULL || resp->resp == NULL ||
      *resp->resp == '\000') {

    if (retval == PAM_SUCCESS && resp && resp->resp) {
      ret = resp->resp;
    }
  } else {
    ret = resp->resp;
  }

  // Deallocate temporary storage.
  if (resp) {
    if (!ret) {
      free(resp->resp);
    }
    free(resp);
  }

  return ret;
}

#ifndef RANDOM_DEV
#define RANDOM_DEV "/dev/urandom"
#endif

int random_bytes(void *buf, size_t cnt) {
  int fd;
  ssize_t n;

  fd = open(RANDOM_DEV, O_RDONLY);
  if (fd < 0)
    return (0);

  n = read(fd, buf, cnt);
  close(fd);
  if (n < 0 || (size_t) n != cnt)
    return (0);

  return (1);
}
