/* $Id: ssh.c,v 1.54 1999/06/28 13:42:00 levitte Exp $ */


#include "gnu_extras.h"

#if defined(__DECC) || defined(__GNUC__)
#include <unistd.h>
#endif
#include <stdio.h>
#include <sys/file.h>
#ifdef __DECC
#include <fcntl.h>
#endif
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <stsdef.h>
#include <ssdef.h>
#include <lib$routines.h>
#include <starlet.h>
#include <errno.h>
#include <ctype.h>
#ifdef __GNUC__
#define __DECC
#endif
#include <openssl/bio.h>
#include <openssl/err.h>
#include <openssl/bn.h>
#include <openssl/rsa.h>
#include <openssl/rand.h>
#include <openssl/md5.h>
#ifdef __GNUC__
#undef __DECC
#endif
#include "fish.h"
#include "ssh.h"
#include "callback.h"
#include "vms.h"
#include "log.h"
#include "util.h"
#include "compress.h"
#include "fishmsg.h"
#include "user_info.h"
#include "buffer.h"
#include "hostfile.h"

typedef struct {
    unsigned char cookie[8];
    RSA *servkey;
    RSA *hostkey;
    unsigned long flags;
    unsigned long cipher_mask;
    unsigned long auth_mask;
} ServKey;

void *ssh_init(long (*down_write)(unsigned char *,long,void *,Erf,void*),
		void *down_state, Erf erf, void *erfp)
{
    ssh_state *state;

    state = xmalloc(sizeof(ssh_state));
    if (!state) {
	ssh_F_memfull();
	return NULL;
    }

    state->down_write = down_write;
    state->down_state = down_state;
    state->up_dispatch = NULL;
    state->up_setconnid = NULL;
    state->up_state = NULL;
    state->dispatchread = 0;
    state->dispatchpkt = NULL;
    state->dispatchsizeread = 0;

    state->sshprefs = NULL;
    state->protophase = SSH_PHASE_VERSION_WAIT;

    state->encrypt = NULL;
    state->decrypt = NULL;
    state->cryptclean = NULL;

    state->term = NULL;
    state->termrows = state->termcols = state->termx = state->termy = 0;
    state->connid = NULL;

    state->userkey.bits = 0;

    state->info = 0;

    state->compress = 0;

    ERR_load_crypto_strings();

    return state;
}

void ssh_initprefs(ssh_prefs *p)
{
    char *cp;
    char default_directory[256];

    p->host = NULL;
    p->port = 22;
    get_user_info(&p->uai, 0);
    strcpy(default_directory, p->uai.device);
    strcat(default_directory, p->uai.directory);
    default_directory[strlen(default_directory)-1] = '\0';
    cp = strchr(default_directory, ':') + 1;
    if (*cp == '[')
	strcat(default_directory, ".SSH]");
    else 
	strcat(default_directory, ".SSH>");
    p->default_directory = xstrndup(default_directory,
				    strlen(default_directory));
    /* We need that directory anyhow... */
    if (mkdir(p->default_directory,0) == 0)
	lib$signal(FISH_M_CREATED, 1, p->default_directory);
    else
	switch (errno) {
	case EEXIST:
	    /* This is perfectly OK, we tried to create a directory that
	       is already there... */
	    break;
	default:
	    lib$signal(FISH_M_CREATEFAIL, 1, p->default_directory, vaxc$errno);
	    break;
	}
    p->username = xstrdup(getenv("USER"));
    for (cp = p->username; *cp; cp++)
	*cp = tolower(*cp);
    p->password = 0;
    p->ciphers = 0;		/* The user will specify later */
    p->ttymodes = NULL;
    p->ttymodelen = 0;
    p->commands = 0;
    p->infile = 0;
    p->inmode = 0;
    p->outfile = 0;
    p->outmode = 0;
    p->force_tty = 0;

    p->bits = 1024;
    p->cipher_type = SSH_CIPHER_3DES;
}

void ssh_set_prefs(ssh_prefs *prefs, void *state_, Erf erf, void *erfp)
{
    ssh_state *state = state_;

    state->sshprefs = prefs;
    state->infd = 0;

    if (prefs->infile) {
	if (prefs->inmode)
	    state->infd = open(prefs->infile,O_RDONLY,0,
			       "rfm=fix","mrs=512","rat=none");
	else
	    state->infd = open(prefs->infile,O_RDONLY,0);
	if (state->infd < 0) {
	    lib$signal(FISH_M_OPENIN, 1, prefs->infile, vaxc$errno);
	    exit(0);
	}
    }
    if (prefs->outfile) {
	if (prefs->outmode)
	    state->outfd = open(prefs->outfile,O_CREAT|O_TRUNC|O_WRONLY,0,
				"rfm=fix","mrs=512","rat=none");
	else
	    state->outfd = open(prefs->outfile,O_CREAT|O_TRUNC|O_WRONLY,0);
	if (state->outfd < 0) {
	    lib$signal(FISH_M_OPENOUT, 1, prefs->outfile, vaxc$errno);
	    exit(0);
	}
    }
}

void ssh_clearprefs(ssh_prefs *p)
{
    if (p->host) xfree(p->host);
    p->port = 0;
    if (p->username) xfree(p->username);
    if (p->default_directory) xfree(p->default_directory);
    if (p->ttymodes) xfree(p->ttymodes);
    memset(p, 0, sizeof(ssh_prefs));
}

static int put_int(void *buf, unsigned long l);

/* Send a SSH_CMSG_WINDOW_SIZE describing the current terminal */
static void send_term(ssh_state *state, Erf erf, void *erfp)
{
    long ret;
    char _buf[16];
    general_buffer buf;

    buf_init_static(&buf, _buf, sizeof _buf);

    buf_append_int(&buf, state->termrows);
    buf_append_int(&buf, state->termcols);
    buf_append_int(&buf, state->termx);
    buf_append_int(&buf, state->termy);

    ret = ssh_write_type(SSH_CMSG_WINDOW_SIZE, &buf, state, erf, erfp);
    if (ret < 16) {
	ssh_F_noconn();
    }
}

