/* Copyright (c) 2004-2007, Sara Golemon <sarag@libssh2.org>
 * Copyright (c) 2010-2014 by Daniel Stenberg
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms,
 * with or without modification, are permitted provided
 * that the following conditions are met:
 *
 *   Redistributions of source code must retain the above
 *   copyright notice, this list of conditions and the
 *   following disclaimer.
 *
 *   Redistributions in binary form must reproduce the above
 *   copyright notice, this list of conditions and the following
 *   disclaimer in the documentation and/or other materials
 *   provided with the distribution.
 *
 *   Neither the name of the copyright holder nor the names
 *   of any other contributors may be used to endorse or
 *   promote products derived from this software without
 *   specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
 * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
 * OF SUCH DAMAGE.
 */

#include "libssh2_priv.h"
#include "libssh2_publickey.h"
#include "channel.h"
#include "session.h"

#define LIBSSH2_PUBLICKEY_VERSION               2

/* Numericised response codes -- Not IETF, just local representation */
#define LIBSSH2_PUBLICKEY_RESPONSE_STATUS       0
#define LIBSSH2_PUBLICKEY_RESPONSE_VERSION      1
#define LIBSSH2_PUBLICKEY_RESPONSE_PUBLICKEY    2

typedef struct _LIBSSH2_PUBLICKEY_CODE_LIST
{
    int code;
    const char *name;
    int name_len;
} LIBSSH2_PUBLICKEY_CODE_LIST;

static const LIBSSH2_PUBLICKEY_CODE_LIST publickey_response_codes[] =
{
    {LIBSSH2_PUBLICKEY_RESPONSE_STATUS, "status", sizeof("status") - 1},
    {LIBSSH2_PUBLICKEY_RESPONSE_VERSION, "version", sizeof("version") - 1},
    {LIBSSH2_PUBLICKEY_RESPONSE_PUBLICKEY, "publickey",
     sizeof("publickey") - 1},
    {0, NULL, 0}
};

/* PUBLICKEY status codes -- IETF defined */
#define LIBSSH2_PUBLICKEY_SUCCESS               0
#define LIBSSH2_PUBLICKEY_ACCESS_DENIED         1
#define LIBSSH2_PUBLICKEY_STORAGE_EXCEEDED      2
#define LIBSSH2_PUBLICKEY_VERSION_NOT_SUPPORTED 3
#define LIBSSH2_PUBLICKEY_KEY_NOT_FOUND         4
#define LIBSSH2_PUBLICKEY_KEY_NOT_SUPPORTED     5
#define LIBSSH2_PUBLICKEY_KEY_ALREADY_PRESENT   6
#define LIBSSH2_PUBLICKEY_GENERAL_FAILURE       7
#define LIBSSH2_PUBLICKEY_REQUEST_NOT_SUPPORTED 8

#define LIBSSH2_PUBLICKEY_STATUS_CODE_MAX       8

static const LIBSSH2_PUBLICKEY_CODE_LIST publickey_status_codes[] = {
    {LIBSSH2_PUBLICKEY_SUCCESS, "success", sizeof("success") - 1},
    {LIBSSH2_PUBLICKEY_ACCESS_DENIED, "access denied",
     sizeof("access denied") - 1},
    {LIBSSH2_PUBLICKEY_STORAGE_EXCEEDED, "storage exceeded",
     sizeof("storage exceeded") - 1},
    {LIBSSH2_PUBLICKEY_VERSION_NOT_SUPPORTED, "version not supported",
     sizeof("version not supported") - 1},
    {LIBSSH2_PUBLICKEY_KEY_NOT_FOUND, "key not found",
     sizeof("key not found") - 1},
    {LIBSSH2_PUBLICKEY_KEY_NOT_SUPPORTED, "key not supported",
     sizeof("key not supported") - 1},
    {LIBSSH2_PUBLICKEY_KEY_ALREADY_PRESENT, "key already present",
     sizeof("key already present") - 1},
    {LIBSSH2_PUBLICKEY_GENERAL_FAILURE, "general failure",
     sizeof("general failure") - 1},
    {LIBSSH2_PUBLICKEY_REQUEST_NOT_SUPPORTED, "request not supported",
     sizeof("request not supported") - 1},
    {0, NULL, 0}
};

/*
 * publickey_status_error
 *
 * Format an error message from a status code
 */
static void
publickey_status_error(const LIBSSH2_PUBLICKEY *pkey,
                       LIBSSH2_SESSION *session, int status)
{
    const char *msg;

    /* GENERAL_FAILURE got remapped between version 1 and 2 */
    if(status == 6 && pkey && pkey->version == 1) {
        status = 7;
    }

    if(status < 0 || status > LIBSSH2_PUBLICKEY_STATUS_CODE_MAX) {
        msg = "unknown";
    }
    else {
        msg = publickey_status_codes[status].name;
    }

    _libssh2_error(session, LIBSSH2_ERROR_PUBLICKEY_PROTOCOL, msg);
}

/*
 * publickey_packet_receive
 *
 * Read a packet from the subsystem
 */
