/*
 * pageant.c: cross-platform code to implement Pageant.
 */

#include <stddef.h>
#include <stdlib.h>
#include <assert.h>

#include "putty.h"
#include "mpint.h"
#include "ssh.h"
#include "sshcr.h"
#include "pageant.h"

/*
 * We need this to link with the RSA code, because rsa_ssh1_encrypt()
 * pads its data with random bytes. Since we only use rsa_ssh1_decrypt()
 * and the signing functions, which are deterministic, this should
 * never be called.
 *
 * If it _is_ called, there is a _serious_ problem, because it
 * won't generate true random numbers. So we must scream, panic,
 * and exit immediately if that should happen.
 */
void random_read(void *buf, size_t size)
{
    modalfatalbox("Internal error: attempt to use random numbers in Pageant");
}

static bool pageant_local = false;

struct PageantClientDialogId {
    int dummy;
};

typedef struct PageantPrivateKeySort PageantPrivateKeySort;
typedef struct PageantPublicKeySort PageantPublicKeySort;
typedef struct PageantPrivateKey PageantPrivateKey;
typedef struct PageantPublicKey PageantPublicKey;
typedef struct PageantAsyncOp PageantAsyncOp;
typedef struct PageantAsyncOpVtable PageantAsyncOpVtable;
typedef struct PageantClientRequestNode PageantClientRequestNode;
typedef struct PageantKeyRequestNode PageantKeyRequestNode;

struct PageantClientRequestNode {
    PageantClientRequestNode *prev, *next;
};
struct PageantKeyRequestNode {
    PageantKeyRequestNode *prev, *next;
};

struct PageantClientInfo {
    PageantClient *pc; /* goes to NULL when client is unregistered */
    PageantClientRequestNode head;
};

struct PageantAsyncOp {
    const PageantAsyncOpVtable *vt;
    PageantClientInfo *info;
    PageantClientRequestNode cr;
    PageantClientRequestId *reqid;
};
struct PageantAsyncOpVtable {
    void (*coroutine)(PageantAsyncOp *pao);
    void (*free)(PageantAsyncOp *pao);
};
static inline void pageant_async_op_coroutine(PageantAsyncOp *pao)
{ pao->vt->coroutine(pao); }
static inline void pageant_async_op_free(PageantAsyncOp *pao)
{
    delete_callbacks_for_context(pao);
    pao->vt->free(pao);
}
static inline void pageant_async_op_unlink(PageantAsyncOp *pao)
{
    pao->cr.prev->next = pao->cr.next;
    pao->cr.next->prev = pao->cr.prev;
}
static inline void pageant_async_op_unlink_and_free(PageantAsyncOp *pao)
{
    pageant_async_op_unlink(pao);
    pageant_async_op_free(pao);
}
static void pageant_async_op_callback(void *vctx)
{
    pageant_async_op_coroutine((PageantAsyncOp *)vctx);
}

/*
 * Master lists of all the keys we have stored, in any form at all.
 *
 * We store private and public keys in separate lists, because
 * multiple public keys can share the same private key (due to one
 * having a certificate and the other not, or having more than one
 * different certificate). And when we decrypt or re-encrypt a private
 * key, we don't really want to faff about doing it multiple times if
 * there's more than one public key it goes with. If someone tries to
 * re-encrypt a key to make their machine safer against unattended
 * access, then it would be embarrassing to find they'd forgotten to
 * re-encrypt the _other_ copy of it; conversely, once you've
 * decrypted a key, it's pointless to make someone type yet another
 * passphrase.
 *
 * (Causing multiple keys to become decrypted in one go isn't a
 * security hole in its own right, because the signatures generated by
 * certified and uncertified keys are identical. So an attacker
 * gaining access to an agent containing one encrypted and one
 * cleartext key with the same private half would still be *able* to
 * generate signatures that went with the encrypted one, even if the
 * agent refused to hand them out in response to the most obvious kind
 * of request.)
 */
struct PageantPrivateKeySort {
    /*
     * Information used by the sorting criterion for the private key
     * tree.
     */
    int ssh_version; /* 1 or 2; primary sort key */
    ptrlen base_pub; /* secondary sort key; never includes a certificate */
};
static int privkey_cmpfn(void *av, void *bv)
{
    PageantPrivateKeySort *a = (PageantPrivateKeySort *)av;
    PageantPrivateKeySort *b = (PageantPrivateKeySort *)bv;

    if (a->ssh_version != b->ssh_version)
        return a->ssh_version < b->ssh_version ? -1 : +1;
    else
        return ptrlen_strcmp(a->base_pub, b->base_pub);
}

struct PageantPublicKeySort {
    /*
     * Information used by the sorting criterion for the public key
     * tree. Begins with the private key sorting criterion, so that
     * all the public keys sharing a private key appear adjacent in
     * the tree. That's a reasonably sensible order to list them in
     * for the user, and more importantly, it makes it easy to
     * discover when we're deleting the last public key that goes with
     * a particular private one, so as to delete that too. Easier than
     * messing about with fragile reference counts.
     */
    PageantPrivateKeySort priv;
    ptrlen full_pub;    /* may match priv.base_pub, or may include a cert */
};
static int pubkey_cmpfn(void *av, void *bv)
{
    PageantPublicKeySort *a = (PageantPublicKeySort *)av;
    PageantPublicKeySort *b = (PageantPublicKeySort *)bv;

    int c = privkey_cmpfn(&a->priv, &b->priv);
    if (c)
        return c;
    else
        return ptrlen_strcmp(a->full_pub, b->full_pub);
}

struct PageantPrivateKey {
    PageantPrivateKeySort sort;
    strbuf *base_pub;              /* the true owner of sort.base_pub */
    union {
        RSAKey *rkey;              /* if sort.priv.ssh_version == 1 */
        ssh_key *skey;             /* if sort.priv.ssh_version == 2 */
    };
    strbuf *encrypted_key_file;
    /* encrypted_key_comment stores the comment belonging to the
     * encrypted key file. This is used when presenting deferred
     * decryption prompts, because if the user had encrypted their
     * uncert and cert keys with different passphrases, the passphrase
     * prompt must reliably signal which file they're supposed to be
     * entering the passphrase for. */
    char *encrypted_key_comment;
    bool decryption_prompt_active;
    PageantKeyRequestNode blocked_requests;
    PageantClientDialogId dlgid;
};
static tree234 *privkeytree;

struct PageantPublicKey {
    PageantPublicKeySort sort;
    strbuf *base_pub;            /* the true owner of sort.priv.base_pub */
    strbuf *full_pub;            /* the true owner of sort.full_pub */
    char *comment;
};
static tree234 *pubkeytree;

typedef struct PageantSignOp PageantSignOp;
struct PageantSignOp {
    PageantPrivateKey *priv;
    strbuf *data_to_sign;
    unsigned flags;
    int crLine;
    unsigned char failure_type;

    PageantKeyRequestNode pkr;
    PageantAsyncOp pao;
};

/* Master lock that indicates whether a GUI request is currently in
 * progress */
static bool gui_request_in_progress = false;
static PageantKeyRequestNode requests_blocked_on_gui =
    { &requests_blocked_on_gui, &requests_blocked_on_gui };

static void failure(PageantClient *pc, PageantClientRequestId *reqid,
                    strbuf *sb, unsigned char type, const char *fmt, ...);
static void fail_requests_for_key(PageantPrivateKey *priv, const char *reason);
static PageantPublicKey *pageant_nth_pubkey(int ssh_version, int i);

static void pk_priv_free(PageantPrivateKey *priv)
{
    if (priv->base_pub)
        strbuf_free(priv->base_pub);
    if (priv->sort.ssh_version == 1 && priv->rkey) {
        freersakey(priv->rkey);
        sfree(priv->rkey);
    }
    if (priv->sort.ssh_version == 2 && priv->skey) {
        ssh_key_free(priv->skey);
    }
    if (priv->encrypted_key_file)
        strbuf_free(priv->encrypted_key_file);
    if (priv->encrypted_key_comment)
        sfree(priv->encrypted_key_comment);
    fail_requests_for_key(priv, "key deleted from Pageant while signing "
                          "request was pending");
    sfree(priv);
}

static void pk_pub_free(PageantPublicKey *pub)
{
    if (pub->full_pub)
        strbuf_free(pub->full_pub);
    sfree(pub->comment);
    sfree(pub);
}

static strbuf *makeblob1(RSAKey *rkey)
{
    strbuf *blob = strbuf_new();
    rsa_ssh1_public_blob(BinarySink_UPCAST(blob), rkey,
                         RSA_SSH1_EXPONENT_FIRST);
    return blob;
}

static strbuf *makeblob2full(ssh_key *key)
{
    strbuf *blob = strbuf_new();
    ssh_key_public_blob(key, BinarySink_UPCAST(blob));
    return blob;
}

static strbuf *makeblob2base(ssh_key *key)
{
    strbuf *blob = strbuf_new();
    ssh_key_public_blob(ssh_key_base_key(key), BinarySink_UPCAST(blob));
    return blob;
}

static PageantPrivateKey *pub_to_priv(PageantPublicKey *pub)
{
    PageantPrivateKey *priv = find234(privkeytree, &pub->sort.priv, NULL);
    assert(priv && "Public and private trees out of sync!");
    return priv;
}

static PageantPublicKey *findpubkey1(RSAKey *reqkey)
{
    strbuf *blob = makeblob1(reqkey);
    PageantPublicKeySort sort;
    sort.priv.ssh_version = 1;
    sort.priv.base_pub = ptrlen_from_strbuf(blob);
    sort.full_pub = ptrlen_from_strbuf(blob);
    PageantPublicKey *toret = find234(pubkeytree, &sort, NULL);
    strbuf_free(blob);
    return toret;
}

/*
 * Constructs the base_pub element of a PageantPublicKeySort, starting
 * from full_pub. This may involve allocating a strbuf to store it in,
 * which must survive until after you've finished using the resulting
 * PageantPublicKeySort. Hence, the strbuf (if any) is returned from
 * this function, and if it's non-NULL then the caller must eventually
 * free it.
 */
static strbuf *make_base_pub_2(PageantPublicKeySort *sort)
{
    /* Start with the fallback option of making base_pub equal full_pub */
    sort->priv.base_pub = sort->full_pub;

    /* Now reconstruct a distinct base_pub without a cert, if possible
     * and necessary */
    strbuf *base_pub = NULL;
    BinarySource src[1];
    BinarySource_BARE_INIT_PL(src, sort->full_pub);
    ptrlen algname = get_string(src);
    const ssh_keyalg *alg = find_pubkey_alg_len(algname);
    if (alg && alg->is_certificate) {
        ssh_key *key = ssh_key_new_pub(alg, sort->full_pub);
        if (key) {
            base_pub = strbuf_new();
            ssh_key_public_blob(ssh_key_base_key(key),
                                BinarySink_UPCAST(base_pub));
            sort->priv.base_pub = ptrlen_from_strbuf(base_pub);
            ssh_key_free(key);
        }
    }

    return base_pub; /* caller must free once they're done with sort */
}

