/*
* 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) {
InputStream is = manifestEntry.getInputStream();
input = new Manifest();
input.read(is);
is.close();
}
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);
input.close();
}
/** 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);
}
}
}