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