static PageantPublicKey *findpubkey2(ptrlen full_pub)
{
    PageantPublicKeySort sort;
    sort.priv.ssh_version = 2;
    sort.full_pub = full_pub;
    strbuf *base_pub = make_base_pub_2(&sort);
    PageantPublicKey *toret = find234(pubkeytree, &sort, NULL);
    if (base_pub)
        strbuf_free(base_pub);
    return toret;
}

static int find_first_pubkey_for_version(int ssh_version)
{
    PageantPublicKeySort sort;
    sort.priv.ssh_version = ssh_version;
    sort.priv.base_pub = PTRLEN_LITERAL("");
    sort.full_pub = PTRLEN_LITERAL("");
    int pos;
    if (findrelpos234(pubkeytree, &sort, NULL, REL234_GE, &pos))
        return pos;
    return count234(pubkeytree);
}

static int count_keys(int ssh_version)
{
    return (find_first_pubkey_for_version(ssh_version + 1) -
            find_first_pubkey_for_version(ssh_version));
}
int pageant_count_ssh1_keys(void) { return count_keys(1); }
int pageant_count_ssh2_keys(void) { return count_keys(2); }

/*
 * Common code to add a key to the trees. We fill in as many fields
 * here as we can share between SSH versions: the ptrlens in the
 * sorting field, the whole of pub->sort.priv, and the linked list of
 * blocked requests.
 */
static bool pageant_add_key_common(PageantPublicKey *pub,
                                   PageantPrivateKey *priv)
{
    int ssh_version = priv->sort.ssh_version;

    priv->sort.base_pub = ptrlen_from_strbuf(priv->base_pub);

    pub->base_pub = strbuf_dup(priv->sort.base_pub);
    pub->sort.priv.ssh_version = priv->sort.ssh_version;
    pub->sort.priv.base_pub = ptrlen_from_strbuf(pub->base_pub);
    pub->sort.full_pub = ptrlen_from_strbuf(pub->full_pub);
    priv->blocked_requests.next = priv->blocked_requests.prev =
        &priv->blocked_requests;

    /*
     * Try to add the private key to privkeytree, or combine new parts
     * of it with what's already there.
     */
    PageantPrivateKey *priv_in_tree = add234(privkeytree, priv);
    if (priv_in_tree == priv) {
        /* The key wasn't in the tree at all, and we've just added it. */
    } else {
        /* The key was already in the tree, so we'll be freeing priv. */

        if (ssh_version == 2 && priv->skey && !priv_in_tree->skey) {
            /* The key was only stored encrypted, and now we have an
             * unencrypted version to add to the existing record. */
            priv_in_tree->skey = priv->skey;
            priv->skey = NULL;       /* so pk_priv_free won't free it */
        }

        if (ssh_version == 2 && priv->encrypted_key_file &&
            !priv_in_tree->encrypted_key_file) {
            /* Conversely, the key was only stored in clear, and now
             * we have an encrypted version to add to it. */
            priv_in_tree->encrypted_key_file = priv->encrypted_key_file;
            priv->encrypted_key_file = NULL;
            priv_in_tree->encrypted_key_comment = priv->encrypted_key_comment;
            priv->encrypted_key_comment = NULL;
        }

        pk_priv_free(priv);
    }

    /*
     * Try to add the public key.
     */
    PageantPublicKey *pub_in_tree = add234(pubkeytree, pub);
    if (pub_in_tree == pub) {
        /* Successfully added a new key. */
        return true;
    } else {
        /* This public key was already there. */
        pk_pub_free(pub);
        return false;
    }
}

static bool pageant_add_ssh1_key(RSAKey *rkey)
{
    PageantPublicKey *pub = snew(PageantPublicKey);
    memset(pub, 0, sizeof(PageantPublicKey));
    PageantPrivateKey *priv = snew(PageantPrivateKey);
    memset(priv, 0, sizeof(PageantPrivateKey));

    priv->sort.ssh_version = 1;
    priv->base_pub = makeblob1(rkey);
    pub->full_pub = makeblob1(rkey);

    if (rkey->comment)
        pub->comment = dupstr(rkey->comment);

    priv->rkey = snew(RSAKey);
    duprsakey(priv->rkey, rkey);

    return pageant_add_key_common(pub, priv);
}

static bool pageant_add_ssh2_key(ssh2_userkey *skey)
{
    PageantPublicKey *pub = snew(PageantPublicKey);
    memset(pub, 0, sizeof(PageantPublicKey));
    PageantPrivateKey *priv = snew(PageantPrivateKey);
    memset(priv, 0, sizeof(PageantPrivateKey));

    priv->sort.ssh_version = 2;
    priv->base_pub = makeblob2base(skey->key);
    pub->full_pub = makeblob2full(skey->key);

    if (skey->comment)
        pub->comment = dupstr(skey->comment);

    /* Duplicate the ssh_key to go in priv */
    {
        strbuf *tmp = strbuf_new_nm();
        ssh_key_openssh_blob(skey->key, BinarySink_UPCAST(tmp));
        BinarySource src[1];
        BinarySource_BARE_INIT_PL(src, ptrlen_from_strbuf(tmp));
        priv->skey = ssh_key_new_priv_openssh(ssh_key_alg(skey->key), src);
        strbuf_free(tmp);
    }

    return pageant_add_key_common(pub, priv);
}

static bool pageant_add_ssh2_key_encrypted(PageantPublicKeySort sort,
                                           const char *comment, ptrlen keyfile)
{
    PageantPublicKey *pub = snew(PageantPublicKey);
    memset(pub, 0, sizeof(PageantPublicKey));
    PageantPrivateKey *priv = snew(PageantPrivateKey);
    memset(priv, 0, sizeof(PageantPrivateKey));

    assert(sort.priv.ssh_version == 2);
    priv->sort.ssh_version = sort.priv.ssh_version;
    priv->base_pub = strbuf_dup(sort.priv.base_pub);
    pub->full_pub = strbuf_dup(sort.full_pub);

    pub->comment = dupstr(comment);

    priv->encrypted_key_file = strbuf_dup_nm(keyfile);
    priv->encrypted_key_comment = dupstr(comment);

    return pageant_add_key_common(pub, priv);
}

static void remove_pubkey_cleanup(PageantPublicKey *pub)
{
    /* Common function called when we've just removed a public key
     * from pubkeytree: we must also check whether that was the last
     * public key sharing a private half, and if so, remove the
     * corresponding private entry too. */

    PageantPublicKeySort pubsearch;
    pubsearch.priv = pub->sort.priv;
    pubsearch.full_pub = PTRLEN_LITERAL("");
    PageantPublicKey *pubfound = findrel234(
        pubkeytree, &pubsearch, NULL, REL234_GE);

    if (pubfound && !privkey_cmpfn(&pub->sort.priv, &pubfound->sort.priv)) {
        /* There's still a public key which has the same sort.priv as
         * the one we've just removed. We're good. */
    } else {
        /* We've just removed the last public key of the family, so
         * delete the private half as well. */
        PageantPrivateKey *priv = del234(privkeytree, &pub->sort.priv);
        assert(priv);
        assert(!privkey_cmpfn(&priv->sort, &pub->sort.priv));
        pk_priv_free(priv);
    }
}

static PageantPublicKey *del_pubkey_pos(int pos)
{
    PageantPublicKey *deleted = delpos234(pubkeytree, pos);
    remove_pubkey_cleanup(deleted);
    return deleted;
}

static void del_pubkey(PageantPublicKey *to_delete)
{
    PageantPublicKey *deleted = del234(pubkeytree, to_delete);
    remove_pubkey_cleanup(deleted);
}

static void remove_all_keys(int ssh_version)
{
    int start = find_first_pubkey_for_version(ssh_version);
    int end = find_first_pubkey_for_version(ssh_version + 1);
    while (end > start) {
        PageantPublicKey *pub = del_pubkey_pos(--end);
        assert(pub->sort.priv.ssh_version == ssh_version);
        pk_pub_free(pub);
    }
}

static void list_keys(BinarySink *bs, int ssh_version, bool extended)
{
    int i;
    PageantPublicKey *pub;

    put_uint32(bs, count_keys(ssh_version));
    for (i = find_first_pubkey_for_version(ssh_version);
         NULL != (pub = index234(pubkeytree, i)); i++) {
        if (pub->sort.priv.ssh_version != ssh_version)
            break;

        if (ssh_version > 1)
            put_stringpl(bs, pub->sort.full_pub);
        else
            put_datapl(bs, pub->sort.full_pub); /* no header */

        put_stringpl(bs, ptrlen_from_asciz(pub->comment));

        if (extended) {
            assert(ssh_version == 2);  /* extended lists not supported in v1 */

            /*
             * Append to each key entry a string containing extension
             * data. This string begins with a flags word, and may in
             * future contain further data if flag bits are set saying
             * that it does. Hence, it's wrapped in a containing
             * string, so that clients that only partially understand
             * it can still find the parts they do understand.
             */
            PageantPrivateKey *priv = pub_to_priv(pub);

            strbuf *sb = strbuf_new();

            uint32_t flags = 0;
            if (!priv->skey)
                flags |= LIST_EXTENDED_FLAG_HAS_NO_CLEARTEXT_KEY;
            if (priv->encrypted_key_file)
                flags |= LIST_EXTENDED_FLAG_HAS_ENCRYPTED_KEY_FILE;
            put_uint32(sb, flags);

            put_stringsb(bs, sb);
        }
    }
}

void pageant_make_keylist1(BinarySink *bs) { list_keys(bs, 1, false); }
void pageant_make_keylist2(BinarySink *bs) { list_keys(bs, 2, false); }
void pageant_make_keylist_extended(BinarySink *bs) { list_keys(bs, 2, true); }

void pageant_register_client(PageantClient *pc)
{
    pc->info = snew(PageantClientInfo);
    pc->info->pc = pc;
    pc->info->head.prev = pc->info->head.next = &pc->info->head;
}

void pageant_unregister_client(PageantClient *pc)
{
    PageantClientInfo *info = pc->info;
    assert(info);
    assert(info->pc == pc);

    while (pc->info->head.next != &pc->info->head) {
        PageantAsyncOp *pao = container_of(pc->info->head.next,
                                           PageantAsyncOp, cr);
        pageant_async_op_unlink_and_free(pao);
    }

    sfree(pc->info);
}

static PRINTF_LIKE(5, 6) void failure(
    PageantClient *pc, PageantClientRequestId *reqid, strbuf *sb,
    unsigned char type, const char *fmt, ...)
{
    strbuf_clear(sb);
    put_byte(sb, type);
    if (!pc->suppress_logging) {
        va_list ap;
        va_start(ap, fmt);
        char *msg = dupvprintf(fmt, ap);
        va_end(ap);
        pageant_client_log(pc, reqid, "reply: SSH_AGENT_FAILURE (%s)", msg);
        sfree(msg);
    }
}

static void signop_link_to_key(PageantSignOp *so)
{
    assert(!so->pkr.prev);
    assert(!so->pkr.next);

    so->pkr.prev = so->priv->blocked_requests.prev;
    so->pkr.next = &so->priv->blocked_requests;
    so->pkr.prev->next = &so->pkr;
    so->pkr.next->prev = &so->pkr;
}