static int
publickey_packet_receive(LIBSSH2_PUBLICKEY * pkey,
                         unsigned char **data, size_t *data_len)
{
    LIBSSH2_CHANNEL *channel = pkey->channel;
    LIBSSH2_SESSION *session = channel->session;
    unsigned char buffer[4];
    int rc;
    *data = NULL; /* default to nothing returned */
    *data_len = 0;

    if(pkey->receive_state == libssh2_NB_state_idle) {
        rc = _libssh2_channel_read(channel, 0, (char *) buffer, 4);
        if(rc == LIBSSH2_ERROR_EAGAIN) {
            return rc;
        }
        else if(rc != 4) {
            return _libssh2_error(session, LIBSSH2_ERROR_PUBLICKEY_PROTOCOL,
                                  "Invalid response from publickey subsystem");
        }

        pkey->receive_packet_len = _libssh2_ntohu32(buffer);
        pkey->receive_packet =
            LIBSSH2_ALLOC(session, pkey->receive_packet_len);
        if(!pkey->receive_packet) {
            return _libssh2_error(session, LIBSSH2_ERROR_ALLOC,
                                  "Unable to allocate publickey response "
                                  "buffer");
        }

        pkey->receive_state = libssh2_NB_state_sent;
    }

    if(pkey->receive_state == libssh2_NB_state_sent) {
        rc = _libssh2_channel_read(channel, 0, (char *) pkey->receive_packet,
                                   pkey->receive_packet_len);
        if(rc == LIBSSH2_ERROR_EAGAIN) {
            return rc;
        }
        else if(rc != (int)pkey->receive_packet_len) {
            LIBSSH2_FREE(session, pkey->receive_packet);
            pkey->receive_packet = NULL;
            pkey->receive_state = libssh2_NB_state_idle;
            return _libssh2_error(session, LIBSSH2_ERROR_SOCKET_TIMEOUT,
                                  "Timeout waiting for publickey subsystem "
                                  "response packet");
        }

        *data = pkey->receive_packet;
        *data_len = pkey->receive_packet_len;
    }

    pkey->receive_state = libssh2_NB_state_idle;

    return 0;
}

/* publickey_response_id
 *
 * Translate a string response name to a numeric code
 * Data will be incremented by 4 + response_len on success only
 */
static int
publickey_response_id(unsigned char **pdata, size_t data_len)
{
    size_t response_len;
    unsigned char *data = *pdata;
    const LIBSSH2_PUBLICKEY_CODE_LIST *codes = publickey_response_codes;

    if(data_len < 4) {
        /* Malformed response */
        return -1;
    }
    response_len = _libssh2_ntohu32(data);
    data += 4;
    data_len -= 4;
    if(data_len < response_len) {
        /* Malformed response */
        return -1;
    }

    while(codes->name) {
        if((unsigned long)codes->name_len == response_len &&
            strncmp(codes->name, (char *) data, response_len) == 0) {
            *pdata = data + response_len;
            return codes->code;
        }
        codes++;
    }

    return -1;
}

/* publickey_response_success
 *
 * Generic helper routine to wait for success response and nothing else
 */
static int
publickey_response_success(LIBSSH2_PUBLICKEY * pkey)
{
    LIBSSH2_SESSION *session = pkey->channel->session;
    unsigned char *data, *s;
    size_t data_len;
    int response;

    while(1) {
        int rc = publickey_packet_receive(pkey, &data, &data_len);
        if(rc == LIBSSH2_ERROR_EAGAIN) {
            return rc;
        }
        else if(rc) {
            return _libssh2_error(session, LIBSSH2_ERROR_SOCKET_TIMEOUT,
                                  "Timeout waiting for response from "
                                  "publickey subsystem");
        }

        if(data_len < 4) {
            return _libssh2_error(session, LIBSSH2_ERROR_BUFFER_TOO_SMALL,
                                  "Publickey response too small");
        }

        s = data;
        response = publickey_response_id(&s, data_len);

        switch(response) {
        case LIBSSH2_PUBLICKEY_RESPONSE_STATUS:
            /* Error, or processing complete */
        {
            unsigned long status = 0;

            if(data_len < 8) {
                return _libssh2_error(session, LIBSSH2_ERROR_BUFFER_TOO_SMALL,
                                      "Publickey response too small");
            }

            status = _libssh2_ntohu32(s);

            LIBSSH2_FREE(session, data);

            if(status == LIBSSH2_PUBLICKEY_SUCCESS)
                return 0;

            publickey_status_error(pkey, session, status);
            return -1;
        }
        default:
            LIBSSH2_FREE(session, data);
            if(response < 0) {
                return _libssh2_error(session,
                                      LIBSSH2_ERROR_PUBLICKEY_PROTOCOL,
                                      "Invalid publickey subsystem response");
            }
            /* Unknown/Unexpected */
            _libssh2_error(session, LIBSSH2_ERROR_PUBLICKEY_PROTOCOL,
                           "Unexpected publickey subsystem response");
            data = NULL;
        }
    }
    /* never reached, but include `return` to silence compiler warnings */
    return -1;
}

/* *****************
 * Publickey API *
 ***************** */

/*
 * publickey_init
 *
 * Startup the publickey subsystem
 */