void ssh_setterm(char *term, int rows, int cols, int x, int y, void *state_,
    Erf erf, void *erfp)
{
    ssh_state *state = state_;

    CALLBACK_PROLOGUE

    if (state->term) {
	xfree(state->term);
	state->term = NULL;
    }
    if (term) {
	state->term = xmalloc(strlen(term)+1);
	if (!state->term) {
	    ssh_F_memfull();
	    goto setterm_out;
	}
	strcpy(state->term, term);
    }
    state->termrows = rows;
    state->termcols = cols;
    state->termx = x;
    state->termy = y;

    /* Send the new terminal if it's appropriate to do so */
    if (state->protophase == SSH_PHASE_INTERACTIVE) {
	send_term(state,erf,erfp);
    }

setterm_out:
    ;
    CALLBACK_EPILOGUE
}

/* This is called from below, giving a textual description of the connection */
void ssh_setconnid(char *connid, void *state_, Erf erf, void *erfp)
{
    ssh_state *state = state_;

    CALLBACK_PROLOGUE

    if (state->connid) {
	xfree(state->connid);
	state->connid = NULL;
    }
    if (connid) {
	state->connid = xmalloc(strlen(connid)+1+5);
	if (!state->connid) {
	    ssh_F_memfull();
	    goto connid_out;
	}
	strcpy(state->connid, "SSH: ");
	strcpy(state->connid+5, connid);
    }

    if (state->up_setconnid) {
	(state->up_setconnid)(state->connid, state->up_state,erf,erfp);
    }

connid_out:
    ;
    CALLBACK_EPILOGUE
}

void ssh_set_dispatch(
    void (*up_dispatch)(unsigned char *,long,void *,Erf,void*),
    void *up_state, void *state_, Erf erf, void *erfp)
{
    ssh_state *state = state_;

    state->up_dispatch = up_dispatch;
    state->up_state = up_state;
}

void ssh_set_setconnid(void (*up_setconnid)(char *, void *, Erf, void *),
    void *state_, Erf erf, void *erfp)
{
    ssh_state *state = state_;

    state->up_setconnid = up_setconnid;
    if (state->up_setconnid) {
	(state->up_setconnid)(state->connid, state->up_state,erf,erfp);
    }
}

static int get_servkey(general_buffer_reader *br, ServKey *sk)
{
    unsigned long servbits, hostbits;
    long bdiff;

    sk->servkey = RSA_new();
    sk->hostkey = RSA_new();
    if (!bufread_bytes(br, sk->cookie, 8)
	|| !bufread_int(br, &servbits)
	|| !bufread_bn(br, &sk->servkey->e)
	|| !bufread_bn(br, &sk->servkey->n)
	|| !bufread_int(br, &hostbits)
	|| !bufread_bn(br, &sk->hostkey->e)
	|| !bufread_bn(br, &sk->hostkey->n)
	|| !bufread_int(br, &sk->flags)
	|| !bufread_int(br, &sk->cipher_mask)
	|| !bufread_int(br, &sk->auth_mask)) goto servkey_err;
    servbits = BN_num_bits(sk->servkey->n);
    hostbits = BN_num_bits(sk->hostkey->n);
    bdiff = (long)servbits - (long)hostbits;
    if (bdiff < 0) bdiff = -bdiff;
    if (bdiff < 104) goto servkey_err;
    if (servbits < 512 || hostbits < 512) goto servkey_err;
    return SS$_NORMAL;

servkey_err:
    if (sk->servkey) RSA_free(sk->servkey);
    sk->servkey = 0;
    if (sk->hostkey) RSA_free(sk->hostkey);
    sk->hostkey = 0;
    return FISH_M_BADPUBK;
}

static int gen_sesskey(ServKey *sk, unsigned char *sesskey, BIGNUM **encs,
		       ssh_state *state)
{
    unsigned char *sidbuf = NULL;
    int servlen, hostlen, longlen;
    int i;
    
    /* Generate the session id.  The RFC says to do:
	MD5(servkey->n || hostkey->n || cookie)
       but the sshd distribution actually does:
	MD5(hostkey->n || servkey->n || cookie)
       We do it the "wrong" way (the way that's implemented).
    */
    sidbuf = xmalloc(BN_num_bytes(sk->servkey->n) +
		  BN_num_bytes(sk->hostkey->n) + 8);
    if (!sidbuf) goto gen_sesskey_err;
    hostlen = BN_bn2bin(sk->hostkey->n, sidbuf);
    servlen = BN_bn2bin(sk->servkey->n, sidbuf+hostlen);
    memmove(sidbuf+servlen+hostlen, sk->cookie, 8);
    RAND_seed(sidbuf, servlen+hostlen+8);
    MD5(sidbuf, servlen+hostlen+8, state->sessid);

    /* Generate session key: 32 random bytes */
    RAND_bytes(sesskey, 32);

    /* Encrypt (sesskey XOR state->sessid) with the shorter, then the longer, of the
    servkey and hostkey. */
    longlen = (hostlen > servlen) ? hostlen : servlen;
    memmove(sidbuf+longlen-32, sesskey, 32);
    for(i=0;i<16;++i) sidbuf[longlen-32+i] ^= state->sessid[i];
    if (hostlen > servlen) {
	if (RSA_public_encrypt(32, sidbuf+hostlen-32, sidbuf+hostlen-servlen,
	    sk->servkey, RSA_PKCS1_PADDING) < 0) goto gen_sesskey_rsaerr;
	if (RSA_public_encrypt(servlen, sidbuf+hostlen-servlen, sidbuf,
	    sk->hostkey, RSA_PKCS1_PADDING) < 0) goto gen_sesskey_rsaerr;
    } else {
	if (RSA_public_encrypt(32, sidbuf+servlen-32, sidbuf+servlen-hostlen,
	    sk->hostkey, RSA_PKCS1_PADDING) < 0) goto gen_sesskey_rsaerr;
	if (RSA_public_encrypt(hostlen, sidbuf+servlen-hostlen, sidbuf,
	    sk->servkey, RSA_PKCS1_PADDING) < 0) goto gen_sesskey_rsaerr;
    }
    *encs = BN_bin2bn(sidbuf, longlen, NULL);
    if (!*encs) goto gen_sesskey_err;
    
    xfree(sidbuf); sidbuf = NULL;
    return SS$_NORMAL;

 gen_sesskey_rsaerr: {
     BIO *bio_err = 0;

     if (bio_err == 0)
	 if ((bio_err=BIO_new(BIO_s_file())) != 0)
	     BIO_set_fp(bio_err,stderr,BIO_NOCLOSE|BIO_FP_TEXT);

     ERR_print_errors(bio_err);
 }

 gen_sesskey_err:
    if (sidbuf) xfree(sidbuf); sidbuf = NULL;
    return FISH_M_NOSESSK;
}