static void signop_link_to_pending_gui_request(PageantSignOp *so)
{
    assert(!so->pkr.prev);
    assert(!so->pkr.next);

    so->pkr.prev = requests_blocked_on_gui.prev;
    so->pkr.next = &requests_blocked_on_gui;
    so->pkr.prev->next = &so->pkr;
    so->pkr.next->prev = &so->pkr;
}

static void signop_unlink(PageantSignOp *so)
{
    if (so->pkr.next) {
        assert(so->pkr.prev);
        so->pkr.next->prev = so->pkr.prev;
        so->pkr.prev->next = so->pkr.next;
        so->pkr.prev = so->pkr.next = NULL;
    } else {
        assert(!so->pkr.prev);
    }
}

static void signop_free(PageantAsyncOp *pao)
{
    PageantSignOp *so = container_of(pao, PageantSignOp, pao);
    signop_unlink(so);
    strbuf_free(so->data_to_sign);
    sfree(so);
}

static bool request_passphrase(PageantClient *pc, PageantPrivateKey *priv)
{
    if (!priv->decryption_prompt_active) {
        assert(!gui_request_in_progress);

        bool created_dlg = pageant_client_ask_passphrase(
            pc, &priv->dlgid, priv->encrypted_key_comment);

        if (!created_dlg)
            return false;

        gui_request_in_progress = true;
        priv->decryption_prompt_active = true;
    }

    return true;
}

static void signop_coroutine(PageantAsyncOp *pao)
{
    PageantSignOp *so = container_of(pao, PageantSignOp, pao);
    strbuf *response;

    crBegin(so->crLine);

    while (!so->priv->skey && gui_request_in_progress) {
        signop_link_to_pending_gui_request(so);
        crReturnV;
        signop_unlink(so);
    }

    if (!so->priv->skey) {
        assert(so->priv->encrypted_key_file);

        if (!request_passphrase(so->pao.info->pc, so->priv)) {
            response = strbuf_new();
            failure(so->pao.info->pc, so->pao.reqid, response,
                    so->failure_type, "on-demand decryption could not "
                    "prompt for a passphrase");
            goto respond;
        }

        signop_link_to_key(so);
        crReturnV;
        signop_unlink(so);
    }

    uint32_t supported_flags = ssh_key_supported_flags(so->priv->skey);
    if (so->flags & ~supported_flags) {
        /*
         * We MUST reject any message containing flags we don't
         * understand.
         */
        response = strbuf_new();
        failure(so->pao.info->pc, so->pao.reqid, response, so->failure_type,
                "unsupported flag bits 0x%08"PRIx32,
                so->flags & ~supported_flags);
        goto respond;
    }

    char *invalid = ssh_key_invalid(so->priv->skey, so->flags);
    if (invalid) {
        response = strbuf_new();
        failure(so->pao.info->pc, so->pao.reqid, response, so->failure_type,
                "key invalid: %s", invalid);
        sfree(invalid);
        goto respond;
    }

    strbuf *signature = strbuf_new();
    ssh_key_sign(so->priv->skey, ptrlen_from_strbuf(so->data_to_sign),
                 so->flags, BinarySink_UPCAST(signature));

    response = strbuf_new();
    put_byte(response, SSH2_AGENT_SIGN_RESPONSE);
    put_stringsb(response, signature);

  respond:
    pageant_client_got_response(so->pao.info->pc, so->pao.reqid,
                                ptrlen_from_strbuf(response));
    strbuf_free(response);

    pageant_async_op_unlink_and_free(&so->pao);
    crFinishFreedV;
}

static const PageantAsyncOpVtable signop_vtable = {
    .coroutine = signop_coroutine,
    .free = signop_free,
};

static void fail_requests_for_key(PageantPrivateKey *priv, const char *reason)
{
    while (priv->blocked_requests.next != &priv->blocked_requests) {
        PageantSignOp *so = container_of(priv->blocked_requests.next,
                                         PageantSignOp, pkr);
        signop_unlink(so);
        strbuf *sb = strbuf_new();
        failure(so->pao.info->pc, so->pao.reqid, sb, so->failure_type,
                "%s", reason);
        pageant_client_got_response(so->pao.info->pc, so->pao.reqid,
                                    ptrlen_from_strbuf(sb));
        strbuf_free(sb);
        pageant_async_op_unlink_and_free(&so->pao);
    }
}

static void unblock_requests_for_key(PageantPrivateKey *priv)
{
    for (PageantKeyRequestNode *pkr = priv->blocked_requests.next;
         pkr != &priv->blocked_requests; pkr = pkr->next) {
        PageantSignOp *so = container_of(pkr, PageantSignOp, pkr);
        queue_toplevel_callback(pageant_async_op_callback, &so->pao);
    }
}

static void unblock_pending_gui_requests(void)
{
    for (PageantKeyRequestNode *pkr = requests_blocked_on_gui.next;
         pkr != &requests_blocked_on_gui; pkr = pkr->next) {
        PageantSignOp *so = container_of(pkr, PageantSignOp, pkr);
        queue_toplevel_callback(pageant_async_op_callback, &so->pao);
    }
}

void pageant_passphrase_request_success(PageantClientDialogId *dlgid,
                                        ptrlen passphrase)
{
    PageantPrivateKey *priv = container_of(dlgid, PageantPrivateKey, dlgid);

    assert(gui_request_in_progress);
    gui_request_in_progress = false;
    priv->decryption_prompt_active = false;

    if (!priv->skey) {
        const char *error;

        BinarySource src[1];
        BinarySource_BARE_INIT_PL(src, ptrlen_from_strbuf(
                                      priv->encrypted_key_file));

        strbuf *ppsb = strbuf_dup_nm(passphrase);
        ssh2_userkey *skey = ppk_load_s(src, ppsb->s, &error);
        strbuf_free(ppsb);

        if (!skey) {
            fail_requests_for_key(priv, "unable to decrypt key");
            return;
        } else if (skey == SSH2_WRONG_PASSPHRASE) {
            /*
             * Find a PageantClient to use for another attempt at
             * request_passphrase.
             */
            PageantKeyRequestNode *pkr = priv->blocked_requests.next;
            if (pkr == &priv->blocked_requests) {
                /*
                 * Special case: if all the requests have gone away at
                 * this point, we need not bother putting up a request
                 * at all any more.
                 */
                return;
            }

            PageantSignOp *so = container_of(priv->blocked_requests.next,
                                             PageantSignOp, pkr);

            priv->decryption_prompt_active = false;
            if (!request_passphrase(so->pao.info->pc, so->priv)) {
                fail_requests_for_key(priv, "unable to continue creating "
                                      "passphrase prompts");
            }
            return;
        } else {
            priv->skey = skey->key;
            sfree(skey->comment);
            sfree(skey);
            keylist_update();
        }
    }

    unblock_requests_for_key(priv);

    unblock_pending_gui_requests();
}

void pageant_passphrase_request_refused(PageantClientDialogId *dlgid)
{
    PageantPrivateKey *priv = container_of(dlgid, PageantPrivateKey, dlgid);

    assert(gui_request_in_progress);
    gui_request_in_progress = false;
    priv->decryption_prompt_active = false;

    fail_requests_for_key(priv, "user refused to supply passphrase");

    unblock_pending_gui_requests();
}

typedef struct PageantImmOp PageantImmOp;
struct PageantImmOp {
    int crLine;
    strbuf *response;

    PageantAsyncOp pao;
};

static void immop_free(PageantAsyncOp *pao)
{
    PageantImmOp *io = container_of(pao, PageantImmOp, pao);
    if (io->response)
        strbuf_free(io->response);
    sfree(io);
}

static void immop_coroutine(PageantAsyncOp *pao)
{
    PageantImmOp *io = container_of(pao, PageantImmOp, pao);

    crBegin(io->crLine);

    if (0) crReturnV;

    pageant_client_got_response(io->pao.info->pc, io->pao.reqid,
                                ptrlen_from_strbuf(io->response));
    pageant_async_op_unlink_and_free(&io->pao);
    crFinishFreedV;
}

static const PageantAsyncOpVtable immop_vtable = {
    .coroutine = immop_coroutine,
    .free = immop_free,
};

static bool reencrypt_key(PageantPublicKey *pub)
{
    PageantPrivateKey *priv = pub_to_priv(pub);

    if (priv->sort.ssh_version != 2) {
        /*
         * We don't support storing SSH-1 keys in encrypted form at
         * all.
         */
        return false;
    }

    if (!priv->encrypted_key_file) {
        /*
         * We can't re-encrypt a key if it doesn't have an encrypted
         * form. (We could make one up, of course - but with what
         * passphrase that we could expect the user to know later?)
         */
        return false;
    }

    /* Only actually free priv->skey if it exists. But we return success
     * regardless, so that 'please ensure this key isn't stored
     * decrypted' is idempotent. */
    if (priv->skey) {
        ssh_key_free(priv->skey);
        priv->skey = NULL;
    }

    return true;
}

#define DECL_EXT_ENUM(id, name) id,
enum Extension { KNOWN_EXTENSIONS(DECL_EXT_ENUM) EXT_UNKNOWN };
#define DEF_EXT_NAMES(id, name) PTRLEN_DECL_LITERAL(name),
static const ptrlen extension_names[] = { KNOWN_EXTENSIONS(DEF_EXT_NAMES) };

