/* pggsymcrypt.c - conventional encryption
 *      Copyright (C) 1999 Michael Roth <mroth@gnupg.org>
 *
 * This file is part of PGG (Privacy Guard Glue).
 *
 * PGG is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * PGG is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA
 */


#include <includes.h>
#include <pgg.h>
#include <pggdebug.h>
#include <pggsymcrypt.h>


/*
 * Type casts
 */
#define sc		((PggSymcryptPtr)(_sc))
#define old_sc		((PggSymcryptPtr)(_old_sc))


PggSymcrypt pgg_symcrypt_new(PggErrenv errenv)
{
    PggSymcryptPtr	new_sc;
    
    PGG_CHECK_SKIP_ARG(NULL);
    
    if (!( new_sc = _malloc(PggSymcrypt) ))
        PGG_RETURN_ERR_ARG(RESOURCE, MEMORY, NULL);
    
    memset(new_sc, 0, _size(PggSymcrypt));
    
    new_sc->magic      = PggSymcryptMAGIC;
    new_sc->refcounter = 1;
    
    return _hd(PggSymcrypt, new_sc);
}


PggSymcrypt pgg_symcrypt_clone(PggSymcrypt _old_sc, PggErrenv errenv)
{
    PggErrenv		local_errenv;
    PggSymcryptPtr	new_sc;
    int			i;
    
    PGG_STD_ASSERT_ARG(PggSymcrypt, old_sc, NULL);
    
    pgg_errenv_reset(local_errenv);
    
    if (!( new_sc = _malloc(PggSymcrypt) ))
        PGG_RETURN_ERR_ARG(RESOURCE, MEMORY, NULL);
    
    memset(new_sc, 0, _size(PggSymcrypt));
    
    new_sc->magic      = PggSymcryptMAGIC;
    new_sc->refcounter = 1;
    
    if (old_sc->passphrase && !(new_sc->passphrase = strdup(old_sc->passphrase)) ) {
        free(new_sc);
        PGG_RETURN_ERR_ARG(RESOURCE, MEMORY, NULL);
    }
    
    if (old_sc->stdio)
        new_sc->stdio = pgg_stdio_clone(old_sc->stdio, local_errenv);
    
    if (old_sc->opts)
        new_sc->opts = pgg_outopts_clone(old_sc->opts, local_errenv);
    
    if (old_sc->cfg)
        new_sc->cfg = pgg_config_clone(old_sc->cfg, local_errenv);
    
    if (old_sc->algo)
        new_sc->algo = pgg_algo_clone(old_sc->algo, local_errenv);
    
    if (pgg_errenv_is_set(local_errenv)) {
        if (new_sc->passphrase) {
            for(i=0; new_sc->passphrase[i]; ++i)
                new_sc->passphrase[i] = 0;
            free(new_sc->passphrase);
        }
        
        if (new_sc->stdio)
            pgg_stdio_release(new_sc->stdio, NULL);
        
        if (new_sc->opts)
            pgg_outopts_release(new_sc->opts, NULL);
        
        if (new_sc->cfg)
            pgg_config_release(new_sc->cfg, NULL);
        
        if (new_sc->algo)
            pgg_algo_release(new_sc->algo, NULL);
        
        free(new_sc);
        
        pgg_errenv_copy(errenv, local_errenv);
        return NULL;
    }
    
    return _hd(PggSymcrypt, new_sc);
}


void pgg_symcrypt_addref(PggSymcrypt _sc, PggErrenv errenv)
{
    PGG_STD_ASSERT(PggSymcrypt, sc);
    sc->refcounter++;
}


void pgg_symcrypt_release(PggSymcrypt _sc, PggErrenv errenv)
{
    PggErrenv		local_errenv;
    int			i;
    
    PGG_STD_ASSERT(PggSymcrypt, sc);
    
    pgg_errenv_reset(local_errenv);
    
    if (!--sc->refcounter) {
        if (sc->passphrase) {
            for(i=0; sc->passphrase[i]; ++i)		/* Wipe out passphrase */
                sc->passphrase[i] = 0;
            free(sc->passphrase);
        }
        
        if (sc->stdio)
            pgg_stdio_release(sc->stdio, local_errenv);
        
        if (sc->opts)
            pgg_outopts_release(sc->opts, local_errenv);
        
        if (sc->cfg)
            pgg_config_release(sc->cfg, local_errenv);
        
        if (sc->algo)
            pgg_algo_release(sc->algo, local_errenv);
        
        free(sc);
    }
    
    if (pgg_errenv_is_set(local_errenv)) {
        PGG_DEBUG(("errors on releasing symcrypt!?!?"));
        pgg_errenv_copy(errenv, local_errenv);
    }
}