static LIBSSH2_PUBLICKEY *publickey_init(LIBSSH2_SESSION *session)
{
    int response;
    int rc;

    if(session->pkeyInit_state == libssh2_NB_state_idle) {
        session->pkeyInit_data = NULL;
        session->pkeyInit_pkey = NULL;
        session->pkeyInit_channel = NULL;

        _libssh2_debug(session, LIBSSH2_TRACE_PUBLICKEY,
                       "Initializing publickey subsystem");

        session->pkeyInit_state = libssh2_NB_state_allocated;
    }

    if(session->pkeyInit_state == libssh2_NB_state_allocated) {

        session->pkeyInit_channel =
            _libssh2_channel_open(session, "session",
                                  sizeof("session") - 1,
                                  LIBSSH2_CHANNEL_WINDOW_DEFAULT,
                                  LIBSSH2_CHANNEL_PACKET_DEFAULT, NULL,
                                  0);
        if(!session->pkeyInit_channel) {
            if(libssh2_session_last_errno(session) == LIBSSH2_ERROR_EAGAIN)
                /* The error state is already set, so leave it */
                return NULL;
            _libssh2_error(session, LIBSSH2_ERROR_CHANNEL_FAILURE,
                           "Unable to startup channel");
            goto err_exit;
        }

        session->pkeyInit_state = libssh2_NB_state_sent;
    }

    if(session->pkeyInit_state == libssh2_NB_state_sent) {
        rc = _libssh2_channel_process_startup(session->pkeyInit_channel,
                                              "subsystem",
                                              sizeof("subsystem") - 1,
                                              "publickey",
                                              sizeof("publickey") - 1);
        if(rc == LIBSSH2_ERROR_EAGAIN) {
            _libssh2_error(session, LIBSSH2_ERROR_EAGAIN,
                           "Would block starting publickey subsystem");
            return NULL;
        }
        else if(rc) {
            _libssh2_error(session, LIBSSH2_ERROR_CHANNEL_FAILURE,
                           "Unable to request publickey subsystem");
            goto err_exit;
        }

        session->pkeyInit_state = libssh2_NB_state_sent1;
    }

    if(session->pkeyInit_state == libssh2_NB_state_sent1) {
        unsigned char *s;
        rc = _libssh2_channel_extended_data(session->pkeyInit_channel,
                                         LIBSSH2_CHANNEL_EXTENDED_DATA_IGNORE);
        if(rc == LIBSSH2_ERROR_EAGAIN) {
            _libssh2_error(session, LIBSSH2_ERROR_EAGAIN,
                           "Would block starting publickey subsystem");
            return NULL;
        }

        session->pkeyInit_pkey =
            LIBSSH2_CALLOC(session, sizeof(LIBSSH2_PUBLICKEY));
        if(!session->pkeyInit_pkey) {
            _libssh2_error(session, LIBSSH2_ERROR_ALLOC,
                           "Unable to allocate a new publickey structure");
            goto err_exit;
        }
        session->pkeyInit_pkey->channel = session->pkeyInit_channel;
        session->pkeyInit_pkey->version = 0;

        s = session->pkeyInit_buffer;
        _libssh2_htonu32(s, 4 + (sizeof("version") - 1) + 4);
        s += 4;
        _libssh2_htonu32(s, sizeof("version") - 1);
        s += 4;
        memcpy(s, "version", sizeof("version") - 1);
        s += sizeof("version") - 1;
        _libssh2_htonu32(s, LIBSSH2_PUBLICKEY_VERSION);

        session->pkeyInit_buffer_sent = 0;

        _libssh2_debug(session, LIBSSH2_TRACE_PUBLICKEY,
                       "Sending publickey advertising version %d support",
                       (int) LIBSSH2_PUBLICKEY_VERSION);

        session->pkeyInit_state = libssh2_NB_state_sent2;
    }

    if(session->pkeyInit_state == libssh2_NB_state_sent2) {
        rc = _libssh2_channel_write(session->pkeyInit_channel, 0,
                                    session->pkeyInit_buffer,
                                    19 - session->pkeyInit_buffer_sent);
        if(rc == LIBSSH2_ERROR_EAGAIN) {
            _libssh2_error(session, LIBSSH2_ERROR_EAGAIN,
                           "Would block sending publickey version packet");
            return NULL;
        }
        else if(rc < 0) {
            _libssh2_error(session, rc,
                           "Unable to send publickey version packet");
            goto err_exit;
        }
        session->pkeyInit_buffer_sent += rc;
        if(session->pkeyInit_buffer_sent < 19) {
            _libssh2_error(session, LIBSSH2_ERROR_EAGAIN,
                           "Need to be called again to complete this");
            return NULL;
        }

        session->pkeyInit_state = libssh2_NB_state_sent3;
    }

    if(session->pkeyInit_state == libssh2_NB_state_sent3) {
        while(1) {
            unsigned char *s;
            rc = publickey_packet_receive(session->pkeyInit_pkey,
                                          &session->pkeyInit_data,
                                          &session->pkeyInit_data_len);
            if(rc == LIBSSH2_ERROR_EAGAIN) {
                _libssh2_error(session, LIBSSH2_ERROR_EAGAIN,
                               "Would block waiting for response from "
                               "publickey subsystem");
                return NULL;
            }
            else if(rc) {
                _libssh2_error(session, LIBSSH2_ERROR_SOCKET_TIMEOUT,
                               "Timeout waiting for response from "
                               "publickey subsystem");
                goto err_exit;
            }

            s = session->pkeyInit_data;
            if((response =
                 publickey_response_id(&s, session->pkeyInit_data_len)) < 0) {
                _libssh2_error(session, LIBSSH2_ERROR_PUBLICKEY_PROTOCOL,
                               "Invalid publickey subsystem response code");
                goto err_exit;
            }

            if(session->pkeyInit_data_len < 4) {
                _libssh2_error(session, LIBSSH2_ERROR_BUFFER_TOO_SMALL,
                               "Public key init data too small");
                goto err_exit;
            }

            switch(response) {
            case LIBSSH2_PUBLICKEY_RESPONSE_STATUS:
                /* Error */
            {
                unsigned long status, descr_len, lang_len;

                if(session->pkeyInit_data_len >= 8) {
                    status = _libssh2_ntohu32(s);
                    s += 4;
                    descr_len = _libssh2_ntohu32(s);
                    s += 4;
                }
                else {
                    _libssh2_error(session, LIBSSH2_ERROR_BUFFER_TOO_SMALL,
                                   "Public key init data too small");
                    goto err_exit;
                }

                if(s + descr_len + 4 <=
                   session->pkeyInit_data + session->pkeyInit_data_len) {
                    /* description starts here */
                    s += descr_len;
                    lang_len = _libssh2_ntohu32(s);
                    s += 4;
                }
                else {
                    _libssh2_error(session, LIBSSH2_ERROR_BUFFER_TOO_SMALL,
                                   "Public key init data too small");
                    goto err_exit;
                }

                if(s + lang_len <=
                   session->pkeyInit_data + session->pkeyInit_data_len) {
                    /* lang starts here */
                    s += lang_len;
                }
                else {
                    _libssh2_error(session, LIBSSH2_ERROR_BUFFER_TOO_SMALL,
                                   "Public key init data too small");
                    goto err_exit;
                }

                if(s >
                    session->pkeyInit_data + session->pkeyInit_data_len) {
                    _libssh2_error(session,
                                   LIBSSH2_ERROR_PUBLICKEY_PROTOCOL,
                                   "Malformed publickey subsystem packet");
                    goto err_exit;
                }

                publickey_status_error(NULL, session, status);

                goto err_exit;
            }

            case LIBSSH2_PUBLICKEY_RESPONSE_VERSION:
                /* What we want */
                session->pkeyInit_pkey->version = _libssh2_ntohu32(s);
                if(session->pkeyInit_pkey->version >
                    LIBSSH2_PUBLICKEY_VERSION) {
                    _libssh2_debug(session, LIBSSH2_TRACE_PUBLICKEY,
                                   "Truncate remote publickey version "
                                   "from %lu",
                                   session->pkeyInit_pkey->version);
                    session->pkeyInit_pkey->version =
                        LIBSSH2_PUBLICKEY_VERSION;
                }
                _libssh2_debug(session, LIBSSH2_TRACE_PUBLICKEY,
                               "Enabling publickey subsystem version %lu",
                               session->pkeyInit_pkey->version);
                LIBSSH2_FREE(session, session->pkeyInit_data);
                session->pkeyInit_data = NULL;
                session->pkeyInit_state = libssh2_NB_state_idle;
                return session->pkeyInit_pkey;

            default:
                /* Unknown/Unexpected */
                _libssh2_error(session, LIBSSH2_ERROR_PUBLICKEY_PROTOCOL,
                               "Unexpected publickey subsystem response, "
                               "ignoring");
                LIBSSH2_FREE(session, session->pkeyInit_data);
                session->pkeyInit_data = NULL;
            }
        }
    }

    /* Never reached except by direct goto */
  err_exit:
    session->pkeyInit_state = libssh2_NB_state_sent4;
    if(session->pkeyInit_channel) {
        rc = _libssh2_channel_close(session->pkeyInit_channel);
        if(rc == LIBSSH2_ERROR_EAGAIN) {
            _libssh2_error(session, LIBSSH2_ERROR_EAGAIN,
                           "Would block closing channel");
            return NULL;
        }
    }
    if(session->pkeyInit_pkey) {
        LIBSSH2_FREE(session, session->pkeyInit_pkey);
        session->pkeyInit_pkey = NULL;
    }
    if(session->pkeyInit_data) {
        LIBSSH2_FREE(session, session->pkeyInit_data);
        session->pkeyInit_data = NULL;
    }
    session->pkeyInit_state = libssh2_NB_state_idle;
    return NULL;
}