static PageantAsyncOp *pageant_make_op(
    PageantClient *pc, PageantClientRequestId *reqid, ptrlen msgpl)
{
    BinarySource msg[1];
    strbuf *sb = strbuf_new_nm();
    unsigned char failure_type = SSH_AGENT_FAILURE;
    int type;

#define fail(...) failure(pc, reqid, sb, failure_type, __VA_ARGS__)

    BinarySource_BARE_INIT_PL(msg, msgpl);

    type = get_byte(msg);
    if (get_err(msg)) {
        fail("message contained no type code");
        goto responded;
    }

    switch (type) {
      case SSH1_AGENTC_REQUEST_RSA_IDENTITIES: {
        /*
         * Reply with SSH1_AGENT_RSA_IDENTITIES_ANSWER.
         */
        pageant_client_log(pc, reqid,
                           "request: SSH1_AGENTC_REQUEST_RSA_IDENTITIES");

        put_byte(sb, SSH1_AGENT_RSA_IDENTITIES_ANSWER);
        pageant_make_keylist1(BinarySink_UPCAST(sb));

        pageant_client_log(pc, reqid,
                           "reply: SSH1_AGENT_RSA_IDENTITIES_ANSWER");
        if (!pc->suppress_logging) {
            int i;
            PageantPublicKey *pub;
            for (i = 0; NULL != (pub = pageant_nth_pubkey(1, i)); i++) {
                PageantPrivateKey *priv = pub_to_priv(pub);
                char *fingerprint = rsa_ssh1_fingerprint(priv->rkey);
                pageant_client_log(pc, reqid, "returned key: %s",
                                   fingerprint);
                sfree(fingerprint);
            }
        }
        break;
      }
      case SSH2_AGENTC_REQUEST_IDENTITIES: {
        /*
         * Reply with SSH2_AGENT_IDENTITIES_ANSWER.
         */
        pageant_client_log(pc, reqid,
                           "request: SSH2_AGENTC_REQUEST_IDENTITIES");

        put_byte(sb, SSH2_AGENT_IDENTITIES_ANSWER);
        pageant_make_keylist2(BinarySink_UPCAST(sb));

        pageant_client_log(pc, reqid, "reply: SSH2_AGENT_IDENTITIES_ANSWER");
        if (!pc->suppress_logging) {
            int i;
            PageantPublicKey *pub;
            for (i = 0; NULL != (pub = pageant_nth_pubkey(2, i)); i++) {
                char *fingerprint = ssh2_double_fingerprint_blob(
                    pub->sort.full_pub, SSH_FPTYPE_DEFAULT);
                pageant_client_log(pc, reqid, "returned key: %s %s",
                                   fingerprint, pub->comment);
                sfree(fingerprint);
            }
        }
        break;
      }
      case SSH1_AGENTC_RSA_CHALLENGE: {
        /*
         * Reply with either SSH1_AGENT_RSA_RESPONSE or
         * SSH_AGENT_FAILURE, depending on whether we have that key
         * or not.
         */
        RSAKey reqkey;
        PageantPublicKey *pub;
        PageantPrivateKey *priv;
        mp_int *challenge, *response;
        ptrlen session_id;
        unsigned response_type;
        unsigned char response_md5[16];
        int i;

        pageant_client_log(pc, reqid, "request: SSH1_AGENTC_RSA_CHALLENGE");

        response = NULL;
        memset(&reqkey, 0, sizeof(reqkey));

        get_rsa_ssh1_pub(msg, &reqkey, RSA_SSH1_EXPONENT_FIRST);
        challenge = get_mp_ssh1(msg);
        session_id = get_data(msg, 16);
        response_type = get_uint32(msg);

        if (get_err(msg)) {
            fail("unable to decode request");
            goto challenge1_cleanup;
        }
        if (response_type != 1) {
            fail("response type other than 1 not supported");
            goto challenge1_cleanup;
        }

        if (!pc->suppress_logging) {
            char *fingerprint;
            reqkey.comment = NULL;
            fingerprint = rsa_ssh1_fingerprint(&reqkey);
            pageant_client_log(pc, reqid, "requested key: %s", fingerprint);
            sfree(fingerprint);
        }

        if ((pub = findpubkey1(&reqkey)) == NULL) {
            fail("key not found");
            goto challenge1_cleanup;
        }
        priv = pub_to_priv(pub);
        response = rsa_ssh1_decrypt(challenge, priv->rkey);

        {
            ssh_hash *h = ssh_hash_new(&ssh_md5);
            for (i = 0; i < 32; i++)
                put_byte(h, mp_get_byte(response, 31 - i));
            put_datapl(h, session_id);
            ssh_hash_final(h, response_md5);
        }

        put_byte(sb, SSH1_AGENT_RSA_RESPONSE);
        put_data(sb, response_md5, 16);

        pageant_client_log(pc, reqid, "reply: SSH1_AGENT_RSA_RESPONSE");

      challenge1_cleanup:
        if (response)
            mp_free(response);
        mp_free(challenge);
        freersakey(&reqkey);
        break;
      }
      case SSH2_AGENTC_SIGN_REQUEST: {
        /*
         * Reply with either SSH2_AGENT_SIGN_RESPONSE or
         * SSH_AGENT_FAILURE, depending on whether we have that key
         * or not.
         */
        PageantPublicKey *pub;
        ptrlen keyblob, sigdata;
        uint32_t flags;

        pageant_client_log(pc, reqid, "request: SSH2_AGENTC_SIGN_REQUEST");

        keyblob = get_string(msg);
        sigdata = get_string(msg);

        if (get_err(msg)) {
            fail("unable to decode request");
            goto responded;
        }

        /*
         * Later versions of the agent protocol added a flags word
         * on the end of the sign request. That hasn't always been
         * there, so we don't complain if we don't find it.
         *
         * get_uint32 will default to returning zero if no data is
         * available.
         */
        bool have_flags = false;
        flags = get_uint32(msg);
        if (!get_err(msg))
            have_flags = true;

        if (!pc->suppress_logging) {
            char *fingerprint = ssh2_double_fingerprint_blob(
                keyblob, SSH_FPTYPE_DEFAULT);
            pageant_client_log(pc, reqid, "requested key: %s", fingerprint);
            sfree(fingerprint);
        }
        if ((pub = findpubkey2(keyblob)) == NULL) {
            fail("key not found");
            goto responded;
        }

        if (have_flags)
            pageant_client_log(pc, reqid, "signature flags = 0x%08"PRIx32,
                               flags);
        else
            pageant_client_log(pc, reqid, "no signature flags");

        strbuf_free(sb); /* no immediate response */

        PageantSignOp *so = snew(PageantSignOp);
        so->pao.vt = &signop_vtable;
        so->pao.info = pc->info;
        so->pao.cr.prev = pc->info->head.prev;
        so->pao.cr.next = &pc->info->head;
        so->pao.cr.prev->next = so->pao.cr.next->prev = &so->pao.cr;
        so->pao.reqid = reqid;
        so->priv = pub_to_priv(pub);
        so->pkr.prev = so->pkr.next = NULL;
        so->data_to_sign = strbuf_dup(sigdata);
        so->flags = flags;
        so->failure_type = failure_type;
        so->crLine = 0;
        return &so->pao;
        break;
      }
      case SSH1_AGENTC_ADD_RSA_IDENTITY: {
        /*
         * Add to the list and return SSH_AGENT_SUCCESS, or
         * SSH_AGENT_FAILURE if the key was malformed.
         */
        RSAKey *key;

        pageant_client_log(pc, reqid, "request: SSH1_AGENTC_ADD_RSA_IDENTITY");

        key = get_rsa_ssh1_priv_agent(msg);
        key->comment = mkstr(get_string(msg));

        if (get_err(msg)) {
            fail("unable to decode request");
            goto add1_cleanup;
        }

        if (!rsa_verify(key)) {
            fail("key is invalid");
            goto add1_cleanup;
        }

        if (!pc->suppress_logging) {
            char *fingerprint = rsa_ssh1_fingerprint(key);
            pageant_client_log(pc, reqid,
                               "submitted key: %s", fingerprint);
            sfree(fingerprint);
        }

        if (pageant_add_ssh1_key(key)) {
            keylist_update();
            put_byte(sb, SSH_AGENT_SUCCESS);
            pageant_client_log(pc, reqid, "reply: SSH_AGENT_SUCCESS");
            key = NULL;            /* don't free it in cleanup */
        } else {
            fail("key already present");
        }

      add1_cleanup:
        if (key) {
            freersakey(key);
            sfree(key);
        }
        break;
      }
      case SSH2_AGENTC_ADD_IDENTITY: {
        /*
         * Add to the list and return SSH_AGENT_SUCCESS, or
         * SSH_AGENT_FAILURE if the key was malformed.
         */
        ssh2_userkey *key = NULL;
        ptrlen algpl;
        const ssh_keyalg *alg;

        pageant_client_log(pc, reqid, "request: SSH2_AGENTC_ADD_IDENTITY");

        algpl = get_string(msg);

        key = snew(ssh2_userkey);
        key->key = NULL;
        key->comment = NULL;
        alg = find_pubkey_alg_len(algpl);
        if (!alg) {
            fail("algorithm unknown");
            goto add2_cleanup;
        }

        key->key = ssh_key_new_priv_openssh(alg, msg);

        if (!key->key) {
            fail("key setup failed");
            goto add2_cleanup;
        }

        key->comment = mkstr(get_string(msg));

        if (get_err(msg)) {
            fail("unable to decode request");
            goto add2_cleanup;
        }

        if (!pc->suppress_logging) {
            char *fingerprint = ssh2_fingerprint(key->key, SSH_FPTYPE_DEFAULT);
            pageant_client_log(pc, reqid, "submitted key: %s %s",
                               fingerprint, key->comment);
            sfree(fingerprint);
        }

        if (pageant_add_ssh2_key(key)) {
            keylist_update();
            put_byte(sb, SSH_AGENT_SUCCESS);

            pageant_client_log(pc, reqid, "reply: SSH_AGENT_SUCCESS");

            key = NULL;            /* don't clean it up */
        } else {
            fail("key already present");
        }

      add2_cleanup:
        if (key) {
            if (key->key)
                ssh_key_free(key->key);
            if (key->comment)
                sfree(key->comment);
            sfree(key);
        }
        break;
      }
      case SSH1_AGENTC_REMOVE_RSA_IDENTITY: {
        /*
         * Remove from the list and return SSH_AGENT_SUCCESS, or
         * perhaps SSH_AGENT_FAILURE if it wasn't in the list to
         * start with.
         */
        RSAKey reqkey;
        PageantPublicKey *pub;

        pageant_client_log(pc, reqid,
                           "request: SSH1_AGENTC_REMOVE_RSA_IDENTITY");

        memset(&reqkey, 0, sizeof(reqkey));
        get_rsa_ssh1_pub(msg, &reqkey, RSA_SSH1_EXPONENT_FIRST);

        if (get_err(msg)) {
            fail("unable to decode request");
            freersakey(&reqkey);
            goto responded;
        }

        if (!pc->suppress_logging) {
            char *fingerprint;
            reqkey.comment = NULL;
            fingerprint = rsa_ssh1_fingerprint(&reqkey);
            pageant_client_log(pc, reqid, "unwanted key: %s", fingerprint);
            sfree(fingerprint);
        }

        pub = findpubkey1(&reqkey);
        freersakey(&reqkey);
        if (pub) {
            pageant_client_log(pc, reqid, "found with comment: %s",
                               pub->comment);

            del_pubkey(pub);
            keylist_update();
            pk_pub_free(pub);
            put_byte(sb, SSH_AGENT_SUCCESS);

            pageant_client_log(pc, reqid, "reply: SSH_AGENT_SUCCESS");
        } else {
            fail("key not found");
        }
        break;
      }
      case SSH2_AGENTC_REMOVE_IDENTITY: {
        /*
         * Remove from the list and return SSH_AGENT_SUCCESS, or
         * perhaps SSH_AGENT_FAILURE if it wasn't in the list to
         * start with.
         */
        PageantPublicKey *pub;
        ptrlen blob;

        pageant_client_log(pc, reqid, "request: SSH2_AGENTC_REMOVE_IDENTITY");

        blob = get_string(msg);

        if (get_err(msg)) {
            fail("unable to decode request");
            goto responded;
        }

        if (!pc->suppress_logging) {
            char *fingerprint = ssh2_double_fingerprint_blob(
                blob, SSH_FPTYPE_DEFAULT);
            pageant_client_log(pc, reqid, "unwanted key: %s", fingerprint);
            sfree(fingerprint);
        }

        pub = findpubkey2(blob);
        if (!pub) {
            fail("key not found");
            goto responded;
        }

        pageant_client_log(pc, reqid, "found with comment: %s", pub->comment);

        del_pubkey(pub);
        keylist_update();
        pk_pub_free(pub);
        put_byte(sb, SSH_AGENT_SUCCESS);

        pageant_client_log(pc, reqid, "reply: SSH_AGENT_SUCCESS");
        break;
      }
      case SSH1_AGENTC_REMOVE_ALL_RSA_IDENTITIES: {
        /*
         * Remove all SSH-1 keys. Always returns success.
         */
        pageant_client_log(pc, reqid,
                           "request: SSH1_AGENTC_REMOVE_ALL_RSA_IDENTITIES");

        remove_all_keys(1);
        keylist_update();

        put_byte(sb, SSH_AGENT_SUCCESS);

        pageant_client_log(pc, reqid, "reply: SSH_AGENT_SUCCESS");
        break;
      }
      case SSH2_AGENTC_REMOVE_ALL_IDENTITIES: {
        /*
         * Remove all SSH-2 keys. Always returns success.
         */
        pageant_client_log(pc, reqid,
                           "request: SSH2_AGENTC_REMOVE_ALL_IDENTITIES");

        remove_all_keys(2);
        keylist_update();

        put_byte(sb, SSH_AGENT_SUCCESS);

        pageant_client_log(pc, reqid, "reply: SSH_AGENT_SUCCESS");
        break;
      }
      case SSH2_AGENTC_EXTENSION: {
        enum Extension exttype = EXT_UNKNOWN;
        ptrlen extname = get_string(msg);
        pageant_client_log(pc, reqid,
                           "request: SSH2_AGENTC_EXTENSION \"%.*s\"",
                           PTRLEN_PRINTF(extname));

        for (size_t i = 0; i < lenof(extension_names); i++)
            if (ptrlen_eq_ptrlen(extname, extension_names[i])) {
                exttype = i;

                /*
                 * For SSH_AGENTC_EXTENSION requests, the message
                 * code SSH_AGENT_FAILURE is reserved for "I don't
                 * recognise this extension name at all". For any
                 * other kind of failure while processing an
                 * extension we _do_ recognise, we must switch to
                 * returning a different failure code, with
                 * semantics "I understood the extension name, but
                 * something else went wrong".
                 */
                failure_type = SSH_AGENT_EXTENSION_FAILURE;
                break;
            }

        switch (exttype) {
          case EXT_UNKNOWN:
            fail("unrecognised extension name '%.*s'",
                 PTRLEN_PRINTF(extname));
            break;

          case EXT_QUERY:
            /* Standard request to list the supported extensions. */
            put_byte(sb, SSH_AGENT_SUCCESS);
            for (size_t i = 0; i < lenof(extension_names); i++)
                put_stringpl(sb, extension_names[i]);
            pageant_client_log(pc, reqid, "reply: SSH_AGENT_SUCCESS + names");
            break;

          case EXT_ADD_PPK: {
            ptrlen keyfile = get_string(msg);

            if (get_err(msg)) {
                fail("unable to decode request");
                goto responded;
            }

            strbuf *base_pub = NULL;
            strbuf *full_pub = NULL;
            BinarySource src[1];
            const char *error;

            full_pub = strbuf_new();
            char *comment;

            BinarySource_BARE_INIT_PL(src, keyfile);
            if (!ppk_loadpub_s(src, NULL, BinarySink_UPCAST(full_pub),
                               &comment, &error)) {
                fail("failed to extract public key blob: %s", error);
                goto add_ppk_cleanup;
            }

            if (!pc->suppress_logging) {
                char *fingerprint = ssh2_double_fingerprint_blob(
                    ptrlen_from_strbuf(full_pub), SSH_FPTYPE_DEFAULT);
                pageant_client_log(pc, reqid, "add-ppk: %s %s",
                                   fingerprint, comment);
                sfree(fingerprint);
            }

            BinarySource_BARE_INIT_PL(src, keyfile);
            bool encrypted = ppk_encrypted_s(src, NULL);

            if (!encrypted) {
                /* If the key isn't encrypted, then we should just
                 * load and add it in the obvious way. */
                BinarySource_BARE_INIT_PL(src, keyfile);
                ssh2_userkey *skey = ppk_load_s(src, NULL, &error);
                if (!skey) {
                    fail("failed to decode private key: %s", error);
                } else if (pageant_add_ssh2_key(skey)) {
                    keylist_update();
                    put_byte(sb, SSH_AGENT_SUCCESS);

                    pageant_client_log(pc, reqid, "reply: SSH_AGENT_SUCCESS"
                                       " (loaded unencrypted PPK)");
                } else {
                    fail("key already present");
                    if (skey->key)
                        ssh_key_free(skey->key);
                    if (skey->comment)
                        sfree(skey->comment);
                    sfree(skey);
                }
                goto add_ppk_cleanup;
            }

            PageantPublicKeySort sort;
            sort.priv.ssh_version = 2;
            sort.full_pub = ptrlen_from_strbuf(full_pub);
            base_pub = make_base_pub_2(&sort);

            pageant_add_ssh2_key_encrypted(sort, comment, keyfile);
            keylist_update();
            put_byte(sb, SSH_AGENT_SUCCESS);
            pageant_client_log(pc, reqid, "reply: SSH_AGENT_SUCCESS");

          add_ppk_cleanup:
            if (full_pub)
                strbuf_free(full_pub);
            if (base_pub)
                strbuf_free(base_pub);
            sfree(comment);
            break;
          }

          case EXT_REENCRYPT: {
            /*
             * Re-encrypt a single key, in the sense of deleting
             * its unencrypted copy, returning it to the state of
             * only having the encrypted PPK form stored, so that
             * the next attempt to use it will have to re-prompt
             * for the passphrase.
             */
            ptrlen blob = get_string(msg);

            if (get_err(msg)) {
                fail("unable to decode request");
                goto responded;
            }

            if (!pc->suppress_logging) {
                char *fingerprint = ssh2_double_fingerprint_blob(
                    blob, SSH_FPTYPE_DEFAULT);
                pageant_client_log(pc, reqid, "key to re-encrypt: %s",
                                   fingerprint);
                sfree(fingerprint);
            }

            PageantPublicKey *pub = findpubkey2(blob);
            if (!pub) {
                fail("key not found");
                goto responded;
            }

            pageant_client_log(pc, reqid,
                               "found with comment: %s", pub->comment);

            if (!reencrypt_key(pub)) {
                fail("this key couldn't be re-encrypted");
                goto responded;
            }

            keylist_update();
            put_byte(sb, SSH_AGENT_SUCCESS);
            pageant_client_log(pc, reqid, "reply: SSH_AGENT_SUCCESS");
            break;
          }

          case EXT_REENCRYPT_ALL: {
            /*
             * Re-encrypt all keys that have an encrypted form
             * stored. Usually, returns success, but with a uint32
             * appended indicating how many keys remain
             * unencrypted. The exception is if there is at least
             * one key in the agent and _no_ key was successfully
             * re-encrypted; in that situation we've done nothing,
             * and the client didn't _want_ us to do nothing, so
             * we return failure.
             *
             * (Rationale: the 'failure' message ought to be
             * atomic, that is, you shouldn't return failure
             * having made a state change.)
             */
            unsigned nfailures = 0, nsuccesses = 0;
            PageantPublicKey *pub;

            for (int i = 0; (pub = index234(pubkeytree, i)) != NULL; i++) {
                if (reencrypt_key(pub))
                    nsuccesses++;
                else
                    nfailures++;
            }

            if (nsuccesses == 0 && nfailures > 0) {
                fail("no key could be re-encrypted");
            } else {
                keylist_update();
                put_byte(sb, SSH_AGENT_SUCCESS);
                put_uint32(sb, nfailures);
                pageant_client_log(pc, reqid, "reply: SSH_AGENT_SUCCESS "
                                   "(%u keys re-encrypted, %u failures)",
                                   nsuccesses, nfailures);
            }
            break;
          }

          case EXT_LIST_EXTENDED: {
            /*
             * Return a key list like SSH2_AGENTC_REQUEST_IDENTITIES,
             * except that each key is annotated with extra
             * information such as whether it's currently encrypted.
             *
             * The return message type is AGENT_SUCCESS with auxiliary
             * data, which is more like other extension messages. I
             * think it would be confusing to reuse IDENTITIES_ANSWER
             * for a reply message with an incompatible format.
             */
            put_byte(sb, SSH_AGENT_SUCCESS);
            pageant_make_keylist_extended(BinarySink_UPCAST(sb));

            pageant_client_log(pc, reqid,
                               "reply: SSH2_AGENT_SUCCESS + key list");
            if (!pc->suppress_logging) {
                int i;
                PageantPublicKey *pub;
                for (i = 0; NULL != (pub = pageant_nth_pubkey(2, i)); i++) {
                    char *fingerprint = ssh2_double_fingerprint_blob(
                        ptrlen_from_strbuf(pub->full_pub),
                        SSH_FPTYPE_DEFAULT);
                    pageant_client_log(pc, reqid, "returned key: %s %s",
                                       fingerprint, pub->comment);
                    sfree(fingerprint);
                }
            }
            break;
          }
        }
        break;
      }
      default:
        pageant_client_log(pc, reqid, "request: unknown message type %d",
                           type);
        fail("unrecognised message");
        break;
    }

#undef fail

  responded:;

    PageantImmOp *io = snew(PageantImmOp);
    io->pao.vt = &immop_vtable;
    io->pao.info = pc->info;
    io->pao.cr.prev = pc->info->head.prev;
    io->pao.cr.next = &pc->info->head;
    io->pao.cr.prev->next = io->pao.cr.next->prev = &io->pao.cr;
    io->pao.reqid = reqid;
    io->response = sb;
    io->crLine = 0;
    return &io->pao;
}

