package net.i2p.crypto;

import java.io.EOFException;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.security.DigestInputStream;
import java.security.DigestOutputStream;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.cert.X509Certificate;
import java.security.cert.X509CRL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Properties;

import gnu.getopt.Getopt;

import net.i2p.I2PAppContext;
import net.i2p.data.DataFormatException;
import net.i2p.data.DataHelper;
import net.i2p.data.Signature;
import net.i2p.data.SimpleDataStructure;
import net.i2p.util.SecureFileOutputStream;

/**
 *  Succesor to the ".sud" format used in TrustedUpdate.
 *  Format specified in http://www.i2p2.de/updates
 * 
 *  @since 0.9.8
 */
public class SU3File {

    private final I2PAppContext _context;

    private final File _file;
    private String _version;
    private int _versionLength;
    private String _signer;
    private int _signatureLength;
    private int _signerLength;
    private int _fileType = -1;
    private ContentType _contentType;
    private long _contentLength;
    private PublicKey _signerPubkey;
    private boolean _headerVerified;
    private SigType _sigType;
    private boolean _verifySignature = true;
    private File _certFile;

    public static final String MAGIC = "I2Psu3";
    private static final byte[] MAGIC_BYTES = DataHelper.getASCII(MAGIC);
    private static final int FILE_VERSION = 0;
    private static final int MIN_VERSION_BYTES = 16;
    private static final int VERSION_OFFSET = 40; // Signature.SIGNATURE_BYTES; avoid early ctx init

    /**
     *  The file type is advisory and is application-dependent.
     *  The following values are defined but any value 0-255 is allowed.
     */
    public static final int TYPE_ZIP = 0;
    /** @since 0.9.15 */
    public static final int TYPE_XML = 1;
    /** @since 0.9.15 */
    public static final int TYPE_HTML = 2;
    /** @since 0.9.17 */
    public static final int TYPE_XML_GZ = 3;
    /** @since 0.9.28 */
    public static final int TYPE_TXT_GZ = 4;
    /** @since 0.9.51 */
    public static final int TYPE_DMG = 5;
    /** @since 0.9.51 */
    public static final int TYPE_EXE = 6;

    public static final int CONTENT_UNKNOWN = 0;
    public static final int CONTENT_ROUTER = 1;
    public static final int CONTENT_PLUGIN = 2;
    public static final int CONTENT_RESEED = 3;
    /** @since 0.9.15 */
    public static final int CONTENT_NEWS = 4;
    /** @since 0.9.28 */
    public static final int CONTENT_BLOCKLIST = 5;

    /**
     *  The ContentType is the trust domain for the content.
     *  The signer and signature will be checked with the
     *  trusted certificates for that type.
     */
    private enum ContentType {
        UNKNOWN(CONTENT_UNKNOWN, "unknown"),
        ROUTER(CONTENT_ROUTER, "router"),
        PLUGIN(CONTENT_PLUGIN, "plugin"),
        RESEED(CONTENT_RESEED, "reseed"),
        NEWS(CONTENT_NEWS, "news"),
        BLOCKLIST(CONTENT_BLOCKLIST, "blocklist")
        ;

        private final int code;
        private final String name;

        ContentType(int code, String name) {
            this.code = code;
            this.name = name;
        }
        public int getCode() { return code; }
        public String getName() { return name; }

        /** @return null if not supported */
        public static ContentType getByCode(int code) {
            return BY_CODE.get(Integer.valueOf(code));
        }
    }

    private static final Map<Integer, ContentType> BY_CODE = new HashMap<Integer, ContentType>();

    static {
        for (ContentType type : ContentType.values()) {
            BY_CODE.put(Integer.valueOf(type.getCode()), type);
        }
    }

    private static final ContentType DEFAULT_CONTENT_TYPE = ContentType.UNKNOWN;
    // avoid early ctx init
    //private static final SigType DEFAULT_SIG_TYPE = SigType.DSA_SHA1;
    private static final int DEFAULT_SIG_CODE = 6;

    /**
     *
     */
    public SU3File(String file) {
        this(new File(file));
    }

    /**
     *
     */
    public SU3File(File file) {
        this(I2PAppContext.getGlobalContext(), file);
    }

    /**
     *
     */
    public SU3File(I2PAppContext context, File file) {
        _context = context;
        _file = file;
    }

    /**
     *  Should the signature be verified? Default true
     *  @since 0.9.15
     */
    public void setVerifySignature(boolean shouldVerify) {
        _verifySignature = shouldVerify;
    }

    /**
     *  Use this X.509 cert file for verification instead of $I2P/certificates/content_type/foo_at_mail.i2p
     *  @since 0.9.15
     */
    private void setPublicKeyCertificate(File certFile) {
        _certFile = certFile;
    }