/* This is the "state machine" routine that handles incoming packets. */
int ssh_handle_packet_type(ssh_msg_type type, general_buffer_reader *inbuf, 
			    ssh_state *state, Erf erf, void *erfp)
{
    /* General output buffer */
    general_buffer *outbuf;
    long ret;
    unsigned long len;
    unsigned long prefciphers;
    ssh_cipher_type use_cipher = SSH_CIPHER_3DES;
    char *trypw;
    unsigned long trypwlen;
    unsigned char *prefttymodes;
    int prefttymodelen;
    int termlen;
    unsigned long datalen;

    on_brief(log_verbose(type, inbuf, state));

    /* Handle things that may be sent at any time */
    switch(type) {
    case SSH_MSG_DISCONNECT:
	/* Dump the connection. */
	if (bufread_int(inbuf, &len)) {
	    char *s = (char *)bufread_chars_noadjust(inbuf);
	    /* NUL-terminate the string (the memory is allocated because that's
	       where the CRC used to live) */
	    s[bufread_remaining(inbuf)] = '\0';
	    if (len != 0) {
		lib$signal(FISH_M_CLOSED2 & ((1<<28)-1),1,s);
	    } else
		lib$signal(FISH_M_CLOSED & ((1<<28)-1));
	    return FISH_M_CLOSED | 0x10000000;
	}
	return SS$_NORMAL;

    case SSH_MSG_IGNORE:
	return SS$_NORMAL;

    case SSH_MSG_DEBUG:
	if (state->sshprefs->debug)
	{
	    char *buf;
	    if (bufread_strdup(inbuf, &buf)) {
		ssh_debug(buf);
		xfree(buf);
	    }
	}
	return SS$_NORMAL;

    default:
	break;
    }

    /* Where are we in the protocol? */
    switch(state->protophase) {
	/* SSH_PHASE_VERSION_WAIT Start ------------------------------------ */
    case SSH_PHASE_VERSION_WAIT:
	/* We are expecting the server's version number */
	switch(type) {
	case SSH_INTERNAL:
	    /* See what we got */
	    if (bufread_remaining(inbuf) < 9
		|| bufread_remaining(inbuf) == 80
		|| strncmp((char *)bufread_chars_noadjust(inbuf),
			   "SSH-1.", 6)) {
		/* Nope */
		state->protophase = SSH_PHASE_NONE;
		ssh_F_invprot();
		return FISH_M_INVPROT | 0x10000000;
	    }

	    on_info(ssh_infof("Remote version: %.*s",
			      bufread_remaining(inbuf),
			      bufread_chars_noadjust(inbuf)));

	    /* Looks good here; advance the phase */
	    state->protophase = SSH_PHASE_GET_KEYS;
	{
	    char verbuf[1024];
	    general_buffer tmpbuf;
	    unsigned int l;

	    sprintf(verbuf,PILOTSSH_VERSTR_FMT,FISH_VERSION);
	    l = strlen(verbuf);
	    buf_init_static(&tmpbuf, verbuf, l+1);
	    buf_adjust_amount_by(&tmpbuf, l);
	    ret = ssh_write_type(SSH_INTERNAL, &tmpbuf, state, erf, erfp);
	    if (ret < buf_amount(&tmpbuf)) {
		ssh_F_noconn();
		return FISH_M_NOCONN | 0x10000000;
	    }
	}
	    on_info(ssh_info("Exchanging Keys"));
	    break;
	
	default:
	    /* Unknown packets? */
	    ssh_F_invpckt();
	    return FISH_M_INVPCKT | 0x10000000;
	}
	break;
	/* SSH_PHASE_VERSION_WAIT End -------------------------------------- */

	/* SSH_PHASE_GET_KEYS Start ---------------------------------------- */
    case SSH_PHASE_GET_KEYS:
	/* We are expecting the server's keys */
	switch(type) {
	case SSH_SMSG_PUBLIC_KEY: {
	    ServKey servkey;
	    BIGNUM *enc_sesskey = NULL;
	    int enc_sesskey_bytes;
	    int status;
	    char tmpbuf[256];

	    /* Get the keys */
	    if (!(status = get_servkey(inbuf, &servkey))) {
		lib$signal(status);
		return status | 0x10000000;
	    }
	    if (bufread_remaining(inbuf)) {
		RSA_free(servkey.servkey);
		RSA_free(servkey.hostkey);
		ssh_F_badpubk();
		return FISH_M_BADPUBK | 0x10000000;
	    }
	    /* Create the session key */
	    if (!(status = gen_sesskey(&servkey, state->sesskey, &enc_sesskey,
				       state))) {
		RSA_free(servkey.servkey);
		RSA_free(servkey.hostkey);
		lib$signal(status);
		return status | 0x10000000;
	    }

	    sprintf(tmpbuf, "%s%s", state->sshprefs->default_directory,
		    "KNOWN_HOSTS.DAT");
	    status = find_host(state->sshprefs->host, servkey.hostkey, tmpbuf);
	    if (status == FISH_M_HOSTOK) {
		on_state(ssh_infof("Host %s is known and matches the host key",
				   state->sshprefs->host));
	    } else if (status == FISH_M_HOSTNEW) {
		char answer[2];
		lib$signal(FISH_M_HOSTNEW, 1, state->sshprefs->host);
		if (!read_prompted("SYS$INPUT:",
				   "Are you sure you want to continue? [y/N] ",
				   answer, 2, 0, 1, &status)) {
		    lib_put_output("");
		    lib$signal(status);
		}
		if (*answer != 'Y' && *answer != 'y')
		    ssh_F_stopped("user abort");

		status = add_host(state->sshprefs->host, servkey.hostkey,
				  tmpbuf);
		if (!$VMS_STATUS_SUCCESS(status))
		    lib$signal(status);
	    } else if (status == FISH_M_HOSTCHANGED) {
		char answer[2];

		fprintf(stderr,
"@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@\n"
"@            WARNING: HOST IDENTIFICATION HAS CHANGED!            @\n"
"@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@\n"
"IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!\n"
"Someone could be eavesdropping on you right now (man-in-the-middle attack)!\n"
"It is also possible that the host key has just been changed.\n"
"Please contact the system administrator for %s.\n"
"Add correct host key in %s to get rid of this message.\n",
			state->sshprefs->host,
			tmpbuf);
		if (!read_prompted("SYS$INPUT:",
				   "Are you sure you want to continue? [y/N] ",
				   answer, 2, 0, 1, &status)) {
		    lib_put_output("");
		    lib$signal(status);
		}
		if (*answer != 'Y' && *answer != 'y')
		    ssh_F_stopped("user abort");

		state->sshprefs->auths &= ~(SSH_AUTH_NONE | SSH_AUTH_RHOSTS
					    | SSH_AUTH_PASSWORD);
		lib$signal(FISH_M_DISAUTHS, 1, "NONE, rhosts and password");
	    }

	    RSA_free(servkey.servkey);
	    RSA_free(servkey.hostkey);

	    /* If no cipher is declared, use none... */
	    prefciphers = state->sshprefs ? state->sshprefs->ciphers : 0;
	    /* 3DES is mandatory to support */
	    prefciphers |= (1 << SSH_CIPHER_3DES);

#if 0 /* We might need it again */
	    ssh_debugf("My prefered ciphermask: 0x%X", prefciphers);
	    ssh_debugf("Server's prefered ciphermask: 0x%X",
		       servkey.cipher_mask);
#endif

	    servkey.cipher_mask &= prefciphers;

#if 0 /* See above */
	    ssh_debugf("Resulting ciphermask: 0x%X", servkey.cipher_mask);
#endif

	    /* XXX:  This code is getting bulky.  Available ciphers should
	       be handled in a more elegant fashion */
	    if ((servkey.cipher_mask & (1 << SSH_CIPHER_BLOWFISH))) {
		use_cipher = SSH_CIPHER_BLOWFISH;
	    } else if ((servkey.cipher_mask & (1 << SSH_CIPHER_IDEA))) {
		use_cipher = SSH_CIPHER_IDEA;
	    } else if ((servkey.cipher_mask & (1 << SSH_CIPHER_3DES))) {
		use_cipher = SSH_CIPHER_3DES;
	    } else if ((servkey.cipher_mask & (1 << SSH_CIPHER_RC4))) {
		use_cipher = SSH_CIPHER_RC4;
	    } else if ((servkey.cipher_mask & (1 << SSH_CIPHER_DES))) {
		use_cipher = SSH_CIPHER_DES;
	    } else if ((servkey.cipher_mask & (1 << SSH_CIPHER_NONE))) {
		use_cipher = SSH_CIPHER_NONE;
	    } else {
		BN_free(enc_sesskey);
		ssh_F_nocompc();
		return FISH_M_NOCOMPC | 0x10000000;
	    }
	    state->used_cipher = use_cipher;

	    state->auth_mask = servkey.auth_mask & state->sshprefs->auths;

	    /* Send the SSH_CMSG_SESSION_KEY */
	    enc_sesskey_bytes = BN_num_bytes(enc_sesskey);
	    outbuf = buf_init(15+enc_sesskey_bytes);
	    if (!outbuf) {
		BN_free(enc_sesskey);
		ssh_F_memfull();
		return FISH_M_MEMFULL | 0x10000000;
	    }
	    buf_append_bytes(outbuf, &use_cipher, 1);
	    buf_append_bytes(outbuf, servkey.cookie, 8);
	    buf_append_bn(outbuf, enc_sesskey);
	    buf_append_bytes(outbuf, "\0\0\0\0", 4);
	    BN_free(enc_sesskey);
	    ret = ssh_write_type(SSH_CMSG_SESSION_KEY, outbuf,
				 state, erf, erfp);
	    if (ret < buf_amount(outbuf)) {
		buf_destroy(outbuf); outbuf = NULL;
		ssh_F_noconn();
		return FISH_M_NOCONN | 0x10000000;
	    }
	    buf_destroy(outbuf); outbuf = NULL;

	    /* Set up the crypto */
	    ssh_crypto_setup(use_cipher, state, 32, erf, erfp);
	    break;
	}
	case SSH_SMSG_FAILURE:
	    /* Hmmm. Something went wrong. */
	    ssh_F_nokeyex();
	    return FISH_M_NOKEYEX | 0x10000000;

	case SSH_SMSG_SUCCESS:
	    /* OK; go on, as long as we're encrypting now */
	    if (1 || state->encrypt) {
		char *prefuser;
		unsigned long prefuserlen;

		/* XXX: The day Kerberos starts getting used, there will
		   be alternate code to set prefuser. */

		/* Send the USER message */
		if (!state->sshprefs || !state->sshprefs->username ||
		    !*(state->sshprefs->username)) {
		    /* No username was supplied */
		    ssh_F_nouser();
		    return FISH_M_NOUSER | 0x10000000;
		} else {
		    prefuser = state->sshprefs->username;
		}
		prefuserlen = strlen(prefuser);
		outbuf = buf_init(prefuserlen+4);
		buf_append_int(outbuf, prefuserlen);
		buf_append_bytes(outbuf, prefuser, prefuserlen);
		ret = ssh_write_type(SSH_CMSG_USER, outbuf, state, erf, erfp);
		if (ret < buf_amount(outbuf)) {
		    buf_destroy(outbuf); outbuf = NULL;
		    ssh_F_noconn();
		    return FISH_M_NOCONN | 0x10000000;
		}
		buf_destroy(outbuf); outbuf = NULL;

		state->protophase = SSH_PHASE_AUTH;
		state->authphase = SSH_PASS_KERBEROS_TGT;
		state->triedpw = 0;

		break;
	    }
	    /* FALLTHROUGH */

	default:
	    /* Unknown packets? */
	    ssh_F_invpckt();
	    return FISH_M_INVPCKT | 0x10000000;
	}
	break;
	/* SSH_PHASE_GET_KEYS End ------------------------------------------ */

	/* SSH_PHASE_AUTH Start -------------------------------------------- */
    case SSH_PHASE_AUTH:
	/* We are expecting SUCCESS or FAILURE to our login attempts */
	switch(type) {
	case SSH_SMSG_FAILURE:
	    switch(state->authphase) {
	    case SSH_PASS_KERBEROS_TGT:
		/* Not supported, so just trickle through */
		state->authphase = SSH_PASS_AFS_TOKENS;

	    case SSH_PASS_AFS_TOKENS:
		/* Not supported, so just trickle through */
		state->authphase = SSH_AUTH_KERBEROS;

	    case SSH_AUTH_KERBEROS:
		/* Not supported, so just trickle through */
		state->authphase = SSH_AUTH_RHOSTS;

	    case SSH_AUTH_RHOSTS:
		/* Not supported, so just trickle through */
		state->authphase = SSH_AUTH_RHOSTS_RSA;

		if (state->auth_mask & (1 << SSH_AUTH_RHOSTS))
		{
		    char *local_user = getenv("USER");
		    int l = strlen(local_user);

		    outbuf = buf_init(l+4);
		    buf_append_int(outbuf, l);
		    buf_append_bytes(outbuf, local_user, l);
		    ret = ssh_write_type(SSH_CMSG_AUTH_RHOSTS, outbuf,
					 state, erf, erfp);
		    if (ret < buf_amount(outbuf)) {
			buf_destroy(outbuf); outbuf = NULL;
			ssh_F_noconn();
			return FISH_M_NOCONN | 0x10000000;
		    }
		    buf_destroy(outbuf); outbuf = NULL;
		    break;
		}

	    case SSH_AUTH_RHOSTS_RSA:
		/* Not supported, so just trickle through */
		state->authphase = SSH_AUTH_RSA;

	    case SSH_AUTH_RSA:
		/* Default to go to next method */
		state->authphase = SSH_AUTH_TIS;

		if (!(state->auth_mask & (1 << SSH_AUTH_RSA))) {
		    on_info(ssh_info("RSA authentication disabled"));
		} else if (state->userkey.bits == 0) {
		    int authlen;
		    int status;

		    on_info(ssh_info("Trying to authenticate with RSA"));

		    userkey_destroy_static(&state->userkey);
		    userkey_init_static(&state->userkey);
		    status = userkey_read_keyfile(&state->userkey,
						  state->sshprefs->identity_file,
						  state->sshprefs->uai.uic);
		    if (!$VMS_STATUS_SUCCESS(status))
			goto no_rsa;

		    state->authphase = SSH_AUTH_RSA;

		    authlen = BN_num_bytes(state->userkey.key->n);
		    outbuf = buf_init(authlen + 2); /* +2 suggested by wjm */
		    buf_append_bn(outbuf, state->userkey.key->n);
		    ret = ssh_write_type(SSH_CMSG_AUTH_RSA, outbuf,
					 state, erf, erfp);
		    if (ret < buf_amount(outbuf)) {
			buf_destroy(outbuf); outbuf = NULL;
			userkey_destroy_static(&state->userkey);
			ssh_F_noconn();
			return FISH_M_NOCONN | 0x10000000;
		    }
		    buf_destroy(outbuf); outbuf = NULL;

		    break;
		no_rsa:
		    ;
		}

	    case SSH_AUTH_TIS:
		state->authphase = SSH_AUTH_PASSWORD;

		if (state->auth_mask & (1 << SSH_AUTH_TIS))
		{
		    outbuf = buf_init(4);
		    buf_append_int(outbuf, 0);
		    ret = ssh_write_type(SSH_CMSG_AUTH_TIS, outbuf,
					 state, erf, erfp);
		    if (ret < buf_amount(outbuf)) {
			buf_destroy(outbuf); outbuf = NULL;
			ssh_F_noconn();
			return FISH_M_NOCONN | 0x10000000;
		    }
		    buf_destroy(outbuf); outbuf = NULL;
		    break;
		}
		
	    case SSH_AUTH_PASSWORD:
		if (state->auth_mask & (1 << SSH_AUTH_PASSWORD))
		{
		    char pwbuf[80] = "";
		    int status;

		    /* Have we already tried the password? */
		    if (state->triedpw & TRIED_PASSWD)
			if (state->sshprefs->commands && state->sshprefs->password) {
			    ssh_F_pssfail();
			    return FISH_M_PSSFAIL | 0x10000000;
			} else
			    ssh_W_pssfail1();
		    else {
			if (state->sshprefs->password) {
			    if (strlen(state->sshprefs->password) >= sizeof(pwbuf)) {
				ssh_F_pstoolng();
				return FISH_M_PSTOOLNG | 0x10000000;
			    }
			    strcpy(pwbuf, state->sshprefs->password);
			}
			on_info(ssh_infof("Logging in as user %s\n",
					  state->sshprefs->username));
		    }

		    if (pwbuf[0] == '\0') {
			general_buffer *prompt = buf_init(0);

			buf_append_chars_nocount(prompt,
						 state->sshprefs->username);
			buf_append_chars_nocount(prompt, "@");
			buf_append_chars_nocount(prompt,
						 state->sshprefs->host);
			buf_append_chars_nocount(prompt, "'s password: ");

			if (!read_prompted(input_device, buf_chars(prompt),
					   pwbuf, sizeof(pwbuf),
					   0, 0, &status)) {
			    buf_destroy(prompt);
			    lib$signal(status);
			}
			buf_destroy(prompt);
		    }
		    trypw = pwbuf;

		    trypwlen = strlen(trypw);
		    outbuf = buf_init(trypwlen+4);
		    buf_append_int(outbuf, trypwlen);
		    buf_append_bytes(outbuf, trypw, trypwlen);
		    ret = ssh_write_type(SSH_CMSG_AUTH_PASSWORD, outbuf,
					 state, erf, erfp);
		    state->triedpw |= TRIED_PASSWD;
		    if (ret < buf_amount(outbuf)) {
			buf_destroy(outbuf); outbuf = NULL;
			ssh_F_noconn();
			return FISH_M_NOCONN | 0x10000000;
		    }
		    buf_destroy(outbuf); outbuf = NULL;
		    break;
		}

	    default:
		/* All authentication methods have failed */
		ssh_F_perm();
	    }
	    break;

	case SSH_SMSG_SUCCESS:
	    /* Oh, good; they let us in.  Set a max packet size. */
	    outbuf = buf_init(4);
	    buf_append_int(outbuf, MAX_SSH_PACKET_LEN);
	    ret = ssh_write_type(SSH_CMSG_MAX_PACKET_SIZE, outbuf,
				 state, erf, erfp);
	    if (ret < buf_amount(outbuf)) {
		buf_destroy(outbuf); outbuf = NULL;
		ssh_F_noconn();
		return FISH_M_NOCONN | 0x10000000;
	    }
	    buf_destroy(outbuf); outbuf = NULL;

	    state->protophase = SSH_PHASE_PREP;
	    state->prepphase = SSH_PREP_COMPRESSION;
	    break;

	case SSH_SMSG_AUTH_RSA_CHALLENGE:
	{
	    char pwbuf[2048];
	    general_buffer gpwbuf;
	    BIGNUM *tmp;
	    general_buffer *tmpbuf = 0;
	    unsigned char *challenge;
	    unsigned long challengelen;
	    MD5_CTX md;
	    unsigned char responsebuf[16];
	    general_buffer response;
	    int status;

	    buf_init_static(&gpwbuf, pwbuf, sizeof pwbuf);
	    buf_init_static(&response, responsebuf, sizeof responsebuf);

	    bufread_bn(inbuf, &tmp);
	    challengelen = BN_num_bytes(tmp);
	    challenge = xmalloc(challengelen);
	    if (challenge == 0) {
		ssh_F_memfull();
		return FISH_M_MEMFULL | 0x10000000;
	    }
	    BN_bn2bin(tmp, challenge);
	    BN_clear_free(tmp);

	    trypw = 0;
	    pwbuf[0] = '\0';

	    /* Have we already tried the password? */
	    if (state->triedpw & TRIED_RSA)
		if (state->sshprefs->commands && state->sshprefs->password) {
		    ssh_F_rsafail();
		    return FISH_M_RSAFAIL | 0x10000000;
		} else
		    ssh_W_rsafail1();
	    else {
		if (state->sshprefs->password) {
		    if (strlen(state->sshprefs->password) >= sizeof(pwbuf)) {
			ssh_F_ptoolong();
			return FISH_M_PTOOLONG | 0x10000000;
		    }
		    buf_append_chars_nocount(&gpwbuf,
					     state->sshprefs->password);
		}
	    }

	    while((status = userkey_decrypt_private_part(&state->userkey,
							 &gpwbuf))
		  == FISH_M_UKBADPASS
		  && trypw == 0) {
		/* bad password, so let's try to get one */
		char prompt[73];
		static char prefix[] = "Enter passphrase for RSA key '";
		int pn = sizeof(prefix) - 1;
		int n = strlen(state->userkey.comment);

		memcpy(prompt, prefix, pn);
		if (n - pn - 4 > 73) {
		    memcpy(prompt + pn, state->userkey.comment, 73 - pn - 7);
		    memcpy(prompt + 73 - 7, "...': ", 7);
		} else {
		    memcpy(prompt + pn, state->userkey.comment, n);
		    memcpy(prompt + (n + pn), "': ", 4);
		}

		if (pwbuf[0] == '\0') {
		    if (!read_prompted(input_device, prompt,
				       pwbuf, sizeof(pwbuf),
				       0, 0, &status)) {
			lib$signal(status);
		    }
		    buf_adjust_amount_to(&gpwbuf, strlen(pwbuf));
		}
		trypw = pwbuf;
	    }

	    if (status == SS$_NORMAL) {
		RSA_private_decrypt(challengelen, challenge, challenge,
				    state->userkey.key, RSA_NO_PADDING);

		/* Compute response */
		MD5_Init(&md);
		MD5_Update(&md, challenge+challengelen-32, 32);
		MD5_Update(&md, state->sessid, 16);
		MD5_Final(responsebuf, &md);
	    } else if (status == FISH_M_UKBADPASS) {
		lib$signal(FISH_M_RSAFAIL2);
	    } else {
		lib$signal(FISH_M_BADKEYFILE,
			   1, state->sshprefs->identity_file);
	    }

	    if (status != SS$_NORMAL) {
		/* We will still create a message,
		   so protocol won't be broken */
		memset(responsebuf, 0, 16);
		state->authphase = SSH_AUTH_TIS;
	    }
	    state->triedpw |= TRIED_RSA;

	    userkey_destroy_static(&state->userkey);

	    buf_adjust_amount_by(&response, 16);
	    ret = ssh_write_type(SSH_CMSG_AUTH_RSA_RESPONSE, &response,
				 state, erf, erfp);
	    if (ret < response.amount) {
		ssh_F_noconn();
		return FISH_M_NOCONN | 0x10000000;
	    }
	    break;
	}

	    /* XXXXX This is NOT TESTED, and should probably be rewritten */
	case SSH_SMSG_AUTH_TIS_CHALLENGE:
	{
	    char pwbuf[80];

	    int status;
	    extern tty_s *input_tty;

	    /* Try a password */
	    input_tty = tty_noecho(input_device, &status);
	    if (fgets(pwbuf, sizeof(pwbuf), stdin) == NULL)
		trypw = "";
	    else
		trypw = pwbuf;

	    state->triedpw |= TRIED_TIS;
	    tty_setmode(input_tty, &status);
	    tty_close(input_tty, &status);
	    input_tty = 0;

	    if (strlen(pwbuf) > 0 && pwbuf[strlen(pwbuf)-1] == '\n')
		pwbuf[strlen(pwbuf)-1] = '\0';

	    trypwlen = strlen(trypw);
	    outbuf = buf_init(trypwlen+4);
	    buf_append_int(outbuf, trypwlen);
	    buf_append_bytes(outbuf, trypw, trypwlen);
	    ret = ssh_write_type(SSH_CMSG_AUTH_TIS_RESPONSE, outbuf,
				 state, erf, erfp);
	    if (ret < buf_amount(outbuf)) {
		buf_destroy(outbuf); outbuf = NULL;
		ssh_F_noconn();
		return FISH_M_NOCONN | 0x10000000;
	    }
	    buf_destroy(outbuf); outbuf = NULL;
	    break;
	}

	default:
	{
	    ssh_F_invpckt1(type);
	    return FISH_M_INVPCKT1 | 0x10000000;
	}
	}
	break;
	/* SSH_PHASE_AUTH End ---------------------------------------------- */

	/* SSH_PHASE_PREP Start -------------------------------------------- */
	/* This phase looks a little odd, as in the next subphase takes
	   care of the answer from the previous one... */
    case SSH_PHASE_PREP:
	switch(state->prepphase) {
	case SSH_PREP_COMPRESSION:
	    switch(type) {
		/* We've sent the MAX_PACKET_SIZE request.
		   Accept either answer. */
	    case SSH_SMSG_SUCCESS:
	    case SSH_SMSG_FAILURE:
		/* Request compression */

		/* Prepare the next phase already */
		state->prepphase = SSH_PREP_PTY;

		if (state->sshprefs->compress_p) {
		    outbuf = buf_init(4);
		    buf_append_int(outbuf, state->sshprefs->compress_p);
		    ret = ssh_write_type(SSH_CMSG_REQUEST_COMPRESSION, outbuf,
					 state, erf, erfp);
		    if (ret < buf_amount(outbuf)) {
			buf_destroy(outbuf); outbuf = NULL;
			ssh_F_noconn();
			return FISH_M_NOCONN | 0x10000000;
		    }
		    buf_destroy(outbuf); outbuf = NULL;
		    return SS$_NORMAL;
		} else
		    type = SSH_INTERNAL;
		break;
	    default:
		/* Unknown packets? */
		ssh_F_invpckt();
		return FISH_M_INVPCKT | 0x10000000;
	    }

	case SSH_PREP_PTY:
	    switch(type) {
	    case SSH_SMSG_SUCCESS:
		if (state->sshprefs->compress_p) {
		    state->compress = state->sshprefs->compress_p;
		    compress_init(0, state->compress);
		    decompress_init(0);
		}
		/* Trickle through, since we might request a PTY anyway */
	    case SSH_INTERNAL:
	    case SSH_SMSG_FAILURE:
		/* Get a pty */

		/* Prepare the next phase already */
		state->prepphase = SSH_PREP_X11;

		if (state->sshprefs->commands && !state->sshprefs->force_tty) {
		    /* We don't want a pty, so let's skip this
		       and trickle through */
		    type = SSH_INTERNAL;
		    break;
		}

		if (!state->sshprefs || !state->sshprefs->ttymodes) {
		    prefttymodes = SSH_DEF_TTYMODES;
		    prefttymodelen = SSH_DEF_TTYMODELEN;
		} else {
		    prefttymodes = state->sshprefs->ttymodes;
		    prefttymodelen = state->sshprefs->ttymodelen;
		}
		termlen = strlen(state->term ? state->term : SSH_DEF_TERM);
		outbuf = buf_init(prefttymodelen+20+termlen);
		buf_append_int(outbuf, termlen);
		buf_append_bytes(outbuf,
				 state->term ? state->term : SSH_DEF_TERM,
				 termlen);
		buf_append_int(outbuf, state->termrows);
		buf_append_int(outbuf, state->termcols);
		buf_append_int(outbuf, state->termx);
		buf_append_int(outbuf, state->termy);
		buf_append_bytes(outbuf, prefttymodes, prefttymodelen);
		ret = ssh_write_type(SSH_CMSG_REQUEST_PTY, outbuf,
				     state, erf, erfp);
		if (ret < buf_amount(outbuf)) {
		    buf_destroy(outbuf); outbuf = NULL;
		    ssh_F_noconn();
		    return FISH_M_NOCONN | 0x10000000;
		}
		buf_destroy(outbuf); outbuf = NULL;
		return SS$_NORMAL;

	    default:
		/* Unknown packets? */
		ssh_F_invpckt();
		return FISH_M_INVPCKT | 0x10000000;
	    }

	case SSH_PREP_X11:
	    switch(type) {
	    case SSH_SMSG_FAILURE:
		ssh_F_pty();
		return FISH_M_PTY | 0x10000000;
	    case SSH_INTERNAL:
	    case SSH_SMSG_SUCCESS:
		/* Not yet supported, just trickle through
		   to the next subphase */
		state->prepphase = SSH_PREP_PORT_FORWARD;
		type = SSH_INTERNAL;
		break;
	    default:
		/* Unknown packets? */
		ssh_F_invpckt();
		return FISH_M_INVPCKT | 0x10000000;
	    }

	case SSH_PREP_PORT_FORWARD:
	    switch(type) {
		/* The previous phase isn't implemented,
		   so accept either type. */
	    case SSH_SMSG_SUCCESS:
	    case SSH_INTERNAL:
	    case SSH_SMSG_FAILURE:
		/* Not yet supported, just trickle through
		   to the next subphase */
		state->prepphase = SSH_PREP_AGENT_FORWARD;
		type = SSH_INTERNAL;
		break;
	    default:
		/* Unknown packets? */
		ssh_F_invpckt();
		return FISH_M_INVPCKT | 0x10000000;
	    }

	case SSH_PREP_AGENT_FORWARD:
	    switch(type) {
		/* The previous phase isn't implemented,
		   so accept either type. */
	    case SSH_SMSG_SUCCESS:
	    case SSH_INTERNAL:
	    case SSH_SMSG_FAILURE:
		/* Not yet supported, just trickle through
		   to the next subphase */
		state->prepphase = SSH_PREP_SHELL_OR_CMD;
		type = SSH_INTERNAL;
		break;
	    default:
		/* Unknown packets? */
		ssh_F_invpckt();
		return FISH_M_INVPCKT | 0x10000000;
	    }

	case SSH_PREP_SHELL_OR_CMD:
	    switch(type) {
		/* The previous phase isn't implemented,
		   so accept either type. */
	    case SSH_SMSG_SUCCESS:
	    case SSH_INTERNAL:
	    case SSH_SMSG_FAILURE:
	    {
		int l = 0;
		unsigned char *buf;

		/* Ready to send a command or go interactive. */
		if (state->sshprefs->commands) {
		    on_state(lib$signal(FISH_M_COMMAND,
					1,state->sshprefs->commands));
		    l = strlen(state->sshprefs->commands);
		    outbuf = buf_init(l+4);
		    buf_append_int(outbuf, l);
		    buf_append_bytes(outbuf, state->sshprefs->commands, l);
		    ret = ssh_write_type(SSH_CMSG_EXEC_CMD, outbuf,
					 state, erf, erfp);
		    buf_destroy(outbuf);
		} else
		    ret = ssh_write_type(SSH_CMSG_EXEC_SHELL, NULL, state,
					 erf, erfp);
		if (ret < l) {
		    ssh_F_noconn();
		    return FISH_M_NOCONN | 0x10000000;
		}
		state->protophase = SSH_PHASE_INTERACTIVE;
		connected(state, erf, erfp);
		return SS$_NORMAL;
	    }
	    default:
		/* Unknown packets? */
		ssh_F_invpckt();
		return FISH_M_INVPCKT | 0x10000000;
	    }
	}
	break;
	/* SSH_PHASE_PREP End ---------------------------------------------- */

	/* SSH_PHASE_INTERACTIVE Start ------------------------------------- */
    case SSH_PHASE_INTERACTIVE:
	/* We expect data from the other side now */
	switch(type) {
	case SSH_SMSG_STDOUT_DATA:
	case SSH_SMSG_STDERR_DATA:
	    if (!bufread_int(inbuf, &datalen)
		|| bufread_remaining(inbuf) != datalen) {
		ssh_F_badpckt();
		return FISH_M_BADPCKT | 0x10000000;
	    }
	    if (state->up_dispatch) {
		(state->up_dispatch)(bufread_chars_noadjust(inbuf),
				     bufread_remaining(inbuf),
				     state->up_state, erf, erfp);
	    }
	    break;
	    
	case SSH_SMSG_EXITSTATUS:
	    /* The session is over.  Send a confirmation and shut it down. */
	    ssh_write_type(SSH_CMSG_EXIT_CONFIRMATION, NULL, state,
			   erf, erfp);
	    lib$signal(FISH_M_CLOSED);
	    sys$setef(done_ef);
	    return FISH_M_CLOSED;

	default:
	    /* Unknown packets? */
	    ssh_F_invpckt();
	    return FISH_M_INVPCKT | 0x10000000;
	}
	break;
	/* SSH_PHASE_INTERACTIVE End --------------------------------------- */

    default:
	/* Unknown phase? */
	ssh_F_invphase();
	return FISH_M_INVPHASE | 0x10000000;
    }
}

/* Emacs local variables

Local variables:
eval: (set-c-style "BSD")
end:

*/