void pgg_symcrypt_set_passphrase(PggSymcrypt _sc, char *passphrase, PggErrenv errenv)
{
    int			i;
    char *		new_passphrase = NULL;
    
    PGG_STD_ASSERT(PggSymcrypt, sc);
    PGG_ASSERT(passphrase, ARGUMENT, NULLPTR);		/* We don't allow a NULL passphrase because this is worthless */
    
    if (passphrase)
        if (!( new_passphrase = strdup(passphrase) ))
            PGG_RETURN_ERR(RESOURCE, MEMORY);
    
    if (sc->passphrase) {
        for(i=0; sc->passphrase[i]; ++i)
            sc->passphrase[i] = 0;
        free(sc->passphrase);
    }
    
    sc->passphrase = new_passphrase;
}


void pgg_symcrypt_set_stdio(PggSymcrypt _sc, PggStdio stdio, PggErrenv errenv)
{
    PggErrenv		local_errenv;
    
    PGG_STD_ASSERT(PggSymcrypt, sc);
    
    pgg_errenv_reset(local_errenv);
    
    if (stdio)
        pgg_stdio_addref(stdio, local_errenv);
    
    if (!pgg_errenv_is_set(local_errenv) && sc->stdio)
        pgg_stdio_release(sc->stdio, NULL);
    
    if (!pgg_errenv_is_set(local_errenv))
        sc->stdio = stdio;
    else
        pgg_errenv_copy(errenv, local_errenv);
}


void pgg_symcrypt_set_outopts(PggSymcrypt _sc, PggOutopts opts, PggErrenv errenv)
{
    PggErrenv		local_errenv;
    
    PGG_STD_ASSERT(PggSymcrypt, sc);
    
    pgg_errenv_reset(local_errenv);
    
    if (opts)
        pgg_outopts_addref(opts, local_errenv);
    
    if (!pgg_errenv_is_set(local_errenv) && sc->opts)
        pgg_outopts_release(sc->opts, local_errenv);
    
    if (!pgg_errenv_is_set(local_errenv))
        sc->opts = opts;
    else
        pgg_errenv_copy(errenv, local_errenv);
}


void pgg_symcrypt_set_config(PggSymcrypt _sc, PggConfig cfg, PggErrenv errenv)
{
    PggErrenv		local_errenv;
    
    PGG_STD_ASSERT(PggSymcrypt, sc);
    
    pgg_errenv_reset(local_errenv);
    
    if (cfg)
        pgg_config_addref(cfg, local_errenv);
    
    if (!pgg_errenv_is_set(local_errenv) && sc->cfg)
        pgg_config_release(sc->cfg, local_errenv);
    
    if (!pgg_errenv_is_set(local_errenv))
        sc->cfg = cfg;
    else
       pgg_errenv_copy(errenv, local_errenv);
}


void pgg_symcrypt_set_algo(PggSymcrypt _sc, PggAlgo algo, PggErrenv errenv)
{
    PggErrenv           local_errenv;
    
    PGG_STD_ASSERT(PggSymcrypt, sc);
    
    pgg_errenv_reset(local_errenv);
    
    if (algo)
        pgg_algo_addref(algo, local_errenv);
    
    if (!pgg_errenv_is_set(local_errenv) && sc->algo)
        pgg_algo_release(sc->algo, local_errenv);
    
    if (!pgg_errenv_is_set(local_errenv))
        sc->algo = algo;
    else
        pgg_errenv_copy(errenv, local_errenv);
}


/*
 * FIXME: we need a maintenance review on this function. Especially error
 * handling and resource management.
 */