    /**
     *  This does not check the signature, but it will fail if the signer is unknown,
     *  unless setVerifySignature(false) has been called.
     */
    public String getVersionString() throws IOException {
        verifyHeader();
        return _version;
    }

    /**
     *  This does not check the signature, but it will fail if the signer is unknown,
     *  unless setVerifySignature(false) has been called.
     */
    public String getSignerString() throws IOException {
        verifyHeader();
        return _signer;
    }

    /**
     *  This does not check the signature, but it will fail if the signer is unknown,
     *  unless setVerifySignature(false) has been called.
     *
     *  @return null if unknown
     *  @since 0.9.9
     */
    public SigType getSigType() throws IOException {
        verifyHeader();
        return _sigType;
    }

    /**
     *  The ContentType is the trust domain for the content.
     *  The signer and signature will be checked with the
     *  trusted certificates for that type.
     *
     *  This does not check the signature, but it will fail if the signer is unknown,
     *  unless setVerifySignature(false) has been called.
     *
     *  @return -1 if unknown
     *  @since 0.9.9
     */
    public int getContentType() throws IOException {
        verifyHeader();
        return _contentType != null ? _contentType.getCode() : -1;
    }

    /**
     *  The file type is advisory and is application-dependent.
     *  The following values are defined but any value 0-255 is allowed.
     *
     *  This does not check the signature, but it will fail if the signer is unknown,
     *  unless setVerifySignature(false) has been called.
     *
     *  @return 0-255 or -1 if unknown
     *  @since 0.9.15
     */
    public int getFileType() throws IOException {
        verifyHeader();
        return _fileType;
    }

    /**
     *  This does not check the signature, but it will fail if the signer is unknown,
     *  unless setVerifySignature(false) has been called.
     *
     *  Throws IOE if verify vails.
     */
    public void verifyHeader() throws IOException {
        if (_headerVerified)
            return;
        InputStream in = null;
        try {
            in = new FileInputStream(_file);
            verifyHeader(in);
        } catch (DataFormatException dfe) {
            IOException ioe = new IOException("foo");
            ioe.initCause(dfe);
            throw ioe;
        } finally {
            if (in != null) try { in.close(); } catch (IOException ioe) {}
        }
    }

    /**
     *  Throws if verify vails.
     */
    private void verifyHeader(InputStream in) throws IOException, DataFormatException {
        byte[] magic = new byte[MAGIC_BYTES.length];
        DataHelper.read(in, magic);
        if (!DataHelper.eq(magic, MAGIC_BYTES))
            throw new IOException("Not an su3 file");
        skip(in, 1);
        int foo = in.read();
        if (foo != FILE_VERSION)
            throw new IOException("bad file version");
        int sigTypeCode = (int) DataHelper.readLong(in, 2);
        _sigType = SigType.getByCode(sigTypeCode);
        // In verifyAndMigrate it reads this far then rewinds, but we don't need to here
        if (_sigType == null)
            throw new IOException("unknown sig type: " + sigTypeCode);
        _signatureLength = (int) DataHelper.readLong(in, 2);
        if (_signatureLength != _sigType.getSigLen())
            throw new IOException("bad sig length");
        skip(in, 1);
        int _versionLength = in.read();
        if (_versionLength < MIN_VERSION_BYTES)
            throw new IOException("bad version length");
        skip(in, 1);
        _signerLength = in.read();
        if (_signerLength <= 0)
            throw new IOException("bad signer length");
        _contentLength = DataHelper.readLong(in, 8);
        if (_contentLength <= 0)
            throw new IOException("bad content length");
        skip(in, 1);
        _fileType = in.read();
        // Allow any file type
        //if (_fileType != TYPE_ZIP && _fileType != TYPE_XML)
        //    throw new IOException("bad file type");
        skip(in, 1);
        int cType = in.read();
        _contentType = BY_CODE.get(Integer.valueOf(cType));
        if (_contentType == null)
            throw new IOException("unknown content type " + cType);
        skip(in, 12);

        byte[] data = new byte[_versionLength];
        int bytesRead = DataHelper.read(in, data);
        if (bytesRead != _versionLength)
            throw new EOFException();
        int zbyte;
        for (zbyte = 0; zbyte < _versionLength; zbyte++) {
            if (data[zbyte] == 0x00)
                break;
        }
        _version = new String(data, 0, zbyte, "UTF-8");

        data = new byte[_signerLength];
        bytesRead = DataHelper.read(in, data);
        if (bytesRead != _signerLength)
            throw new EOFException();
        _signer = DataHelper.getUTF8(data);

        if (_verifySignature) {
            if (_certFile != null) {
                _signerPubkey = loadKey(_certFile);
            } else {
                // look in both install dir and config dir for the signer cert
                KeyRing ring = new DirKeyRing(new File(_context.getBaseDir(), "certificates"));
                try {
                    _signerPubkey = ring.getKey(_signer, _contentType.getName(), _sigType);
                } catch (GeneralSecurityException gse) {
                    IOException ioe = new IOException("Certificate error for " + _signer + ": " + gse);
                    ioe.initCause(gse);
                    throw ioe;
                }
                if (_signerPubkey == null) {
                    boolean diff = true;
                    try {
                        diff = !_context.getBaseDir().getCanonicalPath().equals(_context.getConfigDir().getCanonicalPath());
                    } catch (IOException ioe) {}
                    if (diff) {
                        ring = new DirKeyRing(new File(_context.getConfigDir(), "certificates"));
                        try {
                            _signerPubkey = ring.getKey(_signer, _contentType.getName(), _sigType);
                        } catch (GeneralSecurityException gse) {
                            IOException ioe = new IOException("Certificate error for " + _signer + ": " + gse);
                            ioe.initCause(gse);
                            throw ioe;
                        }
                    }
                    if (_signerPubkey == null)
                        throw new IOException("unknown signer: " + _signer + " for content type: " + _contentType.getName());
                }
            }
        }
        _headerVerified = true;
    }