/*
 * libssh2_publickey_init
 *
 * Startup the publickey subsystem
 */
LIBSSH2_API LIBSSH2_PUBLICKEY *
libssh2_publickey_init(LIBSSH2_SESSION *session)
{
    LIBSSH2_PUBLICKEY *ptr;

    BLOCK_ADJUST_ERRNO(ptr, session,
                       publickey_init(session));
    return ptr;
}



/*
 * libssh2_publickey_add_ex
 *
 * Add a new public key entry
 */
LIBSSH2_API int
libssh2_publickey_add_ex(LIBSSH2_PUBLICKEY *pkey, const unsigned char *name,
                         unsigned long name_len, const unsigned char *blob,
                         unsigned long blob_len, char overwrite,
                         unsigned long num_attrs,
                         const libssh2_publickey_attribute attrs[])
{
    LIBSSH2_CHANNEL *channel;
    LIBSSH2_SESSION *session;
    /*  19 = packet_len(4) + add_len(4) + "add"(3) + name_len(4) + {name}
        blob_len(4) + {blob} */
    unsigned long i, packet_len = 19 + name_len + blob_len;
    unsigned char *comment = NULL;
    unsigned long comment_len = 0;
    int rc;

    if(!pkey)
        return LIBSSH2_ERROR_BAD_USE;

    channel = pkey->channel;
    session = channel->session;

    if(pkey->add_state == libssh2_NB_state_idle) {
        pkey->add_packet = NULL;

        _libssh2_debug(session, LIBSSH2_TRACE_PUBLICKEY, "Adding %s publickey",
                       name);

        if(pkey->version == 1) {
            for(i = 0; i < num_attrs; i++) {
                /* Search for a comment attribute */
                if(attrs[i].name_len == (sizeof("comment") - 1) &&
                    strncmp(attrs[i].name, "comment",
                            sizeof("comment") - 1) == 0) {
                    comment = (unsigned char *) attrs[i].value;
                    comment_len = attrs[i].value_len;
                    break;
                }
            }
            packet_len += 4 + comment_len;
        }
        else {
            packet_len += 5;    /* overwrite(1) + attribute_count(4) */
            for(i = 0; i < num_attrs; i++) {
                packet_len += 9 + attrs[i].name_len + attrs[i].value_len;
                /* name_len(4) + value_len(4) + mandatory(1) */
            }
        }

        pkey->add_packet = LIBSSH2_ALLOC(session, packet_len);
        if(!pkey->add_packet) {
            return _libssh2_error(session, LIBSSH2_ERROR_ALLOC,
                                  "Unable to allocate memory for "
                                  "publickey \"add\" packet");
        }

        pkey->add_s = pkey->add_packet;
        _libssh2_htonu32(pkey->add_s, packet_len - 4);
        pkey->add_s += 4;
        _libssh2_htonu32(pkey->add_s, sizeof("add") - 1);
        pkey->add_s += 4;
        memcpy(pkey->add_s, "add", sizeof("add") - 1);
        pkey->add_s += sizeof("add") - 1;
        if(pkey->version == 1) {
            _libssh2_htonu32(pkey->add_s, comment_len);
            pkey->add_s += 4;
            if(comment) {
                memcpy(pkey->add_s, comment, comment_len);
                pkey->add_s += comment_len;
            }

            _libssh2_htonu32(pkey->add_s, name_len);
            pkey->add_s += 4;
            memcpy(pkey->add_s, name, name_len);
            pkey->add_s += name_len;
            _libssh2_htonu32(pkey->add_s, blob_len);
            pkey->add_s += 4;
            memcpy(pkey->add_s, blob, blob_len);
            pkey->add_s += blob_len;
        }
        else {
            /* Version == 2 */

            _libssh2_htonu32(pkey->add_s, name_len);
            pkey->add_s += 4;
            memcpy(pkey->add_s, name, name_len);
            pkey->add_s += name_len;
            _libssh2_htonu32(pkey->add_s, blob_len);
            pkey->add_s += 4;
            memcpy(pkey->add_s, blob, blob_len);
            pkey->add_s += blob_len;
            *(pkey->add_s++) = overwrite ? 0x01 : 0;
            _libssh2_htonu32(pkey->add_s, num_attrs);
            pkey->add_s += 4;
            for(i = 0; i < num_attrs; i++) {
                _libssh2_htonu32(pkey->add_s, attrs[i].name_len);
                pkey->add_s += 4;
                memcpy(pkey->add_s, attrs[i].name, attrs[i].name_len);
                pkey->add_s += attrs[i].name_len;
                _libssh2_htonu32(pkey->add_s, attrs[i].value_len);
                pkey->add_s += 4;
                memcpy(pkey->add_s, attrs[i].value, attrs[i].value_len);
                pkey->add_s += attrs[i].value_len;
                *(pkey->add_s++) = attrs[i].mandatory ? 0x01 : 0;
            }
        }

        _libssh2_debug(session, LIBSSH2_TRACE_PUBLICKEY,
                       "Sending publickey \"add\" packet: "
                       "type=%s blob_len=%ld num_attrs=%ld",
                       name, blob_len, num_attrs);

        pkey->add_state = libssh2_NB_state_created;
    }

    if(pkey->add_state == libssh2_NB_state_created) {
        rc = _libssh2_channel_write(channel, 0, pkey->add_packet,
                                    (pkey->add_s - pkey->add_packet));
        if(rc == LIBSSH2_ERROR_EAGAIN) {
            return rc;
        }
        else if((pkey->add_s - pkey->add_packet) != rc) {
            LIBSSH2_FREE(session, pkey->add_packet);
            pkey->add_packet = NULL;
            return _libssh2_error(session, LIBSSH2_ERROR_SOCKET_SEND,
                                  "Unable to send publickey add packet");
        }
        LIBSSH2_FREE(session, pkey->add_packet);
        pkey->add_packet = NULL;

        pkey->add_state = libssh2_NB_state_sent;
    }

    rc = publickey_response_success(pkey);
    if(rc == LIBSSH2_ERROR_EAGAIN) {
        return rc;
    }

    pkey->add_state = libssh2_NB_state_idle;

    return rc;
}