void pageant_handle_msg(PageantClient *pc, PageantClientRequestId *reqid,
                        ptrlen msgpl)
{
    PageantAsyncOp *pao = pageant_make_op(pc, reqid, msgpl);
    queue_toplevel_callback(pageant_async_op_callback, pao);
}

void pageant_init(void)
{
    pageant_local = true;
    pubkeytree = newtree234(pubkey_cmpfn);
    privkeytree = newtree234(privkey_cmpfn);
}

static PageantPublicKey *pageant_nth_pubkey(int ssh_version, int i)
{
    PageantPublicKey *pub = index234(
        pubkeytree, find_first_pubkey_for_version(ssh_version) + i);
    if (pub && pub->sort.priv.ssh_version == ssh_version)
        return pub;
    else
        return NULL;
}

bool pageant_delete_nth_ssh1_key(int i)
{
    PageantPublicKey *pub = del_pubkey_pos(
        find_first_pubkey_for_version(1) + i);
    if (!pub)
        return false;
    pk_pub_free(pub);
    return true;
}

bool pageant_delete_nth_ssh2_key(int i)
{
    PageantPublicKey *pub = del_pubkey_pos(
        find_first_pubkey_for_version(2) + i);
    if (!pub)
        return false;
    pk_pub_free(pub);
    return true;
}

bool pageant_reencrypt_nth_ssh2_key(int i)
{
    PageantPublicKey *pub = index234(
        pubkeytree, find_first_pubkey_for_version(2) + i);
    if (!pub)
        return false;
    return reencrypt_key(pub);
}

void pageant_delete_all(void)
{
    remove_all_keys(1);
    remove_all_keys(2);
}

