diff --git a/.gitmodules b/.gitmodules
index 713ef074c..251cd3fd4 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -22,10 +22,6 @@
 	path = extern/Support
 	url = https://android.googlesource.com/platform/frameworks/support
 	ignore = dirty
-[submodule "extern/zipsigner"]
-	path = extern/zipsigner
-	url = https://gitlab.com/fdroid/zipsigner.git
-	ignore = dirty
 [submodule "extern/spongycastle"]
 	path = extern/spongycastle
 	url = https://github.com/open-keychain/spongycastle
diff --git a/extern/zipsigner b/extern/zipsigner
deleted file mode 160000
index 38e141f23..000000000
--- a/extern/zipsigner
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 38e141f23442da7ca20f2d32596fe35eb7355ada
diff --git a/extern/zipsigner/build.gradle b/extern/zipsigner/build.gradle
new file mode 100644
index 000000000..bbfeb03c2
--- /dev/null
+++ b/extern/zipsigner/build.gradle
@@ -0,0 +1 @@
+apply plugin: 'java'
diff --git a/extern/zipsigner/src/main/java/kellinwood/logging/AbstractLogger.java b/extern/zipsigner/src/main/java/kellinwood/logging/AbstractLogger.java
new file mode 100644
index 000000000..b12146b90
--- /dev/null
+++ b/extern/zipsigner/src/main/java/kellinwood/logging/AbstractLogger.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2010 Ken Ellinwood.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package kellinwood.logging;
+
+import java.io.PrintStream;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+public abstract class AbstractLogger implements LoggerInterface
+{
+
+	protected String category;
+	
+	SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
+	
+	public AbstractLogger( String category) {
+		this.category = category;
+	}
+	
+	protected String format( String level, String message) {
+		return String.format( "%s %s %s: %s\n", dateFormat.format(new Date()), level, category, message);
+	}
+	
+	protected abstract void write( String level, String message, Throwable t);
+
+    protected void writeFixNullMessage( String level, String message, Throwable t) {
+        if (message == null) {
+            if (t != null) message = t.getClass().getName();
+            else message = "null";
+        }
+        write( level, message, t);
+    }
+
+	public void debug(String message, Throwable t) {
+        writeFixNullMessage( DEBUG, message, t);
+	}
+
+	public void debug(String message) {
+        writeFixNullMessage( DEBUG, message, null);
+	}
+
+	public void error(String message, Throwable t) {
+        writeFixNullMessage( ERROR, message, t);
+	}
+
+	public void error(String message) {
+        writeFixNullMessage( ERROR, message, null);
+	}
+
+	public void info(String message, Throwable t) {
+        writeFixNullMessage( INFO, message, t);
+	}
+
+	public void info(String message) {
+        writeFixNullMessage( INFO, message, null);
+	}
+
+	public void warning(String message, Throwable t) {
+        writeFixNullMessage( WARNING, message, t);
+	}
+
+	public void warning(String message) {
+        writeFixNullMessage( WARNING, message, null);
+	}
+
+	public boolean isDebugEnabled() {
+		return true;
+	}
+
+	public boolean isErrorEnabled() {
+		return true;
+	}
+
+	public boolean isInfoEnabled() {
+		return true;
+	}
+
+	public boolean isWarningEnabled() {
+		return true;
+	}
+
+
+}
diff --git a/extern/zipsigner/src/main/java/kellinwood/logging/ConsoleLoggerFactory.java b/extern/zipsigner/src/main/java/kellinwood/logging/ConsoleLoggerFactory.java
new file mode 100644
index 000000000..0e70797e3
--- /dev/null
+++ b/extern/zipsigner/src/main/java/kellinwood/logging/ConsoleLoggerFactory.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2010 Ken Ellinwood.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package kellinwood.logging;
+
+public class ConsoleLoggerFactory implements LoggerFactory {
+
+	public LoggerInterface getLogger(String category) {
+		return new StreamLogger( category, System.out);
+	}
+}
diff --git a/extern/zipsigner/src/main/java/kellinwood/logging/LoggerFactory.java b/extern/zipsigner/src/main/java/kellinwood/logging/LoggerFactory.java
new file mode 100644
index 000000000..2e9f2fe19
--- /dev/null
+++ b/extern/zipsigner/src/main/java/kellinwood/logging/LoggerFactory.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2010 Ken Ellinwood.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package kellinwood.logging;
+
+public interface LoggerFactory {
+
+	public LoggerInterface getLogger( String category);
+}
diff --git a/extern/zipsigner/src/main/java/kellinwood/logging/LoggerInterface.java b/extern/zipsigner/src/main/java/kellinwood/logging/LoggerInterface.java
new file mode 100644
index 000000000..6bda9a77b
--- /dev/null
+++ b/extern/zipsigner/src/main/java/kellinwood/logging/LoggerInterface.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2010 Ken Ellinwood.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package kellinwood.logging;
+
+public interface LoggerInterface {
+
+	public static final String ERROR = "ERROR";
+	public static final String WARNING = "WARNING";
+	public static final String INFO = "INFO";
+	public static final String DEBUG = "DEBUG";
+	
+	public boolean isErrorEnabled();
+	public void error( String message);
+	public void error( String message, Throwable t);
+	
+	
+	public boolean isWarningEnabled();
+	public void warning( String message);
+	public void warning( String message, Throwable t);
+	
+	public boolean isInfoEnabled();	
+	public void info( String message);
+	public void info( String message, Throwable t);
+	
+	public boolean isDebugEnabled();	
+	public void debug( String message);
+	public void debug( String message, Throwable t);
+	
+	
+}
diff --git a/extern/zipsigner/src/main/java/kellinwood/logging/LoggerManager.java b/extern/zipsigner/src/main/java/kellinwood/logging/LoggerManager.java
new file mode 100644
index 000000000..4ac8dcba6
--- /dev/null
+++ b/extern/zipsigner/src/main/java/kellinwood/logging/LoggerManager.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2010 Ken Ellinwood.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package kellinwood.logging;
+
+import java.util.Map;
+import java.util.TreeMap;
+
+public class LoggerManager {
+
+	static LoggerFactory factory = new NullLoggerFactory();
+	
+	static Map<String,LoggerInterface> loggers = new TreeMap<String,LoggerInterface>();
+	
+	public static void setLoggerFactory( LoggerFactory f) {
+		factory = f;
+	}
+	
+	public static LoggerInterface getLogger(String category) {
+		
+		LoggerInterface logger = loggers.get( category);
+		if (logger == null) {
+			logger = factory.getLogger(category);
+			loggers.put( category, logger);
+		}
+		return logger;
+	}
+}
diff --git a/extern/zipsigner/src/main/java/kellinwood/logging/NullLoggerFactory.java b/extern/zipsigner/src/main/java/kellinwood/logging/NullLoggerFactory.java
new file mode 100644
index 000000000..4fdcf3edf
--- /dev/null
+++ b/extern/zipsigner/src/main/java/kellinwood/logging/NullLoggerFactory.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2010 Ken Ellinwood.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package kellinwood.logging;
+
+public class NullLoggerFactory implements LoggerFactory {
+
+	static LoggerInterface logger = new LoggerInterface() {
+
+		public void debug(String message) {
+		}
+
+		public void debug(String message, Throwable t) {
+		}
+
+		public void error(String message) {
+		}
+
+		public void error(String message, Throwable t) {
+		}
+
+		public void info(String message) {
+		}
+
+		public void info(String message, Throwable t) {
+		}
+
+		public boolean isDebugEnabled() {
+			return false;
+		}
+
+		public boolean isErrorEnabled() {
+			return false;
+		}
+
+		public boolean isInfoEnabled() {
+			return false;
+		}
+
+		public boolean isWarningEnabled() {
+			return false;
+		}
+
+		public void warning(String message) {
+		}
+
+		public void warning(String message, Throwable t) {
+		}
+		
+	};
+	
+
+	public LoggerInterface getLogger(String category) {
+		return logger;
+	}
+
+}
diff --git a/extern/zipsigner/src/main/java/kellinwood/logging/StreamLogger.java b/extern/zipsigner/src/main/java/kellinwood/logging/StreamLogger.java
new file mode 100644
index 000000000..511755608
--- /dev/null
+++ b/extern/zipsigner/src/main/java/kellinwood/logging/StreamLogger.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2010 Ken Ellinwood.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package kellinwood.logging;
+
+import java.io.PrintStream;
+
+public class StreamLogger extends AbstractLogger {
+
+	PrintStream out;
+	
+	public StreamLogger( String category, PrintStream out)
+	{
+		super( category);
+		this.out = out;
+	}
+	
+	@Override
+	protected void write(String level, String message, Throwable t) {
+		out.print( format( level, message));
+		if (t != null) t.printStackTrace(out);
+	}
+
+}
diff --git a/extern/zipsigner/src/main/java/kellinwood/security/zipsigner/AutoKeyException.java b/extern/zipsigner/src/main/java/kellinwood/security/zipsigner/AutoKeyException.java
new file mode 100644
index 000000000..6223d32eb
--- /dev/null
+++ b/extern/zipsigner/src/main/java/kellinwood/security/zipsigner/AutoKeyException.java
@@ -0,0 +1,14 @@
+package kellinwood.security.zipsigner;
+
+public class AutoKeyException extends RuntimeException {
+
+    private static final long serialVersionUID = 1L;
+
+    public AutoKeyException( String message) {
+        super(message);
+    }
+    
+    public AutoKeyException( String message, Throwable cause) {
+        super(message, cause);
+    }    
+}
diff --git a/extern/zipsigner/src/main/java/kellinwood/security/zipsigner/Base64.java b/extern/zipsigner/src/main/java/kellinwood/security/zipsigner/Base64.java
new file mode 100644
index 000000000..f178a026c
--- /dev/null
+++ b/extern/zipsigner/src/main/java/kellinwood/security/zipsigner/Base64.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2010 Ken Ellinwood.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package kellinwood.security.zipsigner;
+
+import java.io.ByteArrayOutputStream;
+import java.io.OutputStream;
+import java.lang.reflect.Method;
+
+import kellinwood.logging.LoggerInterface;
+import kellinwood.logging.LoggerManager;
+
+/*
+ * This class provides Base64 encoding services using one of several possible 
+ * implementations available elsewhere in the classpath. Supported implementations 
+ * are android.util.Base64 and org.bouncycastle.util.encoders.Base64Encoder.  
+ * These APIs are accessed via reflection, and as long as at least one is available
+ * Base64 encoding is possible.  This technique provides compatibility across different
+ * Android OS versions, and also allows zipsigner-lib to operate in desktop environments
+ * as long as the BouncyCastle provider jar is in the classpath.
+ * 
+ * android.util.Base64 was added in API level 8 (Android 2.2, Froyo)
+ * org.bouncycastle.util.encoders.Base64Encoder was removed in API level 11 (Android 3.0, Honeycomb)
+ * 
+ */
+@SuppressWarnings("unchecked")
+public class Base64 {
+
+    static Method aEncodeMethod = null;  // Reference to the android.util.Base64.encode() method, if available
+    static Method aDecodeMethod = null;  // Reference to the android.util.Base64.decode() method, if available
+    
+    static Object bEncoder = null; // Reference to an org.bouncycastle.util.encoders.Base64Encoder instance, if available
+    static Method bEncodeMethod = null;  // Reference to the bEncoder.encode() method, if available
+    
+    static Object bDecoder = null; // Reference to an org.bouncycastle.util.encoders.Base64Encoder instance, if available
+    static Method bDecodeMethod = null;  // Reference to the bEncoder.encode() method, if available    
+    
+    static LoggerInterface logger = null;
+    
+    static {
+        
+        Class<Object> clazz;
+    
+        logger = LoggerManager.getLogger( Base64.class.getName());
+        
+        try {
+            clazz = (Class<Object>) Class.forName("android.util.Base64");
+            // Looking for encode( byte[] input, int flags)
+            aEncodeMethod = clazz.getMethod("encode", byte[].class, Integer.TYPE);
+            aDecodeMethod = clazz.getMethod("decode", byte[].class, Integer.TYPE);
+            logger.info( clazz.getName() + " is available.");
+        }
+        catch (ClassNotFoundException x) {} // Ignore
+        catch (Exception x) {
+            logger.error("Failed to initialize use of android.util.Base64", x);
+        }
+        
+        try {
+            clazz = (Class<Object>) Class.forName("org.bouncycastle.util.encoders.Base64Encoder");
+            bEncoder = clazz.newInstance();
+            // Looking for encode( byte[] input, int offset, int length, OutputStream output)
+            bEncodeMethod = clazz.getMethod("encode", byte[].class, Integer.TYPE, Integer.TYPE, OutputStream.class);
+            logger.info( clazz.getName() + " is available.");
+            // Looking for decode( byte[] input, int offset, int length, OutputStream output)
+            bDecodeMethod = clazz.getMethod("decode", byte[].class, Integer.TYPE, Integer.TYPE, OutputStream.class);
+
+        }
+        catch (ClassNotFoundException x) {} // Ignore
+        catch (Exception x) {
+            logger.error("Failed to initialize use of org.bouncycastle.util.encoders.Base64Encoder", x);
+        }
+        
+        if (aEncodeMethod == null && bEncodeMethod == null)
+            throw new IllegalStateException("No base64 encoder implementation is available.");
+    }
+    
+
+    public static String encode( byte[] data) {
+        try {
+            if (aEncodeMethod != null) {
+                // Invoking a static method call, using null for the instance value
+                byte[] encodedBytes = (byte[])aEncodeMethod.invoke(null, data, 2);
+                return new String( encodedBytes);
+            }
+            else if (bEncodeMethod != null) {
+                ByteArrayOutputStream baos = new ByteArrayOutputStream();
+                bEncodeMethod.invoke(bEncoder, data, 0, data.length, baos);
+                return new String( baos.toByteArray());
+            }
+        }
+        catch (Exception x) {
+            throw new IllegalStateException( x.getClass().getName() + ": " + x.getMessage());
+        }
+
+        
+        throw new IllegalStateException("No base64 encoder implementation is available.");
+    }
+    
+    public static byte[] decode( byte[] data) {
+        try {
+            if (aDecodeMethod != null) {
+                // Invoking a static method call, using null for the instance value
+                byte[] decodedBytes = (byte[])aDecodeMethod.invoke(null, data, 2);
+                return decodedBytes;
+            }
+            else if (bDecodeMethod != null) {
+                ByteArrayOutputStream baos = new ByteArrayOutputStream();
+                bDecodeMethod.invoke(bEncoder, data, 0, data.length, baos);
+                return baos.toByteArray();
+            }
+        }
+        catch (Exception x) {
+            throw new IllegalStateException( x.getClass().getName() + ": " + x.getMessage());
+        }
+
+        
+        throw new IllegalStateException("No base64 encoder implementation is available.");
+    }
+}
\ No newline at end of file
diff --git a/extern/zipsigner/src/main/java/kellinwood/security/zipsigner/DefaultResourceAdapter.java b/extern/zipsigner/src/main/java/kellinwood/security/zipsigner/DefaultResourceAdapter.java
new file mode 100644
index 000000000..3ecc1be8e
--- /dev/null
+++ b/extern/zipsigner/src/main/java/kellinwood/security/zipsigner/DefaultResourceAdapter.java
@@ -0,0 +1,33 @@
+package kellinwood.security.zipsigner;
+
+/**
+ * Default resource adapter.
+ */
+public class DefaultResourceAdapter implements ResourceAdapter {
+
+    @Override
+    public String getString(Item item, Object... args) {
+
+        switch (item) {
+            case INPUT_SAME_AS_OUTPUT_ERROR:
+                return "Input and output files are the same.  Specify a different name for the output.";
+            case AUTO_KEY_SELECTION_ERROR:
+                return "Unable to auto-select key for signing " + args[0];
+            case LOADING_CERTIFICATE_AND_KEY:
+                return "Loading certificate and private key";
+            case PARSING_CENTRAL_DIRECTORY:
+                return "Parsing the input's central directory";
+            case GENERATING_MANIFEST:
+                return "Generating manifest";
+            case GENERATING_SIGNATURE_FILE:
+                return "Generating signature file";
+            case GENERATING_SIGNATURE_BLOCK:
+                return "Generating signature block file";
+            case COPYING_ZIP_ENTRY:
+                return String.format("Copying zip entry %d of %d", args[0], args[1]);
+            default:
+                throw new IllegalArgumentException("Unknown item " + item);
+        }
+
+    }
+}
diff --git a/extern/zipsigner/src/main/java/kellinwood/security/zipsigner/HexDumpEncoder.java b/extern/zipsigner/src/main/java/kellinwood/security/zipsigner/HexDumpEncoder.java
new file mode 100644
index 000000000..ad4ca23c3
--- /dev/null
+++ b/extern/zipsigner/src/main/java/kellinwood/security/zipsigner/HexDumpEncoder.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2010 Ken Ellinwood.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package kellinwood.security.zipsigner;
+
+import java.io.IOException;
+import java.io.ByteArrayOutputStream;
+
+/** Produces the classic hex dump with an address column, hex data
+ * section (16 bytes per row) and right-column printable character dislpay.
+ */    
+public class HexDumpEncoder
+{
+
+    static HexEncoder encoder = new HexEncoder();
+
+    public static String encode( byte[] data) {
+
+        try {
+            ByteArrayOutputStream baos = new ByteArrayOutputStream();
+            encoder.encode( data, 0, data.length, baos);
+            byte[] hex = baos.toByteArray();
+
+            StringBuilder hexDumpOut = new StringBuilder();
+            for (int i = 0; i < hex.length; i += 32) {
+
+                int max = Math.min(i+32, hex.length);
+
+                StringBuilder hexOut = new StringBuilder();
+                StringBuilder chrOut = new StringBuilder();
+
+                hexOut.append( String.format("%08x: ", (i/2)));
+
+                for (int j = i; j < max; j+= 2) {
+                    hexOut.append( Character.valueOf( (char)hex[j]));
+                    hexOut.append( Character.valueOf( (char)hex[j+1]));
+                    if ((j+2) % 4 == 0) hexOut.append( ' ');
+
+                    int dataChar = data[j/2];
+                    if (dataChar >= 32 && dataChar < 127) chrOut.append( Character.valueOf( (char)dataChar));
+                    else chrOut.append( '.');
+
+                }
+
+                hexDumpOut.append( hexOut.toString());
+                for (int k = hexOut.length(); k < 50; k++) hexDumpOut.append(' ');
+                hexDumpOut.append( "  ");
+                hexDumpOut.append( chrOut);
+                hexDumpOut.append("\n");
+            }
+
+            return hexDumpOut.toString();
+        }
+        catch (IOException x) {
+            throw new IllegalStateException( x.getClass().getName() + ": " + x.getMessage());
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/extern/zipsigner/src/main/java/kellinwood/security/zipsigner/HexEncoder.java b/extern/zipsigner/src/main/java/kellinwood/security/zipsigner/HexEncoder.java
new file mode 100644
index 000000000..e0c3102e4
--- /dev/null
+++ b/extern/zipsigner/src/main/java/kellinwood/security/zipsigner/HexEncoder.java
@@ -0,0 +1,200 @@
+package kellinwood.security.zipsigner;
+
+/*
+This file is a copy of org.bouncycastle.util.encoders.HexEncoder.
+  
+Please note: our license is an adaptation of the MIT X11 License and should be read as such.
+License
+
+Copyright (c) 2000 - 2011 The Legion Of The Bouncy Castle (http://www.bouncycastle.org)
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+*/
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+public class HexEncoder
+{
+    protected final byte[] encodingTable =
+        {
+            (byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4', (byte)'5', (byte)'6', (byte)'7',
+            (byte)'8', (byte)'9', (byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e', (byte)'f'
+        };
+    
+    /*
+     * set up the decoding table.
+     */
+    protected final byte[] decodingTable = new byte[128];
+
+    protected void initialiseDecodingTable()
+    {
+        for (int i = 0; i < encodingTable.length; i++)
+        {
+            decodingTable[encodingTable[i]] = (byte)i;
+        }
+        
+        decodingTable['A'] = decodingTable['a'];
+        decodingTable['B'] = decodingTable['b'];
+        decodingTable['C'] = decodingTable['c'];
+        decodingTable['D'] = decodingTable['d'];
+        decodingTable['E'] = decodingTable['e'];
+        decodingTable['F'] = decodingTable['f'];
+    }
+    
+    public HexEncoder()
+    {
+        initialiseDecodingTable();
+    }
+    
+    /**
+     * encode the input data producing a Hex output stream.
+     *
+     * @return the number of bytes produced.
+     */
+    public int encode(
+        byte[]                data,
+        int                    off,
+        int                    length,
+        OutputStream    out) 
+        throws IOException
+    {        
+        for (int i = off; i < (off + length); i++)
+        {
+            int    v = data[i] & 0xff;
+
+            out.write(encodingTable[(v >>> 4)]);
+            out.write(encodingTable[v & 0xf]);
+        }
+
+        return length * 2;
+    }
+
+    private boolean ignore(
+        char    c)
+    {
+        return (c == '\n' || c =='\r' || c == '\t' || c == ' ');
+    }
+    
+    /**
+     * decode the Hex encoded byte data writing it to the given output stream,
+     * whitespace characters will be ignored.
+     *
+     * @return the number of bytes produced.
+     */
+    public int decode(
+        byte[]          data,
+        int             off,
+        int             length,
+        OutputStream    out)
+        throws IOException
+    {
+        byte    b1, b2;
+        int     outLen = 0;
+        
+        int     end = off + length;
+        
+        while (end > off)
+        {
+            if (!ignore((char)data[end - 1]))
+            {
+                break;
+            }
+            
+            end--;
+        }
+        
+        int i = off;
+        while (i < end)
+        {
+            while (i < end && ignore((char)data[i]))
+            {
+                i++;
+            }
+            
+            b1 = decodingTable[data[i++]];
+            
+            while (i < end && ignore((char)data[i]))
+            {
+                i++;
+            }
+            
+            b2 = decodingTable[data[i++]];
+
+            out.write((b1 << 4) | b2);
+            
+            outLen++;
+        }
+
+        return outLen;
+    }
+    
+    /**
+     * decode the Hex encoded String data writing it to the given output stream,
+     * whitespace characters will be ignored.
+     *
+     * @return the number of bytes produced.
+     */
+    public int decode(
+        String          data,
+        OutputStream    out)
+        throws IOException
+    {
+        byte    b1, b2;
+        int     length = 0;
+        
+        int     end = data.length();
+        
+        while (end > 0)
+        {
+            if (!ignore(data.charAt(end - 1)))
+            {
+                break;
+            }
+            
+            end--;
+        }
+        
+        int i = 0;
+        while (i < end)
+        {
+            while (i < end && ignore(data.charAt(i)))
+            {
+                i++;
+            }
+            
+            b1 = decodingTable[data.charAt(i++)];
+            
+            while (i < end && ignore(data.charAt(i)))
+            {
+                i++;
+            }
+            
+            b2 = decodingTable[data.charAt(i++)];
+
+            out.write((b1 << 4) | b2);
+            
+            length++;
+        }
+
+        return length;
+    }
+}
diff --git a/extern/zipsigner/src/main/java/kellinwood/security/zipsigner/KeySet.java b/extern/zipsigner/src/main/java/kellinwood/security/zipsigner/KeySet.java
new file mode 100644
index 000000000..9b08f8826
--- /dev/null
+++ b/extern/zipsigner/src/main/java/kellinwood/security/zipsigner/KeySet.java
@@ -0,0 +1,98 @@
+
+
+/*
+ * Copyright (C) 2010 Ken Ellinwood
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package kellinwood.security.zipsigner;
+
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+
+public class KeySet {
+
+    String name;
+    
+    // certificate
+    X509Certificate publicKey = null;
+    
+    // private key
+    PrivateKey privateKey = null; 
+
+    // signature block template
+    byte[] sigBlockTemplate = null;
+
+    String signatureAlgorithm = "SHA1withRSA";
+    
+    public KeySet() {
+    }
+    
+    public KeySet( String name, X509Certificate publicKey, PrivateKey privateKey, byte[] sigBlockTemplate)
+    {
+        this.name = name;
+        this.publicKey = publicKey;
+        this.privateKey = privateKey;
+        this.sigBlockTemplate = sigBlockTemplate;
+    }
+
+    public KeySet( String name, X509Certificate publicKey, PrivateKey privateKey, String signatureAlgorithm, byte[] sigBlockTemplate)
+    {
+        this.name = name;
+        this.publicKey = publicKey;
+        this.privateKey = privateKey;
+        if (signatureAlgorithm != null) this.signatureAlgorithm = signatureAlgorithm;
+        this.sigBlockTemplate = sigBlockTemplate;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public X509Certificate getPublicKey() {
+        return publicKey;
+    }
+
+    public void setPublicKey(X509Certificate publicKey) {
+        this.publicKey = publicKey;
+    }
+
+    public PrivateKey getPrivateKey() {
+        return privateKey;
+    }
+
+    public void setPrivateKey(PrivateKey privateKey) {
+        this.privateKey = privateKey;
+    }
+
+    public byte[] getSigBlockTemplate() {
+        return sigBlockTemplate;
+    }
+
+    public void setSigBlockTemplate(byte[] sigBlockTemplate) {
+        this.sigBlockTemplate = sigBlockTemplate;
+    }
+
+    public String getSignatureAlgorithm() {
+        return signatureAlgorithm;
+    }
+
+    public void setSignatureAlgorithm(String signatureAlgorithm) {
+        if (signatureAlgorithm == null) signatureAlgorithm = "SHA1withRSA";
+        else this.signatureAlgorithm = signatureAlgorithm;
+    }
+}
diff --git a/extern/zipsigner/src/main/java/kellinwood/security/zipsigner/ProgressEvent.java b/extern/zipsigner/src/main/java/kellinwood/security/zipsigner/ProgressEvent.java
new file mode 100644
index 000000000..c862e55c3
--- /dev/null
+++ b/extern/zipsigner/src/main/java/kellinwood/security/zipsigner/ProgressEvent.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2010 Ken Ellinwood.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package kellinwood.security.zipsigner;
+
+public class ProgressEvent {
+
+    public static final int PRORITY_NORMAL = 0;
+    public static final int PRORITY_IMPORTANT = 1;
+    
+    private String message;
+    private int percentDone;
+    private int priority;
+    
+    public String getMessage() {
+        return message;
+    }
+    public void setMessage(String message) {
+        this.message = message;
+    }
+    public int getPercentDone() {
+        return percentDone;
+    }
+    public void setPercentDone(int percentDone) {
+        this.percentDone = percentDone;
+    }
+    public int getPriority() {
+        return priority;
+    }
+    public void setPriority(int priority) {
+        this.priority = priority;
+    }
+    
+}
diff --git a/extern/zipsigner/src/main/java/kellinwood/security/zipsigner/ProgressHelper.java b/extern/zipsigner/src/main/java/kellinwood/security/zipsigner/ProgressHelper.java
new file mode 100644
index 000000000..be472c003
--- /dev/null
+++ b/extern/zipsigner/src/main/java/kellinwood/security/zipsigner/ProgressHelper.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2010 Ken Ellinwood.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package kellinwood.security.zipsigner;
+
+import java.util.ArrayList;
+
+public class ProgressHelper {
+
+    private int progressTotalItems = 0;
+    private int progressCurrentItem = 0;
+    private ProgressEvent progressEvent = new ProgressEvent();
+    
+    public void initProgress()
+    {
+        progressTotalItems = 10000;
+        progressCurrentItem = 0;        
+    }
+    
+    public int getProgressTotalItems() {
+        return progressTotalItems;
+    }
+
+    public void setProgressTotalItems(int progressTotalItems) {
+        this.progressTotalItems = progressTotalItems;
+    }
+
+    public int getProgressCurrentItem() {
+        return progressCurrentItem;
+    }
+
+    public void setProgressCurrentItem(int progressCurrentItem) {
+        this.progressCurrentItem = progressCurrentItem;
+    }
+
+    public void progress( int priority, String message) {
+
+        progressCurrentItem += 1;
+
+        int percentDone;
+        if (progressTotalItems == 0) percentDone = 0;
+        else percentDone = (100 * progressCurrentItem) / progressTotalItems;
+
+        // Notify listeners here
+        for (ProgressListener listener : listeners) {
+            progressEvent.setMessage(message);
+            progressEvent.setPercentDone(percentDone);
+            progressEvent.setPriority(priority);
+            listener.onProgress( progressEvent);
+        }
+    }
+
+    private ArrayList<ProgressListener> listeners = new ArrayList<ProgressListener>();
+
+    @SuppressWarnings("unchecked")
+    public synchronized void addProgressListener( ProgressListener l)
+    {
+        ArrayList<ProgressListener> list = (ArrayList<ProgressListener>)listeners.clone();
+        list.add(l);
+        listeners = list;
+    }
+
+    @SuppressWarnings("unchecked")
+    public synchronized void removeProgressListener( ProgressListener l)
+    {
+        ArrayList<ProgressListener> list = (ArrayList<ProgressListener>)listeners.clone();
+        list.remove(l);
+        listeners = list;
+    }      
+}
diff --git a/extern/zipsigner/src/main/java/kellinwood/security/zipsigner/ProgressListener.java b/extern/zipsigner/src/main/java/kellinwood/security/zipsigner/ProgressListener.java
new file mode 100644
index 000000000..5bd0b5670
--- /dev/null
+++ b/extern/zipsigner/src/main/java/kellinwood/security/zipsigner/ProgressListener.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2010 Ken Ellinwood.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package kellinwood.security.zipsigner;
+
+public interface ProgressListener {
+
+    /** Called to notify the listener that progress has been made during
+        the zip signing operation.
+     */
+    public void onProgress( ProgressEvent event);
+}
\ No newline at end of file
diff --git a/extern/zipsigner/src/main/java/kellinwood/security/zipsigner/ResourceAdapter.java b/extern/zipsigner/src/main/java/kellinwood/security/zipsigner/ResourceAdapter.java
new file mode 100644
index 000000000..a347cf0db
--- /dev/null
+++ b/extern/zipsigner/src/main/java/kellinwood/security/zipsigner/ResourceAdapter.java
@@ -0,0 +1,20 @@
+package kellinwood.security.zipsigner;
+
+/**
+ * Interface to obtain internationalized strings for the progress events.
+ */
+public interface ResourceAdapter {
+
+    public enum Item {
+        INPUT_SAME_AS_OUTPUT_ERROR,
+        AUTO_KEY_SELECTION_ERROR,
+        LOADING_CERTIFICATE_AND_KEY,
+        PARSING_CENTRAL_DIRECTORY,
+        GENERATING_MANIFEST,
+        GENERATING_SIGNATURE_FILE,
+        GENERATING_SIGNATURE_BLOCK,
+        COPYING_ZIP_ENTRY
+    };
+
+    public String getString( Item item, Object... args);
+}
diff --git a/extern/zipsigner/src/main/java/kellinwood/security/zipsigner/ZipSignature.java b/extern/zipsigner/src/main/java/kellinwood/security/zipsigner/ZipSignature.java
new file mode 100644
index 000000000..ac03a53b6
--- /dev/null
+++ b/extern/zipsigner/src/main/java/kellinwood/security/zipsigner/ZipSignature.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2010 Ken Ellinwood.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package kellinwood.security.zipsigner;
+
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.security.InvalidKeyException;
+import java.security.MessageDigest;
+import java.security.PrivateKey;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+
+@SuppressWarnings("restriction")
+public class ZipSignature {
+
+    byte[] beforeAlgorithmIdBytes =  { 0x30, 0x21 };
+
+    //      byte[] algorithmIdBytes;    
+    //		algorithmIdBytes =  sun.security.x509.AlgorithmId.get("SHA1").encode();    
+    byte[] algorithmIdBytes = {0x30, 0x09, 0x06, 0x05, 0x2B, 0x0E, 0x03, 0x02, 0x1A, 0x05, 0x00 }; 
+
+    byte[] afterAlgorithmIdBytes = { 0x04, 0x14 };
+
+    Cipher cipher;
+
+    MessageDigest md;
+
+
+    public ZipSignature() throws IOException, GeneralSecurityException
+    {
+        md = MessageDigest.getInstance("SHA1");
+        cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
+    }
+
+    public void initSign( PrivateKey privateKey) throws InvalidKeyException 
+    {
+        cipher.init(Cipher.ENCRYPT_MODE, privateKey);
+    }
+
+    public void update( byte[] data) {
+        md.update( data);
+    }
+
+    public void update( byte[] data, int offset, int count) {
+        md.update( data, offset, count);
+    }
+
+    public byte[] sign() throws BadPaddingException, IllegalBlockSizeException 
+    {
+        cipher.update( beforeAlgorithmIdBytes);
+        cipher.update( algorithmIdBytes);
+        cipher.update( afterAlgorithmIdBytes);
+        cipher.update( md.digest());		
+        return cipher.doFinal();
+    }
+}
diff --git a/extern/zipsigner/src/main/java/kellinwood/security/zipsigner/ZipSigner.java b/extern/zipsigner/src/main/java/kellinwood/security/zipsigner/ZipSigner.java
new file mode 100644
index 000000000..d5c0604b2
--- /dev/null
+++ b/extern/zipsigner/src/main/java/kellinwood/security/zipsigner/ZipSigner.java
@@ -0,0 +1,782 @@
+/*
+ * Copyright (C) 2010 Ken Ellinwood
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/* This file is a heavily modified version of com.android.signapk.SignApk.java.
+ * The changes include:
+ *   - addition of the signZip() convenience methods
+ *   - addition of a progress listener interface
+ *   - removal of main()
+ *   - switch to a signature generation method that verifies
+ *     in Android recovery
+ *   - eliminated dependency on sun.security and sun.misc APIs by 
+ *     using signature block template files.
+ */
+
+package kellinwood.security.zipsigner;
+
+import kellinwood.logging.LoggerInterface;
+import kellinwood.logging.LoggerManager;
+import kellinwood.zipio.ZioEntry;
+import kellinwood.zipio.ZipInput;
+import kellinwood.zipio.ZipOutput;
+
+import javax.crypto.Cipher;
+import javax.crypto.EncryptedPrivateKeyInfo;
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.PBEKeySpec;
+import java.io.*;
+import java.lang.reflect.Method;
+import java.net.URL;
+import java.security.*;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.KeySpec;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.util.*;
+import java.util.jar.Attributes;
+import java.util.jar.JarFile;
+import java.util.jar.Manifest;
+import java.util.regex.Pattern;
+
+/**
+ * This is a modified copy of com.android.signapk.SignApk.java.  It provides an
+ * API to sign JAR files (including APKs and Zip/OTA updates) in
+ * a way compatible with the mincrypt verifier, using SHA1 and RSA keys.
+ *
+ * Please see the README.txt file in the root of this project for usage instructions.
+ */
+public class ZipSigner 
+{
+
+    private boolean canceled = false;
+
+    private ProgressHelper progressHelper = new ProgressHelper();
+    private ResourceAdapter resourceAdapter = new DefaultResourceAdapter();
+    
+    static LoggerInterface log = null;
+
+    private static final String CERT_SF_NAME = "META-INF/CERT.SF";
+    private static final String CERT_RSA_NAME = "META-INF/CERT.RSA";
+
+    // Files matching this pattern are not copied to the output.
+    private static Pattern stripPattern =
+        Pattern.compile("^META-INF/(.*)[.](SF|RSA|DSA)$");
+
+    Map<String,KeySet> loadedKeys = new HashMap<String,KeySet>();
+    KeySet keySet = null;
+    
+    public static LoggerInterface getLogger() {
+        if (log == null) log = LoggerManager.getLogger( ZipSigner.class.getName());
+        return log;
+    }
+
+    public static final String MODE_AUTO_TESTKEY = "auto-testkey";
+    public static final String MODE_AUTO_NONE = "auto-none";
+    public static final String MODE_AUTO = "auto";
+    public static final String KEY_NONE = "none";
+    public static final String KEY_TESTKEY = "testkey";
+    
+    // Allowable key modes.
+    public static final String[] SUPPORTED_KEY_MODES =
+        new String[] { MODE_AUTO_TESTKEY, MODE_AUTO, MODE_AUTO_NONE, "media", "platform", "shared", KEY_TESTKEY, KEY_NONE};
+    
+    String keymode = KEY_TESTKEY; // backwards compatible with versions that only signed with this key
+    
+    Map<String,String> autoKeyDetect = new HashMap<String,String>();
+    
+    AutoKeyObservable autoKeyObservable = new AutoKeyObservable();
+    
+    public ZipSigner() throws ClassNotFoundException, IllegalAccessException, InstantiationException
+    {
+        // MD5 of the first 1458 bytes of the signature block generated by the key, mapped to the key name
+        autoKeyDetect.put( "aa9852bc5a53272ac8031d49b65e4b0e", "media");
+        autoKeyDetect.put( "e60418c4b638f20d0721e115674ca11f", "platform");
+        autoKeyDetect.put( "3e24e49741b60c215c010dc6048fca7d", "shared");
+        autoKeyDetect.put( "dab2cead827ef5313f28e22b6fa8479f", "testkey");
+        
+    }
+
+    public ResourceAdapter getResourceAdapter() {
+        return resourceAdapter;
+    }
+
+    public void setResourceAdapter(ResourceAdapter resourceAdapter) {
+        this.resourceAdapter = resourceAdapter;
+    }
+
+    // when the key mode is automatic, the observers are called when the key is determined
+    public void addAutoKeyObserver( Observer o) {
+        autoKeyObservable.addObserver(o);
+    }
+    
+    public String getKeymode() {
+        return keymode;
+    }
+
+    public void setKeymode(String km) throws IOException, GeneralSecurityException  
+    {
+        if (getLogger().isDebugEnabled()) getLogger().debug("setKeymode: " + km);
+        keymode = km;
+        if (keymode.startsWith(MODE_AUTO)) {
+            keySet = null;
+        }
+        else {
+            progressHelper.initProgress();        
+            loadKeys( keymode);
+        }
+    }
+
+    public static String[] getSupportedKeyModes() {
+        return SUPPORTED_KEY_MODES;
+    }
+
+    
+    protected String autoDetectKey( String mode, Map<String,ZioEntry> zioEntries) 
+        throws NoSuchAlgorithmException, IOException 
+    {
+        boolean debug = getLogger().isDebugEnabled();
+        
+        if (!mode.startsWith(MODE_AUTO)) return mode;
+        
+
+        // Auto-determine which keys to use
+        String keyName = null;
+        // Start by finding the signature block file in the input.
+        for (Map.Entry<String,ZioEntry> entry : zioEntries.entrySet()) {
+            String entryName = entry.getKey();
+            if (entryName.startsWith("META-INF/") && entryName.endsWith(".RSA")) {
+                
+                // Compute MD5 of the first 1458 bytes, which is the size of our signature block templates -- 
+                // e.g., the portion of the sig block file that is the same for a given certificate.                    
+                MessageDigest md5 = MessageDigest.getInstance("MD5");
+                byte[] entryData = entry.getValue().getData();
+                if (entryData.length < 1458) break; // sig block too short to be a supported key 
+                md5.update( entryData, 0, 1458);
+                byte[] rawDigest = md5.digest();
+                
+                // Create the hex representation of the digest value
+                StringBuilder builder = new StringBuilder();
+                for( byte b : rawDigest) {
+                    builder.append( String.format("%02x", b));
+                }
+                
+                String md5String = builder.toString();
+                // Lookup the key name
+                keyName = autoKeyDetect.get( md5String); 
+                
+                
+                if (debug) {
+                    if (keyName != null) {
+                        getLogger().debug(String.format("Auto-determined key=%s using md5=%s", keyName, md5String));
+                    } else {
+                        getLogger().debug(String.format("Auto key determination failed for md5=%s", md5String));
+                    }
+                }
+                if (keyName != null) return keyName;
+            }
+        }
+        
+        if (mode.equals( MODE_AUTO_TESTKEY)) {
+            // in auto-testkey mode, fallback to the testkey if it couldn't be determined
+            if (debug) getLogger().debug("Falling back to key="+ keyName);
+            return KEY_TESTKEY;
+            
+        }
+        else if (mode.equals(MODE_AUTO_NONE)) {
+            // in auto-node mode, simply copy the input to the output when the key can't be determined.
+            if (debug) getLogger().debug("Unable to determine key, returning: " + KEY_NONE);
+            return KEY_NONE;
+        }
+        
+        return null;
+    }
+
+    public void issueLoadingCertAndKeysProgressEvent() {
+        progressHelper.progress(ProgressEvent.PRORITY_IMPORTANT, resourceAdapter.getString(ResourceAdapter.Item.LOADING_CERTIFICATE_AND_KEY));
+    }
+
+    // Loads one of the built-in keys (media, platform, shared, testkey)
+    public void loadKeys( String name)
+        throws IOException, GeneralSecurityException
+    {
+        
+        keySet = loadedKeys.get(name);
+        if (keySet != null) return;
+        
+        keySet = new KeySet();
+        keySet.setName(name);
+        loadedKeys.put( name, keySet);
+        
+        if (KEY_NONE.equals(name)) return;
+
+        issueLoadingCertAndKeysProgressEvent();
+
+        // load the private key
+        URL privateKeyUrl = getClass().getResource("/keys/"+name+".pk8");
+        keySet.setPrivateKey(readPrivateKey(privateKeyUrl, null));
+
+        // load the certificate
+        URL publicKeyUrl = getClass().getResource("/keys/"+name+".x509.pem");
+        keySet.setPublicKey(readPublicKey(publicKeyUrl));
+
+        // load the signature block template
+        URL sigBlockTemplateUrl = getClass().getResource("/keys/"+name+".sbt");
+        if (sigBlockTemplateUrl != null) {
+            keySet.setSigBlockTemplate(readContentAsBytes(sigBlockTemplateUrl));
+        }
+    }
+    
+    public void setKeys( String name, X509Certificate publicKey, PrivateKey privateKey, byte[] signatureBlockTemplate)
+    {
+        keySet = new KeySet( name, publicKey, privateKey, signatureBlockTemplate);
+    }
+
+    public void setKeys( String name, X509Certificate publicKey, PrivateKey privateKey, String signatureAlgorithm, byte[] signatureBlockTemplate)
+    {
+        keySet = new KeySet( name, publicKey, privateKey, signatureAlgorithm, signatureBlockTemplate);
+    }
+
+    public KeySet getKeySet() {
+        return keySet;
+    }
+    
+    // Allow the operation to be canceled.
+    public void cancel() {
+        canceled = true;
+    }
+    
+    // Allow the instance to sign again if previously canceled.
+    public void resetCanceled() {
+        canceled = false;
+    }
+
+    public boolean isCanceled() {
+        return canceled;
+    }
+
+    @SuppressWarnings("unchecked")
+    public void loadProvider( String providerClassName)
+        throws ClassNotFoundException, IllegalAccessException, InstantiationException
+    {
+        Class providerClass = Class.forName(providerClassName);
+        Provider provider = (Provider)providerClass.newInstance();
+        Security.insertProviderAt(provider, 1);
+    }
+
+
+    public X509Certificate readPublicKey(URL publicKeyUrl)
+    throws IOException, GeneralSecurityException {
+        InputStream input = publicKeyUrl.openStream();
+        try {
+            CertificateFactory cf = CertificateFactory.getInstance("X.509");
+            return (X509Certificate) cf.generateCertificate(input);
+        } finally {
+            input.close();
+        }
+    }
+
+    /**
+     * Decrypt an encrypted PKCS 8 format private key.
+     *
+     * Based on ghstark's post on Aug 6, 2006 at
+     * http://forums.sun.com/thread.jspa?threadID=758133&messageID=4330949
+     *
+     * @param encryptedPrivateKey The raw data of the private key
+     * @param keyPassword the key password
+     */
+    private KeySpec decryptPrivateKey(byte[] encryptedPrivateKey, String keyPassword)
+    throws GeneralSecurityException {
+        EncryptedPrivateKeyInfo epkInfo;
+        try {
+            epkInfo = new EncryptedPrivateKeyInfo(encryptedPrivateKey);
+        } catch (IOException ex) {
+            // Probably not an encrypted key.
+            return null;
+        }
+
+        char[] keyPasswd = keyPassword.toCharArray();
+
+        SecretKeyFactory skFactory = SecretKeyFactory.getInstance(epkInfo.getAlgName());
+        Key key = skFactory.generateSecret(new PBEKeySpec(keyPasswd));
+
+        Cipher cipher = Cipher.getInstance(epkInfo.getAlgName());
+        cipher.init(Cipher.DECRYPT_MODE, key, epkInfo.getAlgParameters());
+
+        try {
+            return epkInfo.getKeySpec(cipher);
+        } catch (InvalidKeySpecException ex) {
+            getLogger().error("signapk: Password for private key may be bad.");
+            throw ex;
+        }
+    }
+
+    /** Fetch the content at the specified URL and return it as a byte array. */
+    public byte[] readContentAsBytes( URL contentUrl) throws IOException
+    {
+        return readContentAsBytes( contentUrl.openStream());
+    }
+
+    /** Fetch the content from the given stream and return it as a byte array. */
+    public byte[] readContentAsBytes( InputStream input) throws IOException
+    {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+
+        byte[] buffer = new byte[2048];
+
+        int numRead = input.read( buffer);
+        while (numRead != -1) {
+            baos.write( buffer, 0, numRead);
+            numRead = input.read( buffer);
+        }
+
+        byte[] bytes = baos.toByteArray();
+        return bytes;
+    }
+
+    /** Read a PKCS 8 format private key. */
+    public PrivateKey readPrivateKey(URL privateKeyUrl, String keyPassword)
+    throws IOException, GeneralSecurityException {
+        DataInputStream input = new DataInputStream( privateKeyUrl.openStream());
+        try {
+            byte[] bytes = readContentAsBytes( input);
+
+            KeySpec spec = decryptPrivateKey(bytes, keyPassword);
+            if (spec == null) {
+                spec = new PKCS8EncodedKeySpec(bytes);
+            }
+
+            try {
+                return KeyFactory.getInstance("RSA").generatePrivate(spec);
+            } catch (InvalidKeySpecException ex) {
+                return KeyFactory.getInstance("DSA").generatePrivate(spec);
+            }
+        } finally {
+            input.close();
+        }
+    }
+
+    /** Add the SHA1 of every file to the manifest, creating it if necessary. */
+    private Manifest addDigestsToManifest(Map<String,ZioEntry> entries)
+        throws IOException, GeneralSecurityException 
+    {
+        Manifest input = null;
+        ZioEntry manifestEntry = entries.get(JarFile.MANIFEST_NAME);
+        if (manifestEntry != null) {
+            input = new Manifest();
+            input.read( manifestEntry.getInputStream());
+        }
+        Manifest output = new Manifest();
+        Attributes main = output.getMainAttributes();
+        if (input != null) {
+            main.putAll(input.getMainAttributes());
+        } else {
+            main.putValue("Manifest-Version", "1.0");
+            main.putValue("Created-By", "1.0 (Android SignApk)");
+        }
+
+        // BASE64Encoder base64 = new BASE64Encoder();
+        MessageDigest md = MessageDigest.getInstance("SHA1");
+        byte[] buffer = new byte[512];
+        int num;
+
+        // We sort the input entries by name, and add them to the
+        // output manifest in sorted order.  We expect that the output
+        // map will be deterministic.
+
+        TreeMap<String, ZioEntry> byName = new TreeMap<String, ZioEntry>();
+        byName.putAll( entries);
+
+        boolean debug = getLogger().isDebugEnabled();
+        if (debug) getLogger().debug("Manifest entries:");
+        for (ZioEntry entry: byName.values()) {
+            if (canceled) break;
+            String name = entry.getName();
+            if (debug) getLogger().debug(name);
+            if (!entry.isDirectory() && !name.equals(JarFile.MANIFEST_NAME) &&
+                    !name.equals(CERT_SF_NAME) && !name.equals(CERT_RSA_NAME) &&
+                    (stripPattern == null ||
+                     !stripPattern.matcher(name).matches()))
+            {
+
+                progressHelper.progress( ProgressEvent.PRORITY_NORMAL, resourceAdapter.getString(ResourceAdapter.Item.GENERATING_MANIFEST));
+                InputStream data = entry.getInputStream();
+                while ((num = data.read(buffer)) > 0) {
+                    md.update(buffer, 0, num);
+                }
+                    
+                Attributes attr = null;
+                if (input != null) {
+                    java.util.jar.Attributes inAttr = input.getAttributes(name); 
+                    if (inAttr != null) attr = new Attributes( inAttr);
+                }
+                if (attr == null) attr = new Attributes();
+                attr.putValue("SHA1-Digest", Base64.encode(md.digest()));
+                output.getEntries().put(name, attr);
+            }
+        }
+
+        return output;
+    }
+
+
+    /** Write the signature file to the given output stream. */
+    private void generateSignatureFile(Manifest manifest, OutputStream out)
+    throws IOException, GeneralSecurityException {
+        out.write( ("Signature-Version: 1.0\r\n").getBytes());
+        out.write( ("Created-By: 1.0 (Android SignApk)\r\n").getBytes());
+
+
+        // BASE64Encoder base64 = new BASE64Encoder();
+        MessageDigest md = MessageDigest.getInstance("SHA1");
+        PrintStream print = new PrintStream(
+                new DigestOutputStream(new ByteArrayOutputStream(), md),
+                true, "UTF-8");
+
+        // Digest of the entire manifest
+        manifest.write(print);
+        print.flush();
+
+        out.write( ("SHA1-Digest-Manifest: "+ Base64.encode(md.digest()) + "\r\n\r\n").getBytes());
+
+        Map<String, Attributes> entries = manifest.getEntries();
+        for (Map.Entry<String, Attributes> entry : entries.entrySet()) {
+            if (canceled) break;
+            progressHelper.progress( ProgressEvent.PRORITY_NORMAL, resourceAdapter.getString(ResourceAdapter.Item.GENERATING_SIGNATURE_FILE));
+            // Digest of the manifest stanza for this entry.
+            String nameEntry = "Name: " + entry.getKey() + "\r\n"; 
+            print.print( nameEntry);
+            for (Map.Entry<Object, Object> att : entry.getValue().entrySet()) {
+                print.print(att.getKey() + ": " + att.getValue() + "\r\n");
+            }
+            print.print("\r\n");
+            print.flush();
+
+            out.write( nameEntry.getBytes());
+            out.write( ("SHA1-Digest: " +  Base64.encode(md.digest()) + "\r\n\r\n").getBytes());
+        }
+
+    }
+
+    /** Write a .RSA file with a digital signature. */
+    @SuppressWarnings("unchecked")
+    private void writeSignatureBlock( KeySet keySet, byte[] signatureFileBytes, OutputStream out)
+        throws IOException, GeneralSecurityException
+    {
+        if (keySet.getSigBlockTemplate() != null) {
+
+            // Can't use default Signature on Android.  Although it generates a signature that can be verified by jarsigner,
+            // the recovery program appears to require a specific algorithm/mode/padding.  So we use the custom ZipSignature instead.
+            // Signature signature = Signature.getInstance("SHA1withRSA");
+            ZipSignature signature = new ZipSignature();
+            signature.initSign(keySet.getPrivateKey());
+            signature.update(signatureFileBytes);
+            byte[] signatureBytes = signature.sign();
+
+            out.write( keySet.getSigBlockTemplate());
+            out.write( signatureBytes);
+
+            if (getLogger().isDebugEnabled()) {
+
+                MessageDigest md = MessageDigest.getInstance("SHA1");
+                md.update( signatureFileBytes);
+                byte[] sfDigest = md.digest();
+                getLogger().debug( "Sig File SHA1: \n" + HexDumpEncoder.encode( sfDigest));
+
+                getLogger().debug( "Signature: \n" + HexDumpEncoder.encode(signatureBytes));
+
+                Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
+                cipher.init(Cipher.DECRYPT_MODE, keySet.getPublicKey());
+
+                byte[] tmpData = cipher.doFinal( signatureBytes);
+                getLogger().debug( "Signature Decrypted: \n" + HexDumpEncoder.encode(tmpData));
+            }
+        }
+        else {
+            try {
+                byte[] sigBlock = null;
+                // Use reflection to call the optional generator.
+                Class generatorClass = Class.forName("kellinwood.security.zipsigner.optional.SignatureBlockGenerator");
+                Method generatorMethod = generatorClass.getMethod("generate", KeySet.class, (new byte[1]).getClass());
+                sigBlock = (byte[])generatorMethod.invoke(null, keySet, signatureFileBytes);
+                out.write(sigBlock);
+            } catch (Exception x) {
+                throw new RuntimeException(x.getMessage(),x);
+            }
+        }
+    }
+
+    /**
+     * Copy all the files in a manifest from input to output.  We set
+     * the modification times in the output to a fixed time, so as to
+     * reduce variation in the output file and make incremental OTAs
+     * more efficient.
+     */
+    private void copyFiles(Manifest manifest, Map<String,ZioEntry> input, ZipOutput output, long timestamp)
+        throws IOException 
+    {
+        Map<String, Attributes> entries = manifest.getEntries();
+        List<String> names = new ArrayList<String>(entries.keySet());
+        Collections.sort(names);
+        int i = 1;
+        for (String name : names) {
+            if (canceled) break;
+            progressHelper.progress(ProgressEvent.PRORITY_NORMAL, resourceAdapter.getString(ResourceAdapter.Item.COPYING_ZIP_ENTRY, i, names.size()));
+            i += 1;
+            ZioEntry inEntry = input.get(name);
+            inEntry.setTime(timestamp);
+            output.write(inEntry);
+
+        }
+    }
+
+    /**
+     * Copy all the files from input to output. 
+     */
+    private void copyFiles(Map<String,ZioEntry> input, ZipOutput output)
+        throws IOException 
+    {
+        int i = 1;
+        for (ZioEntry inEntry : input.values()) {
+            if (canceled) break;
+            progressHelper.progress( ProgressEvent.PRORITY_NORMAL, resourceAdapter.getString(ResourceAdapter.Item.COPYING_ZIP_ENTRY, i, input.size()));
+            i += 1;
+            output.write(inEntry);
+        }
+    }
+
+    /**
+     * @deprecated - use the version that takes the passwords as char[]
+     */
+    public void signZip( URL keystoreURL,
+                         String keystoreType,
+                         String keystorePw,
+                         String certAlias,
+                         String certPw,
+                         String inputZipFilename,
+                         String outputZipFilename)
+        throws ClassNotFoundException, IllegalAccessException, InstantiationException,
+        IOException, GeneralSecurityException
+    {
+        signZip( keystoreURL, keystoreType, keystorePw.toCharArray(), certAlias, certPw.toCharArray(), "SHA1withRSA", inputZipFilename, outputZipFilename);
+    }
+
+    public void signZip( URL keystoreURL, 
+            String keystoreType,
+            char[] keystorePw,
+            String certAlias,
+            char[] certPw,
+            String signatureAlgorithm,
+            String inputZipFilename, 
+            String outputZipFilename)
+        throws ClassNotFoundException, IllegalAccessException, InstantiationException,
+        IOException, GeneralSecurityException
+    {
+        InputStream keystoreStream = null;
+
+
+        try {
+            KeyStore keystore = null;
+            if (keystoreType == null) keystoreType = KeyStore.getDefaultType();
+            keystore = KeyStore.getInstance(keystoreType);
+
+            keystoreStream = keystoreURL.openStream();
+            keystore.load(keystoreStream, keystorePw);
+            Certificate cert = keystore.getCertificate(certAlias);
+            X509Certificate publicKey = (X509Certificate)cert;
+            Key key = keystore.getKey(certAlias, certPw);
+            PrivateKey privateKey = (PrivateKey)key;
+            
+            setKeys( "custom", publicKey, privateKey, signatureAlgorithm, null);
+
+            signZip( inputZipFilename, outputZipFilename);
+        }
+        finally {
+            if (keystoreStream != null) keystoreStream.close();
+        }
+    }
+
+
+    
+
+    /** Sign the input with the default test key and certificate.  
+     *  Save result to output file.
+     */
+    public void signZip( Map<String,ZioEntry> zioEntries, String outputZipFilename)
+        throws IOException, GeneralSecurityException
+    {
+        progressHelper.initProgress();        
+        signZip( zioEntries, new FileOutputStream(outputZipFilename), outputZipFilename);
+    }
+    
+
+    /** Sign the file using the given public key cert, private key,
+     *  and signature block template.  The signature block template
+     *  parameter may be null, but if so
+     *  android-sun-jarsign-support.jar must be in the classpath.
+     */
+    public void signZip( String inputZipFilename, String outputZipFilename)
+        throws IOException, GeneralSecurityException
+    {
+        File inFile = new File( inputZipFilename).getCanonicalFile();
+        File outFile = new File( outputZipFilename).getCanonicalFile();
+        
+        if (inFile.equals(outFile)) {
+            throw new IllegalArgumentException( resourceAdapter.getString(ResourceAdapter.Item.INPUT_SAME_AS_OUTPUT_ERROR));
+        }        
+
+        progressHelper.initProgress();        
+        progressHelper.progress( ProgressEvent.PRORITY_IMPORTANT, resourceAdapter.getString(ResourceAdapter.Item.PARSING_CENTRAL_DIRECTORY));
+        
+        ZipInput input = ZipInput.read( inputZipFilename);
+        signZip( input.getEntries(), new FileOutputStream( outputZipFilename), outputZipFilename);
+    }
+    
+    /** Sign the 
+     *  and signature block template.  The signature block template
+     *  parameter may be null, but if so
+     *  android-sun-jarsign-support.jar must be in the classpath.
+     */
+    public void signZip( Map<String,ZioEntry> zioEntries, OutputStream outputStream, String outputZipFilename)
+        throws IOException, GeneralSecurityException    
+    {
+        boolean debug =  getLogger().isDebugEnabled();
+        
+        progressHelper.initProgress();
+        if (keySet == null) {
+            if (!keymode.startsWith(MODE_AUTO)) 
+                throw new IllegalStateException("No keys configured for signing the file!");
+            
+            // Auto-determine which keys to use
+            String keyName = this.autoDetectKey( keymode, zioEntries);
+            if (keyName == null) 
+                throw new AutoKeyException( resourceAdapter.getString(ResourceAdapter.Item.AUTO_KEY_SELECTION_ERROR, new File( outputZipFilename).getName()));
+            
+            autoKeyObservable.notifyObservers(keyName);
+
+            loadKeys( keyName);
+            
+        }
+
+
+        
+        ZipOutput zipOutput = null;
+
+        try {
+
+
+            zipOutput = new ZipOutput( outputStream);
+
+            if (KEY_NONE.equals(keySet.getName())) {
+                progressHelper.setProgressTotalItems(zioEntries.size());
+                progressHelper.setProgressCurrentItem(0);                
+                copyFiles(zioEntries, zipOutput);
+                return;
+            }
+            
+            // Calculate total steps to complete for accurate progress percentages.
+            int progressTotalItems = 0;
+            for (ZioEntry entry: zioEntries.values()) {
+                String name = entry.getName();
+                if (!entry.isDirectory() && !name.equals(JarFile.MANIFEST_NAME) &&
+                        !name.equals(CERT_SF_NAME) && !name.equals(CERT_RSA_NAME) &&
+                        (stripPattern == null ||
+                                !stripPattern.matcher(name).matches()))
+                {
+                    progressTotalItems += 3;  // digest for manifest, digest in sig file, copy data
+                }
+            }
+            progressTotalItems += 1; // CERT.RSA generation
+            progressHelper.setProgressTotalItems(progressTotalItems);
+            progressHelper.setProgressCurrentItem(0);
+
+            // Assume the certificate is valid for at least an hour.
+            long timestamp = keySet.getPublicKey().getNotBefore().getTime() + 3600L * 1000;
+            
+            // MANIFEST.MF
+            // progress(ProgressEvent.PRORITY_NORMAL, JarFile.MANIFEST_NAME);
+            Manifest manifest = addDigestsToManifest(zioEntries);
+            if (canceled) return;
+            ZioEntry ze = new ZioEntry( JarFile.MANIFEST_NAME);
+            ze.setTime(timestamp);
+            manifest.write(ze.getOutputStream());
+            zipOutput.write(ze);
+
+
+            // CERT.SF
+            ze = new ZioEntry(CERT_SF_NAME);
+            ze.setTime(timestamp);
+            
+            ByteArrayOutputStream out = new ByteArrayOutputStream();
+            generateSignatureFile(manifest, out);
+            if (canceled) return;
+            byte[] sfBytes = out.toByteArray();
+            if (debug) {
+                getLogger().debug( "Signature File: \n" + new String( sfBytes) + "\n" + 
+                        HexDumpEncoder.encode( sfBytes));
+            }
+            ze.getOutputStream().write(sfBytes);
+            zipOutput.write(ze);
+
+            // CERT.RSA
+            progressHelper.progress( ProgressEvent.PRORITY_NORMAL, resourceAdapter.getString(ResourceAdapter.Item.GENERATING_SIGNATURE_BLOCK));
+            ze = new ZioEntry(CERT_RSA_NAME);
+            ze.setTime(timestamp);
+            writeSignatureBlock(keySet, sfBytes, ze.getOutputStream());
+            zipOutput.write( ze);
+            if (canceled) return;
+
+            // Everything else
+            copyFiles(manifest, zioEntries, zipOutput, timestamp);
+            if (canceled) return;
+            
+        }
+        finally {
+            zipOutput.close();
+            if (canceled) {
+                try {
+                    if (outputZipFilename != null) new File( outputZipFilename).delete();
+                }
+                catch (Throwable t) {
+                    getLogger().warning( t.getClass().getName() + ":" + t.getMessage());
+                }
+            }
+        }
+    }
+    
+    public void addProgressListener( ProgressListener l)
+    {
+        progressHelper.addProgressListener(l);
+    }
+
+    public synchronized void removeProgressListener( ProgressListener l)
+    {
+        progressHelper.removeProgressListener(l);
+    }     
+
+    
+    public static class AutoKeyObservable extends Observable
+    {
+        @Override
+        public void notifyObservers(Object arg) {
+            super.setChanged();
+            super.notifyObservers(arg);
+        }
+        
+    }
+}
\ No newline at end of file
diff --git a/extern/zipsigner/src/main/java/kellinwood/security/zipsigner/optional/CertCreator.java b/extern/zipsigner/src/main/java/kellinwood/security/zipsigner/optional/CertCreator.java
new file mode 100644
index 000000000..3a15ceae4
--- /dev/null
+++ b/extern/zipsigner/src/main/java/kellinwood/security/zipsigner/optional/CertCreator.java
@@ -0,0 +1,132 @@
+package kellinwood.security.zipsigner.optional;
+
+import kellinwood.security.zipsigner.KeySet;
+import org.spongycastle.jce.X509Principal;
+import org.spongycastle.x509.X509V3CertificateGenerator;
+
+import java.io.File;
+import java.io.IOException;
+import java.math.BigInteger;
+import java.security.*;
+import java.security.cert.X509Certificate;
+import java.util.Date;
+
+
+/**
+ * All methods create self-signed certificates.
+ */
+public class CertCreator {
+
+    /** Creates a new keystore and self-signed key.  The key will have the same password as the key, and will be
+     *  RSA 2048, with the cert signed using SHA1withRSA.  The certificate will have a validity of
+     *  30 years).
+     *
+     * @param storePath - pathname of the new keystore file
+     * @param password - keystore and key password
+     * @param keyName - the new key will have this as its alias within the keystore
+     * @param distinguishedNameValues - contains Country, State, Locality,...,Common Name, etc.
+     */
+    public static void createKeystoreAndKey( String storePath, char[] password,
+                                      String keyName, DistinguishedNameValues distinguishedNameValues)
+    {
+        createKeystoreAndKey(storePath, password, "RSA", 2048, keyName, password, "SHA1withRSA", 30,
+            distinguishedNameValues);
+    }
+
+
+    public static KeySet createKeystoreAndKey( String storePath, char[] storePass,
+                                      String keyAlgorithm, int keySize, String keyName, char[] keyPass,
+                                      String certSignatureAlgorithm, int certValidityYears, DistinguishedNameValues distinguishedNameValues) {
+        try {
+
+            KeySet keySet = createKey(keyAlgorithm, keySize, keyName, certSignatureAlgorithm, certValidityYears,
+                distinguishedNameValues);
+
+
+            KeyStore privateKS = KeyStoreFileManager.createKeyStore(storePath, storePass);
+
+            privateKS.setKeyEntry(keyName, keySet.getPrivateKey(),
+                keyPass,
+                new java.security.cert.Certificate[]{keySet.getPublicKey()});
+
+            File sfile = new File(storePath);
+            if (sfile.exists()) {
+                throw new IOException("File already exists: " + storePath);
+            }
+            KeyStoreFileManager.writeKeyStore( privateKS, storePath, storePass);
+
+            return keySet;
+        } catch (RuntimeException x) {
+            throw x;
+        } catch ( Exception x) {
+            throw new RuntimeException( x.getMessage(), x);
+        }
+    }
+
+    /** Create a new key and store it in an existing keystore.
+     *
+     */
+    public static KeySet createKey( String storePath, char[] storePass,
+                           String keyAlgorithm, int keySize, String keyName, char[] keyPass,
+                           String certSignatureAlgorithm, int certValidityYears,
+                           DistinguishedNameValues distinguishedNameValues) {
+        try {
+
+            KeySet keySet = createKey(keyAlgorithm, keySize, keyName, certSignatureAlgorithm, certValidityYears,
+                distinguishedNameValues);
+
+            KeyStore privateKS = KeyStoreFileManager.loadKeyStore(storePath, storePass);
+
+            privateKS.setKeyEntry(keyName, keySet.getPrivateKey(),
+                keyPass,
+                new java.security.cert.Certificate[]{keySet.getPublicKey()});
+
+            KeyStoreFileManager.writeKeyStore( privateKS, storePath, storePass);
+
+            return keySet;
+
+        } catch (RuntimeException x) {
+            throw x;
+        } catch ( Exception x) {
+            throw new RuntimeException(x.getMessage(), x);
+        }
+    }
+
+    public static KeySet createKey( String keyAlgorithm, int keySize, String keyName,
+                           String certSignatureAlgorithm, int certValidityYears, DistinguishedNameValues distinguishedNameValues)
+    {
+        try {
+            KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(keyAlgorithm);
+            keyPairGenerator.initialize(keySize);
+            KeyPair KPair = keyPairGenerator.generateKeyPair();
+
+            X509V3CertificateGenerator v3CertGen = new X509V3CertificateGenerator();
+
+            X509Principal principal = distinguishedNameValues.getPrincipal();
+
+            // generate a postitive serial number
+            BigInteger serialNumber = BigInteger.valueOf(new SecureRandom().nextInt());
+            while (serialNumber.compareTo(BigInteger.ZERO) < 0) {
+                serialNumber = BigInteger.valueOf(new SecureRandom().nextInt());
+            }
+            v3CertGen.setSerialNumber(serialNumber);
+            v3CertGen.setIssuerDN( principal);
+            v3CertGen.setNotBefore(new Date(System.currentTimeMillis() - 1000L * 60L * 60L * 24L * 30L));
+            v3CertGen.setNotAfter(new Date(System.currentTimeMillis() + (1000L * 60L * 60L * 24L * 366L * (long)certValidityYears)));
+            v3CertGen.setSubjectDN(principal);
+
+            v3CertGen.setPublicKey(KPair.getPublic());
+            v3CertGen.setSignatureAlgorithm(certSignatureAlgorithm);
+
+            X509Certificate PKCertificate = v3CertGen.generate(KPair.getPrivate(),"BC");
+
+            KeySet keySet = new KeySet();
+            keySet.setName(keyName);
+            keySet.setPrivateKey(KPair.getPrivate());
+            keySet.setPublicKey(PKCertificate);
+            return keySet;
+        } catch (Exception x) {
+            throw new RuntimeException(x.getMessage(), x);
+        }
+    }
+}
diff --git a/extern/zipsigner/src/main/java/kellinwood/security/zipsigner/optional/CustomKeySigner.java b/extern/zipsigner/src/main/java/kellinwood/security/zipsigner/optional/CustomKeySigner.java
new file mode 100644
index 000000000..e6e0e7033
--- /dev/null
+++ b/extern/zipsigner/src/main/java/kellinwood/security/zipsigner/optional/CustomKeySigner.java
@@ -0,0 +1,37 @@
+package kellinwood.security.zipsigner.optional;
+
+import kellinwood.security.zipsigner.ZipSigner;
+
+import java.security.Key;
+import java.security.KeyStore;
+import java.security.PrivateKey;
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+
+/**
+ */
+public class CustomKeySigner {
+
+    /** KeyStore-type agnostic.  This method will sign the zip file, automatically handling JKS or BKS keystores. */
+    public static void signZip( ZipSigner zipSigner,
+                         String keystorePath,
+                         char[] keystorePw,
+                         String certAlias,
+                         char[] certPw,
+                         String signatureAlgorithm,
+                         String inputZipFilename,
+                         String outputZipFilename)
+        throws Exception
+    {
+        zipSigner.issueLoadingCertAndKeysProgressEvent();
+        KeyStore keystore = KeyStoreFileManager.loadKeyStore( keystorePath, keystorePw);
+        Certificate cert = keystore.getCertificate(certAlias);
+        X509Certificate publicKey = (X509Certificate)cert;
+        Key key = keystore.getKey(certAlias, certPw);
+        PrivateKey privateKey = (PrivateKey)key;
+
+        zipSigner.setKeys( "custom", publicKey, privateKey, signatureAlgorithm, null);
+        zipSigner.signZip( inputZipFilename, outputZipFilename);
+    }
+
+}
diff --git a/extern/zipsigner/src/main/java/kellinwood/security/zipsigner/optional/DistinguishedNameValues.java b/extern/zipsigner/src/main/java/kellinwood/security/zipsigner/optional/DistinguishedNameValues.java
new file mode 100644
index 000000000..3febea007
--- /dev/null
+++ b/extern/zipsigner/src/main/java/kellinwood/security/zipsigner/optional/DistinguishedNameValues.java
@@ -0,0 +1,89 @@
+package kellinwood.security.zipsigner.optional;
+
+import org.spongycastle.asn1.ASN1ObjectIdentifier;
+import org.spongycastle.asn1.x500.style.BCStyle;
+import org.spongycastle.jce.X509Principal;
+
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Vector;
+
+
+/**
+ * Helper class for dealing with the distinguished name RDNs.
+ */
+public class DistinguishedNameValues extends LinkedHashMap<ASN1ObjectIdentifier,String> {
+    
+    public DistinguishedNameValues() {
+        put(BCStyle.C,null);
+        put(BCStyle.ST,null);
+        put(BCStyle.L,null);
+        put(BCStyle.STREET,null);
+        put(BCStyle.O,null);
+        put(BCStyle.OU,null);
+        put(BCStyle.CN,null);
+    }
+
+    public String put(ASN1ObjectIdentifier oid, String value) {
+        if (value != null && value.equals("")) value = null;
+        if (containsKey(oid)) super.put(oid,value); // preserve original ordering
+        else {
+            super.put(oid,value);
+//            String cn = remove(BCStyle.CN); // CN will always be last.
+//            put(BCStyle.CN,cn);
+        }
+        return value;
+    }
+
+    public void setCountry( String country) {
+        put(BCStyle.C,country);
+    }
+    
+    public void setState( String state) {
+        put(BCStyle.ST,state);
+    }
+    
+    public void setLocality( String locality) {
+        put(BCStyle.L,locality);
+    }
+
+    public void setStreet( String street) {
+        put( BCStyle.STREET, street);
+    }
+
+    public void setOrganization( String organization) {
+        put(BCStyle.O,organization);
+    }
+    
+    public void setOrganizationalUnit( String organizationalUnit) {
+        put(BCStyle.OU,organizationalUnit);
+    }
+    
+    public void setCommonName( String commonName) {
+        put(BCStyle.CN,commonName);
+    }
+
+    @Override
+    public int size() {
+        int result = 0;
+        for( String value : values()) {
+            if (value != null) result += 1;
+        }
+        return result;
+    }
+
+    public X509Principal getPrincipal() {
+        Vector<ASN1ObjectIdentifier> oids = new Vector<ASN1ObjectIdentifier>();
+        Vector<String> values = new Vector<String>();
+
+        for (Map.Entry<ASN1ObjectIdentifier,String> entry : entrySet()) {
+            if (entry.getValue() != null && !entry.getValue().equals("")) {
+                oids.add( entry.getKey());
+                values.add( entry.getValue());
+            }
+        }
+
+        return new X509Principal(oids,values);
+    }
+}
diff --git a/extern/zipsigner/src/main/java/kellinwood/security/zipsigner/optional/Fingerprint.java b/extern/zipsigner/src/main/java/kellinwood/security/zipsigner/optional/Fingerprint.java
new file mode 100644
index 000000000..5f075f49e
--- /dev/null
+++ b/extern/zipsigner/src/main/java/kellinwood/security/zipsigner/optional/Fingerprint.java
@@ -0,0 +1,69 @@
+package kellinwood.security.zipsigner.optional;
+
+import kellinwood.logging.LoggerInterface;
+import kellinwood.logging.LoggerManager;
+import kellinwood.security.zipsigner.Base64;
+import org.spongycastle.util.encoders.HexTranslator;
+
+import java.security.MessageDigest;
+
+/**
+ * User: ken
+ * Date: 1/17/13
+ */
+public class Fingerprint {
+
+    static LoggerInterface logger = LoggerManager.getLogger(Fingerprint.class.getName());
+
+    static byte[] calcDigest( String algorithm, byte[] encodedCert) {
+        byte[] result = null;
+        try {
+            MessageDigest messageDigest = MessageDigest.getInstance(algorithm);
+            messageDigest.update(encodedCert);
+            result = messageDigest.digest();
+        } catch (Exception x) {
+            logger.error(x.getMessage(),x);
+        }
+        return result;
+    }
+
+    public static String hexFingerprint( String algorithm, byte[] encodedCert) {
+        try {
+            byte[] digest = calcDigest(algorithm,encodedCert);
+            if (digest == null) return null;
+            HexTranslator hexTranslator = new HexTranslator();
+            byte[] hex = new byte[digest.length * 2];
+            hexTranslator.encode(digest, 0, digest.length, hex, 0);
+            StringBuilder builder = new StringBuilder();
+            for (int i = 0; i < hex.length; i += 2) {
+                builder.append((char)hex[i]);
+                builder.append((char)hex[i+1]);
+                if (i != (hex.length - 2)) builder.append(':');
+            }
+            return builder.toString().toUpperCase();
+        } catch (Exception x) {
+            logger.error(x.getMessage(),x);
+        }
+        return null;
+    }
+
+//    public static void main(String[] args) {
+//        byte[] data = "The Silence of the Lambs is a really good movie.".getBytes();
+//        System.out.println(hexFingerprint("MD5", data));
+//        System.out.println(hexFingerprint("SHA1", data));
+//        System.out.println(base64Fingerprint("SHA1", data));
+//
+//    }
+
+    public static String base64Fingerprint( String algorithm, byte[] encodedCert) {
+        String result = null;
+        try {
+            byte[] digest = calcDigest(algorithm,encodedCert);
+            if (digest == null) return result;
+            return Base64.encode(digest);
+        } catch (Exception x) {
+            logger.error(x.getMessage(),x);
+        }
+        return result;
+    }
+}
diff --git a/extern/zipsigner/src/main/java/kellinwood/security/zipsigner/optional/JKS.java b/extern/zipsigner/src/main/java/kellinwood/security/zipsigner/optional/JKS.java
new file mode 100644
index 000000000..4ba822bb5
--- /dev/null
+++ b/extern/zipsigner/src/main/java/kellinwood/security/zipsigner/optional/JKS.java
@@ -0,0 +1,553 @@
+/* JKS.java -- implementation of the "JKS" key store.
+   Copyright (C) 2003  Casey Marshall <rsdio@metastatic.org>
+
+Permission to use, copy, modify, distribute, and sell this software and
+its documentation for any purpose is hereby granted without fee,
+provided that the above copyright notice appear in all copies and that
+both that copyright notice and this permission notice appear in
+supporting documentation.  No representations are made about the
+suitability of this software for any purpose.  It is provided "as is"
+without express or implied warranty.
+
+This program was derived by reverse-engineering Sun's own
+implementation, using only the public API that is available in the 1.4.1
+JDK.  Hence nothing in this program is, or is derived from, anything
+copyrighted by Sun Microsystems.  While the "Binary Evaluation License
+Agreement" that the JDK is licensed under contains blanket statements
+that forbid reverse-engineering (among other things), it is my position
+that US copyright law does not and cannot forbid reverse-engineering of
+software to produce a compatible implementation.  There are, in fact,
+numerous clauses in copyright law that specifically allow
+reverse-engineering, and therefore I believe it is outside of Sun's
+power to enforce restrictions on reverse-engineering of their software,
+and it is irresponsible for them to claim they can.  */
+
+
+package kellinwood.security.zipsigner.optional;
+
+import java.io.ByteArrayInputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.InputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+import java.security.DigestInputStream;
+import java.security.DigestOutputStream;
+import java.security.Key;
+import java.security.KeyFactory;
+import java.security.KeyStoreException;
+import java.security.KeyStoreSpi;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.security.UnrecoverableKeyException;
+
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.PKCS8EncodedKeySpec;
+
+import java.util.Date;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Vector;
+
+import javax.crypto.EncryptedPrivateKeyInfo;
+import javax.crypto.spec.SecretKeySpec;
+
+/**
+ * This is an implementation of Sun's proprietary key store
+ * algorithm, called "JKS" for "Java Key Store". This implementation was
+ * created entirely through reverse-engineering.
+ *
+ * <p>The format of JKS files is, from the start of the file:
+ *
+ * <ol>
+ * <li>Magic bytes. This is a four-byte integer, in big-endian byte
+ * order, equal to <code>0xFEEDFEED</code>.</li>
+ * <li>The version number (probably), as a four-byte integer (all
+ * multibyte integral types are in big-endian byte order). The current
+ * version number (in modern distributions of the JDK) is 2.</li>
+ * <li>The number of entrires in this keystore, as a four-byte
+ * integer. Call this value <i>n</i></li>
+ * <li>Then, <i>n</i> times:
+ *  <ol>
+ *  <li>The entry type, a four-byte int. The value 1 denotes a private
+ *  key entry, and 2 denotes a trusted certificate.</li>
+ *  <li>The entry's alias, formatted as strings such as those written
+ *  by <a
+ *  href="http://java.sun.com/j2se/1.4.1/docs/api/java/io/DataOutput.html#writeUTF(java.lang.String)">DataOutput.writeUTF(String)</a>.</li>
+ *  <li>An eight-byte integer, representing the entry's creation date,
+ *  in milliseconds since the epoch.
+ *
+ *  <p>Then, if the entry is a private key entry:
+ *   <ol>
+ *   <li>The size of the encoded key as a four-byte int, then that
+ *   number of bytes. The encoded key is the DER encoded bytes of the
+ *   <a
+ *   href="http://java.sun.com/j2se/1.4.1/docs/api/javax/crypto/EncryptedPrivateKeyInfo.html">EncryptedPrivateKeyInfo</a> structure (the
+ *   encryption algorithm is discussed later).</li>
+ *   <li>A four-byte integer, followed by that many encoded
+ *   certificates, encoded as described in the trusted certificates
+ *   section.</li>
+ *   </ol>
+ *
+ *  <p>Otherwise, the entry is a trusted certificate, which is encoded
+ *  as the name of the encoding algorithm (e.g. X.509), encoded the same
+ *  way as alias names. Then, a four-byte integer representing the size
+ *  of the encoded certificate, then that many bytes representing the
+ *  encoded certificate (e.g. the DER bytes in the case of X.509).
+ *  </li>
+ *  </ol>
+ * </li>
+ * <li>Then, the signature.</li>
+ * </ol>
+ * </ol>
+ * </li>
+ * </ol>
+ *
+ * <p>(See <a href="genkey.java">this file</a> for some idea of how I
+ * was able to figure out these algorithms)</p>
+ *
+ * <p>Decrypting the key works as follows:
+ *
+ * <ol>
+ * <li>The key length is the length of the ciphertext minus 40. The
+ * encrypted key, <code>ekey</code>, is the middle bytes of the
+ * ciphertext.</li>
+ * <li>Take the first 20 bytes of the encrypted key as a seed value,
+ * <code>K[0]</code>.</li>
+ * <li>Compute <code>K[1] ... K[n]</code>, where
+ * <code>|K[i]| = 20</code>, <code>n = ceil(|ekey| / 20)</code>, and
+ * <code>K[i] = SHA-1(UTF-16BE(password) + K[i-1])</code>.</li>
+ * <li><code>key = ekey ^ (K[1] + ... + K[n])</code>.</li>
+ * <li>The last 20 bytes are the checksum, computed as <code>H =
+ * SHA-1(UTF-16BE(password) + key)</code>. If this value does not match
+ * the last 20 bytes of the ciphertext, output <code>FAIL</code>. Otherwise,
+ * output <code>key</code>.</li>
+ * </ol>
+ *
+ * <p>The signature is defined as <code>SHA-1(UTF-16BE(password) +
+ * US_ASCII("Mighty Aphrodite") + encoded_keystore)</code> (yup, Sun
+ * engineers are just that clever).
+ *
+ * <p>(Above, SHA-1 denotes the secure hash algorithm, UTF-16BE the
+ * big-endian byte representation of a UTF-16 string, and US_ASCII the
+ * ASCII byte representation of the string.)
+ *
+ * <p>The source code of this class should be available in the file <a
+ * href="http://metastatic.org/source/JKS.java">JKS.java</a>.
+ *
+ * @author Casey Marshall (rsdio@metastatic.org)
+ *
+ * Changes by Ken Ellinwood:
+ * ** Fixed a NullPointerException in engineLoad(). This method must return gracefully if the keystore input stream is null.
+ * ** engineGetCertificateEntry() was updated to return the first cert in the chain for private key entries.
+ * ** Lowercase the alias names, otherwise keytool chokes on the file created by this code.
+ * ** Fixed the integrity check in engineLoad(), previously the exception was never thrown regardless of password value.
+ */
+public class JKS extends KeyStoreSpi
+{
+
+    // Constants and fields.
+    // ------------------------------------------------------------------------
+
+    /** Ah, Sun. So goddamned clever with those magic bytes. */
+    private static final int MAGIC = 0xFEEDFEED;
+
+    private static final int PRIVATE_KEY  = 1;
+    private static final int TRUSTED_CERT = 2;
+
+    private final Vector aliases;
+    private final HashMap trustedCerts;
+    private final HashMap privateKeys;
+    private final HashMap certChains;
+    private final HashMap dates;
+
+    // Constructor.
+    // ------------------------------------------------------------------------
+
+    public JKS()
+    {
+        super();
+        aliases = new Vector();
+        trustedCerts = new HashMap();
+        privateKeys = new HashMap();
+        certChains = new HashMap();
+        dates = new HashMap();
+
+    }
+
+    // Instance methods.
+    // ------------------------------------------------------------------------
+
+    public Key engineGetKey(String alias, char[] password)
+        throws NoSuchAlgorithmException, UnrecoverableKeyException
+    {
+        alias = alias.toLowerCase();
+
+        if (!privateKeys.containsKey(alias))
+            return null;
+        byte[] key = decryptKey((byte[]) privateKeys.get(alias),
+            charsToBytes(password));
+        Certificate[] chain = engineGetCertificateChain(alias);
+        if (chain.length > 0)
+        {
+            try
+            {
+                // Private and public keys MUST have the same algorithm.
+                KeyFactory fact = KeyFactory.getInstance(
+                    chain[0].getPublicKey().getAlgorithm());
+                return fact.generatePrivate(new PKCS8EncodedKeySpec(key));
+            }
+            catch (InvalidKeySpecException x)
+            {
+                throw new UnrecoverableKeyException(x.getMessage());
+            }
+        }
+        else
+            return new SecretKeySpec(key, alias);
+    }
+
+    public Certificate[] engineGetCertificateChain(String alias)
+    {
+        alias = alias.toLowerCase();
+        return (Certificate[]) certChains.get(alias);
+    }
+
+    public Certificate engineGetCertificate(String alias)
+    {
+        alias = alias.toLowerCase();
+        if (engineIsKeyEntry(alias)) {
+            Certificate[] certChain = (Certificate[]) certChains.get(alias);
+            if (certChain != null && certChain.length > 0) return certChain[0];
+        }
+        return (Certificate) trustedCerts.get(alias);
+    }
+
+    public Date engineGetCreationDate(String alias)
+    {
+        alias = alias.toLowerCase();
+        return (Date) dates.get(alias);
+    }
+
+    // XXX implement writing methods.
+
+    public void engineSetKeyEntry(String alias, Key key, char[] passwd, Certificate[] certChain)
+        throws KeyStoreException
+    {
+        alias = alias.toLowerCase();
+        if (trustedCerts.containsKey(alias))
+            throw new KeyStoreException("\"" + alias + " is a trusted certificate entry");
+        privateKeys.put(alias, encryptKey(key, charsToBytes(passwd)));
+        if (certChain != null)
+            certChains.put(alias, certChain);
+        else
+            certChains.put(alias, new Certificate[0]);
+        if (!aliases.contains(alias))
+        {
+            dates.put(alias, new Date());
+            aliases.add(alias);
+        }
+    }
+
+    public void engineSetKeyEntry(String alias, byte[] encodedKey, Certificate[] certChain)
+        throws KeyStoreException
+    {
+        alias = alias.toLowerCase();
+        if (trustedCerts.containsKey(alias))
+            throw new KeyStoreException("\"" + alias + "\" is a trusted certificate entry");
+        try
+        {
+            new EncryptedPrivateKeyInfo(encodedKey);
+        }
+        catch (IOException ioe)
+        {
+            throw new KeyStoreException("encoded key is not an EncryptedPrivateKeyInfo");
+        }
+        privateKeys.put(alias, encodedKey);
+        if (certChain != null)
+            certChains.put(alias, certChain);
+        else
+            certChains.put(alias, new Certificate[0]);
+        if (!aliases.contains(alias))
+        {
+            dates.put(alias, new Date());
+            aliases.add(alias);
+        }
+    }
+
+    public void engineSetCertificateEntry(String alias, Certificate cert)
+        throws KeyStoreException
+    {
+        alias = alias.toLowerCase();
+        if (privateKeys.containsKey(alias))
+            throw new KeyStoreException("\"" + alias + "\" is a private key entry");
+        if (cert == null)
+            throw new NullPointerException();
+        trustedCerts.put(alias, cert);
+        if (!aliases.contains(alias))
+        {
+            dates.put(alias, new Date());
+            aliases.add(alias);
+        }
+    }
+
+    public void engineDeleteEntry(String alias) throws KeyStoreException
+    {
+        alias = alias.toLowerCase();
+        aliases.remove(alias);
+    }
+
+    public Enumeration engineAliases()
+    {
+        return aliases.elements();
+    }
+
+    public boolean engineContainsAlias(String alias)
+    {
+        alias = alias.toLowerCase();
+        return aliases.contains(alias);
+    }
+
+    public int engineSize()
+    {
+        return aliases.size();
+    }
+
+    public boolean engineIsKeyEntry(String alias)
+    {
+        alias = alias.toLowerCase();
+        return privateKeys.containsKey(alias);
+    }
+
+    public boolean engineIsCertificateEntry(String alias)
+    {
+        alias = alias.toLowerCase();
+        return trustedCerts.containsKey(alias);
+    }
+
+    public String engineGetCertificateAlias(Certificate cert)
+    {
+        for (Iterator keys = trustedCerts.keySet().iterator(); keys.hasNext(); )
+        {
+            String alias = (String) keys.next();
+            if (cert.equals(trustedCerts.get(alias)))
+                return alias;
+        }
+        return null;
+    }
+
+    public void engineStore(OutputStream out, char[] passwd)
+        throws IOException, NoSuchAlgorithmException, CertificateException
+    {
+        MessageDigest md = MessageDigest.getInstance("SHA1");
+        md.update(charsToBytes(passwd));
+        md.update("Mighty Aphrodite".getBytes("UTF-8"));
+        DataOutputStream dout = new DataOutputStream(new DigestOutputStream(out, md));
+        dout.writeInt(MAGIC);
+        dout.writeInt(2);
+        dout.writeInt(aliases.size());
+        for (Enumeration e = aliases.elements(); e.hasMoreElements(); )
+        {
+            String alias = (String) e.nextElement();
+            if (trustedCerts.containsKey(alias))
+            {
+                dout.writeInt(TRUSTED_CERT);
+                dout.writeUTF(alias);
+                dout.writeLong(((Date) dates.get(alias)).getTime());
+                writeCert(dout, (Certificate) trustedCerts.get(alias));
+            }
+            else
+            {
+                dout.writeInt(PRIVATE_KEY);
+                dout.writeUTF(alias);
+                dout.writeLong(((Date) dates.get(alias)).getTime());
+                byte[] key = (byte[]) privateKeys.get(alias);
+                dout.writeInt(key.length);
+                dout.write(key);
+                Certificate[] chain = (Certificate[]) certChains.get(alias);
+                dout.writeInt(chain.length);
+                for (int i = 0; i < chain.length; i++)
+                    writeCert(dout, chain[i]);
+            }
+        }
+        byte[] digest = md.digest();
+        dout.write(digest);
+    }
+
+    public void engineLoad(InputStream in, char[] passwd)
+        throws IOException, NoSuchAlgorithmException, CertificateException
+    {
+        MessageDigest md = MessageDigest.getInstance("SHA");
+        if (passwd != null) md.update(charsToBytes(passwd));
+        md.update("Mighty Aphrodite".getBytes("UTF-8")); // HAR HAR
+        aliases.clear();
+        trustedCerts.clear();
+        privateKeys.clear();
+        certChains.clear();
+        dates.clear();
+        if (in == null) return;
+        DataInputStream din = new DataInputStream(new DigestInputStream(in, md));
+        if (din.readInt() != MAGIC)
+            throw new IOException("not a JavaKeyStore");
+        din.readInt();  // version no.
+        final int n = din.readInt();
+        aliases.ensureCapacity(n);
+        if (n < 0)
+            throw new LoadKeystoreException("Malformed key store");
+        for (int i = 0; i < n; i++)
+        {
+            int type = din.readInt();
+            String alias = din.readUTF();
+            aliases.add(alias);
+            dates.put(alias, new Date(din.readLong()));
+            switch (type)
+            {
+                case PRIVATE_KEY:
+                    int len = din.readInt();
+                    byte[] encoded = new byte[len];
+                    din.read(encoded);
+                    privateKeys.put(alias, encoded);
+                    int count = din.readInt();
+                    Certificate[] chain = new Certificate[count];
+                    for (int j = 0; j < count; j++)
+                        chain[j] = readCert(din);
+                    certChains.put(alias, chain);
+                    break;
+
+                case TRUSTED_CERT:
+                    trustedCerts.put(alias, readCert(din));
+                    break;
+
+                default:
+                    throw new LoadKeystoreException("Malformed key store");
+            }
+        }
+
+        if (passwd != null) {
+            byte[] computedHash = md.digest();
+            byte[] storedHash = new byte[20];
+            din.read(storedHash);
+            if (!MessageDigest.isEqual(storedHash, computedHash)) {
+                throw new LoadKeystoreException("Incorrect password, or integrity check failed.");
+            }
+        }
+    }
+
+    // Own methods.
+    // ------------------------------------------------------------------------
+
+    private static Certificate readCert(DataInputStream in)
+        throws IOException, CertificateException, NoSuchAlgorithmException
+    {
+        String type = in.readUTF();
+        int len = in.readInt();
+        byte[] encoded = new byte[len];
+        in.read(encoded);
+        CertificateFactory factory = CertificateFactory.getInstance(type);
+        return factory.generateCertificate(new ByteArrayInputStream(encoded));
+    }
+
+    private static void writeCert(DataOutputStream dout, Certificate cert)
+        throws IOException, CertificateException
+    {
+        dout.writeUTF(cert.getType());
+        byte[] b = cert.getEncoded();
+        dout.writeInt(b.length);
+        dout.write(b);
+    }
+
+    private static byte[] decryptKey(byte[] encryptedPKI, byte[] passwd)
+        throws UnrecoverableKeyException
+    {
+        try
+        {
+            EncryptedPrivateKeyInfo epki =
+                new EncryptedPrivateKeyInfo(encryptedPKI);
+            byte[] encr = epki.getEncryptedData();
+            byte[] keystream = new byte[20];
+            System.arraycopy(encr, 0, keystream, 0, 20);
+            byte[] check = new byte[20];
+            System.arraycopy(encr, encr.length-20, check, 0, 20);
+            byte[] key = new byte[encr.length - 40];
+            MessageDigest sha = MessageDigest.getInstance("SHA1");
+            int count = 0;
+            while (count < key.length)
+            {
+                sha.reset();
+                sha.update(passwd);
+                sha.update(keystream);
+                sha.digest(keystream, 0, keystream.length);
+                for (int i = 0; i < keystream.length && count < key.length; i++)
+                {
+                    key[count] = (byte) (keystream[i] ^ encr[count+20]);
+                    count++;
+                }
+            }
+            sha.reset();
+            sha.update(passwd);
+            sha.update(key);
+            if (!MessageDigest.isEqual(check, sha.digest()))
+                throw new UnrecoverableKeyException("checksum mismatch");
+            return key;
+        }
+        catch (Exception x)
+        {
+            throw new UnrecoverableKeyException(x.getMessage());
+        }
+    }
+
+    private static byte[] encryptKey(Key key, byte[] passwd)
+        throws KeyStoreException
+    {
+        try
+        {
+            MessageDigest sha = MessageDigest.getInstance("SHA1");
+            SecureRandom rand = SecureRandom.getInstance("SHA1PRNG");
+            byte[] k = key.getEncoded();
+            byte[] encrypted = new byte[k.length + 40];
+            byte[] keystream = rand.getSeed(20);
+            System.arraycopy(keystream, 0, encrypted, 0, 20);
+            int count = 0;
+            while (count < k.length)
+            {
+                sha.reset();
+                sha.update(passwd);
+                sha.update(keystream);
+                sha.digest(keystream, 0, keystream.length);
+                for (int i = 0; i < keystream.length && count < k.length; i++)
+                {
+                    encrypted[count+20] = (byte) (keystream[i] ^ k[count]);
+                    count++;
+                }
+            }
+            sha.reset();
+            sha.update(passwd);
+            sha.update(k);
+            sha.digest(encrypted, encrypted.length - 20, 20);
+            // 1.3.6.1.4.1.42.2.17.1.1 is Sun's private OID for this
+            // encryption algorithm.
+            return new EncryptedPrivateKeyInfo("1.3.6.1.4.1.42.2.17.1.1",
+                encrypted).getEncoded();
+        }
+        catch (Exception x)
+        {
+            throw new KeyStoreException(x.getMessage());
+        }
+    }
+
+    private static byte[] charsToBytes(char[] passwd)
+    {
+        byte[] buf = new byte[passwd.length * 2];
+        for (int i = 0, j = 0; i < passwd.length; i++)
+        {
+            buf[j++] = (byte) (passwd[i] >>> 8);
+            buf[j++] = (byte)  passwd[i];
+        }
+        return buf;
+    }
+}
diff --git a/extern/zipsigner/src/main/java/kellinwood/security/zipsigner/optional/JksKeyStore.java b/extern/zipsigner/src/main/java/kellinwood/security/zipsigner/optional/JksKeyStore.java
new file mode 100644
index 000000000..8ecc66d38
--- /dev/null
+++ b/extern/zipsigner/src/main/java/kellinwood/security/zipsigner/optional/JksKeyStore.java
@@ -0,0 +1,12 @@
+package kellinwood.security.zipsigner.optional;
+
+
+import java.security.KeyStore;
+
+public class JksKeyStore extends KeyStore {
+
+    public JksKeyStore() {
+        super(new JKS(), KeyStoreFileManager.getProvider(), "jks");
+    }
+
+}
diff --git a/extern/zipsigner/src/main/java/kellinwood/security/zipsigner/optional/KeyNameConflictException.java b/extern/zipsigner/src/main/java/kellinwood/security/zipsigner/optional/KeyNameConflictException.java
new file mode 100644
index 000000000..2e434da62
--- /dev/null
+++ b/extern/zipsigner/src/main/java/kellinwood/security/zipsigner/optional/KeyNameConflictException.java
@@ -0,0 +1,4 @@
+package kellinwood.security.zipsigner.optional;
+
+public class KeyNameConflictException extends Exception {
+}
diff --git a/extern/zipsigner/src/main/java/kellinwood/security/zipsigner/optional/KeyStoreFileManager.java b/extern/zipsigner/src/main/java/kellinwood/security/zipsigner/optional/KeyStoreFileManager.java
new file mode 100644
index 000000000..198d6df2e
--- /dev/null
+++ b/extern/zipsigner/src/main/java/kellinwood/security/zipsigner/optional/KeyStoreFileManager.java
@@ -0,0 +1,283 @@
+package kellinwood.security.zipsigner.optional;
+
+
+import kellinwood.logging.LoggerInterface;
+import kellinwood.logging.LoggerManager;
+import org.spongycastle.jce.provider.BouncyCastleProvider;
+
+import java.io.*;
+import java.security.*;
+import java.security.cert.Certificate;
+
+
+
+/**
+ */
+public class KeyStoreFileManager {
+
+    static Provider provider = new BouncyCastleProvider();
+
+    public static Provider getProvider() { return provider; }
+
+    public static void setProvider(Provider provider) {
+        if (KeyStoreFileManager.provider != null) Security.removeProvider( KeyStoreFileManager.provider.getName());
+        KeyStoreFileManager.provider = provider;
+        Security.addProvider( provider);
+    }
+
+    static LoggerInterface logger = LoggerManager.getLogger( KeyStoreFileManager.class.getName());
+
+    static {
+        // Add the spongycastle version of the BC provider so that the implementation classes returned
+        // from the keystore are all from the spongycastle libs.
+        Security.addProvider(getProvider());
+    }
+
+
+    public static KeyStore loadKeyStore( String keystorePath, String encodedPassword)
+        throws Exception{
+        char password[] = null;
+        try {
+            if (encodedPassword != null) {
+                password = PasswordObfuscator.getInstance().decodeKeystorePassword( keystorePath, encodedPassword);
+            }
+            return loadKeyStore(keystorePath, password);
+        } finally {
+            if (password != null) PasswordObfuscator.flush(password);
+        }
+    }
+
+    public static KeyStore createKeyStore( String keystorePath, char[] password)
+        throws Exception
+    {
+        KeyStore ks = null;
+        if (keystorePath.toLowerCase().endsWith(".bks")) {
+            ks = KeyStore.getInstance("bks", new BouncyCastleProvider());
+        }
+        else ks = new JksKeyStore();
+        ks.load(null, password);
+
+        return ks;
+    }
+
+    public static KeyStore loadKeyStore( String keystorePath, char[] password)
+        throws Exception
+    {
+        KeyStore ks = null;
+        try {
+            ks = new JksKeyStore();
+            FileInputStream fis = new FileInputStream( keystorePath);
+            ks.load( fis, password);
+            fis.close();
+            return ks;
+        } catch (LoadKeystoreException x) {
+            // This type of exception is thrown when the keystore is a JKS keystore, but the file is malformed
+            // or the validity/password check failed.  In this case don't bother to attempt loading it as a BKS keystore.
+            throw x;
+        } catch (Exception x) {
+            // logger.warning( x.getMessage(), x);
+            try {
+                ks = KeyStore.getInstance("bks", getProvider());
+                FileInputStream fis = new FileInputStream( keystorePath);
+                ks.load( fis, password);
+                fis.close();
+                return ks;
+            } catch (Exception e) {
+                throw new RuntimeException("Failed to load keystore: " + e.getMessage(), e);
+            }
+        }
+    }
+
+    public static void writeKeyStore( KeyStore ks, String keystorePath, String encodedPassword)
+        throws Exception
+    {
+        char password[] = null;
+        try {
+            password = PasswordObfuscator.getInstance().decodeKeystorePassword( keystorePath, encodedPassword);
+            writeKeyStore( ks, keystorePath, password);
+        } finally {
+            if (password != null) PasswordObfuscator.flush(password);
+        }
+    }
+
+    public static void writeKeyStore( KeyStore ks, String keystorePath, char[] password)
+        throws Exception
+    {
+
+        File keystoreFile = new File( keystorePath);
+        try {
+            if (keystoreFile.exists()) {
+                // I've had some trouble saving new verisons of the keystore file in which the file becomes empty/corrupt.
+                // Saving the new version to a new file and creating a backup of the old version.
+                File tmpFile = File.createTempFile( keystoreFile.getName(), null, keystoreFile.getParentFile());
+                FileOutputStream fos = new FileOutputStream( tmpFile);
+                ks.store(fos, password);
+                fos.flush();
+                fos.close();
+                /* create a backup of the previous version
+                int i = 1;
+                File backup = new File( keystorePath + "." + i + ".bak");
+                while (backup.exists()) {
+                    i += 1;
+                    backup = new File( keystorePath + "." + i + ".bak");
+                }
+                renameTo(keystoreFile, backup);
+                */
+                renameTo(tmpFile, keystoreFile);
+            } else {
+                FileOutputStream fos = new FileOutputStream( keystorePath);
+                ks.store(fos, password);
+                fos.close();
+            }
+        } catch (Exception x) {
+            try {
+                File logfile = File.createTempFile("zipsigner-error", ".log", keystoreFile.getParentFile());
+                PrintWriter pw = new PrintWriter(new FileWriter( logfile));
+                x.printStackTrace( pw);
+                pw.flush();
+                pw.close();
+            } catch (Exception y) {}
+            throw x;
+        }
+    }
+
+
+    static void copyFile(File srcFile, File destFile, boolean preserveFileDate) throws IOException {
+        if (destFile.exists() && destFile.isDirectory()) {
+            throw new IOException("Destination '" + destFile + "' exists but is a directory");
+        }
+
+        FileInputStream input = new FileInputStream(srcFile);
+        try {
+            FileOutputStream output = new FileOutputStream(destFile);
+            try {
+                byte[] buffer = new byte[4096];
+                long count = 0;
+                int n = 0;
+                while (-1 != (n = input.read(buffer))) {
+                    output.write(buffer, 0, n);
+                    count += n;
+                }
+            } finally {
+                try { output.close();  } catch (IOException x) {} // Ignore
+            }
+        } finally {
+            try { input.close(); } catch (IOException x) {}
+        }
+
+        if (srcFile.length() != destFile.length()) {
+            throw new IOException("Failed to copy full contents from '" +
+                srcFile + "' to '" + destFile + "'");
+        }
+        if (preserveFileDate) {
+            destFile.setLastModified(srcFile.lastModified());
+        }
+    }
+
+
+    public static void renameTo(File fromFile, File toFile)
+        throws IOException
+    {
+        copyFile(fromFile, toFile, true);
+        if (!fromFile.delete()) throw new IOException("Failed to delete " + fromFile);
+    }
+
+    public static void deleteKey(String storePath, String storePass, String keyName)
+        throws Exception
+    {
+        KeyStore ks = loadKeyStore( storePath, storePass);
+        ks.deleteEntry( keyName);
+        writeKeyStore(ks, storePath, storePass);
+    }
+
+    public static String renameKey( String keystorePath, String storePass, String oldKeyName, String newKeyName, String keyPass)
+        throws Exception
+    {
+        char[] keyPw = null;
+
+        try {
+            KeyStore ks = loadKeyStore(keystorePath, storePass);
+            if (ks instanceof JksKeyStore) newKeyName = newKeyName.toLowerCase();
+
+            if (ks.containsAlias(newKeyName)) throw new KeyNameConflictException();
+
+            keyPw = PasswordObfuscator.getInstance().decodeAliasPassword( keystorePath, oldKeyName, keyPass);
+            Key key = ks.getKey(oldKeyName, keyPw);
+            Certificate cert = ks.getCertificate( oldKeyName);
+
+            ks.setKeyEntry(newKeyName, key, keyPw, new Certificate[] { cert});
+            ks.deleteEntry( oldKeyName);
+
+            writeKeyStore(ks, keystorePath, storePass);
+            return newKeyName;
+        }
+        finally {
+            PasswordObfuscator.flush(keyPw);
+        }
+    }
+
+    public static KeyStore.Entry getKeyEntry( String keystorePath, String storePass, String keyName, String keyPass)
+        throws Exception
+    {
+        char[] keyPw = null;
+        KeyStore.PasswordProtection passwordProtection = null;
+
+        try {
+            KeyStore ks = loadKeyStore(keystorePath, storePass);
+            keyPw = PasswordObfuscator.getInstance().decodeAliasPassword( keystorePath, keyName, keyPass);
+            passwordProtection = new KeyStore.PasswordProtection(keyPw);
+            return ks.getEntry( keyName, passwordProtection);
+        }
+        finally {
+            if (keyPw != null) PasswordObfuscator.flush(keyPw);
+            if (passwordProtection != null) passwordProtection.destroy();
+        }
+    }
+
+    public static boolean containsKey( String keystorePath, String storePass, String keyName)
+        throws Exception
+    {
+        KeyStore ks = loadKeyStore(keystorePath, storePass);
+        return ks.containsAlias( keyName);
+    }
+
+
+    /**
+     *
+     * @param keystorePath
+     * @param encodedPassword
+     * @throws Exception if the password is invalid
+     */
+    public static void validateKeystorePassword( String keystorePath, String encodedPassword)
+        throws Exception
+    {
+        char[] password = null;
+        try {
+            KeyStore ks = KeyStoreFileManager.loadKeyStore( keystorePath, encodedPassword);
+        } finally {
+            if (password != null) PasswordObfuscator.flush(password);
+        }
+
+    }
+
+    /**
+     *
+     * @param keystorePath
+     * @param keyName
+     * @param encodedPassword
+     * @throws java.security.UnrecoverableKeyException if the password is invalid
+     */
+    public static void validateKeyPassword( String keystorePath, String keyName, String encodedPassword)
+        throws Exception
+    {
+        char[] password = null;
+        try {
+            KeyStore ks = KeyStoreFileManager.loadKeyStore( keystorePath, (char[])null);
+            password = PasswordObfuscator.getInstance().decodeAliasPassword(keystorePath,keyName, encodedPassword);
+            ks.getKey(keyName, password);
+        } finally {
+            if (password != null) PasswordObfuscator.flush(password);
+        }
+
+    }
+}
diff --git a/extern/zipsigner/src/main/java/kellinwood/security/zipsigner/optional/LoadKeystoreException.java b/extern/zipsigner/src/main/java/kellinwood/security/zipsigner/optional/LoadKeystoreException.java
new file mode 100644
index 000000000..d146abb16
--- /dev/null
+++ b/extern/zipsigner/src/main/java/kellinwood/security/zipsigner/optional/LoadKeystoreException.java
@@ -0,0 +1,24 @@
+package kellinwood.security.zipsigner.optional;
+
+import java.io.IOException;
+
+/**
+ * Thrown by JKS.engineLoad() for errors that occur after determining the keystore is actually a JKS keystore.
+ */
+public class LoadKeystoreException extends IOException {
+
+    public LoadKeystoreException() {
+    }
+
+    public LoadKeystoreException(String message) {
+        super(message);
+    }
+
+    public LoadKeystoreException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public LoadKeystoreException(Throwable cause) {
+        super(cause);
+    }
+}
diff --git a/extern/zipsigner/src/main/java/kellinwood/security/zipsigner/optional/PasswordObfuscator.java b/extern/zipsigner/src/main/java/kellinwood/security/zipsigner/optional/PasswordObfuscator.java
new file mode 100644
index 000000000..e76334e0d
--- /dev/null
+++ b/extern/zipsigner/src/main/java/kellinwood/security/zipsigner/optional/PasswordObfuscator.java
@@ -0,0 +1,127 @@
+package kellinwood.security.zipsigner.optional;
+
+import kellinwood.logging.LoggerInterface;
+import kellinwood.logging.LoggerManager;
+import kellinwood.security.zipsigner.Base64;
+
+import javax.crypto.Cipher;
+import javax.crypto.spec.SecretKeySpec;
+import java.io.*;
+
+public class PasswordObfuscator {
+
+    private static PasswordObfuscator instance = null;
+    
+    static final String x = "harold-and-maude";   
+
+    LoggerInterface logger;
+    SecretKeySpec skeySpec;
+    
+    private PasswordObfuscator() {
+        logger = LoggerManager.getLogger( PasswordObfuscator.class.getName());
+        skeySpec = new SecretKeySpec(x.getBytes(), "AES");
+    }
+
+    public static PasswordObfuscator getInstance() {
+        if (instance == null) instance = new PasswordObfuscator();
+        return instance;
+    }
+
+    public String encodeKeystorePassword( String keystorePath, String password) {
+        return encode( keystorePath, password);
+    }
+
+    public String encodeKeystorePassword( String keystorePath, char[] password) {
+        return encode( keystorePath, password);
+    }
+
+    public String encodeAliasPassword( String keystorePath, String aliasName, String password) {
+        return encode( keystorePath+aliasName, password);
+    }
+
+    public String encodeAliasPassword( String keystorePath, String aliasName, char[] password) {
+        return encode( keystorePath+aliasName, password);
+    }
+
+    public char[] decodeKeystorePassword( String keystorePath, String password) {
+        return decode(keystorePath,password);
+    }
+
+    public char[] decodeAliasPassword( String keystorePath, String aliasName, String password) {
+        return decode(keystorePath+aliasName,password);
+    }
+
+    public String encode( String junk, String password) {
+        if (password == null) return null;
+        char[] c = password.toCharArray();
+        String result = encode( junk, c);
+        flush(c);
+        return result;
+    }
+
+    public String encode( String junk, char[] password) {
+        if (password == null) return null;
+        try {
+            // Instantiate the cipher
+            Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
+            cipher.init(Cipher.ENCRYPT_MODE, skeySpec);
+            ByteArrayOutputStream baos = new ByteArrayOutputStream();
+            Writer w = new OutputStreamWriter(baos);
+            w.write(junk);
+            w.write(password);
+            w.flush();
+            byte[] encoded = cipher.doFinal( baos.toByteArray());
+            return Base64.encode( encoded);
+        } catch (Exception x) {
+            logger.error("Failed to obfuscate password", x);
+        }
+        return null;
+    }
+    
+    public char[] decode( String junk, String password) {
+        if (password == null) return null;
+        try {
+            // Instantiate the cipher  
+            Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
+            SecretKeySpec skeySpec = new SecretKeySpec(x.getBytes(), "AES");
+            cipher.init(Cipher.DECRYPT_MODE, skeySpec);
+            byte[] bytes = cipher.doFinal( Base64.decode(password.getBytes()));
+            BufferedReader r = new BufferedReader( new InputStreamReader( new ByteArrayInputStream( bytes)));
+            char[] cb = new char[128];
+            int length = 0;
+            int numRead;
+            while ((numRead = r.read(cb, length, 128-length)) != -1) {
+                length += numRead;
+            }
+            
+            if (length <= junk.length()) return null;
+            
+            char[] result = new char[ length - junk.length()];
+            int j = 0;
+            for (int i = junk.length(); i < length; i++) {
+                result[j] = cb[i];
+                j += 1;
+            }
+            flush(cb);
+            return result;
+            
+        } catch (Exception x) {
+            logger.error("Failed to decode password", x);
+        }
+        return null;
+    }
+    
+    public static void flush( char[] charArray) {
+        if (charArray == null) return;
+        for (int i = 0; i < charArray.length; i++) {
+            charArray[i] = '\0';
+        }
+    }
+
+    public static void flush( byte[] charArray) {
+        if (charArray == null) return;
+        for (int i = 0; i < charArray.length; i++) {
+            charArray[i] = 0;
+        }
+    }
+}
diff --git a/extern/zipsigner/src/main/java/kellinwood/security/zipsigner/optional/SignatureBlockGenerator.java b/extern/zipsigner/src/main/java/kellinwood/security/zipsigner/optional/SignatureBlockGenerator.java
new file mode 100644
index 000000000..e16a12d41
--- /dev/null
+++ b/extern/zipsigner/src/main/java/kellinwood/security/zipsigner/optional/SignatureBlockGenerator.java
@@ -0,0 +1,58 @@
+package kellinwood.security.zipsigner.optional;
+
+import kellinwood.security.zipsigner.KeySet;
+import org.spongycastle.cert.jcajce.JcaCertStore;
+import org.spongycastle.cms.*;
+import org.spongycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder;
+import org.spongycastle.operator.ContentSigner;
+import org.spongycastle.operator.DigestCalculatorProvider;
+import org.spongycastle.operator.jcajce.JcaContentSignerBuilder;
+import org.spongycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
+import org.spongycastle.util.Store;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ *
+ */
+public class SignatureBlockGenerator {
+
+    /**
+     * Sign the given content using the private and public keys from the keySet, and return the encoded CMS (PKCS#7) data.
+     * Use of direct signature and DER encoding produces a block that is verifiable by Android recovery programs.
+     */
+    public static byte[] generate(KeySet keySet, byte[] content) {
+        try {
+            List certList = new ArrayList();
+            CMSTypedData msg = new CMSProcessableByteArray(content);
+
+            certList.add(keySet.getPublicKey());
+
+            Store certs = new JcaCertStore(certList);
+
+            CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
+
+            JcaContentSignerBuilder jcaContentSignerBuilder = new JcaContentSignerBuilder(keySet.getSignatureAlgorithm()).setProvider("SC");
+            ContentSigner sha1Signer = jcaContentSignerBuilder.build(keySet.getPrivateKey());
+
+            JcaDigestCalculatorProviderBuilder jcaDigestCalculatorProviderBuilder = new JcaDigestCalculatorProviderBuilder().setProvider("SC");
+            DigestCalculatorProvider digestCalculatorProvider = jcaDigestCalculatorProviderBuilder.build();
+
+            JcaSignerInfoGeneratorBuilder jcaSignerInfoGeneratorBuilder = new JcaSignerInfoGeneratorBuilder( digestCalculatorProvider);
+            jcaSignerInfoGeneratorBuilder.setDirectSignature(true);
+            SignerInfoGenerator signerInfoGenerator = jcaSignerInfoGeneratorBuilder.build(sha1Signer, keySet.getPublicKey());
+
+            gen.addSignerInfoGenerator( signerInfoGenerator);
+
+            gen.addCertificates(certs);
+
+            CMSSignedData sigData = gen.generate(msg, false);
+            return sigData.toASN1Structure().getEncoded("DER");
+
+        } catch (Exception x) {
+            throw new RuntimeException(x.getMessage(), x);
+        }
+    }
+
+}
diff --git a/extern/zipsigner/src/main/java/kellinwood/zipio/CentralEnd.java b/extern/zipsigner/src/main/java/kellinwood/zipio/CentralEnd.java
new file mode 100644
index 000000000..3d3a8b2b4
--- /dev/null
+++ b/extern/zipsigner/src/main/java/kellinwood/zipio/CentralEnd.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2010 Ken Ellinwood
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package kellinwood.zipio;
+
+import java.io.IOException;
+
+import kellinwood.logging.LoggerInterface;
+import kellinwood.logging.LoggerManager;
+
+public class CentralEnd 
+{
+    public int signature = 0x06054b50; // end of central dir signature    4 bytes
+    public short numberThisDisk = 0;   // number of this disk             2 bytes     
+    public short centralStartDisk = 0; // number of the disk with the start of the central directory  2 bytes
+    public short numCentralEntries;    // total number of entries in the central directory on this disk  2 bytes
+    public short totalCentralEntries;  // total number of entries in the central directory           2 bytes
+
+    public int centralDirectorySize;   // size of the central directory   4 bytes
+    public int centralStartOffset;     // offset of start of central directory with respect to the starting disk number        4 bytes
+    public String fileComment;         // .ZIP file comment       (variable size)
+
+    private static LoggerInterface log;
+
+    public static CentralEnd read(ZipInput input) throws IOException
+    {
+
+        int signature = input.readInt();
+        if (signature != 0x06054b50) {
+            // back up to the signature
+            input.seek( input.getFilePointer() - 4);
+            return null;
+        }
+
+        CentralEnd entry = new CentralEnd();
+
+        entry.doRead( input);
+        return entry;
+    }
+
+    public static LoggerInterface getLogger() {
+        if (log == null) log = LoggerManager.getLogger( CentralEnd.class.getName());
+        return log;
+    }
+
+
+    private void doRead( ZipInput input) throws IOException
+    {
+
+        boolean debug = getLogger().isDebugEnabled();
+
+        numberThisDisk = input.readShort();
+        if (debug) log.debug( String.format("This disk number: 0x%04x", numberThisDisk));
+
+        centralStartDisk = input.readShort();
+        if (debug) log.debug( String.format("Central dir start disk number: 0x%04x", centralStartDisk));
+
+        numCentralEntries = input.readShort();
+        if (debug) log.debug( String.format("Central entries on this disk: 0x%04x", numCentralEntries));
+
+        totalCentralEntries = input.readShort();
+        if (debug) log.debug( String.format("Total number of central entries: 0x%04x", totalCentralEntries));
+
+        centralDirectorySize = input.readInt();
+        if (debug) log.debug( String.format("Central directory size: 0x%08x", centralDirectorySize));
+
+        centralStartOffset = input.readInt();
+        if (debug) log.debug( String.format("Central directory offset: 0x%08x", centralStartOffset));
+
+        short zipFileCommentLen = input.readShort();
+        fileComment = input.readString(zipFileCommentLen);
+        if (debug) log.debug( ".ZIP file comment: " + fileComment);
+
+
+    }
+
+
+    public void write( ZipOutput output) throws IOException
+    {
+
+        boolean debug = getLogger().isDebugEnabled();
+
+        output.writeInt( signature);
+        output.writeShort( numberThisDisk);
+        output.writeShort( centralStartDisk);
+        output.writeShort( numCentralEntries);
+        output.writeShort( totalCentralEntries);
+        output.writeInt( centralDirectorySize );
+        output.writeInt( centralStartOffset );
+        output.writeShort( (short)fileComment.length());
+        output.writeString( fileComment);
+
+
+    }
+
+}
diff --git a/extern/zipsigner/src/main/java/kellinwood/zipio/ZioEntry.java b/extern/zipsigner/src/main/java/kellinwood/zipio/ZioEntry.java
new file mode 100644
index 000000000..a2c80f970
--- /dev/null
+++ b/extern/zipsigner/src/main/java/kellinwood/zipio/ZioEntry.java
@@ -0,0 +1,632 @@
+/*
+ * Copyright (C) 2010 Ken Ellinwood
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package kellinwood.zipio;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.File;
+import java.io.OutputStream;
+import java.io.SequenceInputStream;
+import java.util.Date;
+import java.util.zip.CRC32;
+import java.util.zip.Inflater;
+import java.util.zip.InflaterInputStream;
+
+import kellinwood.logging.LoggerInterface;
+import kellinwood.logging.LoggerManager;
+
+public class ZioEntry implements Cloneable {
+
+    private ZipInput zipInput;
+
+    // public int signature = 0x02014b50;
+    private short versionMadeBy;
+    private short versionRequired;
+    private short generalPurposeBits;
+    private short compression;
+    private short modificationTime;
+    private short modificationDate;
+    private int crc32;
+    private int compressedSize;
+    private int size;
+    private String filename;
+    private byte[] extraData;
+    private short numAlignBytes = 0;
+    private String fileComment;
+    private short diskNumberStart;
+    private short internalAttributes;
+    private int externalAttributes;
+    
+    private int localHeaderOffset;
+    private long dataPosition = -1;
+    private byte[] data = null;
+    private ZioEntryOutputStream entryOut = null;
+    
+
+    private static byte[] alignBytes = new byte[4];
+    
+    private static LoggerInterface log;
+
+    public ZioEntry( ZipInput input) {
+        zipInput = input;
+    }
+
+    public static LoggerInterface getLogger() {
+        if (log == null) log = LoggerManager.getLogger( ZioEntry.class.getName());
+        return log;
+    }
+
+    public ZioEntry( String name) {
+        filename = name;
+        fileComment = "";
+        compression = 8;
+        extraData = new byte[0];
+        setTime( System.currentTimeMillis());
+    }
+
+    
+    public ZioEntry( String name, String sourceDataFile)
+        throws IOException
+    {
+        zipInput = new ZipInput( sourceDataFile);
+        filename = name;
+        fileComment = "";
+        this.compression = 0;
+        this.size = (int)zipInput.getFileLength();
+        this.compressedSize = this.size;
+
+        if (getLogger().isDebugEnabled()) 
+            getLogger().debug(String.format("Computing CRC for %s, size=%d",sourceDataFile,size));
+        
+        // compute CRC
+        CRC32 crc = new CRC32();
+
+        byte[] buffer = new byte[8096];
+
+        int numRead = 0;
+        while (numRead != size) {
+            int count = zipInput.read( buffer, 0, Math.min( buffer.length, (this.size - numRead)));
+            if (count > 0) {
+                crc.update( buffer, 0, count);
+                numRead += count;
+            }
+        }
+
+        this.crc32 = (int)crc.getValue();
+
+        zipInput.seek(0);
+        this.dataPosition = 0;
+        extraData = new byte[0];
+        setTime( new File(sourceDataFile).lastModified());
+    }
+    
+    
+
+    public ZioEntry( String name, String sourceDataFile, short compression, int crc32, int compressedSize, int size)
+        throws IOException
+    {
+        zipInput = new ZipInput( sourceDataFile);
+        filename = name;
+        fileComment = "";
+        this.compression = compression;
+        this.crc32 = crc32;
+        this.compressedSize = compressedSize;
+        this.size = size;
+        this.dataPosition = 0;
+        extraData = new byte[0];
+        setTime( new File(sourceDataFile).lastModified());
+    }
+    
+    // Return a copy with a new name
+    public ZioEntry getClonedEntry( String newName) 
+    {
+        
+        ZioEntry clone;
+        try {
+            clone = (ZioEntry)this.clone();
+        } 
+        catch (CloneNotSupportedException e) 
+        {
+            throw new IllegalStateException("clone() failed!");
+        }
+        clone.setName(newName);
+        return clone;
+    }
+    
+    public void readLocalHeader() throws IOException
+    {
+        ZipInput input = zipInput;
+        int tmp;
+        boolean debug = getLogger().isDebugEnabled();
+
+        input.seek( localHeaderOffset);
+
+        if (debug) getLogger().debug( String.format("FILE POSITION: 0x%08x", input.getFilePointer()));
+
+        // 0 	4 	Local file header signature = 0x04034b50
+        int signature = input.readInt();
+        if (signature != 0x04034b50) {
+            throw new IllegalStateException( String.format("Local header not found at pos=0x%08x, file=%s", input.getFilePointer(), filename));
+        }
+
+        // This method is usually called just before the data read, so
+        // its only purpose currently is to position the file pointer
+        // for the data read.  The entry's attributes might also have
+        // been changed since the central dir entry was read (e.g.,
+        // filename), so throw away the values here.
+
+        int tmpInt;
+        short tmpShort;
+        
+        // 4 	2 	Version needed to extract (minimum)
+        /* versionRequired */ tmpShort =  input.readShort();
+        if (debug) log.debug(String.format("Version required: 0x%04x", tmpShort /*versionRequired*/));
+
+        // 6 	2 	General purpose bit flag
+        /* generalPurposeBits */ tmpShort = input.readShort();
+        if (debug) log.debug(String.format("General purpose bits: 0x%04x", tmpShort /* generalPurposeBits */));
+        
+        // 8 	2 	Compression method
+        /* compression */ tmpShort = input.readShort();
+        if (debug) log.debug(String.format("Compression: 0x%04x", tmpShort /* compression */));
+
+        // 10 	2 	File last modification time
+        /* modificationTime */ tmpShort = input.readShort();
+        if (debug) log.debug(String.format("Modification time: 0x%04x", tmpShort /* modificationTime */));
+
+        // 12 	2 	File last modification date
+        /* modificationDate */ tmpShort = input.readShort();
+        if (debug) log.debug(String.format("Modification date: 0x%04x", tmpShort /* modificationDate */));
+
+        // 14 	4 	CRC-32
+        /* crc32 */ tmpInt = input.readInt();
+        if (debug) log.debug(String.format("CRC-32: 0x%04x", tmpInt /*crc32*/));
+
+        // 18 	4 	Compressed size
+        /* compressedSize*/ tmpInt = input.readInt();
+        if (debug) log.debug(String.format("Compressed size: 0x%04x", tmpInt /*compressedSize*/));
+
+        // 22 	4 	Uncompressed size
+        /* size */ tmpInt = input.readInt();
+        if (debug) log.debug(String.format("Size: 0x%04x", tmpInt /*size*/ ));
+
+        // 26 	2 	File name length (n)
+        short fileNameLen = input.readShort();
+        if (debug) log.debug(String.format("File name length: 0x%04x", fileNameLen));
+
+        // 28 	2 	Extra field length (m)
+        short extraLen = input.readShort();
+        if (debug) log.debug(String.format("Extra length: 0x%04x", extraLen));
+
+        // 30 	n 	File name      
+        String filename = input.readString(fileNameLen);
+        if (debug) log.debug("Filename: " + filename);
+
+        // Extra data
+        byte[] extra = input.readBytes( extraLen);
+
+        // Record the file position of this entry's data.
+        dataPosition = input.getFilePointer();
+        if (debug) log.debug(String.format("Data position: 0x%08x",dataPosition));
+
+    }
+
+    public void writeLocalEntry( ZipOutput output) throws IOException
+    {
+        if (data == null && dataPosition < 0 && zipInput != null) {
+            readLocalHeader();
+        }
+        
+        localHeaderOffset = (int)output.getFilePointer();
+
+        boolean debug = getLogger().isDebugEnabled();
+        
+        if (debug) {
+            getLogger().debug( String.format("Writing local header at 0x%08x - %s", localHeaderOffset, filename));
+        }
+        
+        if (entryOut != null) {
+            entryOut.close();
+            size = entryOut.getSize();
+            data = ((ByteArrayOutputStream)entryOut.getWrappedStream()).toByteArray();
+            compressedSize = data.length;
+            crc32 = entryOut.getCRC();
+        }
+        
+        output.writeInt( 0x04034b50);
+        output.writeShort( versionRequired);
+        output.writeShort( generalPurposeBits);
+        output.writeShort( compression);
+        output.writeShort( modificationTime);
+        output.writeShort( modificationDate);
+        output.writeInt( crc32);
+        output.writeInt( compressedSize);
+        output.writeInt( size);
+        output.writeShort( (short)filename.length());
+
+        numAlignBytes = 0;
+
+        // Zipalign if the file is uncompressed, i.e., "Stored", and file size is not zero.
+        if (compression == 0) {
+
+            long dataPos = output.getFilePointer() + // current position
+            2 +                                  // plus size of extra data length
+            filename.length() +                  // plus filename
+            extraData.length;                    // plus extra data
+
+            short dataPosMod4 = (short)(dataPos % 4);
+
+            if (dataPosMod4 > 0) {
+                numAlignBytes = (short)(4 - dataPosMod4);
+            }
+        }
+
+        
+        // 28 	2 	Extra field length (m)
+        output.writeShort( (short)(extraData.length + numAlignBytes));
+
+        // 30 	n 	File name
+        output.writeString( filename);
+
+        // Extra data
+        output.writeBytes( extraData);
+
+        // Zipalign bytes
+        if (numAlignBytes > 0) {
+            output.writeBytes( alignBytes, 0, numAlignBytes);
+        }
+
+        if (debug) getLogger().debug(String.format("Data position 0x%08x", output.getFilePointer()));
+        if (data != null) {
+            output.writeBytes( data);
+            if (debug) getLogger().debug(String.format("Wrote %d bytes", data.length));
+        }
+        else {
+
+            if (debug) getLogger().debug(String.format("Seeking to position 0x%08x", dataPosition));
+            zipInput.seek( dataPosition);
+            
+            int bufferSize = Math.min( compressedSize, 8096);
+            byte[] buffer = new byte[bufferSize];
+            long totalCount = 0;
+            
+            while (totalCount != compressedSize) {
+                int numRead = zipInput.in.read( buffer, 0, (int)Math.min( compressedSize -  totalCount, bufferSize));  
+                if (numRead > 0) {
+                    output.writeBytes(buffer, 0, numRead);
+                    if (debug) getLogger().debug(String.format("Wrote %d bytes", numRead));
+                    totalCount += numRead;
+                }
+                else throw new IllegalStateException(String.format("EOF reached while copying %s with %d bytes left to go", filename, compressedSize -  totalCount));
+            }
+        }
+    }		
+    
+    public static ZioEntry read(ZipInput input) throws IOException
+    {
+
+        // 0    4   Central directory header signature = 0x02014b50
+        int signature = input.readInt();
+        if (signature != 0x02014b50) {
+            // back up to the signature
+            input.seek( input.getFilePointer() - 4);
+            return null;
+        }
+
+        ZioEntry entry = new ZioEntry( input);
+
+        entry.doRead( input);
+        return entry;
+    }
+
+    private void doRead( ZipInput input) throws IOException
+    {
+
+        boolean debug = getLogger().isDebugEnabled();
+
+        // 4    2   Version needed to extract (minimum)
+        versionMadeBy = input.readShort();
+        if (debug) log.debug(String.format("Version made by: 0x%04x", versionMadeBy));
+
+        // 4    2   Version required
+        versionRequired = input.readShort();
+        if (debug) log.debug(String.format("Version required: 0x%04x", versionRequired));
+
+        // 6    2   General purpose bit flag
+        generalPurposeBits = input.readShort();
+        if (debug) log.debug(String.format("General purpose bits: 0x%04x", generalPurposeBits));
+        // Bits 1, 2, 3, and 11 are allowed to be set (first bit is bit zero).  Any others are a problem.
+        if ((generalPurposeBits & 0xF7F1) != 0x0000) {
+            throw new IllegalStateException("Can't handle general purpose bits == "+String.format("0x%04x",generalPurposeBits));
+        }
+
+        // 8    2   Compression method
+        compression = input.readShort();
+        if (debug) log.debug(String.format("Compression: 0x%04x", compression));
+
+        // 10   2   File last modification time
+        modificationTime = input.readShort();
+        if (debug) log.debug(String.format("Modification time: 0x%04x", modificationTime));
+
+        // 12   2   File last modification date
+        modificationDate = input.readShort();
+        if (debug) log.debug(String.format("Modification date: 0x%04x", modificationDate));
+
+        // 14   4   CRC-32
+        crc32 = input.readInt();
+        if (debug) log.debug(String.format("CRC-32: 0x%04x", crc32));
+
+        // 18   4   Compressed size
+        compressedSize = input.readInt();
+        if (debug) log.debug(String.format("Compressed size: 0x%04x", compressedSize));
+
+        // 22   4   Uncompressed size
+        size = input.readInt();
+        if (debug) log.debug(String.format("Size: 0x%04x", size));
+
+        // 26   2   File name length (n)
+        short fileNameLen = input.readShort();
+        if (debug) log.debug(String.format("File name length: 0x%04x", fileNameLen));
+
+        // 28   2   Extra field length (m)
+        short extraLen = input.readShort();
+        if (debug) log.debug(String.format("Extra length: 0x%04x", extraLen));
+
+        short fileCommentLen = input.readShort();
+        if (debug) log.debug(String.format("File comment length: 0x%04x", fileCommentLen));
+
+        diskNumberStart = input.readShort();
+        if (debug) log.debug(String.format("Disk number start: 0x%04x", diskNumberStart));
+
+        internalAttributes = input.readShort();
+        if (debug) log.debug(String.format("Internal attributes: 0x%04x", internalAttributes));
+
+        externalAttributes = input.readInt();
+        if (debug) log.debug(String.format("External attributes: 0x%08x", externalAttributes));
+
+        localHeaderOffset = input.readInt();
+        if (debug) log.debug(String.format("Local header offset: 0x%08x", localHeaderOffset));
+
+        // 30   n   File name      
+        filename = input.readString(fileNameLen);
+        if (debug) log.debug("Filename: " + filename);
+
+        extraData = input.readBytes( extraLen);
+
+        fileComment = input.readString( fileCommentLen);
+        if (debug) log.debug("File comment: " + fileComment);
+
+        generalPurposeBits = (short)(generalPurposeBits & 0x0800); // Don't write a data descriptor, preserve UTF-8 encoded filename bit
+        
+        // Don't write zero-length entries with compression.
+        if (size == 0) {
+            compressedSize = 0;
+            compression = 0;
+            crc32 = 0;
+        }
+
+    }
+
+    /** Returns the entry's data. */
+    public byte[] getData() throws IOException
+    {
+        if (data != null) return data;
+        
+        byte[] tmpdata = new byte[size];
+        
+        InputStream din = getInputStream();
+        int count = 0;
+        
+        while (count != size) {
+            int numRead = din.read( tmpdata, count, size-count);
+            if (numRead < 0) throw new IllegalStateException(String.format("Read failed, expecting %d bytes, got %d instead", size, count));
+            count += numRead;
+        }
+        return tmpdata;
+    }
+
+    // Returns an input stream for reading the entry's data. 
+    public InputStream getInputStream() throws IOException {
+        return getInputStream(null);
+    }
+
+    // Returns an input stream for reading the entry's data. 
+    public InputStream getInputStream(OutputStream monitorStream) throws IOException {
+        
+        if (entryOut != null) {
+            entryOut.close();
+            size = entryOut.getSize();
+            data = ((ByteArrayOutputStream)entryOut.getWrappedStream()).toByteArray();
+            compressedSize = data.length;
+            crc32 = entryOut.getCRC();
+            entryOut = null;
+            InputStream rawis = new ByteArrayInputStream( data);
+            if (compression == 0) return rawis;
+            else {
+                // Hacky, inflate using a sequence of input streams that returns 1 byte more than the actual length of the data.  
+                // This extra dummy byte is required by InflaterInputStream when the data doesn't have the header and crc fields (as it is in zip files). 
+                return new InflaterInputStream( new SequenceInputStream(rawis, new ByteArrayInputStream(new byte[1])), new Inflater( true));
+            }
+        }
+        
+        ZioEntryInputStream dataStream;
+        dataStream = new ZioEntryInputStream(this);
+        if (monitorStream != null) dataStream.setMonitorStream( monitorStream);
+        if (compression != 0)  {
+            // Note: When using nowrap=true with Inflater it is also necessary to provide 
+            // an extra "dummy" byte as input. This is required by the ZLIB native library 
+            // in order to support certain optimizations.
+            dataStream.setReturnDummyByte(true);
+            return new InflaterInputStream( dataStream, new Inflater( true));
+        }
+        else return dataStream;
+    }
+
+    // Returns an output stream for writing an entry's data.
+    public OutputStream getOutputStream() 
+    {
+        entryOut = new ZioEntryOutputStream( compression, new ByteArrayOutputStream());
+        return entryOut;
+    }
+
+
+    public void write( ZipOutput output) throws IOException {
+        boolean debug = getLogger().isDebugEnabled();
+
+
+        output.writeInt( 0x02014b50);
+        output.writeShort( versionMadeBy);
+        output.writeShort( versionRequired);
+        output.writeShort( generalPurposeBits);
+        output.writeShort( compression);
+        output.writeShort( modificationTime);
+        output.writeShort( modificationDate);
+        output.writeInt( crc32);
+        output.writeInt( compressedSize);
+        output.writeInt( size);
+        output.writeShort( (short)filename.length());
+        output.writeShort( (short)(extraData.length + numAlignBytes));
+        output.writeShort( (short)fileComment.length());
+        output.writeShort( diskNumberStart);
+        output.writeShort( internalAttributes);
+        output.writeInt( externalAttributes);
+        output.writeInt( localHeaderOffset);
+        
+        output.writeString( filename);
+        output.writeBytes( extraData);
+        if (numAlignBytes > 0) output.writeBytes( alignBytes, 0, numAlignBytes);
+        output.writeString( fileComment);
+
+    }
+
+    /*
+     * Returns timetamp in Java format
+     */
+    public long getTime() {
+        int year = (int)(((modificationDate >> 9) & 0x007f) + 80);
+        int month = (int)(((modificationDate >> 5) & 0x000f) - 1);
+        int day = (int)(modificationDate & 0x001f);
+        int hour = (int)((modificationTime >> 11) & 0x001f);
+        int minute = (int)((modificationTime >> 5) & 0x003f);
+        int seconds = (int)((modificationTime << 1) & 0x003e);
+        Date d = new Date( year, month, day, hour, minute, seconds);
+        return d.getTime();
+    }
+
+    /*
+     * Set the file timestamp (using a Java time value).
+     */
+    public void setTime(long time) {
+        Date d = new Date(time);
+        long dtime;
+        int year = d.getYear() + 1900;
+        if (year < 1980) {
+            dtime = (1 << 21) | (1 << 16);
+        }
+        else {
+            dtime = (year - 1980) << 25 | (d.getMonth() + 1) << 21 |
+            d.getDate() << 16 | d.getHours() << 11 | d.getMinutes() << 5 |
+            d.getSeconds() >> 1;
+        }
+
+        modificationDate = (short)(dtime >> 16);
+        modificationTime = (short)(dtime & 0xFFFF);
+    }
+
+    public boolean isDirectory() {
+        return filename.endsWith("/");
+    }
+    
+    public String getName() {
+        return filename;
+    }
+    
+    public void setName( String filename) {
+        this.filename = filename;
+    }
+    
+    /** Use 0 (STORED), or 8 (DEFLATE). */
+    public void setCompression( int compression) {
+        this.compression = (short)compression;
+    }
+
+    public short getVersionMadeBy() {
+        return versionMadeBy;
+    }
+
+    public short getVersionRequired() {
+        return versionRequired;
+    }
+
+    public short getGeneralPurposeBits() {
+        return generalPurposeBits;
+    }
+
+    public short getCompression() {
+        return compression;
+    }
+
+    public int getCrc32() {
+        return crc32;
+    }
+
+    public int getCompressedSize() {
+        return compressedSize;
+    }
+
+    public int getSize() {
+        return size;
+    }
+
+    public byte[] getExtraData() {
+        return extraData;
+    }
+
+    public String getFileComment() {
+        return fileComment;
+    }
+
+    public short getDiskNumberStart() {
+        return diskNumberStart;
+    }
+
+    public short getInternalAttributes() {
+        return internalAttributes;
+    }
+
+    public int getExternalAttributes() {
+        return externalAttributes;
+    }
+
+    public int getLocalHeaderOffset() {
+        return localHeaderOffset;
+    }
+
+    public long getDataPosition() {
+        return dataPosition;
+    }
+
+    public ZioEntryOutputStream getEntryOut() {
+        return entryOut;
+    }
+
+    public ZipInput getZipInput() {
+        return zipInput;
+    }
+
+}
diff --git a/extern/zipsigner/src/main/java/kellinwood/zipio/ZioEntryInputStream.java b/extern/zipsigner/src/main/java/kellinwood/zipio/ZioEntryInputStream.java
new file mode 100644
index 000000000..8d4b7cecd
--- /dev/null
+++ b/extern/zipsigner/src/main/java/kellinwood/zipio/ZioEntryInputStream.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2010 Ken Ellinwood
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package kellinwood.zipio;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.RandomAccessFile;
+
+import kellinwood.logging.LoggerInterface;
+import kellinwood.logging.LoggerManager;
+
+
+/** Input stream used to read just the data from a zip file entry. */
+public class ZioEntryInputStream extends InputStream {
+
+    RandomAccessFile raf;
+    int size;
+    int offset;
+    LoggerInterface log;
+    boolean debug;
+    boolean returnDummyByte = false;
+    OutputStream monitor = null;
+    
+    public ZioEntryInputStream( ZioEntry entry) throws IOException {
+
+        log = LoggerManager.getLogger( this.getClass().getName());
+        debug = log.isDebugEnabled();
+        offset = 0;
+        size = entry.getCompressedSize();
+        raf = entry.getZipInput().in;
+        long dpos = entry.getDataPosition();
+        if (dpos >= 0) {
+            if (debug) log.debug(String.format("Seeking to %d", entry.getDataPosition()));
+            raf.seek( entry.getDataPosition());
+        }
+        else {
+            // seeks to, then reads, the local header, causing the 
+            // file pointer to be positioned at the start of the data.
+            entry.readLocalHeader();
+        }
+        
+    }
+
+    public void setReturnDummyByte( boolean returnExtraByte) {
+        returnDummyByte = returnExtraByte;
+    }
+    
+    // For debugging, if the monitor is set we write all data read to the monitor.
+    public void setMonitorStream(OutputStream monitorStream) {
+        monitor = monitorStream;
+    }
+    
+    @Override
+    public void close() throws IOException {
+    }
+
+    @Override
+    public boolean markSupported() {
+        return false;
+    }
+
+    @Override
+    public int available() throws IOException {
+        int available = size - offset;
+        if (debug) log.debug(String.format("Available = %d", available));
+        if (available == 0 && returnDummyByte) return 1;
+        else return available;
+    }
+
+    @Override
+    public int read() throws IOException {
+        if ((size - offset) == 0) {
+            if (returnDummyByte) {
+                returnDummyByte = false;
+                return 0;
+            }
+            else return -1;
+        }
+        int b = raf.read();
+        if (b >= 0) {
+            if (monitor != null) monitor.write(b);
+            if (debug) log.debug("Read 1 byte");
+            offset += 1;
+        }
+        else if (debug) log.debug("Read 0 bytes");        
+        return b;
+    }
+
+    @Override
+    public int read(byte[] b, int off, int len) throws IOException {
+        return readBytes( b, off, len);
+    }
+
+    private int readBytes(byte[] b, int off, int len) throws IOException {
+        if ((size - offset) == 0) {
+            if (returnDummyByte) {
+                returnDummyByte = false;
+                b[off] = 0;
+                return 1;
+            }
+            else return -1;
+        }        
+        int numToRead = Math.min( len, available());
+        int numRead = raf.read(b, off, numToRead);
+        if (numRead > 0) {
+            if (monitor != null) monitor.write(b, off, numRead);
+            offset += numRead;
+        }
+        if (debug) log.debug(String.format("Read %d bytes for read(b,%d,%d)", numRead, off, len));
+        return numRead;
+    }
+
+    @Override
+    public int read(byte[] b) throws IOException {
+        return readBytes( b, 0, b.length);
+    }
+
+    @Override
+    public long skip(long n) throws IOException {
+        long numToSkip = Math.min( n, available());
+        raf.seek( raf.getFilePointer() + numToSkip);
+        if (debug) log.debug(String.format("Skipped %d bytes", numToSkip));                
+        return numToSkip;
+    }
+}
+
+
diff --git a/extern/zipsigner/src/main/java/kellinwood/zipio/ZioEntryOutputStream.java b/extern/zipsigner/src/main/java/kellinwood/zipio/ZioEntryOutputStream.java
new file mode 100644
index 000000000..0674c9d84
--- /dev/null
+++ b/extern/zipsigner/src/main/java/kellinwood/zipio/ZioEntryOutputStream.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2010 Ken Ellinwood
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package kellinwood.zipio;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.zip.CRC32;
+import java.util.zip.Deflater;
+import java.util.zip.DeflaterOutputStream;
+
+public class ZioEntryOutputStream extends OutputStream {
+    int size = 0;  // tracks uncompressed size of data
+    CRC32 crc = new CRC32();
+    int crcValue = 0;
+    OutputStream wrapped;
+    OutputStream downstream;
+    
+    public ZioEntryOutputStream( int compression, OutputStream wrapped) 
+    {
+        this.wrapped = wrapped;
+        if (compression != 0)
+            downstream = new DeflaterOutputStream( wrapped, new Deflater( Deflater.BEST_COMPRESSION, true));
+        else downstream = wrapped;
+    }
+    
+    public void close() throws IOException {
+        downstream.flush();
+        downstream.close();
+        crcValue = (int)crc.getValue();
+    }
+
+    public int getCRC() {
+        return crcValue;
+    }
+    
+    public void flush() throws IOException {
+        downstream.flush();
+    }
+
+    public void write(byte[] b) throws IOException {
+        downstream.write(b);
+        crc.update(b);
+        size += b.length;
+    }
+
+    public void write(byte[] b, int off, int len) throws IOException {
+        downstream.write( b, off, len);
+        crc.update( b, off, len);
+        size += len;
+    }
+
+    public void write(int b) throws IOException {
+        downstream.write( b);
+        crc.update( b);
+        size += 1;
+    }
+
+    public int getSize() {
+        return size;
+    }
+
+    public OutputStream getWrappedStream()
+    {
+        return wrapped;
+    }
+    
+}
+
diff --git a/extern/zipsigner/src/main/java/kellinwood/zipio/ZipInput.java b/extern/zipsigner/src/main/java/kellinwood/zipio/ZipInput.java
new file mode 100644
index 000000000..06c381db9
--- /dev/null
+++ b/extern/zipsigner/src/main/java/kellinwood/zipio/ZipInput.java
@@ -0,0 +1,233 @@
+/*
+ * Copyright (C) 2010 Ken Ellinwood
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package kellinwood.zipio;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Collection;
+import java.util.Date;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.jar.Manifest;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+
+import kellinwood.logging.LoggerInterface;
+import kellinwood.logging.LoggerManager;
+
+/**
+ *
+ */
+public class ZipInput 
+{
+
+    static LoggerInterface log;
+
+    public String inputFilename;
+    RandomAccessFile in = null;
+    long fileLength;
+    int scanIterations = 0;
+
+    Map<String,ZioEntry> zioEntries = new LinkedHashMap<String,ZioEntry>();
+    CentralEnd centralEnd;
+    Manifest manifest;
+
+    public ZipInput( String filename) throws IOException
+    {
+        this.inputFilename = filename;
+        in = new RandomAccessFile( new File( inputFilename), "r");
+        fileLength = in.length();
+    }
+
+    private static LoggerInterface getLogger() {
+        if (log == null) log = LoggerManager.getLogger(ZipInput.class.getName());
+        return log;
+    }
+
+    public String getFilename() {
+        return inputFilename;
+    }
+
+    public long getFileLength() {
+        return fileLength;
+    }
+    
+    public static ZipInput read( String filename) throws IOException {
+        ZipInput zipInput = new ZipInput( filename);
+        zipInput.doRead();
+        return zipInput;
+    }
+    
+    
+    public ZioEntry getEntry( String filename) {
+        return zioEntries.get(filename);
+    }
+    
+    public Map<String,ZioEntry> getEntries() {
+        return zioEntries;
+    }
+    
+    /** Returns the names of immediate children in the directory with the given name.
+     *  The path value must end with a "/" character.  Use a value of "/" 
+     *  to get the root entries.
+     */
+    public Collection<String> list(String path) 
+    {
+        if (!path.endsWith("/")) throw new IllegalArgumentException("Invalid path -- does not end with '/'");
+        
+        if (path.startsWith("/")) path = path.substring(1);
+       
+        Pattern p = Pattern.compile( String.format("^%s([^/]+/?).*", path));
+        
+        Set<String> names = new TreeSet<String>();
+        
+        for (String name : zioEntries.keySet()) {
+            Matcher m = p.matcher(name);
+            if (m.matches()) names.add(m.group(1));
+        }
+        return names;
+    }
+    
+    public Manifest getManifest() throws IOException {
+        if (manifest == null) {
+            ZioEntry e = zioEntries.get("META-INF/MANIFEST.MF");
+            if (e != null) {
+                manifest = new Manifest( e.getInputStream());
+            }
+        }
+        return manifest; 
+    }
+
+    /** Scan the end of the file for the end of central directory record (EOCDR).
+        Returns the file offset of the EOCD signature.  The size parameter is an
+        initial buffer size (e.g., 256).
+     */
+    public long scanForEOCDR( int size) throws IOException {
+        if (size > fileLength || size > 65536) throw new IllegalStateException( "End of central directory not found in " + inputFilename);
+
+        int scanSize = (int)Math.min( fileLength, size);
+
+        byte[] scanBuf = new byte[scanSize];
+
+        in.seek( fileLength - scanSize);
+
+        in.readFully( scanBuf);
+
+        for (int i = scanSize - 22; i >= 0; i--) {
+            scanIterations += 1;
+            if (scanBuf[i] == 0x50 && scanBuf[i+1] == 0x4b && scanBuf[i+2] == 0x05 && scanBuf[i+3] == 0x06) {
+                return fileLength - scanSize + i;
+            }
+        }
+
+        return scanForEOCDR( size * 2);
+    }
+                              
+        
+    private void doRead()
+    {
+        try {
+
+            long posEOCDR = scanForEOCDR( 256);
+            in.seek( posEOCDR);
+            centralEnd = CentralEnd.read( this);
+
+            boolean debug = getLogger().isDebugEnabled();
+            if (debug) {
+                getLogger().debug(String.format("EOCD found in %d iterations", scanIterations));
+                getLogger().debug(String.format("Directory entries=%d, size=%d, offset=%d/0x%08x", centralEnd.totalCentralEntries,
+                                                centralEnd.centralDirectorySize, centralEnd.centralStartOffset, centralEnd.centralStartOffset));
+
+                ZipListingHelper.listHeader( getLogger());
+            }
+
+            in.seek( centralEnd.centralStartOffset);            
+
+            for (int i = 0; i < centralEnd.totalCentralEntries; i++) {
+                ZioEntry entry = ZioEntry.read(this);
+                zioEntries.put( entry.getName(), entry);
+                if (debug) ZipListingHelper.listEntry( getLogger(), entry);
+            }
+
+        }
+        catch (Throwable t) {
+            t.printStackTrace();
+        }    	
+    }
+
+    public void close() {
+        if (in != null) try { in.close(); } catch( Throwable t) {}
+    }
+
+    public long getFilePointer() throws IOException {
+        return in.getFilePointer(); 
+    }
+
+    public void seek( long position) throws IOException {
+        in.seek(position);
+    }
+
+    public byte readByte() throws IOException {
+        return in.readByte();
+    }
+    
+    public int readInt() throws IOException{
+        int result = 0;
+        for (int i = 0; i < 4; i++) {
+            result |= (in.readUnsignedByte() << (8 * i));
+        }
+        return result;
+    }
+
+    public short readShort() throws IOException {
+        short result = 0;
+        for (int i = 0; i < 2; i++) {
+            result |= (in.readUnsignedByte() << (8 * i));
+        }
+        return result;
+    }
+
+    public String readString( int length) throws IOException {
+
+        byte[] buffer = new byte[length];
+        for (int i = 0; i < length; i++) {
+            buffer[i] = in.readByte();
+        }
+        return new String(buffer);
+    }
+
+    public byte[] readBytes( int length) throws IOException {
+
+        byte[] buffer = new byte[length];
+        for (int i = 0; i < length; i++) {
+            buffer[i] = in.readByte();
+        }
+        return buffer;
+    }
+
+    public int read( byte[] b, int offset, int length) throws IOException {
+        return in.read( b, offset, length);
+    }
+
+}
+
+
diff --git a/extern/zipsigner/src/main/java/kellinwood/zipio/ZipListingHelper.java b/extern/zipsigner/src/main/java/kellinwood/zipio/ZipListingHelper.java
new file mode 100644
index 000000000..5e583b6b3
--- /dev/null
+++ b/extern/zipsigner/src/main/java/kellinwood/zipio/ZipListingHelper.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2010 Ken Ellinwood
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package kellinwood.zipio;
+
+import java.util.Date;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import kellinwood.logging.LoggerInterface;
+import kellinwood.logging.LoggerManager;
+
+/**
+ *
+ */
+public class ZipListingHelper
+{
+
+    static DateFormat dateFormat = new SimpleDateFormat("MM-dd-yy HH:mm");
+    
+    public static void listHeader(LoggerInterface log)
+    {
+        log.debug(" Length   Method    Size  Ratio   Date   Time   CRC-32    Name");
+        log.debug("--------  ------  ------- -----   ----   ----   ------    ----");
+        
+    }
+
+    public static void listEntry(LoggerInterface log, ZioEntry entry)
+    {
+        int ratio = 0;
+        if (entry.getSize() > 0) ratio = (100 * (entry.getSize() - entry.getCompressedSize())) / entry.getSize();
+        log.debug(String.format("%8d  %6s %8d %4d%% %s  %08x  %s",
+                                entry.getSize(),
+                                entry.getCompression() == 0 ? "Stored" : "Defl:N",
+                                entry.getCompressedSize(),
+                                ratio,
+                                dateFormat.format( new Date( entry.getTime())),
+                                entry.getCrc32(),
+                                entry.getName()));
+    }
+}
+
+
diff --git a/extern/zipsigner/src/main/java/kellinwood/zipio/ZipOutput.java b/extern/zipsigner/src/main/java/kellinwood/zipio/ZipOutput.java
new file mode 100644
index 000000000..77627d971
--- /dev/null
+++ b/extern/zipsigner/src/main/java/kellinwood/zipio/ZipOutput.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2010 Ken Ellinwood
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package kellinwood.zipio;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.List;
+import java.util.LinkedList;
+import java.util.HashSet;
+import java.util.Set;
+import kellinwood.logging.LoggerInterface;
+import kellinwood.logging.LoggerManager;
+
+/**
+ *
+ */
+public class ZipOutput 
+{
+
+    static LoggerInterface log;
+
+    String outputFilename;
+    OutputStream out = null;
+    int filePointer = 0;
+
+    List<ZioEntry> entriesWritten = new LinkedList<ZioEntry>();
+    Set<String> namesWritten = new HashSet<String>();
+    
+    public ZipOutput( String filename) throws IOException
+    {
+        this.outputFilename = filename;
+        File ofile = new File( outputFilename);
+        init(ofile);
+    }
+    
+    public ZipOutput( File outputFile) throws IOException
+    {
+        this.outputFilename = outputFile.getAbsolutePath();
+        File ofile = outputFile;
+        init(ofile);
+    }
+        
+    private void init( File ofile) throws IOException
+    {
+        if (ofile.exists()) ofile.delete();
+        out = new FileOutputStream( ofile);
+        if (getLogger().isDebugEnabled()) ZipListingHelper.listHeader( getLogger());
+        
+    }
+
+    public ZipOutput( OutputStream os) throws IOException
+    {
+        out = os;
+    }
+    
+    private static LoggerInterface getLogger() {
+        if (log == null) log = LoggerManager.getLogger(ZipOutput.class.getName());
+        return log;
+    }
+
+    public void write( ZioEntry entry) throws IOException {
+        String entryName = entry.getName();
+        if (namesWritten.contains( entryName)) {
+            getLogger().warning("Skipping duplicate file in output: " + entryName);
+            return;
+        }
+        entry.writeLocalEntry( this);
+        entriesWritten.add( entry);
+        namesWritten.add( entryName);
+        if (getLogger().isDebugEnabled()) ZipListingHelper.listEntry( getLogger(), entry);
+        
+    }
+
+
+    
+    public void close() throws IOException
+    {
+        CentralEnd centralEnd = new CentralEnd();
+        
+        centralEnd.centralStartOffset = (int)getFilePointer();
+        centralEnd.numCentralEntries = centralEnd.totalCentralEntries = (short)entriesWritten.size();
+        
+        for (ZioEntry entry : entriesWritten) {
+            entry.write( this);
+        }
+        
+        centralEnd.centralDirectorySize = (int)(getFilePointer() - centralEnd.centralStartOffset);
+        centralEnd.fileComment = "";
+        
+        centralEnd.write( this);
+        
+        if (out != null) try { out.close(); } catch( Throwable t) {}
+    }
+
+    public int getFilePointer() throws IOException {
+        return filePointer;
+    }
+
+
+    public void writeInt( int value) throws IOException{
+        byte[] data = new byte[4];
+        for (int i = 0; i < 4; i++) {
+            data[i] = (byte)(value & 0xFF);
+            value = value >> 8;
+        }
+        out.write( data);
+        filePointer += 4;
+    }
+
+    public void writeShort( short value) throws IOException {
+        byte[] data = new byte[2];
+        for (int i = 0; i < 2; i++) {
+            data[i] = (byte)(value & 0xFF);
+            value = (short)(value >> 8);
+        }
+        out.write( data);
+        filePointer += 2;
+    }
+
+    public void writeString( String value) throws IOException {
+
+        byte[] data = value.getBytes();
+        out.write( data);
+        filePointer += data.length;
+    }
+
+    public void writeBytes( byte[] value) throws IOException {
+
+        out.write( value);
+        filePointer += value.length;
+    }
+
+    public void writeBytes( byte[] value, int offset, int length) throws IOException {
+
+        out.write( value, offset, length);
+        filePointer += length;
+    }    
+
+}
+
+