/* libssh2_publickey_remove_ex
 * Remove an existing publickey so that authentication can no longer be
 * performed using it
 */
LIBSSH2_API int
libssh2_publickey_remove_ex(LIBSSH2_PUBLICKEY * pkey,
                            const unsigned char *name, unsigned long name_len,
                            const unsigned char *blob, unsigned long blob_len)
{
    LIBSSH2_CHANNEL *channel;
    LIBSSH2_SESSION *session;
    /* 22 = packet_len(4) + remove_len(4) + "remove"(6) + name_len(4) + {name}
       + blob_len(4) + {blob} */
    unsigned long packet_len = 22 + name_len + blob_len;
    int rc;

    if(!pkey)
        return LIBSSH2_ERROR_BAD_USE;

    channel = pkey->channel;
    session = channel->session;

    if(pkey->remove_state == libssh2_NB_state_idle) {
        pkey->remove_packet = NULL;

        pkey->remove_packet = LIBSSH2_ALLOC(session, packet_len);
        if(!pkey->remove_packet) {
            return _libssh2_error(session, LIBSSH2_ERROR_ALLOC,
                                  "Unable to allocate memory for "
                                  "publickey \"remove\" packet");
        }

        pkey->remove_s = pkey->remove_packet;
        _libssh2_htonu32(pkey->remove_s, packet_len - 4);
        pkey->remove_s += 4;
        _libssh2_htonu32(pkey->remove_s, sizeof("remove") - 1);
        pkey->remove_s += 4;
        memcpy(pkey->remove_s, "remove", sizeof("remove") - 1);
        pkey->remove_s += sizeof("remove") - 1;
        _libssh2_htonu32(pkey->remove_s, name_len);
        pkey->remove_s += 4;
        memcpy(pkey->remove_s, name, name_len);
        pkey->remove_s += name_len;
        _libssh2_htonu32(pkey->remove_s, blob_len);
        pkey->remove_s += 4;
        memcpy(pkey->remove_s, blob, blob_len);
        pkey->remove_s += blob_len;

        _libssh2_debug(session, LIBSSH2_TRACE_PUBLICKEY,
                       "Sending publickey \"remove\" packet: "
                       "type=%s blob_len=%ld",
                       name, blob_len);

        pkey->remove_state = libssh2_NB_state_created;
    }

    if(pkey->remove_state == libssh2_NB_state_created) {
        rc = _libssh2_channel_write(channel, 0, pkey->remove_packet,
                                    (pkey->remove_s - pkey->remove_packet));
        if(rc == LIBSSH2_ERROR_EAGAIN) {
            return rc;
        }
        else if((pkey->remove_s - pkey->remove_packet) != rc) {
            LIBSSH2_FREE(session, pkey->remove_packet);
            pkey->remove_packet = NULL;
            pkey->remove_state = libssh2_NB_state_idle;
            return _libssh2_error(session, LIBSSH2_ERROR_SOCKET_SEND,
                                  "Unable to send publickey remove packet");
        }
        LIBSSH2_FREE(session, pkey->remove_packet);
        pkey->remove_packet = NULL;

        pkey->remove_state = libssh2_NB_state_sent;
    }

    rc = publickey_response_success(pkey);
    if(rc == LIBSSH2_ERROR_EAGAIN) {
        return rc;
    }

    pkey->remove_state = libssh2_NB_state_idle;

    return rc;
}

