From b4717eb06192c3b91cb68fe7ab8d41006a2098fa Mon Sep 17 00:00:00 2001 From: Matthias Valvekens Date: Wed, 29 Jun 2022 18:00:12 +0200 Subject: [PATCH 1/6] First experiments with auto-update RES-635 --- .../research/autoupdate/AutoUpdater.java | 92 +++++++++++++++++++ .../java/com/itextpdf/rups/model/PdfFile.java | 6 ++ 2 files changed, 98 insertions(+) create mode 100644 src/main/java/com/itextpdf/research/autoupdate/AutoUpdater.java diff --git a/src/main/java/com/itextpdf/research/autoupdate/AutoUpdater.java b/src/main/java/com/itextpdf/research/autoupdate/AutoUpdater.java new file mode 100644 index 00000000..05af9610 --- /dev/null +++ b/src/main/java/com/itextpdf/research/autoupdate/AutoUpdater.java @@ -0,0 +1,92 @@ +package com.itextpdf.research.autoupdate; + +import com.itextpdf.kernel.pdf.PdfArray; +import com.itextpdf.kernel.pdf.PdfDictionary; +import com.itextpdf.kernel.pdf.PdfDocument; +import com.itextpdf.kernel.pdf.PdfName; +import com.itextpdf.kernel.pdf.PdfString; +import com.itextpdf.rups.model.PdfFile; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLConnection; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; +import java.util.Base64.Encoder; +import org.bouncycastle.util.encoders.Base64Encoder; + +public class AutoUpdater { + private final PdfFile input; + private final OutputStream output; + + private final PdfDictionary autoUpdateDict; + + private byte[] fileContent; + + public AutoUpdater(PdfFile input, OutputStream output) { + this.input = input; + this.output = output; + this.autoUpdateDict = input.getPdfDocument().getCatalog().getPdfObject() + .getAsDictionary(new PdfName("AutoUpdate")); + } + + public boolean hasAutoUpdate() { + return autoUpdateDict != null; + } + + private byte[] getFileContent() { + if (fileContent == null) { + fileContent = input.getBytes(); + } + return fileContent; + } + + private URL getUpdateURL() throws MalformedURLException { + PdfString repo = autoUpdateDict.getAsString(new PdfName("Repo")); + PdfName addressMode = autoUpdateDict.getAsName(new PdfName("AddressMode")); + StringBuilder urlStr = new StringBuilder(repo.toUnicodeString()); + Encoder enc = Base64.getUrlEncoder(); + if ("ContentDigest".equals(addressMode.getValue())) { + byte[] hash; + try { + MessageDigest md = MessageDigest.getInstance("SHA384"); + md.update(getFileContent()); + hash = md.digest(); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException(e); + } + urlStr.append("/hash/").append(new String(enc.encode(hash), StandardCharsets.UTF_8)); + } else if ("DocumentID".equals(addressMode.getValue())) { + PdfArray arr = input.getPdfDocument().getTrailer().getAsArray(PdfName.ID); + byte[] id1 = arr.getAsString(0).getValueBytes(); + byte[] id2 = arr.getAsString(1).getValueBytes(); + + urlStr.append("/docId/") + .append(new String(enc.encode(id1), StandardCharsets.UTF_8)) + .append('/') + .append(new String(enc.encode(id2), StandardCharsets.UTF_8)); + } + return new URL(urlStr.toString()); + } + + private void downloadUpdate() throws IOException { + // TODO apply integrity check + if (!"Incremental".equals(autoUpdateDict.getAsName(new PdfName("UpdateType")).getValue())) { + throw new IllegalArgumentException("Only Incremental is supported in this PoC"); + } + output.write(getFileContent()); + byte[] buf = new byte[2048]; + URLConnection conn = getUpdateURL().openConnection(); + InputStream is = conn.getInputStream(); + int bytesRead; + while ((bytesRead = is.read(buf)) > 0) { + output.write(buf, 0, bytesRead); + } + } + +} diff --git a/src/main/java/com/itextpdf/rups/model/PdfFile.java b/src/main/java/com/itextpdf/rups/model/PdfFile.java index c4cc6b8a..7d3cac6a 100644 --- a/src/main/java/com/itextpdf/rups/model/PdfFile.java +++ b/src/main/java/com/itextpdf/rups/model/PdfFile.java @@ -226,6 +226,12 @@ public File getDirectory() { return directory; } + public byte[] getBytes() { + byte[] b = new byte[rawContent.length]; + System.arraycopy(rawContent, 0, b, 0, b.length); + return b; + } + public String getRawContent() { try { return new String(rawContent, "Cp1252"); From 6f1ce0c91ec747f33d78662ed2b96d1ea16cee65 Mon Sep 17 00:00:00 2001 From: Matthias Valvekens Date: Thu, 30 Jun 2022 15:01:17 +0200 Subject: [PATCH 2/6] Tentative PASETO code RES-635 --- .../research/autoupdate/AutoUpdater.java | 110 +++++++++++++++-- .../autoupdate/PasetoV4PublicVerifier.java | 114 ++++++++++++++++++ .../UpdateVerificationException.java | 11 ++ 3 files changed, 223 insertions(+), 12 deletions(-) create mode 100644 src/main/java/com/itextpdf/research/autoupdate/PasetoV4PublicVerifier.java create mode 100644 src/main/java/com/itextpdf/research/autoupdate/UpdateVerificationException.java diff --git a/src/main/java/com/itextpdf/research/autoupdate/AutoUpdater.java b/src/main/java/com/itextpdf/research/autoupdate/AutoUpdater.java index 05af9610..6b49a34f 100644 --- a/src/main/java/com/itextpdf/research/autoupdate/AutoUpdater.java +++ b/src/main/java/com/itextpdf/research/autoupdate/AutoUpdater.java @@ -2,11 +2,12 @@ import com.itextpdf.kernel.pdf.PdfArray; import com.itextpdf.kernel.pdf.PdfDictionary; -import com.itextpdf.kernel.pdf.PdfDocument; import com.itextpdf.kernel.pdf.PdfName; import com.itextpdf.kernel.pdf.PdfString; import com.itextpdf.rups.model.PdfFile; +import java.io.ByteArrayOutputStream; +import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -14,11 +15,15 @@ import java.net.URL; import java.net.URLConnection; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.OpenOption; +import java.nio.file.Path; +import java.security.GeneralSecurityException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.security.SignatureException; import java.util.Base64; import java.util.Base64.Encoder; -import org.bouncycastle.util.encoders.Base64Encoder; public class AutoUpdater { private final PdfFile input; @@ -74,18 +79,99 @@ private URL getUpdateURL() throws MalformedURLException { return new URL(urlStr.toString()); } - private void downloadUpdate() throws IOException { - // TODO apply integrity check - if (!"Incremental".equals(autoUpdateDict.getAsName(new PdfName("UpdateType")).getValue())) { - throw new IllegalArgumentException("Only Incremental is supported in this PoC"); + private static boolean nameValueIs(PdfDictionary dict, String key, String expectedValue) { + PdfName valueFound = dict.getAsName(new PdfName(key)); + return valueFound != null && expectedValue.equals(valueFound.getValue()); + } + + private void downloadUpdate() throws IOException, UpdateVerificationException { + // this method only implements a small part of the draft spec, but was written to + // somewhat realistically represent how one would ingest an update "for real", e.g. + // with potentially large update payloads, only exposing update content to the + // caller after verification passes, etc. + + if (!nameValueIs(autoUpdateDict, "UpdateType", "Incremental")) { + throw new IOException("Only Incremental is supported in this PoC"); } + + PdfDictionary integrity = autoUpdateDict.getAsDictionary(new PdfName("Integrity")); + PasetoV4PublicVerifier verifier; + MessageDigest implicitContentDigest; + if (integrity != null) { + boolean isPasetoV4Pub = + nameValueIs(integrity, "CertDataType", "PASETOV4Public"); + PdfString pskStr = integrity.getAsString(new PdfName("PreSharedKey")); + if (!isPasetoV4Pub || pskStr == null) { + throw new UpdateVerificationException("Only PASETOV4Public with pre-shared keys is supported"); + } + try { + verifier = new PasetoV4PublicVerifier(pskStr.getValueBytes()); + } catch (GeneralSecurityException e) { + throw new UpdateVerificationException("Key deser error", e); + } + + try { + implicitContentDigest = MessageDigest.getInstance("SHA384"); + } catch (NoSuchAlgorithmException e) { + throw new UpdateVerificationException("No SHA384", e); + } + } else { + verifier = null; + implicitContentDigest = null; + } + + // stream the update content to disk (out of sight) while also feeding it to the verifier + Path tempFile = Files.createTempFile("pdfupdate", ".bin"); + try (OutputStream tempOut = Files.newOutputStream(tempFile)) { + byte[] buf = new byte[2048]; + URLConnection conn = getUpdateURL().openConnection(); + if (verifier != null) { + try { + long contentLength = conn.getContentLengthLong(); + if (contentLength == -1) { + throw new IOException("Content-Length must be present for this PoC"); + } + verifier.init(conn.getHeaderField("X-PDF-Update-Token").getBytes(StandardCharsets.UTF_8), contentLength); + } catch (GeneralSecurityException e) { + throw new UpdateVerificationException("Cryptographic failure", e); + } + } + InputStream is = conn.getInputStream(); + int bytesRead; + while ((bytesRead = is.read(buf)) > 0) { + tempOut.write(buf, 0, bytesRead); + if (verifier != null) { + try { + verifier.updateImplicit(buf, 0, bytesRead); + implicitContentDigest.update(buf, 0, bytesRead); + } catch (GeneralSecurityException e) { + throw new UpdateVerificationException("Cryptographic failure", e); + } + } + } + + } + + if (verifier != null) { + // byte[] contentDigest = implicitContentDigest.digest(); + try { + verifier.verifyAndGetPayload(); + } catch (SignatureException e) { + throw new UpdateVerificationException("Cryptographic failure", e); + } + } + + + // TODO implement hash comparison part of integrity check + + // all clear -> proceed to output output.write(getFileContent()); - byte[] buf = new byte[2048]; - URLConnection conn = getUpdateURL().openConnection(); - InputStream is = conn.getInputStream(); - int bytesRead; - while ((bytesRead = is.read(buf)) > 0) { - output.write(buf, 0, bytesRead); + try (InputStream tempIn = Files.newInputStream(tempFile)) { + byte[] buf = new byte[2048]; + int bytesRead; + while ((bytesRead = tempIn.read(buf)) > 0) { + output.write(buf, 0, bytesRead); + } } } diff --git a/src/main/java/com/itextpdf/research/autoupdate/PasetoV4PublicVerifier.java b/src/main/java/com/itextpdf/research/autoupdate/PasetoV4PublicVerifier.java new file mode 100644 index 00000000..c859afc8 --- /dev/null +++ b/src/main/java/com/itextpdf/research/autoupdate/PasetoV4PublicVerifier.java @@ -0,0 +1,114 @@ +package com.itextpdf.research.autoupdate; + +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.Signature; +import java.security.SignatureException; +import java.security.spec.InvalidKeySpecException; +import java.util.Base64; +import org.bouncycastle.jcajce.spec.RawEncodedKeySpec; + +public class PasetoV4PublicVerifier { + + public static final int SIGNATURE_LENGTH = 64; + private final PublicKey publicKey; + + private long implicitLength; + private long implicitLengthSoFar = 0; + private Signature sig; + + byte[] decodedPayload; + + public PasetoV4PublicVerifier(PublicKey publicKey) { + this.publicKey = publicKey; + } + + public PasetoV4PublicVerifier(byte[] pubKeyBytes) + throws NoSuchAlgorithmException, InvalidKeySpecException { + this(KeyFactory.getInstance("Ed25519") + .generatePublic(new RawEncodedKeySpec(pubKeyBytes))); + } + + public void init(byte[] token, long implicitLength) + throws UpdateVerificationException, GeneralSecurityException { + if (sig != null) { + throw new IllegalStateException(); + } + String tokenStr = new String(token, StandardCharsets.US_ASCII); + String[] parts = tokenStr.split("\\.", 4); + if (parts.length != 3) { + throw new UpdateVerificationException("Expected 3-part no-footer token"); + } + + if (!"v4".equals(parts[0]) || !"public".equals(parts[1])) { + throw new UpdateVerificationException("Expected v4.public token"); + } + + this.decodedPayload = Base64.getUrlDecoder().decode(parts[2]); + int messageLength = decodedPayload.length - SIGNATURE_LENGTH; + + this.sig = Signature.getInstance("Ed25519"); + this.sig.initVerify(this.publicKey); + // we need to feed the verifier 4 items of PAE data + this.sig.update(le64(4)); + + //header + byte[] h = "v4.public.".getBytes(StandardCharsets.US_ASCII); + this.sig.update(le64(h.length)); + this.sig.update(h); + + // message + this.sig.update(le64(messageLength)); + this.sig.update(decodedPayload, 0, messageLength); + + // footer (empty) + this.sig.update(le64(0)); + + // implicit data + sig.update(le64(implicitLength)); + // the rest is by streaming + this.implicitLength = implicitLength; + } + + public void updateImplicit(byte[] data, int off, int len) throws SignatureException { + if (this.implicitLengthSoFar + len > this.implicitLength) { + throw new IllegalStateException("Too much input"); + } + sig.update(data, off, len); + this.implicitLengthSoFar += len; + } + + public byte[] verifyAndGetPayload() throws SignatureException, UpdateVerificationException { + int messageLength = this.decodedPayload.length - SIGNATURE_LENGTH; + if (this.implicitLengthSoFar != this.implicitLength) { + String msg = String.format( + "Expected %d bytes of input, but got %d.", + this.implicitLength, + this.implicitLengthSoFar); + throw new UpdateVerificationException(msg); + } + if (!sig.verify(this.decodedPayload, messageLength, SIGNATURE_LENGTH)) { + throw new UpdateVerificationException("Invalid signature"); + } + byte[] message = new byte[messageLength]; + System.arraycopy(this.decodedPayload, 0, message, 0, messageLength); + return message; + } + + private static byte[] le64(long l) { + return new byte[] { + (byte) l, + (byte) (l >>> 8), + (byte) (l >>> 16), + (byte) (l >>> 24), + (byte) (l >>> 32), + (byte) (l >>> 40), + (byte) (l >>> 48), + (byte) ((l >>> 56) & 0x7f) // clear msb + }; + } +} diff --git a/src/main/java/com/itextpdf/research/autoupdate/UpdateVerificationException.java b/src/main/java/com/itextpdf/research/autoupdate/UpdateVerificationException.java new file mode 100644 index 00000000..14e5e61c --- /dev/null +++ b/src/main/java/com/itextpdf/research/autoupdate/UpdateVerificationException.java @@ -0,0 +1,11 @@ +package com.itextpdf.research.autoupdate; + +public class UpdateVerificationException extends Exception { + public UpdateVerificationException(String message) { + super(message); + } + + public UpdateVerificationException(String message, Throwable cause) { + super(message, cause); + } +} From f22ccd2afba919950790e2866da77050a7cad0b1 Mon Sep 17 00:00:00 2001 From: Matthias Valvekens Date: Thu, 30 Jun 2022 16:08:33 +0200 Subject: [PATCH 3/6] Some initial token content verification RES-635 --- pom.xml | 5 ++ .../research/autoupdate/AutoUpdater.java | 75 +++++++++++-------- 2 files changed, 50 insertions(+), 30 deletions(-) diff --git a/pom.xml b/pom.xml index 4e7b37f4..d5dfb3b7 100644 --- a/pom.xml +++ b/pom.xml @@ -97,6 +97,11 @@ ${itext.version} test + + com.google.code.gson + gson + 2.9.0 + diff --git a/src/main/java/com/itextpdf/research/autoupdate/AutoUpdater.java b/src/main/java/com/itextpdf/research/autoupdate/AutoUpdater.java index 6b49a34f..f2da47bb 100644 --- a/src/main/java/com/itextpdf/research/autoupdate/AutoUpdater.java +++ b/src/main/java/com/itextpdf/research/autoupdate/AutoUpdater.java @@ -6,8 +6,8 @@ import com.itextpdf.kernel.pdf.PdfString; import com.itextpdf.rups.model.PdfFile; -import java.io.ByteArrayOutputStream; -import java.io.File; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -16,8 +16,8 @@ import java.net.URLConnection; import java.nio.charset.StandardCharsets; import java.nio.file.Files; -import java.nio.file.OpenOption; import java.nio.file.Path; +import java.nio.file.StandardOpenOption; import java.security.GeneralSecurityException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; @@ -51,10 +51,8 @@ private byte[] getFileContent() { return fileContent; } - private URL getUpdateURL() throws MalformedURLException { - PdfString repo = autoUpdateDict.getAsString(new PdfName("Repo")); + private String getResourceIdentifier() { PdfName addressMode = autoUpdateDict.getAsName(new PdfName("AddressMode")); - StringBuilder urlStr = new StringBuilder(repo.toUnicodeString()); Encoder enc = Base64.getUrlEncoder(); if ("ContentDigest".equals(addressMode.getValue())) { byte[] hash; @@ -65,17 +63,31 @@ private URL getUpdateURL() throws MalformedURLException { } catch (NoSuchAlgorithmException e) { throw new IllegalStateException(e); } - urlStr.append("/hash/").append(new String(enc.encode(hash), StandardCharsets.UTF_8)); + return new String(enc.encode(hash), StandardCharsets.UTF_8); } else if ("DocumentID".equals(addressMode.getValue())) { PdfArray arr = input.getPdfDocument().getTrailer().getAsArray(PdfName.ID); byte[] id1 = arr.getAsString(0).getValueBytes(); byte[] id2 = arr.getAsString(1).getValueBytes(); - urlStr.append("/docId/") - .append(new String(enc.encode(id1), StandardCharsets.UTF_8)) - .append('/') - .append(new String(enc.encode(id2), StandardCharsets.UTF_8)); + return String.format( + "%s/%s", + new String(enc.encode(id1), StandardCharsets.UTF_8), + new String(enc.encode(id2), StandardCharsets.UTF_8) + ); + } + throw new IllegalStateException(); + } + + private URL getUpdateURL() throws MalformedURLException { + PdfString repo = autoUpdateDict.getAsString(new PdfName("Repo")); + PdfName addressMode = autoUpdateDict.getAsName(new PdfName("AddressMode")); + StringBuilder urlStr = new StringBuilder(repo.toUnicodeString()); + if ("ContentDigest".equals(addressMode.getValue())) { + urlStr.append("/hash/"); + } else if ("DocumentID".equals(addressMode.getValue())) { + urlStr.append("/docId/"); } + urlStr.append(getResourceIdentifier()); return new URL(urlStr.toString()); } @@ -84,19 +96,20 @@ private static boolean nameValueIs(PdfDictionary dict, String key, String expect return valueFound != null && expectedValue.equals(valueFound.getValue()); } - private void downloadUpdate() throws IOException, UpdateVerificationException { + public void downloadAndApplyUpdate() throws IOException, UpdateVerificationException { // this method only implements a small part of the draft spec, but was written to // somewhat realistically represent how one would ingest an update "for real", e.g. // with potentially large update payloads, only exposing update content to the // caller after verification passes, etc. + // TODO refactor + if (!nameValueIs(autoUpdateDict, "UpdateType", "Incremental")) { throw new IOException("Only Incremental is supported in this PoC"); } PdfDictionary integrity = autoUpdateDict.getAsDictionary(new PdfName("Integrity")); PasetoV4PublicVerifier verifier; - MessageDigest implicitContentDigest; if (integrity != null) { boolean isPasetoV4Pub = nameValueIs(integrity, "CertDataType", "PASETOV4Public"); @@ -109,25 +122,19 @@ private void downloadUpdate() throws IOException, UpdateVerificationException { } catch (GeneralSecurityException e) { throw new UpdateVerificationException("Key deser error", e); } - - try { - implicitContentDigest = MessageDigest.getInstance("SHA384"); - } catch (NoSuchAlgorithmException e) { - throw new UpdateVerificationException("No SHA384", e); - } } else { verifier = null; - implicitContentDigest = null; } // stream the update content to disk (out of sight) while also feeding it to the verifier Path tempFile = Files.createTempFile("pdfupdate", ".bin"); + long contentLength = -1; try (OutputStream tempOut = Files.newOutputStream(tempFile)) { byte[] buf = new byte[2048]; URLConnection conn = getUpdateURL().openConnection(); if (verifier != null) { try { - long contentLength = conn.getContentLengthLong(); + contentLength = conn.getContentLengthLong(); if (contentLength == -1) { throw new IOException("Content-Length must be present for this PoC"); } @@ -143,30 +150,39 @@ private void downloadUpdate() throws IOException, UpdateVerificationException { if (verifier != null) { try { verifier.updateImplicit(buf, 0, bytesRead); - implicitContentDigest.update(buf, 0, bytesRead); } catch (GeneralSecurityException e) { throw new UpdateVerificationException("Cryptographic failure", e); } } } - } if (verifier != null) { - // byte[] contentDigest = implicitContentDigest.digest(); + byte[] payload; try { - verifier.verifyAndGetPayload(); + payload = verifier.verifyAndGetPayload(); } catch (SignatureException e) { throw new UpdateVerificationException("Cryptographic failure", e); } - } - + // verify the token contents + // TODO do this with a schema and null-safe queries + JsonObject el = JsonParser + .parseString(new String(payload, StandardCharsets.UTF_8)) + .getAsJsonObject(); + // TODO check updateType, protocol version + if (!getResourceIdentifier().equals(el.getAsJsonPrimitive("resourceId").getAsString())) { + throw new UpdateVerificationException("Resource ID mismatch"); + } + if (el.getAsJsonPrimitive("updateLength").getAsNumber().longValue() + != contentLength) { + throw new UpdateVerificationException("Length mismatch"); + } - // TODO implement hash comparison part of integrity check + } // all clear -> proceed to output output.write(getFileContent()); - try (InputStream tempIn = Files.newInputStream(tempFile)) { + try (InputStream tempIn = Files.newInputStream(tempFile, StandardOpenOption.DELETE_ON_CLOSE)) { byte[] buf = new byte[2048]; int bytesRead; while ((bytesRead = tempIn.read(buf)) > 0) { @@ -174,5 +190,4 @@ private void downloadUpdate() throws IOException, UpdateVerificationException { } } } - } From f0dbd7939c4a1f758128ce3f76b1885c6f87021f Mon Sep 17 00:00:00 2001 From: Matthias Valvekens Date: Thu, 30 Jun 2022 17:16:28 +0200 Subject: [PATCH 4/6] Use correct key spec RES-635 --- .../itextpdf/research/autoupdate/PasetoV4PublicVerifier.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/itextpdf/research/autoupdate/PasetoV4PublicVerifier.java b/src/main/java/com/itextpdf/research/autoupdate/PasetoV4PublicVerifier.java index c859afc8..2c686d79 100644 --- a/src/main/java/com/itextpdf/research/autoupdate/PasetoV4PublicVerifier.java +++ b/src/main/java/com/itextpdf/research/autoupdate/PasetoV4PublicVerifier.java @@ -3,14 +3,13 @@ import java.nio.charset.StandardCharsets; import java.security.GeneralSecurityException; import java.security.KeyFactory; -import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.PublicKey; import java.security.Signature; import java.security.SignatureException; import java.security.spec.InvalidKeySpecException; +import java.security.spec.X509EncodedKeySpec; import java.util.Base64; -import org.bouncycastle.jcajce.spec.RawEncodedKeySpec; public class PasetoV4PublicVerifier { @@ -30,7 +29,7 @@ public PasetoV4PublicVerifier(PublicKey publicKey) { public PasetoV4PublicVerifier(byte[] pubKeyBytes) throws NoSuchAlgorithmException, InvalidKeySpecException { this(KeyFactory.getInstance("Ed25519") - .generatePublic(new RawEncodedKeySpec(pubKeyBytes))); + .generatePublic(new X509EncodedKeySpec(pubKeyBytes))); } public void init(byte[] token, long implicitLength) From 37b33878641c121b904923ab38f2a43b941e9cac Mon Sep 17 00:00:00 2001 From: Matthias Valvekens Date: Thu, 30 Jun 2022 17:16:44 +0200 Subject: [PATCH 5/6] Logging in AutoUpdater RES-635 --- .../research/autoupdate/AutoUpdater.java | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/itextpdf/research/autoupdate/AutoUpdater.java b/src/main/java/com/itextpdf/research/autoupdate/AutoUpdater.java index f2da47bb..80ccb1a0 100644 --- a/src/main/java/com/itextpdf/research/autoupdate/AutoUpdater.java +++ b/src/main/java/com/itextpdf/research/autoupdate/AutoUpdater.java @@ -4,6 +4,7 @@ import com.itextpdf.kernel.pdf.PdfDictionary; import com.itextpdf.kernel.pdf.PdfName; import com.itextpdf.kernel.pdf.PdfString; +import com.itextpdf.rups.model.LoggerHelper; import com.itextpdf.rups.model.PdfFile; import com.google.gson.JsonObject; @@ -11,9 +12,9 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; -import java.net.URLConnection; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -79,7 +80,7 @@ private String getResourceIdentifier() { } private URL getUpdateURL() throws MalformedURLException { - PdfString repo = autoUpdateDict.getAsString(new PdfName("Repo")); + PdfString repo = autoUpdateDict.getAsString(new PdfName("Repository")); PdfName addressMode = autoUpdateDict.getAsName(new PdfName("AddressMode")); StringBuilder urlStr = new StringBuilder(repo.toUnicodeString()); if ("ContentDigest".equals(addressMode.getValue())) { @@ -87,8 +88,11 @@ private URL getUpdateURL() throws MalformedURLException { } else if ("DocumentID".equals(addressMode.getValue())) { urlStr.append("/docId/"); } - urlStr.append(getResourceIdentifier()); - return new URL(urlStr.toString()); + String resourceId = getResourceIdentifier(); + urlStr.append(resourceId); + URL result = new URL(urlStr.toString()); + LoggerHelper.info("Fetching update with ID " + resourceId + "; URL is " + result, AutoUpdater.class); + return result; } private static boolean nameValueIs(PdfDictionary dict, String key, String expectedValue) { @@ -120,6 +124,7 @@ public void downloadAndApplyUpdate() throws IOException, UpdateVerificationExcep try { verifier = new PasetoV4PublicVerifier(pskStr.getValueBytes()); } catch (GeneralSecurityException e) { + LoggerHelper.error(e.getMessage(), e, AutoUpdater.class); throw new UpdateVerificationException("Key deser error", e); } } else { @@ -131,7 +136,11 @@ public void downloadAndApplyUpdate() throws IOException, UpdateVerificationExcep long contentLength = -1; try (OutputStream tempOut = Files.newOutputStream(tempFile)) { byte[] buf = new byte[2048]; - URLConnection conn = getUpdateURL().openConnection(); + HttpURLConnection conn = (HttpURLConnection) getUpdateURL().openConnection(); + + if (conn.getResponseCode() != 200) { + throw new IOException("Fetch failed; error " + conn.getResponseCode()); + } if (verifier != null) { try { contentLength = conn.getContentLengthLong(); @@ -140,6 +149,7 @@ public void downloadAndApplyUpdate() throws IOException, UpdateVerificationExcep } verifier.init(conn.getHeaderField("X-PDF-Update-Token").getBytes(StandardCharsets.UTF_8), contentLength); } catch (GeneralSecurityException e) { + LoggerHelper.error(e.getMessage(), e, AutoUpdater.class); throw new UpdateVerificationException("Cryptographic failure", e); } } @@ -151,6 +161,7 @@ public void downloadAndApplyUpdate() throws IOException, UpdateVerificationExcep try { verifier.updateImplicit(buf, 0, bytesRead); } catch (GeneralSecurityException e) { + LoggerHelper.error(e.getMessage(), e, AutoUpdater.class); throw new UpdateVerificationException("Cryptographic failure", e); } } @@ -162,6 +173,7 @@ public void downloadAndApplyUpdate() throws IOException, UpdateVerificationExcep try { payload = verifier.verifyAndGetPayload(); } catch (SignatureException e) { + LoggerHelper.error(e.getMessage(), e, AutoUpdater.class); throw new UpdateVerificationException("Cryptographic failure", e); } // verify the token contents From 2c0d5fd9468be51e51d415b100e0e8ba4440f036 Mon Sep 17 00:00:00 2001 From: Matthias Valvekens Date: Thu, 30 Jun 2022 17:31:11 +0200 Subject: [PATCH 6/6] Add AutoUpdate button to menu RES-635 --- .../com/itextpdf/rups/view/RupsMenuBar.java | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/main/java/com/itextpdf/rups/view/RupsMenuBar.java b/src/main/java/com/itextpdf/rups/view/RupsMenuBar.java index fe87b95f..c65faa4e 100644 --- a/src/main/java/com/itextpdf/rups/view/RupsMenuBar.java +++ b/src/main/java/com/itextpdf/rups/view/RupsMenuBar.java @@ -43,6 +43,7 @@ This file is part of the iText (R) project. package com.itextpdf.rups.view; import com.itextpdf.kernel.actions.data.ITextCoreProductData; +import com.itextpdf.research.autoupdate.AutoUpdater; import com.itextpdf.rups.controller.RupsController; import com.itextpdf.rups.event.RupsEvent; import com.itextpdf.rups.io.FileCloseAction; @@ -51,9 +52,14 @@ This file is part of the iText (R) project. import com.itextpdf.rups.io.FileSaveAction; import com.itextpdf.rups.io.OpenInViewerAction; import com.itextpdf.rups.io.filters.PdfFilter; +import com.itextpdf.rups.model.LoggerHelper; import java.awt.event.ActionListener; import java.awt.event.InputEvent; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.HashMap; import java.util.Observable; import java.util.Observer; @@ -61,6 +67,7 @@ This file is part of the iText (R) project. import javax.swing.JMenu; import javax.swing.JMenuBar; import javax.swing.JMenuItem; +import javax.swing.JOptionPane; import javax.swing.KeyStroke; public class RupsMenuBar extends JMenuBar implements Observer { @@ -129,6 +136,26 @@ public RupsMenuBar(RupsController controller) { preferencesWindow.show(controller.getMasterComponent()); } ); + addItem(edit, "AutoUpdate", e -> { + try { + Path out = Files.createTempFile("pdfupdate-out", ".pdf"); + try (OutputStream os = Files.newOutputStream(out)) { + AutoUpdater au = new AutoUpdater(controller.getCurrentFile(), os); + if (au.hasAutoUpdate()) { + au.downloadAndApplyUpdate(); + LoggerHelper.info("Update written to " + out, RupsMenuBar.class); + controller.openNewFile(out.toFile()); + } else { + JOptionPane.showMessageDialog(getParent(), + "This is not an updatable document", + "AutoUpdate error", JOptionPane.ERROR_MESSAGE); + Files.delete(out); + } + } + } catch (Exception ex) { + JOptionPane.showMessageDialog(getParent(), ex.getMessage(), "AutoUpdate error", JOptionPane.ERROR_MESSAGE); + } + }); add(edit); add(Box.createGlue());