    /** skip but update digest */
    private static void skip(InputStream in, int cnt) throws IOException {
        for (int i = 0; i < cnt; i++) {
            if (in.read() < 0)
                throw new EOFException();
        }
    }

    private int getContentOffset() throws IOException {
        verifyHeader();
        return VERSION_OFFSET + _versionLength + _signerLength;
    }

    /**
     *  One-pass verify.
     *  Throws IOE on all format errors.
     *
     *  @return true if signature is good
     *  @since 0.9.9
     */
    public boolean verify() throws IOException {
        return verifyAndMigrate(null);
    }

    /**
     *  One-pass verify and extract the content.
     *  Recommend extracting to a temp location as the sig is not checked until
     *  after extraction. This will delete the file if the sig does not verify.
     *  Throws IOE on all format errors.
     *
     *  @param migrateTo the output file, probably in zip format. Null for verify only.
     *  @return true if signature is good
     */
    public boolean verifyAndMigrate(File migrateTo) throws IOException {
        InputStream in = null;
        FileOutputStream out = null;
        boolean rv = false;
        try {
            in = new BufferedInputStream(new FileInputStream(_file));
            // read 10 bytes to get the sig type
            in.mark(10);
            // following is a dup of that in verifyHeader()
            byte[] magic = new byte[MAGIC_BYTES.length];
            DataHelper.read(in, magic);
            if (!DataHelper.eq(magic, MAGIC_BYTES))
                throw new IOException("Not an su3 file");
            skip(in, 1);
            int foo = in.read();
            if (foo != FILE_VERSION)
                throw new IOException("bad file version");
            skip(in, 1);
            int sigTypeCode = in.read();
            _sigType = SigType.getByCode(sigTypeCode);
            if (_sigType == null)
                throw new IOException("unknown sig type: " + sigTypeCode);
            // end duplicate code
            // rewind
            in.reset();
            MessageDigest md = _sigType.getDigestInstance();
            DigestInputStream din = new DigestInputStream(in, md);
            in = din;
            if (!_headerVerified)
                verifyHeader(in);
            else
                skip(in, getContentOffset());
            if (_verifySignature) {
                if (_signerPubkey == null)
                    throw new IOException("unknown signer: " + _signer + " for content type: " + _contentType.getName());
            }
            if (migrateTo != null)  // else verify only
                out = new SecureFileOutputStream(migrateTo);
            byte[] buf = new byte[16*1024];
            long tot = 0;
            while (tot < _contentLength) {
                int read = in.read(buf, 0, (int) Math.min(buf.length, _contentLength - tot));
                if (read < 0)
                    throw new EOFException();
                if (migrateTo != null)  // else verify only
                    out.write(buf, 0, read);
                tot += read;
            }
            if (_verifySignature) {
                byte[] sha = md.digest();
                din.on(false);
                Signature signature = new Signature(_sigType);
                signature.readBytes(in);
                int avail = in.available();
                if (avail > 0)
                    throw new IOException(avail + " bytes data after sig");
                SimpleDataStructure hash = _sigType.getHashInstance();
                hash.setData(sha);
                //System.out.println("hash\n" + HexDump.dump(sha));
                //System.out.println("sig\n" + HexDump.dump(signature.getData()));
                rv = _context.dsa().verifySignature(signature, hash, _signerPubkey);
            } else {
                rv = true;
            }
        } catch (DataFormatException dfe) {
            IOException ioe = new IOException("foo");
            ioe.initCause(dfe);
            throw ioe;
        } finally {
            if (in != null) try { in.close(); } catch (IOException ioe) {}
            if (out != null) {
                // We will generally be reading this file right back in,
                // so do a POSIX flush and sync to ensure it will be there.
                try {
                    out.flush();
                    out.getFD().sync();
                } catch (IOException ioe) {}
                try { out.close(); } catch (IOException ioe) {}
            }
            if (migrateTo != null && !rv)
                migrateTo.delete();
        }
        return rv;
    }