void pageant_reencrypt_all(void)
{
    PageantPublicKey *pub;
    for (int i = 0; (pub = index234(pubkeytree, i)) != NULL; i++)
        reencrypt_key(pub);
}

/* ----------------------------------------------------------------------
 * The agent plug.
 */

/*
 * An extra coroutine macro, specific to this code which is consuming
 * 'const char *data'.
 */
#define crGetChar(c) do                                         \
    {                                                           \
        while (len == 0) {                                      \
            *crLine = __LINE__; return; case __LINE__:;         \
        }                                                       \
        len--;                                                  \
        (c) = (unsigned char)*data++;                           \
    } while (0)

struct pageant_conn_queued_response {
    struct pageant_conn_queued_response *next, *prev;
    size_t req_index;      /* for indexing requests in log messages */
    strbuf *sb;
    PageantClientRequestId reqid;
};

struct pageant_conn_state {
    Socket *connsock;
    PageantListenerClient *plc;
    unsigned char lenbuf[4], pktbuf[AGENT_MAX_MSGLEN];
    unsigned len, got;
    bool real_packet;
    size_t conn_index;     /* for indexing connections in log messages */
    size_t req_index;      /* for indexing requests in log messages */
    int crLine;            /* for coroutine in pageant_conn_receive */

    struct pageant_conn_queued_response response_queue;

    PageantClient pc;
    Plug plug;
};

static void pageant_conn_closing(Plug *plug, PlugCloseType type,
                                 const char *error_msg)
{
    struct pageant_conn_state *pc = container_of(
        plug, struct pageant_conn_state, plug);
    if (type != PLUGCLOSE_NORMAL)
        pageant_listener_client_log(pc->plc, "c#%"SIZEu": error: %s",
                                    pc->conn_index, error_msg);
    else
        pageant_listener_client_log(pc->plc, "c#%"SIZEu": connection closed",
                                    pc->conn_index);
    sk_close(pc->connsock);
    pageant_unregister_client(&pc->pc);
    sfree(pc);
}

static void pageant_conn_sent(Plug *plug, size_t bufsize)
{
    /* struct pageant_conn_state *pc = container_of(
        plug, struct pageant_conn_state, plug); */

    /*
     * We do nothing here, because we expect that there won't be a
     * need to throttle and unthrottle the connection to an agent -
     * clients will typically not send many requests, and will wait
     * until they receive each reply before sending a new request.
     */
}

static void pageant_conn_log(PageantClient *pc, PageantClientRequestId *reqid,
                             const char *fmt, va_list ap)
{
    struct pageant_conn_state *pcs =
        container_of(pc, struct pageant_conn_state, pc);
    struct pageant_conn_queued_response *qr =
        container_of(reqid, struct pageant_conn_queued_response, reqid);

    char *formatted = dupvprintf(fmt, ap);
    pageant_listener_client_log(pcs->plc, "c#%"SIZEu",r#%"SIZEu": %s",
                                pcs->conn_index, qr->req_index, formatted);
    sfree(formatted);
}

static void pageant_conn_got_response(
    PageantClient *pc, PageantClientRequestId *reqid, ptrlen response)
{
    struct pageant_conn_state *pcs =
        container_of(pc, struct pageant_conn_state, pc);
    struct pageant_conn_queued_response *qr =
        container_of(reqid, struct pageant_conn_queued_response, reqid);

    qr->sb = strbuf_new_nm();
    put_stringpl(qr->sb, response);

    while (pcs->response_queue.next != &pcs->response_queue &&
           pcs->response_queue.next->sb) {
        qr = pcs->response_queue.next;
        sk_write(pcs->connsock, qr->sb->u, qr->sb->len);
        qr->next->prev = qr->prev;
        qr->prev->next = qr->next;
        strbuf_free(qr->sb);
        sfree(qr);
    }
}

static bool pageant_conn_ask_passphrase(
    PageantClient *pc, PageantClientDialogId *dlgid, const char *comment)
{
    struct pageant_conn_state *pcs =
        container_of(pc, struct pageant_conn_state, pc);
    return pageant_listener_client_ask_passphrase(pcs->plc, dlgid, comment);
}

static const PageantClientVtable pageant_connection_clientvt = {
    .log = pageant_conn_log,
    .got_response = pageant_conn_got_response,
    .ask_passphrase = pageant_conn_ask_passphrase,
};

static void pageant_conn_receive(
    Plug *plug, int urgent, const char *data, size_t len)
{
    struct pageant_conn_state *pc = container_of(
        plug, struct pageant_conn_state, plug);
    char c;

    crBegin(pc->crLine);

    while (len > 0) {
        pc->got = 0;
        while (pc->got < 4) {
            crGetChar(c);
            pc->lenbuf[pc->got++] = c;
        }

        pc->len = GET_32BIT_MSB_FIRST(pc->lenbuf);
        pc->got = 0;
        pc->real_packet = (pc->len < AGENT_MAX_MSGLEN-4);

        {
            struct pageant_conn_queued_response *qr =
                snew(struct pageant_conn_queued_response);
            qr->prev = pc->response_queue.prev;
            qr->next = &pc->response_queue;
            qr->prev->next = qr->next->prev = qr;
            qr->sb = NULL;
            qr->req_index = pc->req_index++;
        }

        if (!pc->real_packet) {
            /*
             * Send failure immediately, before consuming the packet
             * data. That way we notify the client reasonably early
             * even if the data channel has just started spewing
             * nonsense.
             */
            pageant_client_log(&pc->pc, &pc->response_queue.prev->reqid,
                               "early reply: SSH_AGENT_FAILURE "
                               "(overlong message, length %u)", pc->len);
            static const unsigned char failure[] = { SSH_AGENT_FAILURE };
            pageant_conn_got_response(&pc->pc, &pc->response_queue.prev->reqid,
                                      make_ptrlen(failure, lenof(failure)));
        }

        while (pc->got < pc->len) {
            crGetChar(c);
            if (pc->real_packet)
                pc->pktbuf[pc->got] = c;
            pc->got++;
        }

        if (pc->real_packet)
            pageant_handle_msg(&pc->pc, &pc->response_queue.prev->reqid,
                               make_ptrlen(pc->pktbuf, pc->len));
    }

    crFinishV;
}

struct pageant_listen_state {
    Socket *listensock;
    PageantListenerClient *plc;
    size_t conn_index;     /* for indexing connections in log messages */

    Plug plug;
};

static void pageant_listen_closing(Plug *plug, PlugCloseType type,
                                   const char *error_msg)
{
    struct pageant_listen_state *pl = container_of(
        plug, struct pageant_listen_state, plug);
    if (type != PLUGCLOSE_NORMAL)
        pageant_listener_client_log(pl->plc, "listening socket: error: %s",
                                    error_msg);
    sk_close(pl->listensock);
    pl->listensock = NULL;
}

static const PlugVtable pageant_connection_plugvt = {
    .closing = pageant_conn_closing,
    .receive = pageant_conn_receive,
    .sent = pageant_conn_sent,
    .log = nullplug_log,
};

static int pageant_listen_accepting(Plug *plug,
                                    accept_fn_t constructor, accept_ctx_t ctx)
{
    struct pageant_listen_state *pl = container_of(
        plug, struct pageant_listen_state, plug);
    struct pageant_conn_state *pc;
    const char *err;
    SocketEndpointInfo *peerinfo;

    pc = snew(struct pageant_conn_state);
    pc->plug.vt = &pageant_connection_plugvt;
    pc->pc.vt = &pageant_connection_clientvt;
    pc->plc = pl->plc;
    pc->response_queue.next = pc->response_queue.prev = &pc->response_queue;
    pc->conn_index = pl->conn_index++;
    pc->req_index = 0;
    pc->crLine = 0;

    pc->connsock = constructor(ctx, &pc->plug);
    if ((err = sk_socket_error(pc->connsock)) != NULL) {
        sk_close(pc->connsock);
        sfree(pc);
        return 1;
    }

    sk_set_frozen(pc->connsock, false);

    peerinfo = sk_peer_info(pc->connsock);
    if (peerinfo && peerinfo->log_text) {
        pageant_listener_client_log(pl->plc,
                                    "c#%"SIZEu": new connection from %s",
                                    pc->conn_index, peerinfo->log_text);
    } else {
        pageant_listener_client_log(pl->plc, "c#%"SIZEu": new connection",
                                    pc->conn_index);
    }
    sk_free_endpoint_info(peerinfo);

    pageant_register_client(&pc->pc);

    return 0;
}

static const PlugVtable pageant_listener_plugvt = {
    .closing = pageant_listen_closing,
    .accepting = pageant_listen_accepting,
    .log = nullplug_log,
};

struct pageant_listen_state *pageant_listener_new(
    Plug **plug, PageantListenerClient *plc)
{
    struct pageant_listen_state *pl = snew(struct pageant_listen_state);
    pl->plug.vt = &pageant_listener_plugvt;
    pl->plc = plc;
    pl->listensock = NULL;
    pl->conn_index = 0;
    *plug = &pl->plug;
    return pl;
}

void pageant_listener_got_socket(struct pageant_listen_state *pl, Socket *sock)
{
    pl->listensock = sock;
}

void pageant_listener_free(struct pageant_listen_state *pl)
{
    if (pl->listensock)
        sk_close(pl->listensock);
    sfree(pl);
}

/* ----------------------------------------------------------------------
 * Code to perform agent operations either as a client, or within the
 * same process as the running agent.
 */

static tree234 *passphrases = NULL;

typedef struct PageantInternalClient {
    strbuf *response;
    bool got_response;
    PageantClient pc;
} PageantInternalClient;

static void internal_client_got_response(
    PageantClient *pc, PageantClientRequestId *reqid, ptrlen response)
{
    PageantInternalClient *pic = container_of(pc, PageantInternalClient, pc);
    strbuf_clear(pic->response);
    put_datapl(pic->response, response);
    pic->got_response = true;
}

static bool internal_client_ask_passphrase(
    PageantClient *pc, PageantClientDialogId *dlgid, const char *comment)
{
    /* No delaying operations are permitted in this mode */
    return false;
}

static const PageantClientVtable internal_clientvt = {
    .log = NULL,
    .got_response = internal_client_got_response,
    .ask_passphrase = internal_client_ask_passphrase,
};

typedef struct PageantClientOp {
    strbuf *buf;
    bool request_made;
    BinarySink_DELEGATE_IMPLEMENTATION;
    BinarySource_IMPLEMENTATION;
} PageantClientOp;

static PageantClientOp *pageant_client_op_new(void)
{
    PageantClientOp *pco = snew(PageantClientOp);
    pco->buf = strbuf_new_for_agent_query();
    pco->request_made = false;
    BinarySink_DELEGATE_INIT(pco, pco->buf);
    BinarySource_INIT(pco, "", 0);
    return pco;
}

static void pageant_client_op_free(PageantClientOp *pco)
{
    if (pco->buf)
        strbuf_free(pco->buf);
    sfree(pco);
}

