1
0
Fork 0
mirror of https://github.com/pgpainless/pgpainless.git synced 2024-12-23 11:27:57 +01:00

Introduce OpenPgpMetadata.FileInfo class for setting/getting file name, mod date, encoding...

This commit is contained in:
Paul Schaub 2021-04-25 00:28:48 +02:00
parent 3e29258ade
commit 2c4a3fca6a
Signed by: vanitasvitae
GPG key ID: 62BEE9264BF17311
7 changed files with 292 additions and 23 deletions

View file

@ -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 <a href="https://tools.ietf.org/html/rfc4880#section-5.9">RFC4880: Literal Data Packet</a>
*/
public enum StreamEncoding {
BINARY(PGPLiteralData.BINARY),
TEXT(PGPLiteralData.TEXT),
UTF8(PGPLiteralData.UTF8),
@Deprecated
LOCAL('l'),
;
private final char code;
private static final Map<Character, StreamEncoding> 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);
}
}

View file

@ -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");

View file

@ -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<Long> recipientKeyIds,
OpenPgpV4Fingerprint decryptionFingerprint,
@ -46,7 +50,8 @@ public class OpenPgpMetadata {
CompressionAlgorithm algorithm,
boolean integrityProtected,
List<OnePassSignature> onePassSignatures,
List<DetachedSignature> detachedSignatures) {
List<DetachedSignature> 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<Long> 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);
}
}
}

View file

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

View file

@ -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 {

View file

@ -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;
}

View file

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