    /**
     *  One-pass wrap and sign the content.
     *  Writes to the file specified in the constructor.
     *  Throws on all errors.
     *
     *  @param content the input file, probably in zip format
     *  @param fileType 0-255, 0 for zip
     *  @param contentType 0-255
     *  @param version 1-255 bytes when converted to UTF-8
     *  @param signer ID of the public key, 1-255 bytes when converted to UTF-8
     */
    public void write(File content, int fileType, int contentType, String version,
                      String signer, PrivateKey privkey, SigType sigType) throws IOException {
        InputStream in = null;
        DigestOutputStream out = null;
        boolean ok = false;
        try {
            in = new BufferedInputStream(new FileInputStream(content));
            MessageDigest md = sigType.getDigestInstance();
            out = new DigestOutputStream(new BufferedOutputStream(new FileOutputStream(_file)), md);
            out.write(MAGIC_BYTES);
            out.write((byte) 0);
            out.write((byte) FILE_VERSION);
            DataHelper.writeLong(out, 2, sigType.getCode());
            DataHelper.writeLong(out, 2, sigType.getSigLen());
            out.write((byte) 0);
            byte[] verBytes = DataHelper.getUTF8(version);
            if (verBytes.length == 0 || verBytes.length > 255)
                throw new IllegalArgumentException("bad version length");
            int verLen = Math.max(verBytes.length, MIN_VERSION_BYTES);
            out.write((byte) verLen);
            out.write((byte) 0);
            byte[] signerBytes = DataHelper.getUTF8(signer);
            if (signerBytes.length == 0 || signerBytes.length > 255)
                throw new IllegalArgumentException("bad signer length");
            out.write((byte) signerBytes.length);
            long contentLength = content.length();
            if (contentLength <= 0)
                throw new IllegalArgumentException("No content");
            DataHelper.writeLong(out, 8, contentLength);
            out.write((byte) 0);
            if (fileType < 0 || fileType > 255)
                throw new IllegalArgumentException("bad content type");
            out.write((byte) fileType);
            out.write((byte) 0);
            if (contentType < 0 || contentType > 255)
                throw new IllegalArgumentException("bad content type");
            out.write((byte) contentType);
            out.write(new byte[12]);
            out.write(verBytes);
            if (verBytes.length < MIN_VERSION_BYTES)
                out.write(new byte[MIN_VERSION_BYTES - verBytes.length]);
            out.write(signerBytes);

            byte[] buf = new byte[16*1024];
            long tot = 0;
            while (tot < contentLength) {
                int read = in.read(buf, 0, (int) Math.min(buf.length, contentLength - tot));
                if (read < 0)
                    throw new EOFException();
                out.write(buf, 0, read);
                tot += read;
            }

            byte[] sha = md.digest();
            out.on(false);
            SimpleDataStructure hash = sigType.getHashInstance();
            hash.setData(sha);
            Signature signature = _context.dsa().sign(hash, privkey, sigType);
            if (signature == null)
                throw new IOException("sig fail");
            //System.out.println("hash\n" + HexDump.dump(sha));
            //System.out.println("sig\n" + HexDump.dump(signature.getData()));
            signature.writeBytes(out);
            ok = true;
        } catch (DataFormatException dfe) {
            IOException ioe = new IOException("foo");
            ioe.initCause(dfe);
            throw ioe;
        } finally {
            if (in != null) try { in.close(); } catch (IOException ioe) {}
            if (out != null) try { out.close(); } catch (IOException ioe) {}
            if (!ok)
                _file.delete();
        }
    }

