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; + } + +} + +