diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/StreamEncoding.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/StreamEncoding.java
new file mode 100644
index 00000000..7b75bf80
--- /dev/null
+++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/StreamEncoding.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2021 Paul Schaub.
+ *
+ * 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 org.pgpainless.algorithm;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.bouncycastle.openpgp.PGPLiteralData;
+
+/**
+ * Encoding of the stream.
+ *
+ * @see RFC4880: Literal Data Packet
+ */
+public enum StreamEncoding {
+ BINARY(PGPLiteralData.BINARY),
+ TEXT(PGPLiteralData.TEXT),
+ UTF8(PGPLiteralData.UTF8),
+ @Deprecated
+ LOCAL('l'),
+ ;
+
+ private final char code;
+
+ private static final Map MAP = new ConcurrentHashMap<>();
+ static {
+ for (StreamEncoding f : StreamEncoding.values()) {
+ MAP.put(f.code, f);
+ }
+ MAP.put('1', LOCAL);
+ }
+
+ StreamEncoding(char code) {
+ this.code = code;
+ }
+
+ public char getCode() {
+ return code;
+ }
+
+ public static StreamEncoding fromCode(int code) {
+ return MAP.get((char) code);
+ }
+}
diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java
index a35a9348..93551d0d 100644
--- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java
+++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java
@@ -53,6 +53,7 @@ import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory;
import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider;
import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory;
import org.pgpainless.algorithm.CompressionAlgorithm;
+import org.pgpainless.algorithm.StreamEncoding;
import org.pgpainless.algorithm.SymmetricKeyAlgorithm;
import org.pgpainless.implementation.ImplementationFactory;
import org.pgpainless.key.OpenPgpV4Fingerprint;
@@ -173,6 +174,11 @@ public final class DecryptionStreamFactory {
private InputStream processPGPLiteralData(@Nonnull PGPObjectFactory objectFactory, PGPLiteralData pgpLiteralData) {
LOGGER.log(LEVEL, "Found PGPLiteralData");
InputStream literalDataInputStream = pgpLiteralData.getInputStream();
+ OpenPgpMetadata.FileInfo fileInfo = new OpenPgpMetadata.FileInfo(
+ pgpLiteralData.getFileName(),
+ pgpLiteralData.getModificationTime(),
+ StreamEncoding.fromCode(pgpLiteralData.getFormat()));
+ resultBuilder.setFileInfo(fileInfo);
if (verifiableOnePassSignatures.isEmpty()) {
LOGGER.log(LEVEL, "No OnePassSignatures found -> We are done");
diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMetadata.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMetadata.java
index 0b6d92da..ecf670e0 100644
--- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMetadata.java
+++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMetadata.java
@@ -17,16 +17,19 @@ package org.pgpainless.decryption_verification;
import java.util.ArrayList;
import java.util.Collections;
+import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
+import org.bouncycastle.openpgp.PGPLiteralData;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPSignature;
import org.pgpainless.algorithm.CompressionAlgorithm;
+import org.pgpainless.algorithm.StreamEncoding;
import org.pgpainless.algorithm.SymmetricKeyAlgorithm;
import org.pgpainless.key.OpenPgpV4Fingerprint;
@@ -39,6 +42,7 @@ public class OpenPgpMetadata {
private final SymmetricKeyAlgorithm symmetricKeyAlgorithm;
private final CompressionAlgorithm compressionAlgorithm;
private final boolean integrityProtected;
+ private final FileInfo fileInfo;
public OpenPgpMetadata(Set recipientKeyIds,
OpenPgpV4Fingerprint decryptionFingerprint,
@@ -46,7 +50,8 @@ public class OpenPgpMetadata {
CompressionAlgorithm algorithm,
boolean integrityProtected,
List onePassSignatures,
- List detachedSignatures) {
+ List detachedSignatures,
+ FileInfo fileInfo) {
this.recipientKeyIds = Collections.unmodifiableSet(recipientKeyIds);
this.decryptionFingerprint = decryptionFingerprint;
@@ -55,6 +60,7 @@ public class OpenPgpMetadata {
this.integrityProtected = integrityProtected;
this.detachedSignatures = Collections.unmodifiableList(detachedSignatures);
this.onePassSignatures = Collections.unmodifiableList(onePassSignatures);
+ this.fileInfo = fileInfo;
}
public Set getRecipientKeyIds() {
@@ -144,6 +150,89 @@ public class OpenPgpMetadata {
}
}
+ public FileInfo getFileInfo() {
+ return fileInfo;
+ }
+
+ public static class FileInfo {
+ public static final String FOR_YOUR_EYES_ONLY = PGPLiteralData.CONSOLE;
+
+ protected final String fileName;
+ protected final Date modicationDate;
+ protected final StreamEncoding streamEncoding;
+
+ public FileInfo(String fileName, Date modicationDate, StreamEncoding streamEncoding) {
+ this.fileName = fileName == null ? "" : fileName;
+ this.modicationDate = modicationDate == null ? PGPLiteralData.NOW : modicationDate;
+ this.streamEncoding = streamEncoding;
+ }
+
+ public static FileInfo binaryStream() {
+ return new FileInfo("", null, StreamEncoding.BINARY);
+ }
+
+ public static FileInfo forYourEyesOnly() {
+ return new FileInfo(FOR_YOUR_EYES_ONLY, null, StreamEncoding.BINARY);
+ }
+
+ public String getFileName() {
+ return fileName;
+ }
+
+ public boolean isForYourEyesOnly() {
+ return FOR_YOUR_EYES_ONLY.equals(fileName);
+ }
+
+ public Date getModificationDate() {
+ return modicationDate;
+ }
+
+ public StreamEncoding getStreamFormat() {
+ return streamEncoding;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == null) {
+ return false;
+ }
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof FileInfo)) {
+ return false;
+ }
+
+ FileInfo o = (FileInfo) other;
+
+ if (getFileName() != null) {
+ if (!getFileName().equals(o.getFileName())) {
+ return false;
+ }
+ } else {
+ if (o.getFileName() != null) {
+ return false;
+ }
+ }
+
+ if (getModificationDate() != null) {
+ if (o.getModificationDate() == null) {
+ return false;
+ }
+ long diff = Math.abs(getModificationDate().getTime() - o.getModificationDate().getTime());
+ if (diff > 1000) {
+ return false;
+ }
+ } else {
+ if (o.getModificationDate() != null) {
+ return false;
+ }
+ }
+
+ return getStreamFormat() == o.getStreamFormat();
+ }
+ }
+
public static Builder getBuilder() {
return new Builder();
}
@@ -157,6 +246,7 @@ public class OpenPgpMetadata {
private SymmetricKeyAlgorithm symmetricKeyAlgorithm = SymmetricKeyAlgorithm.NULL;
private CompressionAlgorithm compressionAlgorithm = CompressionAlgorithm.UNCOMPRESSED;
private boolean integrityProtected = false;
+ private FileInfo fileInfo;
public Builder addRecipientKeyId(Long keyId) {
this.recipientFingerprints.add(keyId);
@@ -195,10 +285,15 @@ public class OpenPgpMetadata {
this.onePassSignatures.add(onePassSignature);
}
+ public Builder setFileInfo(FileInfo fileInfo) {
+ this.fileInfo = fileInfo;
+ return this;
+ }
+
public OpenPgpMetadata build() {
return new OpenPgpMetadata(recipientFingerprints, decryptionFingerprint,
symmetricKeyAlgorithm, compressionAlgorithm, integrityProtected,
- onePassSignatures, detachedSignatures);
+ onePassSignatures, detachedSignatures, fileInfo);
}
}
}
diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionBuilder.java
index d4b1af04..ba80eb6f 100644
--- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionBuilder.java
+++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionBuilder.java
@@ -40,6 +40,7 @@ import org.pgpainless.algorithm.HashAlgorithm;
import org.pgpainless.algorithm.KeyFlag;
import org.pgpainless.algorithm.SignatureType;
import org.pgpainless.algorithm.SymmetricKeyAlgorithm;
+import org.pgpainless.decryption_verification.OpenPgpMetadata;
import org.pgpainless.exception.SecretKeyNotFoundException;
import org.pgpainless.key.OpenPgpV4Fingerprint;
import org.pgpainless.key.protection.SecretKeyRingProtector;
@@ -68,8 +69,7 @@ public class EncryptionBuilder implements EncryptionBuilderInterface {
private HashAlgorithm hashAlgorithm = HashAlgorithm.SHA256;
private CompressionAlgorithm compressionAlgorithm = CompressionAlgorithm.UNCOMPRESSED;
private boolean asciiArmor = false;
- private String fileName;
- private boolean forYourEyesOnly;
+ private OpenPgpMetadata.FileInfo fileInfo;
public EncryptionBuilder() {
this.purpose = EncryptionStream.Purpose.COMMUNICATIONS;
@@ -80,10 +80,9 @@ public class EncryptionBuilder implements EncryptionBuilderInterface {
}
@Override
- public ToRecipients onOutputStream(@Nonnull OutputStream outputStream, String fileName, boolean forYourEyesOnly) {
+ public ToRecipients onOutputStream(@Nonnull OutputStream outputStream, OpenPgpMetadata.FileInfo fileInfo) {
this.outputStream = outputStream;
- this.fileName = fileName == null ? "" : fileName;
- this.forYourEyesOnly = forYourEyesOnly;
+ this.fileInfo = fileInfo;
return new ToRecipientsImpl();
}
@@ -435,8 +434,7 @@ public class EncryptionBuilder implements EncryptionBuilderInterface {
EncryptionBuilder.this.hashAlgorithm,
EncryptionBuilder.this.compressionAlgorithm,
EncryptionBuilder.this.asciiArmor,
- fileName,
- forYourEyesOnly);
+ fileInfo);
}
}
diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionBuilderInterface.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionBuilderInterface.java
index 8d33036b..d1b7b01e 100644
--- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionBuilderInterface.java
+++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionBuilderInterface.java
@@ -18,6 +18,7 @@ package org.pgpainless.encryption_signing;
import javax.annotation.Nonnull;
import java.io.IOException;
import java.io.OutputStream;
+import java.util.Date;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKey;
@@ -28,6 +29,7 @@ import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.bouncycastle.openpgp.PGPSecretKeyRingCollection;
import org.pgpainless.algorithm.CompressionAlgorithm;
import org.pgpainless.algorithm.HashAlgorithm;
+import org.pgpainless.algorithm.StreamEncoding;
import org.pgpainless.algorithm.SymmetricKeyAlgorithm;
import org.pgpainless.decryption_verification.OpenPgpMetadata;
import org.pgpainless.exception.SecretKeyNotFoundException;
@@ -48,7 +50,7 @@ public interface EncryptionBuilderInterface {
* @return api handle
*/
default ToRecipients onOutputStream(@Nonnull OutputStream outputStream) {
- return onOutputStream(outputStream,false);
+ return onOutputStream(outputStream, OpenPgpMetadata.FileInfo.binaryStream());
}
/**
* Create a {@link EncryptionStream} on an {@link OutputStream} that contains the plain data which shall
@@ -57,9 +59,11 @@ public interface EncryptionBuilderInterface {
* @param outputStream outputStream
* @param forYourEyesOnly flag indicating that the data is intended for the recipients eyes only
* @return api handle
+ *
+ * @deprecated use {@link #onOutputStream(OutputStream, OpenPgpMetadata.FileInfo)} instead.
*/
default ToRecipients onOutputStream(@Nonnull OutputStream outputStream, boolean forYourEyesOnly) {
- return onOutputStream(outputStream, "", forYourEyesOnly);
+ return onOutputStream(outputStream, forYourEyesOnly ? OpenPgpMetadata.FileInfo.forYourEyesOnly() : OpenPgpMetadata.FileInfo.binaryStream());
}
/**
@@ -70,8 +74,22 @@ public interface EncryptionBuilderInterface {
* @param fileName name of the file (or "" if the encrypted data is not a file)
* @param forYourEyesOnly flag indicating that the data is intended for the recipients eyes only
* @return api handle
+ *
+ * @deprecated use {@link #onOutputStream(OutputStream, OpenPgpMetadata.FileInfo)} instead.
*/
- ToRecipients onOutputStream(@Nonnull OutputStream outputStream, String fileName, boolean forYourEyesOnly);
+ default ToRecipients onOutputStream(@Nonnull OutputStream outputStream, String fileName, boolean forYourEyesOnly) {
+ return onOutputStream(outputStream, new OpenPgpMetadata.FileInfo(forYourEyesOnly ? "_CONSOLE" : fileName, new Date(), StreamEncoding.BINARY));
+ }
+
+ /**
+ * Create an {@link EncryptionStream} on an {@link OutputStream} that contains the plain data which shall
+ * be encrypted and/or signed.
+ *
+ * @param outputStream outputStream
+ * @param fileInfo file information
+ * @return api handle
+ */
+ ToRecipients onOutputStream(@Nonnull OutputStream outputStream, OpenPgpMetadata.FileInfo fileInfo);
interface ToRecipients {
diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java
index 58a227b1..b19092ca 100644
--- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java
+++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java
@@ -18,7 +18,6 @@ package org.pgpainless.encryption_signing;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Collections;
-import java.util.Date;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
@@ -31,7 +30,6 @@ import org.bouncycastle.bcpg.BCPGOutputStream;
import org.bouncycastle.openpgp.PGPCompressedDataGenerator;
import org.bouncycastle.openpgp.PGPEncryptedDataGenerator;
import org.bouncycastle.openpgp.PGPException;
-import org.bouncycastle.openpgp.PGPLiteralData;
import org.bouncycastle.openpgp.PGPLiteralDataGenerator;
import org.bouncycastle.openpgp.PGPPrivateKey;
import org.bouncycastle.openpgp.PGPPublicKey;
@@ -118,8 +116,7 @@ public final class EncryptionStream extends OutputStream {
@Nonnull HashAlgorithm hashAlgorithm,
@Nonnull CompressionAlgorithm compressionAlgorithm,
boolean asciiArmor,
- @Nonnull String fileName,
- boolean forYourEyesOnly)
+ @Nonnull OpenPgpMetadata.FileInfo fileInfo)
throws IOException, PGPException {
this.symmetricKeyAlgorithm = symmetricKeyAlgorithm;
@@ -138,8 +135,9 @@ public final class EncryptionStream extends OutputStream {
prepareSigning();
prepareCompression();
prepareOnePassSignatures();
- prepareLiteralDataProcessing(fileName, forYourEyesOnly);
+ prepareLiteralDataProcessing(fileInfo);
prepareResultBuilder();
+ resultBuilder.setFileInfo(fileInfo);
}
private void prepareArmor() {
@@ -227,14 +225,13 @@ public final class EncryptionStream extends OutputStream {
}
}
- private void prepareLiteralDataProcessing(@Nonnull String fileName, boolean forYourEyesOnly) throws IOException {
+ private void prepareLiteralDataProcessing(@Nonnull OpenPgpMetadata.FileInfo fileInfo) throws IOException {
literalDataGenerator = new PGPLiteralDataGenerator();
- String name = fileName;
- if (forYourEyesOnly) {
- name = PGPLiteralData.CONSOLE;
- }
literalDataStream = literalDataGenerator.open(outermostStream,
- PGPLiteralData.BINARY, name, new Date(), new byte[BUFFER_SIZE]);
+ fileInfo.getStreamFormat().getCode(),
+ fileInfo.getFileName(),
+ fileInfo.getModificationDate(),
+ new byte[BUFFER_SIZE]);
outermostStream = literalDataStream;
}
diff --git a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/FileInfoTest.java b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/FileInfoTest.java
new file mode 100644
index 00000000..3e3306f5
--- /dev/null
+++ b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/FileInfoTest.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2021 Paul Schaub.
+ *
+ * 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 org.pgpainless.encryption_signing;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.NoSuchAlgorithmException;
+import java.util.Collections;
+import java.util.Date;
+
+import org.bouncycastle.openpgp.PGPException;
+import org.bouncycastle.openpgp.PGPPublicKeyRing;
+import org.bouncycastle.openpgp.PGPSecretKeyRing;
+import org.bouncycastle.openpgp.PGPSecretKeyRingCollection;
+import org.bouncycastle.util.io.Streams;
+import org.junit.jupiter.api.Test;
+import org.pgpainless.PGPainless;
+import org.pgpainless.algorithm.StreamEncoding;
+import org.pgpainless.decryption_verification.DecryptionStream;
+import org.pgpainless.decryption_verification.OpenPgpMetadata;
+import org.pgpainless.key.protection.SecretKeyRingProtector;
+import org.pgpainless.key.util.KeyRingUtils;
+
+public class FileInfoTest {
+
+ @Test
+ public void textFile() throws NoSuchAlgorithmException, PGPException, InvalidAlgorithmParameterException, IOException {
+ OpenPgpMetadata.FileInfo fileInfo = new OpenPgpMetadata.FileInfo("message.txt", new Date(), StreamEncoding.TEXT);
+ executeWith(fileInfo);
+ }
+
+ @Test
+ public void binaryStream() throws NoSuchAlgorithmException, PGPException, InvalidAlgorithmParameterException, IOException {
+ OpenPgpMetadata.FileInfo fileInfo = OpenPgpMetadata.FileInfo.binaryStream();
+ executeWith(fileInfo);
+ }
+
+ @Test
+ public void forYourEyesOnly() throws NoSuchAlgorithmException, PGPException, InvalidAlgorithmParameterException, IOException {
+ OpenPgpMetadata.FileInfo fileInfo = OpenPgpMetadata.FileInfo.forYourEyesOnly();
+ executeWith(fileInfo);
+ }
+
+ public void executeWith(OpenPgpMetadata.FileInfo fileInfo) throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException, IOException {
+ PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().simpleEcKeyRing("alice@wonderland.lit");
+ PGPPublicKeyRing publicKeys = KeyRingUtils.publicKeyRingFrom(secretKeys);
+
+ String data = "Hello, World!";
+
+ ByteArrayInputStream dataIn = new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8));
+ ByteArrayOutputStream dataOut = new ByteArrayOutputStream();
+ EncryptionStream encryptionStream = PGPainless.encryptAndOrSign()
+ .onOutputStream(dataOut, fileInfo)
+ .toRecipients(publicKeys)
+ .usingSecureAlgorithms()
+ .doNotSign()
+ .noArmor();
+
+ Streams.pipeAll(dataIn, encryptionStream);
+ encryptionStream.close();
+
+ OpenPgpMetadata.FileInfo cryptInfo = encryptionStream.getResult().getFileInfo();
+ assertEquals(fileInfo, cryptInfo);
+
+ ByteArrayInputStream cryptIn = new ByteArrayInputStream(dataOut.toByteArray());
+ ByteArrayOutputStream plainOut = new ByteArrayOutputStream();
+
+ DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify()
+ .onInputStream(cryptIn)
+ .decryptWith(SecretKeyRingProtector.unprotectedKeys(), new PGPSecretKeyRingCollection(Collections.singleton(secretKeys)))
+ .doNotVerify()
+ .build();
+ Streams.pipeAll(decryptionStream, plainOut);
+
+ decryptionStream.close();
+
+ OpenPgpMetadata.FileInfo decryptInfo = decryptionStream.getResult().getFileInfo();
+ assertEquals(fileInfo, decryptInfo);
+ }
+}