diff --git a/base/src/main/java/proguard/io/DataEntry.java b/base/src/main/java/proguard/io/DataEntry.java index 420380c28..ed8fbe380 100644 --- a/base/src/main/java/proguard/io/DataEntry.java +++ b/base/src/main/java/proguard/io/DataEntry.java @@ -45,6 +45,14 @@ public interface DataEntry */ public long getSize(); + /** + * Returns the modification time of this data entry in milliseconds, + * since the epoch (1970-01-01T00:00:00Z), or -1 if unknown. + */ + public default long getModificationTime() + { + return -1; + } /** * Returns whether the data entry represents a directory. diff --git a/base/src/main/java/proguard/io/FileDataEntry.java b/base/src/main/java/proguard/io/FileDataEntry.java index 11b4992ec..78b93a425 100644 --- a/base/src/main/java/proguard/io/FileDataEntry.java +++ b/base/src/main/java/proguard/io/FileDataEntry.java @@ -107,6 +107,13 @@ public long getSize() } + @Override + public long getModificationTime() + { + return file.lastModified(); + } + + @Override public boolean isDirectory() { diff --git a/base/src/main/java/proguard/io/NamedDataEntry.java b/base/src/main/java/proguard/io/NamedDataEntry.java index 8989b1db7..440dbc179 100644 --- a/base/src/main/java/proguard/io/NamedDataEntry.java +++ b/base/src/main/java/proguard/io/NamedDataEntry.java @@ -19,6 +19,7 @@ import java.io.IOException; import java.io.InputStream; +import java.time.Instant; /** * This DataEntry represents a named output entry with a parent. diff --git a/base/src/main/java/proguard/io/WrappedDataEntry.java b/base/src/main/java/proguard/io/WrappedDataEntry.java index 7418dd795..465df35a4 100644 --- a/base/src/main/java/proguard/io/WrappedDataEntry.java +++ b/base/src/main/java/proguard/io/WrappedDataEntry.java @@ -63,6 +63,13 @@ public long getSize() } + @Override + public long getModificationTime() + { + return wrappedEntry.getModificationTime(); + } + + @Override public boolean isDirectory() { diff --git a/base/src/main/java/proguard/io/ZipDataEntry.java b/base/src/main/java/proguard/io/ZipDataEntry.java index 0746ff419..c32c7ef4f 100644 --- a/base/src/main/java/proguard/io/ZipDataEntry.java +++ b/base/src/main/java/proguard/io/ZipDataEntry.java @@ -78,6 +78,12 @@ public long getSize() } + @Override + public long getModificationTime() + { + return zipEntry.getLastModifiedTime().toMillis(); + } + @Override public boolean isDirectory() { diff --git a/base/src/main/java/proguard/io/ZipFileDataEntry.java b/base/src/main/java/proguard/io/ZipFileDataEntry.java index 3ab4960d2..623d523ff 100644 --- a/base/src/main/java/proguard/io/ZipFileDataEntry.java +++ b/base/src/main/java/proguard/io/ZipFileDataEntry.java @@ -79,6 +79,13 @@ public long getSize() } + @Override + public long getModificationTime() + { + return zipEntry.getLastModifiedTime().toMillis(); + } + + @Override public boolean isDirectory() { diff --git a/base/src/main/java/proguard/io/ZipWriter.java b/base/src/main/java/proguard/io/ZipWriter.java index c2aeaf2ab..ebc7ce73e 100644 --- a/base/src/main/java/proguard/io/ZipWriter.java +++ b/base/src/main/java/proguard/io/ZipWriter.java @@ -17,10 +17,14 @@ */ package proguard.io; +import java.time.Instant; +import java.time.LocalDateTime; + import proguard.classfile.TypeConstants; import proguard.util.StringMatcher; import java.io.*; +import java.time.ZoneId; /** * This {@link DataEntryWriter} sends data entries to the zip files specified by @@ -53,7 +57,7 @@ public ZipWriter(DataEntryWriter dataEntryWriter) this(null, 1, false, - 0, + -1, dataEntryWriter); } @@ -66,7 +70,8 @@ public ZipWriter(DataEntryWriter dataEntryWriter) * uncompressed entries. * @param useZip64 Whether to write out the archive in zip64 format. * @param modificationTime the modification date and time of the zip - * entries, in DOS format. + * entries, in DOS format. If {@code -1}, the modification time + * of each {@link DataEntry} will be passed through. * @param dataEntryWriter the data entry writer that can provide * output streams for the zip archives. */ @@ -98,7 +103,8 @@ public ZipWriter(StringMatcher uncompressedFilter, * @param extraUncompressedAlignment the desired alignment for the data of * entries matching extraAlignmentFilter. * @param modificationTime the modification date and time of the zip - * entries, in DOS format. + * entries, in DOS format. If {@code -1}, the modification time + * of each {@link DataEntry} will be passed through. * @param dataEntryWriter the data entry writer that can provide * output streams for the zip archives. */ @@ -129,7 +135,8 @@ public ZipWriter(StringMatcher uncompressedFilter, * uncompressed entries. * @param useZip64 Whether to write out the archive in zip64 format. * @param modificationTime the modification date and time of the zip - * entries, in DOS format. + * entries, in DOS format. If {@code -1}, the modification time + * of each {@link DataEntry} will be passed through. * @param dataEntryWriter the data entry writer that can provide * output streams for the zip archives. */ @@ -161,7 +168,8 @@ public ZipWriter(StringMatcher uncompressedFilter, * @param extraUncompressedAlignment the desired alignment for the data of * entries matching extraAlignmentFilter. * @param modificationTime the modification date and time of the zip - * entries, in DOS format. + * entries, in DOS format. If {@code -1}, the modification time + * of each {@link DataEntry} will be passed through. * @param dataEntryWriter the data entry writer that can provide * output streams for the zip archives. */ @@ -195,7 +203,8 @@ public ZipWriter(StringMatcher uncompressedFilter, * @param extraUncompressedAlignment the desired alignment for the data of * entries matching extraAlignmentFilter. * @param modificationTime the modification date and time of the zip - * entries, in DOS format. + * entries, in DOS format. If {@code -1}, the modification time + * of each {@link DataEntry} will be passed through. * @param dataEntryWriter the data entry writer that can provide * output streams for the zip archives. */ @@ -240,13 +249,12 @@ public boolean createDirectory(DataEntry dataEntry) throws IOException OutputStream outputStream = currentZipOutput.createOutputStream(name, false, - modificationTime); + getDosModificationTime(dataEntry)); outputStream.close(); return true; } - @Override public boolean sameOutputStream(DataEntry dataEntry1, DataEntry dataEntry2) @@ -290,7 +298,31 @@ public OutputStream createOutputStream(DataEntry dataEntry) throws IOException currentZipOutput.createOutputStream(name, compress1 && compress2, uncompressedAlignment, - modificationTime); + getDosModificationTime(dataEntry)); + } + + + /** + * Returns the modification time of a {@link DataEntry} in dos format. + *

+ * If {@link ZipWriter#modificationTime} is -1, the modification time of the + * {@link DataEntry} will be used, otherwise {@link ZipWriter#modificationTime} + * will be used for all encountered {@link DataEntry}s. + *

+ * If a {@link DataEntry} has no valid modification time, the {@link Instant#now()} + * will be assumed. + */ + private int getDosModificationTime(DataEntry dataEntry) + { + int dosModificationTime = modificationTime; + if (dosModificationTime == -1) { + long dataEntryModificationTime = dataEntry.getModificationTime(); + if (dataEntryModificationTime == -1) { + dataEntryModificationTime = Instant.now().toEpochMilli(); + } + dosModificationTime = (int) javaToDosTime(dataEntryModificationTime); + } + return dosModificationTime; } @@ -378,4 +410,31 @@ private void finish() throws IOException currentZipOutput = null; } } + + /* + * Utility method for conversion of a java time to dos time. + * Copied from + * https://github.com/apache/commons-compress/blob/master/src/main/java/org/apache/commons/compress/archivers/zip/ZipUtil.java + * + * Licenced under Apache Licence v2. + */ + private static final long DOSTIME_BEFORE_1980 = 1 << 21 | 1 << 16; // 0x210000 + + // version with integer overflow fixed - see https://bugs.openjdk.org/browse/JDK-8130914 + private static long javaToDosTime(final long t) + { + final LocalDateTime ldt = + LocalDateTime.ofInstant(Instant.ofEpochMilli(t), ZoneId.systemDefault()); + + if (ldt.getYear() < 1980) { + return DOSTIME_BEFORE_1980; + } + + return (ldt.getYear() - 1980 << 25 + | ldt.getMonthValue() << 21 + | ldt.getDayOfMonth() << 16 + | ldt.getHour() << 11 + | ldt.getMinute() << 5 + | ldt.getSecond() >> 1) & 0xffffffffL; + } } diff --git a/base/src/test/kotlin/proguard/io/ZipWriterTest.kt b/base/src/test/kotlin/proguard/io/ZipWriterTest.kt new file mode 100644 index 000000000..11724b7ab --- /dev/null +++ b/base/src/test/kotlin/proguard/io/ZipWriterTest.kt @@ -0,0 +1,102 @@ +/* + * ProGuard -- shrinking, optimization, obfuscation, and preverification + * of Java bytecode. + */ + +package proguard.io + +import io.kotest.core.spec.style.FreeSpec +import io.kotest.matchers.shouldBe +import java.io.* +import java.nio.file.attribute.FileTime +import java.time.Instant +import java.time.temporal.ChronoUnit +import java.util.zip.ZipEntry +import java.util.zip.ZipOutputStream + + +class ZipWriterTest : FreeSpec({ + + "Given a zip input file" - { + val time = 1700853925000 + //val time = Instant.now().toEpochMilli() + + // Truncate to seconds at most, as the dos time has a maximum granularity of seconds. + val modificationTime = Instant.ofEpochMilli(time).truncatedTo(ChronoUnit.SECONDS) + val zipArchiveAsByteArray = createZipArchive(modificationTime) + + val dataEntry = + StreamingDataEntry("zipArchive", + ByteArrayInputStream(zipArchiveAsByteArray.toByteArray())) + + "When using a pass-through ZipWriter" - { + val os = ByteArrayOutputStream() + + val writer = ZipWriter( + FixedOutputStreamWriter(os)) + + val dataEntryReader = + JarReader( + DataEntryCopier( + writer)) + + dataEntryReader.read(dataEntry) + + writer.close() + + "Then the resulting modification time should be the same as the original one" { + getModificationTime(ByteArrayInputStream(os.toByteArray()))?.toEpochMilli() shouldBe modificationTime.toEpochMilli() + } + } + } +}) { + companion object { + fun createZipArchive(modificationTime: Instant): ByteArrayOutputStream { + val baos = ByteArrayOutputStream() + baos.use { + val zipOutputStream = ZipOutputStream(baos) + val entry = ZipEntry("test.txt") + entry.setLastModifiedTime(FileTime.fromMillis(modificationTime.toEpochMilli())) + zipOutputStream.putNextEntry(entry) + zipOutputStream.closeEntry() + } + return baos + } + + fun getModificationTime(inputStream: InputStream): Instant? { + var modificationTime: Instant? = null + + val dataEntry = StreamingDataEntry("zipArchive", inputStream) + + val dataEntryReader = + JarReader { + modificationTime = Instant.ofEpochMilli(it.modificationTime) + } + + dataEntryReader.read(dataEntry) + return modificationTime + } + } +} + +class FixedOutputStreamWriter (private val os: OutputStream) : DataEntryWriter { + override fun createDirectory(dataEntry: DataEntry): Boolean { + return true + } + + override fun sameOutputStream(dataEntry1: DataEntry, dataEntry2: DataEntry): Boolean { + return true + } + + override fun createOutputStream(dataEntry: DataEntry): OutputStream { + return os + } + + override fun close() { + os.close() + } + + override fun println(pw: PrintWriter, prefix: String) { + pw.println(prefix + "FixedOutputStreamWriter ()") + } +} diff --git a/examples/src/main/java/proguard/examples/JarSigner.java b/examples/src/main/java/proguard/examples/JarSigner.java new file mode 100644 index 000000000..8f6eb0f1a --- /dev/null +++ b/examples/src/main/java/proguard/examples/JarSigner.java @@ -0,0 +1,131 @@ +package proguard.examples; + +import proguard.io.*; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableEntryException; +import java.security.cert.CertificateException; + +/** + * This sample application illustrates how to sign jars with the ProGuardCORE API. + *