    /**
     * Parses command line arguments when this class is used from the command
     * line.
     * Exits 1 on failure so this can be used in scripts.
     * 
     * @param args Command line parameters.
     */
    public static void main(String[] args) {
        boolean ok = false;
        try {
            // defaults
            String stype = null;
            String ctype = null;
            String ftype = null;
            String kfile = null;
            String crlfile = null;
            String kspass = KeyStoreUtil.DEFAULT_KEYSTORE_PASSWORD;
            boolean error = false;
            boolean shouldVerify = true;
            Getopt g = new Getopt("SU3File", args, "t:c:f:k:xp:r:");
            int c;
            while ((c = g.getopt()) != -1) {
              switch (c) {
                case 't':
                    stype = g.getOptarg();
                    break;

                case 'c':
                    ctype = g.getOptarg();
                    break;

                case 'f':
                    ftype = g.getOptarg();
                    break;

                case 'k':
                    kfile = g.getOptarg();
                    break;

                case 'r':
                    crlfile = g.getOptarg();
                    break;

                case 'x':
                    shouldVerify = false;
                    break;

                case 'p':
                    kspass = g.getOptarg();
                    break;

                case '?':
                case ':':
                default:
                  error = true;
              }
            }

            int idx = g.getOptind();
            String cmd = args[idx].toLowerCase(Locale.US);
            List<String> a = new ArrayList<String>(Arrays.asList(args).subList(idx + 1, args.length));

            if (error) {
                showUsageCLI();
            } else if ("showversion".equals(cmd)) {
                ok = showVersionCLI(a.get(0));
            } else if ("sign".equals(cmd)) {
                if (I2PAppContext.getCurrentContext() == null) {
                    // speed things up by specifying a small PRNG buffer size
                    Properties props = new Properties();
                    props.setProperty("prng.bufferSize", "16384");
                    new I2PAppContext(props);
                }
                ok = signCLI(stype, ctype, ftype, a.get(0), a.get(1), a.get(2), a.get(3), a.get(4), "", kspass);
            } else if ("bulksign".equals(cmd)) {
                if (I2PAppContext.getCurrentContext() == null) {
                    // speed things up by specifying a small PRNG buffer size
                    Properties props = new Properties();
                    props.setProperty("prng.bufferSize", "16384");
                    new I2PAppContext(props);
                }
                ok = bulkSignCLI(stype, ctype, a.get(0), a.get(1), a.get(2), a.get(3), kspass);
            } else if ("verifysig".equals(cmd)) {
                ok = verifySigCLI(a.get(0), kfile);
            } else if ("keygen".equals(cmd)) {
                if (I2PAppContext.getCurrentContext() == null) {
                    // speed things up by specifying a small PRNG buffer size
                    Properties props = new Properties();
                    props.setProperty("prng.bufferSize", "16384");
                    new I2PAppContext(props);
                }
                ok = genKeysCLI(stype, a.get(0), a.get(1), crlfile, a.get(2), kspass);
            } else if ("extract".equals(cmd)) {
                String outfile = (a.size() > 1) ? a.get(1) : null;
                ok = extractCLI(a.get(0), outfile, shouldVerify, kfile);
            } else {
                showUsageCLI();
            }
        } catch (NoSuchElementException nsee) {
            showUsageCLI();
        } catch (IndexOutOfBoundsException ioobe) {
            showUsageCLI();
        }
        if (!ok)
            System.exit(1);
    }

    private static final void showUsageCLI() {
        System.err.println("Usage: SU3File keygen       [-t type|code] [-p keystorepw] [-r crlFile.crl] publicKeyFile.crt keystore.ks you@mail.i2p\n" +
                           "       SU3File sign         [-t type|code] [-c type|code] [-f type|code] [-p keystorepw] inputFile.zip signedFile.su3 keystore.ks version you@mail.i2p\n" +
                           "       SU3File bulksign     [-t type|code] [-c type|code] [-p keystorepw] directory keystore.ks version you@mail.i2p\n" +
                           "                            (signs all .zip, .xml, and .xml.gz files in the directory)\n" +
                           "       SU3File showversion  signedFile.su3\n" +
                           "       SU3File verifysig    [-k file.crt] signedFile.su3  ## -k use this pubkey cert for verification\n" +
                           "       SU3File extract      [-x] [-k file.crt] signedFile.su3 [outFile]   ## -x don't check sig");
        System.err.println("Default keystore password: \"" + KeyStoreUtil.DEFAULT_KEYSTORE_PASSWORD + '"');
        System.err.println(dumpTypes());
    }

    /** @since 0.9.9 */
    private static String dumpTypes() {
        StringBuilder buf = new StringBuilder(256);
        buf.append("Available signature types (-t):\n");
        for (SigType t : EnumSet.allOf(SigType.class)) {
            if (!t.isAvailable())
                continue;
            if (t == SigType.EdDSA_SHA512_Ed25519 ||
                t == SigType.RedDSA_SHA512_Ed25519)
                continue; // not supported by keytool, and does double hashing right now
            buf.append("      ").append(t).append("\t(code: ").append(t.getCode()).append(')');
            if (t.getCode() == DEFAULT_SIG_CODE)
                buf.append(" DEFAULT");
            buf.append('\n');
        }
        buf.append("Available content types (-c):\n");
        for (ContentType t : EnumSet.allOf(ContentType.class)) {
            buf.append("      ").append(t).append("\t(code: ").append(t.getCode()).append(')');
            if (t == DEFAULT_CONTENT_TYPE)
                buf.append(" DEFAULT");
            buf.append('\n');
        }
        buf.append("Available file types (-f):\n" +
                   "      ZIP\t(code: 0) DEFAULT\n" +
                   "      XML\t(code: 1)\n" +
                   "      HTML\t(code: 2)\n" +
                   "      XML_GZ\t(code: 3)\n" +
                   "      TXT_GZ\t(code: 4)\n" +
                   "      DMG\t(code: 5)\n" +
                   "      EXE\t(code: 6)\n" +
                   "      (user defined)\t(code: 7-255)\n");
        return buf.toString();
    }