/* libssh2_publickey_list_fetch
 * Fetch a list of supported public key from a server
 */
LIBSSH2_API int
libssh2_publickey_list_fetch(LIBSSH2_PUBLICKEY * pkey, unsigned long *num_keys,
                             libssh2_publickey_list ** pkey_list)
{
    LIBSSH2_CHANNEL *channel;
    LIBSSH2_SESSION *session;
    libssh2_publickey_list *list = NULL;
    unsigned long buffer_len = 12, keys = 0, max_keys = 0, i;
    /* 12 = packet_len(4) + list_len(4) + "list"(4) */
    int response;
    int rc;

    if(!pkey)
        return LIBSSH2_ERROR_BAD_USE;

    channel = pkey->channel;
    session = channel->session;

    if(pkey->listFetch_state == libssh2_NB_state_idle) {
        pkey->listFetch_data = NULL;

        pkey->listFetch_s = pkey->listFetch_buffer;
        _libssh2_htonu32(pkey->listFetch_s, buffer_len - 4);
        pkey->listFetch_s += 4;
        _libssh2_htonu32(pkey->listFetch_s, sizeof("list") - 1);
        pkey->listFetch_s += 4;
        memcpy(pkey->listFetch_s, "list", sizeof("list") - 1);
        pkey->listFetch_s += sizeof("list") - 1;

        _libssh2_debug(session, LIBSSH2_TRACE_PUBLICKEY,
                       "Sending publickey \"list\" packet");

        pkey->listFetch_state = libssh2_NB_state_created;
    }

    if(pkey->listFetch_state == libssh2_NB_state_created) {
        rc = _libssh2_channel_write(channel, 0,
                                    pkey->listFetch_buffer,
                                    (pkey->listFetch_s -
                                     pkey->listFetch_buffer));
        if(rc == LIBSSH2_ERROR_EAGAIN) {
            return rc;
        }
        else if((pkey->listFetch_s - pkey->listFetch_buffer) != rc) {
            pkey->listFetch_state = libssh2_NB_state_idle;
            return _libssh2_error(session, LIBSSH2_ERROR_SOCKET_SEND,
                                  "Unable to send publickey list packet");
        }

        pkey->listFetch_state = libssh2_NB_state_sent;
    }

    while(1) {
        rc = publickey_packet_receive(pkey, &pkey->listFetch_data,
                                      &pkey->listFetch_data_len);
        if(rc == LIBSSH2_ERROR_EAGAIN) {
            return rc;
        }
        else if(rc) {
            _libssh2_error(session, LIBSSH2_ERROR_SOCKET_TIMEOUT,
                           "Timeout waiting for response from "
                           "publickey subsystem");
            goto err_exit;
        }

        pkey->listFetch_s = pkey->listFetch_data;
        if((response =
             publickey_response_id(&pkey->listFetch_s,
                                   pkey->listFetch_data_len)) < 0) {
            _libssh2_error(session, LIBSSH2_ERROR_PUBLICKEY_PROTOCOL,
                           "Invalid publickey subsystem response code");
            goto err_exit;
        }

        switch(response) {
        case LIBSSH2_PUBLICKEY_RESPONSE_STATUS:
            /* Error, or processing complete */
        {
            unsigned long status, descr_len, lang_len;

            if(pkey->listFetch_s + 8 <=
               pkey->listFetch_data + pkey->listFetch_data_len) {
                status = _libssh2_ntohu32(pkey->listFetch_s);
                pkey->listFetch_s += 4;
                descr_len = _libssh2_ntohu32(pkey->listFetch_s);
                pkey->listFetch_s += 4;
            }
            else {
                _libssh2_error(session, LIBSSH2_ERROR_BUFFER_TOO_SMALL,
                               "ListFetch data too short");
                goto err_exit;
            }

            if(pkey->listFetch_s + descr_len + 4 <=
               pkey->listFetch_data + pkey->listFetch_data_len) {
                /* description starts at pkey->listFetch_s */
                pkey->listFetch_s += descr_len;
                lang_len = _libssh2_ntohu32(pkey->listFetch_s);
                pkey->listFetch_s += 4;
            }
            else {
                _libssh2_error(session, LIBSSH2_ERROR_BUFFER_TOO_SMALL,
                               "ListFetch data too short");
                goto err_exit;
            }

            if(pkey->listFetch_s + lang_len <=
               pkey->listFetch_data + pkey->listFetch_data_len) {
                /* lang starts at pkey->listFetch_s */
                pkey->listFetch_s += lang_len;
            }
            else {
                _libssh2_error(session, LIBSSH2_ERROR_BUFFER_TOO_SMALL,
                               "ListFetch data too short");
                goto err_exit;
            }

            if(pkey->listFetch_s >
                pkey->listFetch_data + pkey->listFetch_data_len) {
                _libssh2_error(session, LIBSSH2_ERROR_PUBLICKEY_PROTOCOL,
                               "Malformed publickey subsystem packet");
                goto err_exit;
            }

            if(status == LIBSSH2_PUBLICKEY_SUCCESS) {
                LIBSSH2_FREE(session, pkey->listFetch_data);
                pkey->listFetch_data = NULL;
                *pkey_list = list;
                *num_keys = keys;
                pkey->listFetch_state = libssh2_NB_state_idle;
                return 0;
            }

            publickey_status_error(pkey, session, status);
            goto err_exit;
        }
        case LIBSSH2_PUBLICKEY_RESPONSE_PUBLICKEY:
            /* What we want */
            if(keys >= max_keys) {
                libssh2_publickey_list *newlist;
                /* Grow the key list if necessary */
                max_keys += 8;
                newlist =
                    LIBSSH2_REALLOC(session, list,
                                    (max_keys +
                                     1) * sizeof(libssh2_publickey_list));
                if(!newlist) {
                    _libssh2_error(session, LIBSSH2_ERROR_ALLOC,
                                   "Unable to allocate memory for "
                                   "publickey list");
                    goto err_exit;
                }
                list = newlist;
            }
            if(pkey->version == 1) {
                unsigned long comment_len;

                if(pkey->listFetch_s + 4 <=
                   pkey->listFetch_data + pkey->listFetch_data_len) {
                    comment_len = _libssh2_ntohu32(pkey->listFetch_s);
                    pkey->listFetch_s += 4;
                }
                else {
                    _libssh2_error(session, LIBSSH2_ERROR_BUFFER_TOO_SMALL,
                                   "ListFetch data too short");
                    goto err_exit;
                }

                if(comment_len) {
                    list[keys].num_attrs = 1;
                    list[keys].attrs =
                        LIBSSH2_ALLOC(session,
                                      sizeof(libssh2_publickey_attribute));
                    if(!list[keys].attrs) {
                        _libssh2_error(session, LIBSSH2_ERROR_ALLOC,
                                       "Unable to allocate memory for "
                                       "publickey attributes");
                        goto err_exit;
                    }
                    list[keys].attrs[0].name = "comment";
                    list[keys].attrs[0].name_len = sizeof("comment") - 1;
                    list[keys].attrs[0].value = (char *) pkey->listFetch_s;
                    list[keys].attrs[0].value_len = comment_len;
                    list[keys].attrs[0].mandatory = 0;

                    pkey->listFetch_s += comment_len;
                }
                else {
                    list[keys].num_attrs = 0;
                    list[keys].attrs = NULL;
                }

                if(pkey->listFetch_s + 4 <=
                    pkey->listFetch_data + pkey->listFetch_data_len) {
                    list[keys].name_len = _libssh2_ntohu32(pkey->listFetch_s);
                    pkey->listFetch_s += 4;
                }
                else {
                    _libssh2_error(session, LIBSSH2_ERROR_BUFFER_TOO_SMALL,
                                   "ListFetch data too short");
                    goto err_exit;
                }

                if(pkey->listFetch_s + list[keys].name_len <=
                   pkey->listFetch_data + pkey->listFetch_data_len) {
                    list[keys].name = pkey->listFetch_s;
                    pkey->listFetch_s += list[keys].name_len;
                }
                else {
                    _libssh2_error(session, LIBSSH2_ERROR_BUFFER_TOO_SMALL,
                                   "ListFetch data too short");
                    goto err_exit;
                }

                if(pkey->listFetch_s + 4 <=
                   pkey->listFetch_data + pkey->listFetch_data_len) {
                    list[keys].blob_len = _libssh2_ntohu32(pkey->listFetch_s);
                    pkey->listFetch_s += 4;
                }
                else {
                    _libssh2_error(session, LIBSSH2_ERROR_BUFFER_TOO_SMALL,
                                   "ListFetch data too short");
                    goto err_exit;
                }

                if(pkey->listFetch_s + list[keys].blob_len <=
                   pkey->listFetch_data + pkey->listFetch_data_len) {
                    list[keys].blob = pkey->listFetch_s;
                    pkey->listFetch_s += list[keys].blob_len;
                }
                else {
                    _libssh2_error(session, LIBSSH2_ERROR_BUFFER_TOO_SMALL,
                                   "ListFetch data too short");
                    goto err_exit;
                }
            }
            else {
                /* Version == 2 */

                if(pkey->listFetch_s + 4 <=
                   pkey->listFetch_data + pkey->listFetch_data_len) {
                    list[keys].name_len = _libssh2_ntohu32(pkey->listFetch_s);
                    pkey->listFetch_s += 4;
                }
                else {
                    _libssh2_error(session, LIBSSH2_ERROR_BUFFER_TOO_SMALL,
                                   "ListFetch data too short");
                    goto err_exit;
                }

                if(pkey->listFetch_s + list[keys].name_len <=
                   pkey->listFetch_data + pkey->listFetch_data_len) {
                    list[keys].name = pkey->listFetch_s;
                    pkey->listFetch_s += list[keys].name_len;
                }
                else {
                    _libssh2_error(session, LIBSSH2_ERROR_BUFFER_TOO_SMALL,
                                   "ListFetch data too short");
                    goto err_exit;
                }

                if(pkey->listFetch_s + 4 <=
                   pkey->listFetch_data + pkey->listFetch_data_len) {
                    list[keys].blob_len = _libssh2_ntohu32(pkey->listFetch_s);
                    pkey->listFetch_s += 4;
                }
                else {
                    _libssh2_error(session, LIBSSH2_ERROR_BUFFER_TOO_SMALL,
                                   "ListFetch data too short");
                    goto err_exit;
                }

                if(pkey->listFetch_s + list[keys].blob_len <=
                   pkey->listFetch_data + pkey->listFetch_data_len) {
                    list[keys].blob = pkey->listFetch_s;
                    pkey->listFetch_s += list[keys].blob_len;
                }
                else {
                    _libssh2_error(session, LIBSSH2_ERROR_BUFFER_TOO_SMALL,
                                   "ListFetch data too short");
                    goto err_exit;
                }

                if(pkey->listFetch_s + 4 <=
                   pkey->listFetch_data + pkey->listFetch_data_len) {
                    list[keys].num_attrs = _libssh2_ntohu32(pkey->listFetch_s);
                    pkey->listFetch_s += 4;
                }
                else {
                    _libssh2_error(session, LIBSSH2_ERROR_BUFFER_TOO_SMALL,
                                   "ListFetch data too short");
                    goto err_exit;
                }

                if(list[keys].num_attrs) {
                    list[keys].attrs =
                        LIBSSH2_ALLOC(session,
                                      list[keys].num_attrs *
                                      sizeof(libssh2_publickey_attribute));
                    if(!list[keys].attrs) {
                        _libssh2_error(session, LIBSSH2_ERROR_ALLOC,
                                       "Unable to allocate memory for "
                                       "publickey attributes");
                        goto err_exit;
                    }
                    for(i = 0; i < list[keys].num_attrs; i++) {
                        if(pkey->listFetch_s + 4 <=
                           pkey->listFetch_data + pkey->listFetch_data_len) {
                            list[keys].attrs[i].name_len =
                                _libssh2_ntohu32(pkey->listFetch_s);
                            pkey->listFetch_s += 4;
                        }
                        else {
                            _libssh2_error(session,
                                           LIBSSH2_ERROR_BUFFER_TOO_SMALL,
                                           "ListFetch data too short");
                            goto err_exit;
                        }

                        if(pkey->listFetch_s + list[keys].attrs[i].name_len <=
                           pkey->listFetch_data + pkey->listFetch_data_len) {
                            list[keys].attrs[i].name =
                                (char *) pkey->listFetch_s;
                            pkey->listFetch_s += list[keys].attrs[i].name_len;
                        }
                        else {
                            _libssh2_error(session,
                                           LIBSSH2_ERROR_BUFFER_TOO_SMALL,
                                           "ListFetch data too short");
                            goto err_exit;
                        }

                        if(pkey->listFetch_s + 4 <=
                           pkey->listFetch_data + pkey->listFetch_data_len) {
                            list[keys].attrs[i].value_len =
                                _libssh2_ntohu32(pkey->listFetch_s);
                            pkey->listFetch_s += 4;
                        }
                        else {
                            _libssh2_error(session,
                                           LIBSSH2_ERROR_BUFFER_TOO_SMALL,
                                           "ListFetch data too short");
                            goto err_exit;
                        }

                        if(pkey->listFetch_s +
                           list[keys].attrs[i].value_len <=
                           pkey->listFetch_data + pkey->listFetch_data_len) {
                            list[keys].attrs[i].value =
                                (char *) pkey->listFetch_s;
                            pkey->listFetch_s += list[keys].attrs[i].value_len;
                        }
                        else {
                            _libssh2_error(session,
                                           LIBSSH2_ERROR_BUFFER_TOO_SMALL,
                                           "ListFetch data too short");
                            goto err_exit;
                        }

                        /* actually an ignored value */
                        list[keys].attrs[i].mandatory = 0;
                    }
                }
                else {
                    list[keys].attrs = NULL;
                }
            }
            /* To be FREEd in libssh2_publickey_list_free() */
            list[keys].packet = pkey->listFetch_data;
            keys++;

            list[keys].packet = NULL;   /* Terminate the list */
            pkey->listFetch_data = NULL;
            break;
        default:
            /* Unknown/Unexpected */
            _libssh2_error(session, LIBSSH2_ERROR_PUBLICKEY_PROTOCOL,
                           "Unexpected publickey subsystem response");
            LIBSSH2_FREE(session, pkey->listFetch_data);
            pkey->listFetch_data = NULL;
        }
    }

    /* Only reached via explicit goto */
  err_exit:
    if(pkey->listFetch_data) {
        LIBSSH2_FREE(session, pkey->listFetch_data);
        pkey->listFetch_data = NULL;
    }
    if(list) {
        libssh2_publickey_list_free(pkey, list);
    }
    pkey->listFetch_state = libssh2_NB_state_idle;
    return -1;
}