static unsigned pageant_client_op_query(PageantClientOp *pco)
{
    /* Since we use the same strbuf for the request and the response,
     * check by assertion that we aren't embarrassingly sending a
     * previous response back to the agent */
    assert(!pco->request_made);
    pco->request_made = true;

    if (!pageant_local) {
        void *response_raw;
        int resplen_raw;
        agent_query_synchronous(pco->buf, &response_raw, &resplen_raw);
        strbuf_clear(pco->buf);
        put_data(pco->buf, response_raw, resplen_raw);
        sfree(response_raw);

        /* The data coming back from agent_query_synchronous will have
         * its length field prepended. So we start by parsing it as an
         * SSH-formatted string, and then reinitialise our
         * BinarySource with the interior of that string. */
        BinarySource_INIT_PL(pco, ptrlen_from_strbuf(pco->buf));
        BinarySource_INIT_PL(pco, get_string(pco));
    } else {
        PageantInternalClient pic;
        PageantClientRequestId reqid;

        pic.pc.vt = &internal_clientvt;
        pic.pc.suppress_logging = true;
        pic.response = pco->buf;
        pic.got_response = false;
        pageant_register_client(&pic.pc);

        assert(pco->buf->len > 4);
        PageantAsyncOp *pao = pageant_make_op(
            &pic.pc, &reqid, make_ptrlen(pco->buf->s + 4, pco->buf->len - 4));
        while (!pic.got_response)
            pageant_async_op_coroutine(pao);

        pageant_unregister_client(&pic.pc);

        BinarySource_INIT_PL(pco, ptrlen_from_strbuf(pco->buf));
    }

    /* Strip off and directly return the type byte, which every client
     * will need, to save a boilerplate get_byte at each call site */
    unsigned reply_type = get_byte(pco);
    if (get_err(pco))
        reply_type = 256;              /* out-of-range code */
    return reply_type;
}

/*
 * After processing a list of filenames, we want to forget the
 * passphrases.
 */
void pageant_forget_passphrases(void)
{
    if (!passphrases)                  /* in case we never set it up at all */
        return;

    while (count234(passphrases) > 0) {
        char *pp = index234(passphrases, 0);
        smemclr(pp, strlen(pp));
        delpos234(passphrases, 0);
        sfree(pp);
    }
}

typedef struct KeyListEntry {
    ptrlen blob, comment;
    uint32_t flags;
} KeyListEntry;
typedef struct KeyList {
    strbuf *raw_data;
    KeyListEntry *keys;
    size_t nkeys;
    bool broken;
} KeyList;

static void keylist_free(KeyList *kl)
{
    sfree(kl->keys);
    strbuf_free(kl->raw_data);
    sfree(kl);
}

static PageantClientOp *pageant_request_keylist_1(void)
{
    PageantClientOp *pco = pageant_client_op_new();
    put_byte(pco, SSH1_AGENTC_REQUEST_RSA_IDENTITIES);
    if (pageant_client_op_query(pco) == SSH1_AGENT_RSA_IDENTITIES_ANSWER)
        return pco;
    pageant_client_op_free(pco);
    return NULL;
}

static PageantClientOp *pageant_request_keylist_2(void)
{
    PageantClientOp *pco = pageant_client_op_new();
    put_byte(pco, SSH2_AGENTC_REQUEST_IDENTITIES);
    if (pageant_client_op_query(pco) == SSH2_AGENT_IDENTITIES_ANSWER)
        return pco;
    pageant_client_op_free(pco);
    return NULL;
}

static PageantClientOp *pageant_request_keylist_extended(void)
{
    PageantClientOp *pco = pageant_client_op_new();
    put_byte(pco, SSH2_AGENTC_EXTENSION);
    put_stringpl(pco, extension_names[EXT_LIST_EXTENDED]);
    if (pageant_client_op_query(pco) == SSH_AGENT_SUCCESS)
        return pco;
    pageant_client_op_free(pco);
    return NULL;
}

static KeyList *pageant_get_keylist(unsigned ssh_version)
{
    PageantClientOp *pco;
    bool list_is_extended = false;

    if (ssh_version == 1) {
        pco = pageant_request_keylist_1();
    } else {
        if ((pco = pageant_request_keylist_extended()) != NULL)
            list_is_extended = true;
        else
            pco = pageant_request_keylist_2();
    }

    if (!pco)
        return NULL;

    KeyList *kl = snew(KeyList);
    kl->nkeys = get_uint32(pco);
    kl->keys = snewn(kl->nkeys, struct KeyListEntry);
    kl->broken = false;

    for (size_t i = 0; i < kl->nkeys && !get_err(pco); i++) {
        if (ssh_version == 1) {
            int bloblen = rsa_ssh1_public_blob_len(
                make_ptrlen(get_ptr(pco), get_avail(pco)));
            if (bloblen < 0) {
                kl->broken = true;
                bloblen = 0;
            }
            kl->keys[i].blob = get_data(pco, bloblen);
        } else {
            kl->keys[i].blob = get_string(pco);
        }
        kl->keys[i].comment = get_string(pco);

        if (list_is_extended) {
            ptrlen key_ext_info = get_string(pco);
            BinarySource src[1];
            BinarySource_BARE_INIT_PL(src, key_ext_info);

            kl->keys[i].flags = get_uint32(src);
        } else {
            kl->keys[i].flags = 0;
        }
    }

    if (get_err(pco))
        kl->broken = true;
    kl->raw_data = pco->buf;
    pco->buf = NULL;
    pageant_client_op_free(pco);
    return kl;
}

int pageant_add_keyfile(Filename *filename, const char *passphrase,
                        char **retstr, bool add_encrypted)
{
    RSAKey *rkey = NULL;
    ssh2_userkey *skey = NULL;
    bool needs_pass;
    int ret;
    int attempts;
    char *comment;
    const char *this_passphrase;
    const char *error = NULL;
    int type;

    if (!passphrases) {
        passphrases = newtree234(NULL);
    }

    *retstr = NULL;

    type = key_type(filename);
    if (type != SSH_KEYTYPE_SSH1 && type != SSH_KEYTYPE_SSH2) {
        *retstr = dupprintf("Couldn't load this key (%s)",
                            key_type_to_str(type));
        return PAGEANT_ACTION_FAILURE;
    }

    if (add_encrypted && type == SSH_KEYTYPE_SSH1) {
        *retstr = dupprintf("Can't add SSH-1 keys in encrypted form");
        return PAGEANT_ACTION_FAILURE;
    }

    /*
     * See if the key is already loaded (in the primary Pageant,
     * which may or may not be us).
     */
    {
        strbuf *blob = strbuf_new();
        KeyList *kl;

        if (type == SSH_KEYTYPE_SSH1) {
            if (!rsa1_loadpub_f(filename, BinarySink_UPCAST(blob),
                                NULL, &error)) {
                *retstr = dupprintf("Couldn't load private key (%s)", error);
                strbuf_free(blob);
                return PAGEANT_ACTION_FAILURE;
            }
            kl = pageant_get_keylist(1);
        } else {
            if (!ppk_loadpub_f(filename, NULL, BinarySink_UPCAST(blob),
                               NULL, &error)) {
                *retstr = dupprintf("Couldn't load private key (%s)", error);
                strbuf_free(blob);
                return PAGEANT_ACTION_FAILURE;
            }
            kl = pageant_get_keylist(2);
        }

        if (kl) {
            if (kl->broken) {
                *retstr = dupstr("Received broken key list from agent");
                keylist_free(kl);
                strbuf_free(blob);
                return PAGEANT_ACTION_FAILURE;
            }

            for (size_t i = 0; i < kl->nkeys; i++) {
                /*
                 * If the key already exists in the agent, we're done,
                 * except in the following special cases:
                 *
                 * It's encrypted in the agent, and we're being asked
                 * to add it unencrypted, in which case we still want
                 * to upload the unencrypted version to cause the key
                 * to become decrypted.
                 * (Rationale: if you know in advance you're going to
                 * want it, and don't want to be interrupted at an
                 * unpredictable moment to be asked for the
                 * passphrase.)
                 *
                 * The agent only has cleartext, and we're being asked
                 * to add it encrypted, in which case we'll add the
                 * encrypted form.
                 * (Rationale: if you might want to re-encrypt the key
                 * at some future point, but it happened to have been
                 * initially added in cleartext, perhaps by something
                 * other than Pageant.)
                 */
                if (ptrlen_eq_ptrlen(ptrlen_from_strbuf(blob),
                                     kl->keys[i].blob)) {
                    bool have_unencrypted =
                        !(kl->keys[i].flags &
                          LIST_EXTENDED_FLAG_HAS_NO_CLEARTEXT_KEY);
                    bool have_encrypted =
                        (kl->keys[i].flags &
                         LIST_EXTENDED_FLAG_HAS_ENCRYPTED_KEY_FILE);
                    if ((have_unencrypted && !add_encrypted)
                        || (have_encrypted && add_encrypted)) {
                        /* Key is already present in the desired form;
                         * we can now leave. */
                        keylist_free(kl);
                        strbuf_free(blob);
                        return PAGEANT_ACTION_OK;
                    }
                }
            }

            keylist_free(kl);
        }

        strbuf_free(blob);
    }

    if (add_encrypted) {
        const char *load_error;
        LoadedFile *lf = lf_load_keyfile(filename, &load_error);
        if (!lf) {
            *retstr = dupstr(load_error);
            return PAGEANT_ACTION_FAILURE;
        }

        PageantClientOp *pco = pageant_client_op_new();
        put_byte(pco, SSH2_AGENTC_EXTENSION);
        put_stringpl(pco, extension_names[EXT_ADD_PPK]);
        put_string(pco, lf->data, lf->len);

        lf_free(lf);

        unsigned reply = pageant_client_op_query(pco);
        pageant_client_op_free(pco);

        if (reply != SSH_AGENT_SUCCESS) {
            if (reply == SSH_AGENT_FAILURE) {
                /* The agent didn't understand the protocol extension
                 * at all. */
                *retstr = dupstr("Agent doesn't support adding "
                                 "encrypted keys");
            } else {
                *retstr = dupstr("The already running agent "
                                 "refused to add the key.");
            }
            return PAGEANT_ACTION_FAILURE;
        }

        return PAGEANT_ACTION_OK;
    }

    error = NULL;
    if (type == SSH_KEYTYPE_SSH1)
        needs_pass = rsa1_encrypted_f(filename, &comment);
    else
        needs_pass = ppk_encrypted_f(filename, &comment);
    attempts = 0;
    if (type == SSH_KEYTYPE_SSH1)
        rkey = snew(RSAKey);

    /*
     * Loop round repeatedly trying to load the key, until we either
     * succeed, fail for some serious reason, or run out of
     * passphrases to try.
     */
    while (1) {
        if (needs_pass) {

            /*
             * If we've been given a passphrase on input, try using
             * it. Otherwise, try one from our tree234 of previously
             * useful passphrases.
             */
            if (passphrase) {
                this_passphrase = (attempts == 0 ? passphrase : NULL);
            } else {
                this_passphrase = (const char *)index234(passphrases, attempts);
            }

            if (!this_passphrase) {
                /*
                 * Run out of passphrases to try.
                 */
                *retstr = comment;
                sfree(rkey);
                return PAGEANT_ACTION_NEED_PP;
            }
        } else
            this_passphrase = "";

        if (type == SSH_KEYTYPE_SSH1)
            ret = rsa1_load_f(filename, rkey, this_passphrase, &error);
        else {
            skey = ppk_load_f(filename, this_passphrase, &error);
            if (skey == SSH2_WRONG_PASSPHRASE)
                ret = -1;
            else if (!skey)
                ret = 0;
            else
                ret = 1;
        }

        if (ret == 0) {
            /*
             * Failed to load the key file, for some reason other than
             * a bad passphrase.
             */
            *retstr = dupstr(error);
            sfree(rkey);
            if (comment)
                sfree(comment);
            return PAGEANT_ACTION_FAILURE;
        } else if (ret == 1) {
            /*
             * Successfully loaded the key file.
             */
            break;
        } else {
            /*
             * Passphrase wasn't right; go round again.
             */
            attempts++;
        }
    }

    /*
     * If we get here, we've successfully loaded the key into
     * rkey/skey, but not yet added it to the agent.
     */

    /*
     * If the key was successfully decrypted, save the passphrase for
     * use with other keys we try to load.
     */
    {
        char *pp_copy = dupstr(this_passphrase);
        if (addpos234(passphrases, pp_copy, 0) != pp_copy) {
            /* No need; it was already there. */
            smemclr(pp_copy, strlen(pp_copy));
            sfree(pp_copy);
        }
    }

    if (comment)
        sfree(comment);

    if (type == SSH_KEYTYPE_SSH1) {
        PageantClientOp *pco = pageant_client_op_new();
        put_byte(pco, SSH1_AGENTC_ADD_RSA_IDENTITY);
        rsa_ssh1_private_blob_agent(BinarySink_UPCAST(pco), rkey);
        put_stringz(pco, rkey->comment);
        unsigned reply = pageant_client_op_query(pco);
        pageant_client_op_free(pco);

        freersakey(rkey);
        sfree(rkey);

        if (reply != SSH_AGENT_SUCCESS) {
            *retstr = dupstr("The already running agent "
                             "refused to add the key.");
            return PAGEANT_ACTION_FAILURE;
        }
    } else {
        PageantClientOp *pco = pageant_client_op_new();
        put_byte(pco, SSH2_AGENTC_ADD_IDENTITY);
        put_stringz(pco, ssh_key_ssh_id(skey->key));
        ssh_key_openssh_blob(skey->key, BinarySink_UPCAST(pco));
        put_stringz(pco, skey->comment);
        unsigned reply = pageant_client_op_query(pco);
        pageant_client_op_free(pco);

        sfree(skey->comment);
        ssh_key_free(skey->key);
        sfree(skey);

        if (reply != SSH_AGENT_SUCCESS) {
            *retstr = dupstr("The already running agent "
                             "refused to add the key.");
            return PAGEANT_ACTION_FAILURE;
        }
    }
    return PAGEANT_ACTION_OK;
}