void pgg_symcrypt_encrypt(PggSymcrypt _sc, PggErrenv errenv)
{
    int			state;
    PggExe		exe;
    int			event;
    int			status;
    PggErrenv		local_errenv;
    
    PGG_STD_ASSERT(PggSymcrypt, sc);
    PGG_ASSERT(sc->passphrase, REQUEST, STATE);
    PGG_ASSERT(sc->stdio, REQUEST, STATE);
    
    pgg_errenv_reset(local_errenv);
    
    exe = pgg_exe_new(local_errenv);
    
    if (sc->cfg)
        pgg_config_setup_exe(sc->cfg, exe, local_errenv);
    else {
        PggConfig cfg = pgg_config_new(local_errenv);
        pgg_config_setup_exe(cfg, exe, local_errenv);
        pgg_config_release(cfg, local_errenv);
    }
    
    if (sc->algo)
        pgg_algo_setup_exe(sc->algo, exe, local_errenv);
    
    if (sc->opts)
        pgg_outopts_setup_exe(sc->opts, exe, local_errenv);
    
    pgg_exe_add_arg(exe, "--symmetric", local_errenv);
    
    pgg_stdio_setup_exe(sc->stdio, exe, local_errenv);
    
    pgg_exe_request_status(exe, local_errenv);
    pgg_exe_request_shm(exe, local_errenv);
    
    pgg_exe_start(exe, local_errenv);
    
    if (pgg_errenv_is_set(local_errenv)) {
        PGG_DEBUG(("errors on setting up the exe environment"));
        pgg_exe_release(exe, NULL);
        pgg_errenv_copy(errenv, local_errenv);
        return;
    }
    
    PGG_DEBUG(("pgg_exe_start(exe) ok"));
    
    state = 0;
    while (state < 3) {
        switch ((event = pgg_exe_wait_event(exe, local_errenv))) {
            case PGG_EXE_EVENT_STATUS:
                switch ((status = pgg_exe_get_status_code(exe, local_errenv))) {
                    case PGG_EXE_STATUS_NEED_PASSPHRASE_SYM:
                        state = 1;
                        break;
                    
                    case PGG_EXE_STATUS_SHM_GET_HIDDEN:
                        if (state == 1 && strcmp(pgg_exe_get_status_arg(exe, local_errenv), "passphrase.enter")==0) {
                            pgg_exe_reply_str(exe, sc->passphrase, local_errenv);
                            state = 2;
                        }
                        else
                           PGG_RETURN_ERR(UNKNOWN, NONE); 
                        break;
                    
                    case PGG_EXE_STATUS_SHM_GET_BOOL:
                        if (strcmp(pgg_exe_get_status_arg(exe, local_errenv), "openfile.overwrite.okay") == 0) {
                            pgg_exe_reply_bool(exe, 1, local_errenv);
                        }
                        break;
                    
                    default:
                        PGG_DEBUG(("unknown status: %d\n", status));
                }
                break;
            
            case PGG_EXE_EVENT_FINISHED:
                if (state != 2) {
                    pgg_exe_release(exe, local_errenv);
                    PGG_RETURN_ERR(UNKNOWN, NONE);
                }
                state = 3;
                break;
            
            default:
                PGG_DEBUG(("unknown event: %d\n", event));
                pgg_exe_release(exe, local_errenv);
                PGG_RETURN_ERR(UNKNOWN, NONE);
        }
        
        if (pgg_errenv_is_set(local_errenv)) {
            PGG_DEBUG(("unhandled error during event handling"));
            pgg_errenv_copy(errenv, local_errenv);
            return;
        }
    }
    
    pgg_exe_release(exe, local_errenv);
    
    if (pgg_errenv_is_set(local_errenv)) {
        PGG_DEBUG(("unhandled error after complete event handling occured"));
        pgg_errenv_copy(errenv, local_errenv);
    }
}