    /**
     *  @param ctype number or name
     *  @return null if not found
     *  @since 0.9.9
     */
    private static ContentType parseContentType(String ctype) {
        try {
            return ContentType.valueOf(ctype.toUpperCase(Locale.US));
        } catch (IllegalArgumentException iae) {
            try {
                int code = Integer.parseInt(ctype);
                return ContentType.getByCode(code);
            } catch (NumberFormatException nfe) {
                return null;
             }
        }
    }

    /** @return success */
    private static final boolean showVersionCLI(String signedFile) {
        try {
            SU3File file = new SU3File(signedFile);
            file.setVerifySignature(false);
            String versionString = file.getVersionString();
            if (versionString.equals(""))
                System.out.println("No version string found in file '" + signedFile + "'");
            else {
                long ver = 0;
                try {
                    ver = Long.parseLong(versionString);
                } catch (NumberFormatException nfe) {}
                if (ver > 1000000000L)
                    System.out.println("Version:  " + versionString + " (" + DataHelper.formatTime(ver * 1000) + ')');
                else
                    System.out.println("Version:  " + versionString);
            }
            String signerString = file.getSignerString();
            if (signerString.equals(""))
                System.out.println("No signer string found in file '" + signedFile + "'");
            else
                System.out.println("Signer:   " + signerString);
            if (file._sigType != null)
                System.out.println("SigType:  " + file._sigType);
            if (file._contentType != null)
                System.out.println("Content:  " + file._contentType);
            String ftype;
            if (file._fileType == TYPE_ZIP)
                ftype = "ZIP";
            else if (file._fileType == TYPE_XML)
                ftype = "XML";
            else if (file._fileType == TYPE_HTML)
                ftype = "HTML";
            else if (file._fileType == TYPE_XML_GZ)
                ftype = "XML_GZ";
            else if (file._fileType == TYPE_DMG)
                ftype = "DMG";
            else if (file._fileType == TYPE_EXE)
                ftype = "EXE";
            else
                ftype = Integer.toString(file._fileType);
                System.out.println("FileType: " + ftype);
            return !versionString.equals("");
        } catch (IOException ioe) {
            ioe.printStackTrace();
            return false;
        }
    }

    /**
     *  Zip, xml, and xml.gz only
     *  @return success
     *  @since 0.9.9
     */
    private static final boolean bulkSignCLI(String stype, String ctype, String dir,
                                     String privateKeyFile, String version, String signerName, String kspass) {
        File d = new File(dir);
        if (!d.isDirectory()) {
            System.out.println("Directory does not exist: " + d);
            return false;
        }
        File[] files = d.listFiles();
        if (files == null || files.length == 0) {
            System.out.println("No zip files found in " + d);
            return false;
        }
        String keypw = "";
        try {
            while (keypw.length() < 6) {
                System.out.print("Enter password for key \"" + signerName + "\": ");
                keypw = DataHelper.readLine(System.in);
                if (keypw == null) {
                    System.out.println("\nEOF reading password");
                    return false;
                }
                keypw = keypw.trim();
                if (keypw.length() > 0 && keypw.length() < 6)
                    System.out.println("Key password must be at least 6 characters");
            }
        } catch (IOException ioe) {
            System.out.println("Error asking for password");
            ioe.printStackTrace();
            return false;
        }
        int success = 0;
        for (File in : files) {
            String inputFile = in.getPath();
            int len;
            String ftype;
            if (inputFile.endsWith(".zip")) {
                len = 4;
                ftype = "ZIP";
            } else if (inputFile.endsWith(".xml")) {
                len = 4;
                ftype = "XML";
            } else if (inputFile.endsWith(".xml.gz")) {
                len = 7;
                ftype = "XML_GZ";
            } else {
                continue;
            }
            String signedFile = inputFile.substring(0, inputFile.length() - len) + ".su3";
            boolean rv = signCLI(stype, ctype, ftype, inputFile, signedFile,
                                 privateKeyFile, version, signerName, keypw, kspass);
            if (!rv)
                return false;
            success++;
        }
        if (success == 0)
            System.out.println("No files processed in " + d);
        return success > 0;
    }