int pageant_enum_keys(pageant_key_enum_fn_t callback, void *callback_ctx,
                      char **retstr)
{
    KeyList *kl1 = NULL, *kl2 = NULL;
    struct pageant_pubkey cbkey;
    int toret = PAGEANT_ACTION_FAILURE;

    kl1 = pageant_get_keylist(1);
    if (kl1 && kl1->broken) {
        *retstr = dupstr("Received broken SSH-1 key list from agent");
        goto out;
    }

    kl2 = pageant_get_keylist(2);
    if (kl2 && kl2->broken) {
        *retstr = dupstr("Received broken SSH-2 key list from agent");
        goto out;
    }

    if (kl1) {
        for (size_t i = 0; i < kl1->nkeys; i++) {
            cbkey.blob = strbuf_dup(kl1->keys[i].blob);
            cbkey.comment = mkstr(kl1->keys[i].comment);
            cbkey.ssh_version = 1;

            /* Decode public blob into a key in order to fingerprint it */
            RSAKey rkey;
            memset(&rkey, 0, sizeof(rkey));
            {
                BinarySource src[1];
                BinarySource_BARE_INIT_PL(src, kl1->keys[i].blob);
                get_rsa_ssh1_pub(src, &rkey, RSA_SSH1_EXPONENT_FIRST);
                if (get_err(src)) {
                    *retstr = dupstr(
                        "Received an invalid SSH-1 key from agent");
                    goto out;
                }
            }
            char **fingerprints = rsa_ssh1_fake_all_fingerprints(&rkey);
            freersakey(&rkey);

            callback(callback_ctx, fingerprints, cbkey.comment,
                     kl1->keys[i].flags, &cbkey);

            strbuf_free(cbkey.blob);
            sfree(cbkey.comment);
            ssh2_free_all_fingerprints(fingerprints);
        }
    }

    if (kl2) {
        for (size_t i = 0; i < kl2->nkeys; i++) {
            cbkey.blob = strbuf_dup(kl2->keys[i].blob);
            cbkey.comment = mkstr(kl2->keys[i].comment);
            cbkey.ssh_version = 2;

            char **fingerprints =
                ssh2_all_fingerprints_for_blob(kl2->keys[i].blob);

            callback(callback_ctx, fingerprints, cbkey.comment,
                     kl2->keys[i].flags, &cbkey);

            ssh2_free_all_fingerprints(fingerprints);
            sfree(cbkey.comment);
            strbuf_free(cbkey.blob);
        }
    }

    *retstr = NULL;
    toret = PAGEANT_ACTION_OK;
  out:
    if (kl1)
        keylist_free(kl1);
    if (kl2)
        keylist_free(kl2);
    return toret;
}

int pageant_delete_key(struct pageant_pubkey *key, char **retstr)
{
    PageantClientOp *pco = pageant_client_op_new();

    if (key->ssh_version == 1) {
        put_byte(pco, SSH1_AGENTC_REMOVE_RSA_IDENTITY);
        put_data(pco, key->blob->s, key->blob->len);
    } else {
        put_byte(pco, SSH2_AGENTC_REMOVE_IDENTITY);
        put_string(pco, key->blob->s, key->blob->len);
    }

    unsigned reply = pageant_client_op_query(pco);
    pageant_client_op_free(pco);

    if (reply != SSH_AGENT_SUCCESS) {
        *retstr = dupstr("Agent failed to delete key");
        return PAGEANT_ACTION_FAILURE;
    } else {
        *retstr = NULL;
        return PAGEANT_ACTION_OK;
    }
}

int pageant_delete_all_keys(char **retstr)
{
    PageantClientOp *pco;
    unsigned reply;

    pco = pageant_client_op_new();
    put_byte(pco, SSH2_AGENTC_REMOVE_ALL_IDENTITIES);
    reply = pageant_client_op_query(pco);
    pageant_client_op_free(pco);
    if (reply != SSH_AGENT_SUCCESS) {
        *retstr = dupstr("Agent failed to delete SSH-2 keys");
        return PAGEANT_ACTION_FAILURE;
    }

    pco = pageant_client_op_new();
    put_byte(pco, SSH1_AGENTC_REMOVE_ALL_RSA_IDENTITIES);
    reply = pageant_client_op_query(pco);
    pageant_client_op_free(pco);
    if (reply != SSH_AGENT_SUCCESS) {
        *retstr = dupstr("Agent failed to delete SSH-1 keys");
        return PAGEANT_ACTION_FAILURE;
    }

    *retstr = NULL;
    return PAGEANT_ACTION_OK;
}

int pageant_reencrypt_key(struct pageant_pubkey *key, char **retstr)
{
    PageantClientOp *pco = pageant_client_op_new();

    if (key->ssh_version == 1) {
        *retstr = dupstr("Can't re-encrypt an SSH-1 key");
        pageant_client_op_free(pco);
        return PAGEANT_ACTION_FAILURE;
    } else {
        put_byte(pco, SSH2_AGENTC_EXTENSION);
        put_stringpl(pco, extension_names[EXT_REENCRYPT]);
        put_string(pco, key->blob->s, key->blob->len);
    }

    unsigned reply = pageant_client_op_query(pco);
    pageant_client_op_free(pco);

    if (reply != SSH_AGENT_SUCCESS) {
        if (reply == SSH_AGENT_FAILURE) {
            /* The agent didn't understand the protocol extension at all. */
            *retstr = dupstr("Agent doesn't support encrypted keys");
        } else {
            *retstr = dupstr("Agent failed to re-encrypt key");
        }
        return PAGEANT_ACTION_FAILURE;
    } else {
        *retstr = NULL;
        return PAGEANT_ACTION_OK;
    }
}

int pageant_reencrypt_all_keys(char **retstr)
{
    PageantClientOp *pco = pageant_client_op_new();
    put_byte(pco, SSH2_AGENTC_EXTENSION);
    put_stringpl(pco, extension_names[EXT_REENCRYPT_ALL]);
    unsigned reply = pageant_client_op_query(pco);
    uint32_t failures = get_uint32(pco);
    pageant_client_op_free(pco);
    if (reply != SSH_AGENT_SUCCESS) {
        if (reply == SSH_AGENT_FAILURE) {
            /* The agent didn't understand the protocol extension at all. */
            *retstr = dupstr("Agent doesn't support encrypted keys");
        } else {
            *retstr = dupstr("Agent failed to re-encrypt any keys");
        }
        return PAGEANT_ACTION_FAILURE;
    } else if (failures == 1) {
        /* special case for English grammar */
        *retstr = dupstr("1 key remains unencrypted");
        return PAGEANT_ACTION_WARNING;
    } else if (failures > 0) {
        *retstr = dupprintf("%"PRIu32" keys remain unencrypted", failures);
        return PAGEANT_ACTION_WARNING;
    } else {
        *retstr = NULL;
        return PAGEANT_ACTION_OK;
    }
}

int pageant_sign(struct pageant_pubkey *key, ptrlen message, strbuf *out,
                 uint32_t flags, char **retstr)
{
    PageantClientOp *pco = pageant_client_op_new();
    put_byte(pco, SSH2_AGENTC_SIGN_REQUEST);
    put_string(pco, key->blob->s, key->blob->len);
    put_stringpl(pco, message);
    put_uint32(pco, flags);
    unsigned reply = pageant_client_op_query(pco);
    ptrlen signature = get_string(pco);

    if (reply == SSH2_AGENT_SIGN_RESPONSE && !get_err(pco)) {
        *retstr = NULL;
        put_datapl(out, signature);
        pageant_client_op_free(pco);
        return PAGEANT_ACTION_OK;
    } else {
        *retstr = dupstr("Agent failed to create signature");
        pageant_client_op_free(pco);
        return PAGEANT_ACTION_FAILURE;
    }
}

struct pageant_pubkey *pageant_pubkey_copy(struct pageant_pubkey *orig)
{
    struct pageant_pubkey *copy = snew(struct pageant_pubkey);
    copy->blob = strbuf_new();
    put_data(copy->blob, orig->blob->s, orig->blob->len);
    copy->comment = orig->comment ? dupstr(orig->comment) : NULL;
    copy->ssh_version = orig->ssh_version;
    return copy;
}

void pageant_pubkey_free(struct pageant_pubkey *key)
{
    sfree(key->comment);
    strbuf_free(key->blob);
    sfree(key);
}
