package net.i2p.router;
/*
 * free (adj.): unencumbered; not under the control of others
 * Written by jrandom in 2003 and released into the public domain 
 * with no warranty of any kind, either expressed or implied.  
 * It probably won't make your computer catch on fire, or eat 
 * your children, but it might.  Use at your own risk.
 *
 */

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import net.i2p.crypto.SigType;
import net.i2p.data.DataFormatException;
import net.i2p.data.DataStructure;
import net.i2p.data.Destination;
import net.i2p.data.Hash;
import net.i2p.data.PrivateKey;
import net.i2p.data.PublicKey;
import net.i2p.data.SigningPrivateKey;
import net.i2p.data.SigningPublicKey;
import net.i2p.router.startup.CreateRouterInfoJob;
import net.i2p.util.Log;
import net.i2p.util.SecureDirectory;
import net.i2p.util.SecureFileOutputStream;

/**
 * Maintain all of the key pairs for the router.
 * Router keys are written to files in a backup directory.
 * LeaseSet keys are not written to files.
 *
 */
public class KeyManager {
    private final Log _log;
    private final RouterContext _context;
    private PrivateKey _privateKey;
    private PublicKey _publicKey;
    private SigningPrivateKey _signingPrivateKey;
    private SigningPublicKey _signingPublicKey;
    private final Map<Hash, LeaseSetKeys> _leaseSetKeys; // Destination --> LeaseSetKeys
    
    public final static String PROP_KEYDIR = "router.keyBackupDir";
    public final static String DEFAULT_KEYDIR = "keyBackup";
    public final static String KEYFILE_PRIVATE_ENC = "privateEncryption.key";
    public final static String KEYFILE_PUBLIC_ENC = "publicEncryption.key";
    public final static String KEYFILE_PRIVATE_SIGNING = "privateSigning.key";
    public final static String KEYFILE_PUBLIC_SIGNING = "publicSigning.key";
    
    public KeyManager(RouterContext context) {
        _context = context;
        _log = _context.logManager().getLog(KeyManager.class);	
        _leaseSetKeys = new ConcurrentHashMap<Hash, LeaseSetKeys>();
    }
    
    /**
     *  Read keys in from disk, blocking
     *
     *  @deprecated we never read keys in anymore
     */
    @Deprecated
    public void startup() {
        // run inline so keys are loaded immediately
        (new SynchronizeKeysJob()).runJob();
    }
    
    /**
     *  Configure the router's keys.
     *  @since 0.9.4 replace individual setters
     */
    public void setKeys(PublicKey key1, PrivateKey key2, SigningPublicKey key3, SigningPrivateKey key4) { 
        synchronized(this) {
            _publicKey = key1; 
            _privateKey = key2; 
            _signingPublicKey = key3; 
            _signingPrivateKey = key4; 
        }
        queueWrite();
    }

    /**
     * Router key
     * @return will be null on error or before startup() or setKeys() is called
     */
    public synchronized PrivateKey getPrivateKey() { return _privateKey; }

    /**
     * Router key
     * @return will be null on error or before startup() or setKeys() is called
     */
    public synchronized PublicKey getPublicKey() { return _publicKey; }

    /**
     * Router key
     * @return will be null on error or before startup() or setKeys() is called
     */
    public synchronized SigningPrivateKey getSigningPrivateKey() { return _signingPrivateKey; }

    /**
     * Router key
     * @return will be null on error or before startup() or setKeys() is called
     */
    public synchronized SigningPublicKey getSigningPublicKey() { return _signingPublicKey; }
    
    /**
     *  Client with a single key
     *
     *  @param leaseRevocationPrivateKey unused, may be null
     */
    public void registerKeys(Destination dest, SigningPrivateKey leaseRevocationPrivateKey, PrivateKey endpointDecryptionKey) {
        if (_log.shouldInfo())
            _log.info("Registering keys for destination " + dest.toBase32());
        LeaseSetKeys keys = new LeaseSetKeys(dest, leaseRevocationPrivateKey, endpointDecryptionKey);
        _leaseSetKeys.put(dest.calculateHash(), keys);
    }
    
    /**
     *  Client with multiple keys
     *
     *  @param leaseRevocationPrivateKey unused, may be null
     *  @since 0.9.44
     */
    public void registerKeys(Destination dest, SigningPrivateKey leaseRevocationPrivateKey, List<PrivateKey> endpointDecryptionKeys) {
        if (_log.shouldInfo())
            _log.info("Registering keys for destination " + dest.toBase32());
        LeaseSetKeys keys = new LeaseSetKeys(dest, leaseRevocationPrivateKey, endpointDecryptionKeys);
        _leaseSetKeys.put(dest.calculateHash(), keys);
    }
   
    /**
     *  Read/Write the router keys from/to disk
     */
    private void queueWrite() {
        _context.jobQueue().addJob(new SynchronizeKeysJob());
    }

    /** client */
    public LeaseSetKeys unregisterKeys(Destination dest) {
        if (_log.shouldInfo())
            _log.info("Unregistering keys for destination " + dest.calculateHash().toBase64());
        return _leaseSetKeys.remove(dest.calculateHash());
    }
    