/* libssh2_publickey_list_free
 * Free a previously fetched list of public keys
 */
LIBSSH2_API void
libssh2_publickey_list_free(LIBSSH2_PUBLICKEY * pkey,
                            libssh2_publickey_list * pkey_list)
{
    LIBSSH2_SESSION *session;
    libssh2_publickey_list *p = pkey_list;

    if(!pkey || !p)
        return;

    session = pkey->channel->session;

    while(p->packet) {
        if(p->attrs) {
            LIBSSH2_FREE(session, p->attrs);
        }
        LIBSSH2_FREE(session, p->packet);
        p++;
    }

    LIBSSH2_FREE(session, pkey_list);
}

/* libssh2_publickey_shutdown
 * Shutdown the publickey subsystem
 */
LIBSSH2_API int
libssh2_publickey_shutdown(LIBSSH2_PUBLICKEY *pkey)
{
    LIBSSH2_SESSION *session;
    int rc;

    if(!pkey)
        return LIBSSH2_ERROR_BAD_USE;

    session = pkey->channel->session;

    /*
     * Make sure all memory used in the state variables are free
     */
    if(pkey->receive_packet) {
        LIBSSH2_FREE(session, pkey->receive_packet);
        pkey->receive_packet = NULL;
    }
    if(pkey->add_packet) {
        LIBSSH2_FREE(session, pkey->add_packet);
        pkey->add_packet = NULL;
    }
    if(pkey->remove_packet) {
        LIBSSH2_FREE(session, pkey->remove_packet);
        pkey->remove_packet = NULL;
    }
    if(pkey->listFetch_data) {
        LIBSSH2_FREE(session, pkey->listFetch_data);
        pkey->listFetch_data = NULL;
    }

    rc = _libssh2_channel_free(pkey->channel);
    if(rc == LIBSSH2_ERROR_EAGAIN)
        return rc;

    LIBSSH2_FREE(session, pkey);
    return 0;
}