void pgg_symcrypt_decrypt(PggSymcrypt _sc, PggErrenv errenv)
{
    int			state;
    PggExe		exe;
    int			event;
    int			status;
    PggErrenv		local_errenv;
    
    PGG_STD_ASSERT(PggSymcrypt, sc);
    PGG_ASSERT(sc->passphrase, REQUEST, STATE);
    PGG_ASSERT(sc->stdio, REQUEST, STATE);
    
    pgg_errenv_reset(local_errenv);
    
    exe = pgg_exe_new(local_errenv);
    
    if (sc->cfg)
        pgg_config_setup_exe(sc->cfg, exe, local_errenv);
    else {
        PggConfig cfg = pgg_config_new(local_errenv);
        pgg_config_setup_exe(cfg, exe, local_errenv);
        pgg_config_release(cfg, local_errenv);
    }
    
    pgg_exe_add_arg(exe, "--decrypt", local_errenv);
    
    pgg_stdio_setup_exe(sc->stdio, exe, local_errenv);
    
    pgg_exe_request_status(exe, local_errenv);
    pgg_exe_request_shm(exe, local_errenv);
    
    pgg_exe_start(exe, local_errenv);
    
    if (pgg_errenv_is_set(local_errenv)) {
        PGG_DEBUG(("problems on starting gpg"));
        pgg_exe_release(exe, NULL);
        pgg_errenv_copy(errenv, local_errenv);
        return;
    }
    
    state = 0;
    while (state < 4) {
        switch ((event = pgg_exe_wait_event(exe, local_errenv))) {
            case PGG_EXE_EVENT_STATUS:
                switch ((status = pgg_exe_get_status_code(exe, local_errenv))) {
                    case PGG_EXE_STATUS_NEED_PASSPHRASE_SYM:
                        state = 1;
                        break;
                    
                    case PGG_EXE_STATUS_SHM_GET_HIDDEN:
                        if (state == 1 && strcmp(pgg_exe_get_status_arg(exe, local_errenv), "passphrase.enter")==0) {
                            pgg_exe_reply_str(exe, sc->passphrase, local_errenv);
                            state = 2;
                        }    
                        else {
                            PGG_DEBUG(("unknown string request: \"%s\"", pgg_exe_get_status_arg(exe, NULL)));
                            pgg_exe_release(exe, NULL);
                            PGG_RETURN_ERR(GNUPG, UNEXPECTED);
                        }
                        break;
                    
                    case PGG_EXE_STATUS_SHM_GET_BOOL:
                        if (strcmp(pgg_exe_get_status_arg(exe, local_errenv), "openfile.overwrite.okay") == 0) {
                            pgg_exe_reply_bool(exe, 1, local_errenv);
                        }
                        else {
                            PGG_DEBUG(("unknown bool request: \"%s\"", pgg_exe_get_status_arg(exe, NULL)));
                            pgg_exe_release(exe, NULL); 
                            PGG_RETURN_ERR(GNUPG, UNEXPECTED);
                        }
                        break;
                    
                    case PGG_EXE_STATUS_DECRYPTION_OKAY:
                        state = 3;	/* Yeah, we got it */
                        break;
                    
                    case PGG_EXE_STATUS_DECRYPTION_FAILED:
                        PGG_DEBUG(("decryption failed (wrong passphrase)"));
                        pgg_exe_release(exe, NULL);
                        PGG_RETURN_ERR(CRYPT, PASSPHRASE);
                        break;
                    
                    case PGG_EXE_STATUS_MISSING_PASSPHRASE:
                        PGG_DEBUG(("no passphrase (i.e. a passphrase with length null)"));
                        pgg_exe_release(exe, NULL);
                        PGG_RETURN_ERR(CRYPT, PASSPHRASE);
                        break;
                    
                    default:
                        PGG_DEBUG(("unknown status: %d (ignoring)\n", status));
                }
                break;
            
            case PGG_EXE_EVENT_FINISHED:
                if (state != 3) {
                    PGG_DEBUG(("unexpected exit of GnuPG"));
                    pgg_exe_release(exe, NULL);
                    PGG_RETURN_ERR(UNKNOWN, NONE);
                }
                state = 4;
                break;
            
            default:
                PGG_DEBUG(("unknown event: %d\n", event));
                pgg_exe_release(exe, local_errenv);
                PGG_RETURN_ERR(UNKNOWN, NONE);
        }
        
        if (pgg_errenv_is_set(local_errenv)) {
            PGG_DEBUG(("unhandled error during event handling"));
            pgg_exe_release(exe, NULL);
            pgg_errenv_copy(errenv, local_errenv);
            return;
        }
    }
    
    pgg_exe_release(exe, local_errenv);
    
    if (pgg_errenv_is_set(local_errenv)) {
        PGG_DEBUG(("unhandled error after complete event handling occured"));
        pgg_errenv_copy(errenv, local_errenv);
    }
}






