Replace zipsigner submodule by checked in code

This commit is contained in:
Daniel Martí 2015-03-31 19:10:03 +02:00
parent 474cc194ca
commit f8f77babe8
40 changed files with 4699 additions and 5 deletions

4
.gitmodules vendored
View File

@ -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

1
extern/zipsigner vendored

@ -1 +0,0 @@
Subproject commit 38e141f23442da7ca20f2d32596fe35eb7355ada

1
extern/zipsigner/build.gradle vendored Normal file
View File

@ -0,0 +1 @@
apply plugin: 'java'

View File

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

View File

@ -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);
}
}

View File

@ -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);
}

View File

@ -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);
}

View File

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

View File

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

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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.");
}
}

View File

@ -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);
}
}
}

View File

@ -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());
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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();
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

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

View File

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

View File

@ -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");
}
}

View File

@ -0,0 +1,4 @@
package kellinwood.security.zipsigner.optional;
public class KeyNameConflictException extends Exception {
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}

View File

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

View File

@ -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);
}
}
}

View File

@ -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);
}
}

View File

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

View File

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

View File

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

View File

@ -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);
}
}

View File

@ -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()));
}
}

View File

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