    /**
     *  @return success
     *  @since 0.9.9
     */
    private static final boolean signCLI(String stype, String ctype, String ftype, String inputFile, String signedFile,
                                         String privateKeyFile, String version, String signerName, String keypw, String kspass) {
        SigType type = stype == null ? SigType.getByCode(Integer.valueOf(DEFAULT_SIG_CODE)) : SigType.parseSigType(stype);
        if (type == null) {
            System.out.println("Signature type " + stype + " is not supported");
            return false;
        }
        ContentType ct = ctype == null ? DEFAULT_CONTENT_TYPE : parseContentType(ctype);
        if (ct == null) {
            System.out.println("Content type " + ctype + " is not supported");
            return false;
        }
        int ft = TYPE_ZIP;
        if (ftype != null) {
            if (ftype.equalsIgnoreCase("ZIP")) {
                ft = TYPE_ZIP;
            } else if (ftype.equalsIgnoreCase("XML")) {
                ft = TYPE_XML;
            } else if (ftype.equalsIgnoreCase("HTML")) {
                ft = TYPE_HTML;
            } else if (ftype.equalsIgnoreCase("XML_GZ")) {
                ft = TYPE_XML_GZ;
            } else if (ftype.equalsIgnoreCase("DMG")) {
                ft = TYPE_DMG;
            } else if (ftype.equalsIgnoreCase("EXE")) {
                ft = TYPE_EXE;
            } else {
                try {
                    ft = Integer.parseInt(ftype);
                } catch (NumberFormatException nfe) {
                    ft = -1;
                }
                if (ft < 0 || ft > 255) {
                    System.out.println("File type " + ftype + " is not supported");
                    return false;
                }
                if (ft  > TYPE_EXE)
                    System.out.println("Warning: File type " + ftype + " is undefined");
            }
        }
        return signCLI(type, ct, ft, inputFile, signedFile, privateKeyFile, version, signerName, keypw, kspass);
    }

    /**
     *  @return success
     *  @since 0.9.9
     */
    private static final boolean signCLI(SigType type, ContentType ctype, int ftype, String inputFile, String signedFile,
                                         String privateKeyFile, String version, String signerName, String keypw, String kspass) {
        try {
            while (keypw.length() < 6) {
                System.out.print("Enter password for key \"" + signerName + "\": ");
                keypw = DataHelper.readLine(System.in);
                if (keypw == null) {
                    System.out.println("\nEOF reading password");
                    return false;
                }
                keypw = keypw.trim();
                if (keypw.length() > 0 && keypw.length() < 6)
                    System.out.println("Key password must be at least 6 characters");
            }
            File pkfile = new File(privateKeyFile);
            PrivateKey pk = KeyStoreUtil.getPrivateKey(pkfile, kspass, signerName, keypw);
            if (pk == null) {
                System.out.println("Private key for " + signerName + " not found in keystore " + privateKeyFile);
                return false;
            }
            // now fix the sig type based on the private key
            SigType oldType = type;
            type = SigUtil.fromJavaKey(pk).getType();
            if (oldType != type)
                System.out.println("Warning: Using private key type " + type + ", ignoring specified type " + oldType);
            SU3File file = new SU3File(signedFile);
            file.write(new File(inputFile), ftype, ctype.getCode(), version, signerName, pk, type);
            System.out.println("Input file '" + inputFile + "' signed and written to '" + signedFile + "'");
            return true;
        } catch (GeneralSecurityException gse) {
            System.out.println("Error signing input file '" + inputFile + "'");
            gse.printStackTrace();
            return false;
        } catch (IOException ioe) {
            System.out.println("Error signing input file '" + inputFile + "'");
            ioe.printStackTrace();
            return false;
        }
    }

    /** @return valid */
    private static final boolean verifySigCLI(String signedFile, String pkFile) {
        InputStream in = null;
        try {
            SU3File file = new SU3File(signedFile);
            if (pkFile != null)
                file.setPublicKeyCertificate(new File(pkFile));
            boolean isValidSignature = file.verify();
            if (isValidSignature)
                System.out.println("Signature VALID (signed by " + file.getSignerString() + ' ' + file._sigType + ')');
            else
                System.out.println("Signature INVALID (signed by " + file.getSignerString() + ' ' + file._sigType +')');
            return isValidSignature;
        } catch (IOException ioe) {
            System.out.println("Error verifying input file '" + signedFile + "'");
            ioe.printStackTrace();
            return false;
        }
    }