    /** client */
    public LeaseSetKeys getKeys(Destination dest) {
        return getKeys(dest.calculateHash());
    }

    /** client */
    public LeaseSetKeys getKeys(Hash dest) {
            return _leaseSetKeys.get(dest);
    }
    
    /**
     *  Read/Write the 4 files in keyBackup/
     *  As of 0.9.4 this is run on-demand only, there's no need to
     *  periodically sync.
     *  Actually, there's little need for this at all.
     *  If router.keys is corrupt, we should just make a new router identity,
     *  there's no real reason to try so hard to recover our old keys.
     */
    private class SynchronizeKeysJob extends JobImpl {
        public SynchronizeKeysJob() {
            super(KeyManager.this._context);
        }

        public void runJob() {
            String keyDir = getContext().getProperty(PROP_KEYDIR, DEFAULT_KEYDIR);
            File dir = new SecureDirectory(getContext().getRouterDir(), keyDir);
            if (!dir.exists())
                dir.mkdirs();
            if (dir.exists() && dir.isDirectory() && dir.canRead() && dir.canWrite()) {
                synchronized(KeyManager.this) {
                    syncKeys(dir);
                }
            } else {
                _log.log(Log.CRIT, "Unable to synchronize keys in " + keyDir + " - permissions problem?");
            }
        }
        
        private void syncKeys(File keyDir) {
            syncPrivateKey(keyDir);
            syncPublicKey(keyDir);
            SigType type = CreateRouterInfoJob.getSigTypeConfig(getContext());
            syncSigningKey(keyDir, type);
            syncVerificationKey(keyDir, type);
        }

        private void syncPrivateKey(File keyDir) {
            DataStructure ds;
            File keyFile = new File(keyDir, KEYFILE_PRIVATE_ENC);
            boolean exists = (_privateKey != null);
            if (exists)
                ds = _privateKey;
            else
                ds = new PrivateKey();
            DataStructure readin = syncKey(keyFile, ds, exists);
            if (readin != null && !exists)
                _privateKey = (PrivateKey) readin;
        }

        private void syncPublicKey(File keyDir) {
            DataStructure ds;
            File keyFile = new File(keyDir, KEYFILE_PUBLIC_ENC);
            boolean exists = (_publicKey != null);
            if (exists)
                ds = _publicKey;
            else
                ds = new PublicKey();
            DataStructure readin = syncKey(keyFile, ds, exists);
            if (readin != null && !exists)
                _publicKey = (PublicKey) readin;
        }

        /**
         *  @param type the SigType to expect on read-in, ignored on write
         */
        private void syncSigningKey(File keyDir, SigType type) {
            DataStructure ds;
            File keyFile = new File(keyDir, KEYFILE_PRIVATE_SIGNING);
            boolean exists = (_signingPrivateKey != null);
            if (exists)
                ds = _signingPrivateKey;
            else
                ds = new SigningPrivateKey(type);
            DataStructure readin = syncKey(keyFile, ds, exists);
            if (readin != null && !exists)
                _signingPrivateKey = (SigningPrivateKey) readin;
        }

        /**
         *  @param type the SigType to expect on read-in, ignored on write
         */
        private void syncVerificationKey(File keyDir, SigType type) {
            DataStructure ds;
            File keyFile = new File(keyDir, KEYFILE_PUBLIC_SIGNING);
            boolean exists = (_signingPublicKey != null);
            if (exists)
                ds = _signingPublicKey;
            else
                ds = new SigningPublicKey(type);
            DataStructure readin = syncKey(keyFile, ds, exists);
            if (readin != null && !exists)
                _signingPublicKey  = (SigningPublicKey) readin;
        }

        /**
         *  @param param non-null, filled-in if exists is true, or without data if exists is false
         *  @param exists write to file if true, read from file if false
         *  @return structure or null on read error
         */
        private DataStructure syncKey(File keyFile, DataStructure structure, boolean exists) {
            OutputStream out = null;
            InputStream in = null;
            try {
                if (exists) {
                    out = new BufferedOutputStream(new SecureFileOutputStream(keyFile));
                    structure.writeBytes(out);
                    return structure;
                } else {
                    if (keyFile.exists()) {
                        in = new BufferedInputStream(new FileInputStream(keyFile));
                        structure.readBytes(in);
                        return structure;
                    } else {
                        // we don't have it, and its not on disk.  oh well.
                        return null;
                    }
                }
            } catch (IOException ioe) {
                _log.error("Error syncing the structure to " + keyFile.getAbsolutePath(), ioe);
            } catch (DataFormatException dfe) {
                _log.error("Error syncing the structure with " + keyFile.getAbsolutePath(), dfe);
            } finally {
                if (out != null) try { out.close(); } catch (IOException ioe) {}
                if (in != null) try { in.close(); } catch (IOException ioe) {}
            }

            if (exists)
                return structure;
            else
                return null;
        }

        public String getName() { return "Synchronize Keys to Disk"; }
    }
}