+ * Usage: + * java proguard.examples.JarSigner -ks -kspass -key -keypass input.jar output.jar + */ +public class JarSigner +{ + private static final String KEYSTORE = "-ks"; + private static final String KEYSTORE_PASSWORD = "-kspass"; + private static final String KEY_ALIAS = "-key"; + private static final String KEY_PASSWORD = "-keypass"; + + + private static void usage() { + System.err.println("usage: java proguard.examples.JarSigner " + + "-ks -kspass -key " + + "-keypass input.jar output.jar"); + System.exit(1); + } + + private static KeyStore.PrivateKeyEntry getPrivateKeyEntry(String keyStoreFileName, String keyStorePassword, String keyAlias, String keyPassword) { + try (FileInputStream keyStoreInputStream = new FileInputStream(keyStoreFileName)) { + // Get the private key from the key store. + KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + keyStore.load(keyStoreInputStream, keyStorePassword.toCharArray()); + + KeyStore.ProtectionParameter protectionParameter = new KeyStore.PasswordProtection(keyPassword.toCharArray()); + + return (KeyStore.PrivateKeyEntry)keyStore.getEntry(keyAlias, protectionParameter); + } catch (IOException | KeyStoreException | NoSuchAlgorithmException | CertificateException | UnrecoverableEntryException ex) { + throw new RuntimeException(ex); + } + } + + public static void main(String[] args) + { + if (args.length != 10) { + usage(); + } + + String keyStoreFileName = null; + String keyStorePassword = null; + String keyAlias = null; + String keyPassword = null; + + String inputJarFileName = null; + String outputJarFileName = null; + + int argIndex = 0; + while (argIndex < args.length) { + switch (args[argIndex]) { + case KEYSTORE: + keyStoreFileName = args[++argIndex]; + break; + + case KEYSTORE_PASSWORD: + keyStorePassword = args[++argIndex]; + break; + + case KEY_ALIAS: + keyAlias = args[++argIndex]; + break; + + case KEY_PASSWORD: + keyPassword = args[++argIndex]; + break; + + default: + if (inputJarFileName == null) { + inputJarFileName = args[argIndex]; + } else { + outputJarFileName = args[argIndex]; + } + break; + } + + argIndex++; + } + + if (keyStoreFileName == null || + keyStorePassword == null || + keyAlias == null || + keyPassword == null || + inputJarFileName == null || + outputJarFileName == null) { + usage(); + } + + try + { + KeyStore.PrivateKeyEntry privateKeyEntry = + getPrivateKeyEntry(keyStoreFileName, keyStorePassword, keyAlias, keyPassword); + + // We'll write the output to a jar file. + JarWriter jarWriter = + new SignedJarWriter(privateKeyEntry, + new ZipWriter( + new FixedFileWriter( + new File(outputJarFileName)))); + + DataEntrySource source = + new FileSource( + new File(inputJarFileName)); + + // Copy all input entries to the output. + source.pumpDataEntries( + new JarReader( + new DataEntryCopier(jarWriter))); + + jarWriter.close(); + } + catch (IOException e) + { + throw new RuntimeException(e); + } + } +}