    /**
     *  @param outFile if null, will use a name derived from signedFile
     *  @return success
     *  @since 0.9.9
     */
    private static final boolean extractCLI(String signedFile, String outFile, boolean verifySig, String pkFile) {
        InputStream in = null;
        try {
            SU3File file = new SU3File(signedFile);
            if (pkFile != null)
                file.setPublicKeyCertificate(new File(pkFile));
            file.setVerifySignature(verifySig);
            if (outFile == null) {
                outFile = signedFile;
                if (outFile.endsWith(".su3") && outFile.length() > 4)
                    outFile = outFile.substring(0, outFile.length() - 4);
                String sfx;
                switch (file.getFileType()) {
                  case TYPE_ZIP:
                    sfx = ".zip";
                    break;
                  case TYPE_XML:
                    sfx = ".xml";
                    break;
                  case TYPE_HTML:
                    sfx = ".html";
                    break;
                  case TYPE_XML_GZ:
                    sfx = ".xml.gz";
                    break;
                  case TYPE_TXT_GZ:
                    sfx = ".txt.gz";
                    break;
                  case TYPE_DMG:
                    sfx = ".dmg";
                    break;
                  case TYPE_EXE:
                    sfx = ".exe";
                    break;
                  default:
                    sfx = ".extracted";
                    break;
                }
                outFile = outFile + sfx;
                // above causes failure, quick fix
                file = new SU3File(signedFile);
                file.setVerifySignature(verifySig);
            }
            File out = new File(outFile);
            boolean ok = file.verifyAndMigrate(out);
            if (ok)
                System.out.println("File extracted to " + outFile + " (signed by " + file.getSignerString() + ' ' + file._sigType + ')');
            else
                System.out.println("Signature INVALID (signed by " + file.getSignerString() + ' ' + file._sigType +')');
            return ok;
        } catch (IOException ioe) {
            System.out.println("Error extracting from file '" + signedFile + "'");
            ioe.printStackTrace();
            return false;
        }
    }

    /**
     *  @param crlFile may be null; non-null to save
     *  @return success
     *  @since 0.9.9
     */
    private static final boolean genKeysCLI(String stype, String publicKeyFile, String privateKeyFile,
                                            String crlFile, String alias, String kspass) {
        SigType type = stype == null ? SigType.getByCode(Integer.valueOf(DEFAULT_SIG_CODE)) : SigType.parseSigType(stype);
        if (type == null) {
            System.out.println("Signature type " + stype + " is not supported");
            return false;
        }
        return genKeysCLI(type, publicKeyFile, privateKeyFile, crlFile, alias, kspass);
    }

    /**
     *  Writes Java-encoded keys (X.509 for public and PKCS#8 for private)
     *
     *  @param crlFile may be null; non-null to save
     *  @return success
     *  @since 0.9.9
     */
    private static final boolean genKeysCLI(SigType type, String publicKeyFile, String privateKeyFile,
                                            String crlFile, String alias, String kspass) {
        File pubFile = new File(publicKeyFile);
        if (pubFile.exists()) {
            System.out.println("Error: Not overwriting file " + publicKeyFile);
            return false;
        }
        File ksFile = new File(privateKeyFile);
        String keypw = "";
        try {
            while (alias.length() == 0) {
                System.out.print("Enter key name (example@mail.i2p): ");
                alias = DataHelper.readLine(System.in);
                if (alias == null) {
                    System.out.println("\nEOF reading key name");
                    return false;
                }
                alias = alias.trim();
            }
            while (keypw.length() < 6) {
                System.out.print("Enter new key password: ");
                keypw = DataHelper.readLine(System.in);
                if (keypw == null) {
                    System.out.println("\nEOF reading password");
                    return false;
                }
                keypw = keypw.trim();
                if (keypw.length() > 0 && keypw.length() < 6)
                    System.out.println("Key password must be at least 6 characters");
            }
        } catch (IOException ioe) {
            return false;
        }
        OutputStream out = null;
        try {
            Object[] rv =  KeyStoreUtil.createKeysAndCRL(ksFile, kspass, alias,
                                                         alias, "I2P", 3652, type, keypw);
            X509Certificate cert = (X509Certificate) rv[2];
            out = new SecureFileOutputStream(publicKeyFile);
            CertUtil.exportCert(cert, out);
            if (crlFile != null) {
                out.close();
                X509CRL crl = (X509CRL) rv[3];
                out = new SecureFileOutputStream(crlFile);
                CertUtil.exportCRL(crl, out);
            }
        } catch (GeneralSecurityException gse) {
            System.err.println("Error creating keys for " + alias);
            gse.printStackTrace();
            return false;
        } catch (IOException ioe) {
            System.err.println("Error creating keys for " + alias);
            ioe.printStackTrace();
            return false;
        } finally {
            if (out != null) try { out.close(); } catch (IOException ioe) {}
        }
        return true;
    }

    /**
     *  For the -k CLI option
     *  @return non-null, throws IOE on all errors
     *  @since 0.9.15
     */
    private static PublicKey loadKey(File kd) throws IOException {
        try {
            return CertUtil.loadKey(kd);
        } catch (GeneralSecurityException gse) {
            IOException ioe = new IOException("cert error");
            ioe.initCause(gse);
            throw ioe;
        }
    }
}
