diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/cleartext_signatures/ClearsignedMessageUtil.java b/pgpainless-core/src/main/java/org/pgpainless/signature/cleartext_signatures/ClearsignedMessageUtil.java new file mode 100644 index 00000000..9894a7d9 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/cleartext_signatures/ClearsignedMessageUtil.java @@ -0,0 +1,239 @@ +/* + * 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.signature.cleartext_signatures; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import org.bouncycastle.bcpg.ArmoredInputStream; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPObjectFactory; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.PGPSignatureGenerator; +import org.bouncycastle.openpgp.PGPSignatureList; +import org.bouncycastle.util.Strings; +import org.pgpainless.implementation.ImplementationFactory; +import org.pgpainless.util.ArmoredInputStreamFactory; + +/** + * Utility class to deal with cleartext-signed messages. + * Based on Bouncycastle's {@link org.bouncycastle.openpgp.examples.ClearSignedFileProcessor}. + */ +public final class ClearsignedMessageUtil { + + private ClearsignedMessageUtil() { + + } + + /** + * Dearmor a clearsigned message, detach the inband signatures and write the plaintext message to the provided + * messageOutputStream. + * + * @param clearsignedInputStream input stream containing a clearsigned message + * @param messageOutputStream output stream to which the dearmored message shall be written + * @return signatures + * @throws IOException if the message is not clearsigned or some other IO error happens + */ + public static PGPSignatureList detachSignaturesFromInbandClearsignedMessage(InputStream clearsignedInputStream, + OutputStream messageOutputStream) + throws IOException { + ArmoredInputStream in = ArmoredInputStreamFactory.get(clearsignedInputStream); + if (!in.isClearText()) { + throw new IOException("Message is not clearsigned."); + } + + OutputStream out = new BufferedOutputStream(messageOutputStream); + try { + ByteArrayOutputStream lineOut = new ByteArrayOutputStream(); + int lookAhead = readInputLine(lineOut, in); + byte[] lineSep = getLineSeparator(); + + if (lookAhead != -1 && in.isClearText()) { + byte[] line = lineOut.toByteArray(); + out.write(line, 0, getLengthWithoutSeparatorOrTrailingWhitespace(line)); + out.write(lineSep); + + while (lookAhead != -1 && in.isClearText()) { + lookAhead = readInputLine(lineOut, lookAhead, in); + line = lineOut.toByteArray(); + out.write(line, 0, getLengthWithoutSeparatorOrTrailingWhitespace(line)); + out.write(lineSep); + } + } else { + if (lookAhead != -1) { + byte[] line = lineOut.toByteArray(); + out.write(line, 0, getLengthWithoutSeparatorOrTrailingWhitespace(line)); + out.write(lineSep); + } + } + } finally { + out.close(); + } + + PGPObjectFactory objectFactory = new PGPObjectFactory(in, ImplementationFactory.getInstance().getKeyFingerprintCalculator()); + PGPSignatureList signatures = (PGPSignatureList) objectFactory.nextObject(); + + return signatures; + } + + /** + * Initialize the given signature by processing the data from the messageData input stream. + * + * @param signature uninitialized signature + * @param signingKey public signing key + * @param messageData input stream containing the data to which the signature belongs + * @return initialized signature + * + * @throws PGPException if the signature cannot be initialized + * @throws IOException if an IO error happens + */ + public static PGPSignature initializeSignature(PGPSignature signature, PGPPublicKey signingKey, InputStream messageData) + throws PGPException, IOException { + signature.init(ImplementationFactory.getInstance().getPGPContentVerifierBuilderProvider(), signingKey); + + InputStream sigIn = new BufferedInputStream(messageData); + ByteArrayOutputStream lineOut = new ByteArrayOutputStream(); + int lookAhead = readInputLine(lineOut, sigIn); + processLine(signature, lineOut.toByteArray()); + + if (lookAhead != -1) { + do { + lookAhead = readInputLine(lineOut, lookAhead, sigIn); + signature.update((byte) '\r'); + signature.update((byte) '\n'); + processLine(signature, lineOut.toByteArray()); + } while (lookAhead != -1); + } + sigIn.close(); + return signature; + } + + public static int readInputLine(ByteArrayOutputStream bOut, InputStream fIn) + throws IOException { + bOut.reset(); + + int lookAhead = -1; + int ch; + + while ((ch = fIn.read()) >= 0) { + bOut.write(ch); + if (ch == '\r' || ch == '\n') { + lookAhead = readPassedEOL(bOut, ch, fIn); + break; + } + } + + return lookAhead; + } + + public static int readInputLine(ByteArrayOutputStream bOut, int lookAhead, InputStream fIn) + throws IOException { + bOut.reset(); + + int ch = lookAhead; + + do { + bOut.write(ch); + if (ch == '\r' || ch == '\n') { + lookAhead = readPassedEOL(bOut, ch, fIn); + break; + } + } + while ((ch = fIn.read()) >= 0); + + if (ch < 0) { + lookAhead = -1; + } + + return lookAhead; + } + + private static int readPassedEOL(ByteArrayOutputStream bOut, int lastCh, InputStream fIn) + throws IOException { + int lookAhead = fIn.read(); + + if (lastCh == '\r' && lookAhead == '\n') { + bOut.write(lookAhead); + lookAhead = fIn.read(); + } + + return lookAhead; + } + + + private static byte[] getLineSeparator() { + String nl = Strings.lineSeparator(); + byte[] nlBytes = new byte[nl.length()]; + + for (int i = 0; i != nlBytes.length; i++) { + nlBytes[i] = (byte) nl.charAt(i); + } + + return nlBytes; + } + + public static void processLine(PGPSignature sig, byte[] line) { + int length = getLengthWithoutWhiteSpace(line); + if (length > 0) { + sig.update(line, 0, length); + } + } + + public static void processLine(OutputStream aOut, PGPSignatureGenerator sGen, byte[] line) + throws IOException { + // note: trailing white space needs to be removed from the end of + // each line for signature calculation RFC 4880 Section 7.1 + int length = getLengthWithoutWhiteSpace(line); + if (length > 0) { + sGen.update(line, 0, length); + } + + aOut.write(line, 0, line.length); + } + + private static int getLengthWithoutSeparatorOrTrailingWhitespace(byte[] line) { + int end = line.length - 1; + + while (end >= 0 && isWhiteSpace(line[end])) { + end--; + } + + return end + 1; + } + + private static boolean isLineEnding(byte b) { + return b == '\r' || b == '\n'; + } + + private static int getLengthWithoutWhiteSpace(byte[] line) { + int end = line.length - 1; + + while (end >= 0 && isWhiteSpace(line[end])) { + end--; + } + + return end + 1; + } + + private static boolean isWhiteSpace(byte b) { + return isLineEnding(b) || b == '\t' || b == ' '; + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/cleartext_signatures/CleartextSignatureProcessor.java b/pgpainless-core/src/main/java/org/pgpainless/signature/cleartext_signatures/CleartextSignatureProcessor.java index 17e7316d..3361ec78 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/cleartext_signatures/CleartextSignatureProcessor.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/cleartext_signatures/CleartextSignatureProcessor.java @@ -15,36 +15,34 @@ */ package org.pgpainless.signature.cleartext_signatures; -import java.io.BufferedInputStream; -import java.io.BufferedOutputStream; -import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; -import java.io.OutputStream; +import java.util.HashMap; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.logging.Level; +import java.util.logging.Logger; import org.bouncycastle.bcpg.ArmoredInputStream; import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPObjectFactory; import org.bouncycastle.openpgp.PGPPublicKey; import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; import org.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.openpgp.PGPSignatureGenerator; import org.bouncycastle.openpgp.PGPSignatureList; -import org.bouncycastle.util.Strings; import org.pgpainless.PGPainless; import org.pgpainless.exception.SignatureValidationException; -import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.signature.CertificateValidator; import org.pgpainless.util.ArmoredInputStreamFactory; /** * Processor for cleartext-signed messages. - * Based on Bouncycastle's {@link org.bouncycastle.openpgp.examples.ClearSignedFileProcessor}. */ public class CleartextSignatureProcessor { + private static final Logger LOGGER = Logger.getLogger(CleartextSignatureProcessor.class.getName()); + private final ArmoredInputStream in; private final PGPPublicKeyRingCollection verificationKeys; private final MultiPassStrategy multiPassStrategy; @@ -76,182 +74,35 @@ public class CleartextSignatureProcessor { * @throws SignatureValidationException if the signature is invalid. */ public PGPSignature process() throws IOException, PGPException { - if (!in.isClearText()) { - throw new IllegalStateException("Message is not cleartext."); - } + PGPSignatureList signatures = ClearsignedMessageUtil.detachSignaturesFromInbandClearsignedMessage(in, multiPassStrategy.getMessageOutputStream()); + Map signatureValidationExceptions = new HashMap<>(); - OutputStream out = new BufferedOutputStream(multiPassStrategy.getMessageOutputStream()); - - ByteArrayOutputStream lineOut = new ByteArrayOutputStream(); - int lookAhead = readInputLine(lineOut, in); - byte[] lineSep = getLineSeparator(); - - if (lookAhead != -1 && in.isClearText()) { - byte[] line = lineOut.toByteArray(); - out.write(line, 0, getLengthWithoutSeparatorOrTrailingWhitespace(line)); - out.write(lineSep); - - while (lookAhead != -1 && in.isClearText()) { - lookAhead = readInputLine(lineOut, lookAhead, in); - line = lineOut.toByteArray(); - out.write(line, 0, getLengthWithoutSeparatorOrTrailingWhitespace(line)); - out.write(lineSep); + for (PGPSignature signature : signatures) { + PGPPublicKeyRing certificate = null; + PGPPublicKey signingKey = null; + for (PGPPublicKeyRing cert : verificationKeys) { + signingKey = cert.getPublicKey(signature.getKeyID()); + if (signingKey != null) { + certificate = cert; + break; + } } - } else { - if (lookAhead != -1) { - byte[] line = lineOut.toByteArray(); - out.write(line, 0, getLengthWithoutSeparatorOrTrailingWhitespace(line)); - out.write(lineSep); + if (signingKey == null) { + signatureValidationExceptions.put(signature, new NoSuchElementException("Missing verification key with key-id " + Long.toHexString(signature.getKeyID()))); + continue; + } + + try { + ClearsignedMessageUtil.initializeSignature(signature, signingKey, multiPassStrategy.getMessageInputStream()); + CertificateValidator.validateCertificateAndVerifyInitializedSignature(signature, certificate, PGPainless.getPolicy()); + return signature; + } catch (SignatureValidationException e) { + LOGGER.log(Level.INFO, "Cannot verify signature made by key " + Long.toHexString(signature.getKeyID()) + ": " + e.getMessage()); + signatureValidationExceptions.put(signature, e); } } - out.close(); - - PGPObjectFactory objectFactory = new PGPObjectFactory(in, ImplementationFactory.getInstance().getKeyFingerprintCalculator()); - PGPSignatureList signatures = (PGPSignatureList) objectFactory.nextObject(); - PGPSignature signature = signatures.get(0); - - PGPPublicKeyRing signingKeyRing = null; - PGPPublicKey signingKey = null; - for (PGPPublicKeyRing ring : verificationKeys) { - signingKey = ring.getPublicKey(signature.getKeyID()); - if (signingKey != null) { - signingKeyRing = ring; - break; - } - } - if (signingKey == null) { - throw new SignatureValidationException("Missing public key " + Long.toHexString(signature.getKeyID())); - } - - signature.init(ImplementationFactory.getInstance().getPGPContentVerifierBuilderProvider(), signingKey); - - InputStream sigIn = new BufferedInputStream(multiPassStrategy.getMessageInputStream()); - lookAhead = readInputLine(lineOut, sigIn); - processLine(signature, lineOut.toByteArray()); - - if (lookAhead != -1) { - do { - lookAhead = readInputLine(lineOut, lookAhead, sigIn); - signature.update((byte) '\r'); - signature.update((byte) '\n'); - processLine(signature, lineOut.toByteArray()); - } while (lookAhead != -1); - } - sigIn.close(); - - CertificateValidator.validateCertificateAndVerifyInitializedSignature(signature, signingKeyRing, PGPainless.getPolicy()); - return signature; + throw new SignatureValidationException("No valid signatures found.", signatureValidationExceptions); } - private static int readInputLine(ByteArrayOutputStream bOut, InputStream fIn) - throws IOException { - bOut.reset(); - - int lookAhead = -1; - int ch; - - while ((ch = fIn.read()) >= 0) { - bOut.write(ch); - if (ch == '\r' || ch == '\n') { - lookAhead = readPassedEOL(bOut, ch, fIn); - break; - } - } - - return lookAhead; - } - - private static int readInputLine(ByteArrayOutputStream bOut, int lookAhead, InputStream fIn) - throws IOException { - bOut.reset(); - - int ch = lookAhead; - - do { - bOut.write(ch); - if (ch == '\r' || ch == '\n') { - lookAhead = readPassedEOL(bOut, ch, fIn); - break; - } - } - while ((ch = fIn.read()) >= 0); - - if (ch < 0) { - lookAhead = -1; - } - - return lookAhead; - } - - private static int readPassedEOL(ByteArrayOutputStream bOut, int lastCh, InputStream fIn) - throws IOException { - int lookAhead = fIn.read(); - - if (lastCh == '\r' && lookAhead == '\n') { - bOut.write(lookAhead); - lookAhead = fIn.read(); - } - - return lookAhead; - } - - - private static byte[] getLineSeparator() { - String nl = Strings.lineSeparator(); - byte[] nlBytes = new byte[nl.length()]; - - for (int i = 0; i != nlBytes.length; i++) { - nlBytes[i] = (byte) nl.charAt(i); - } - - return nlBytes; - } - - private static void processLine(PGPSignature sig, byte[] line) { - int length = getLengthWithoutWhiteSpace(line); - if (length > 0) { - sig.update(line, 0, length); - } - } - - private static void processLine(OutputStream aOut, PGPSignatureGenerator sGen, byte[] line) - throws IOException { - // note: trailing white space needs to be removed from the end of - // each line for signature calculation RFC 4880 Section 7.1 - int length = getLengthWithoutWhiteSpace(line); - if (length > 0) { - sGen.update(line, 0, length); - } - - aOut.write(line, 0, line.length); - } - - private static int getLengthWithoutSeparatorOrTrailingWhitespace(byte[] line) { - int end = line.length - 1; - - while (end >= 0 && isWhiteSpace(line[end])) { - end--; - } - - return end + 1; - } - - private static boolean isLineEnding(byte b) { - return b == '\r' || b == '\n'; - } - - private static int getLengthWithoutWhiteSpace(byte[] line) { - int end = line.length - 1; - - while (end >= 0 && isWhiteSpace(line[end])) { - end--; - } - - return end + 1; - } - - private static boolean isWhiteSpace(byte b) { - return isLineEnding(b) || b == '\t' || b == ' '; - } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/cleartext_signatures/MultiPassStrategy.java b/pgpainless-core/src/main/java/org/pgpainless/signature/cleartext_signatures/MultiPassStrategy.java index 5f7c76ba..f36eb186 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/cleartext_signatures/MultiPassStrategy.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/cleartext_signatures/MultiPassStrategy.java @@ -47,6 +47,9 @@ public interface MultiPassStrategy { * Provide an {@link InputStream} which contains the data that was previously written away in * {@link #getMessageOutputStream()}. * + * As there may be multiple signatures that need to be processed, each call of this method MUST return + * a new {@link InputStream}. + * * @return input stream * @throws IOException io error */ diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java index 58e4e128..e1bb6fb9 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java @@ -32,7 +32,6 @@ import org.pgpainless.PGPainless; import org.pgpainless.decryption_verification.ConsumerOptions; import org.pgpainless.decryption_verification.DecryptionStream; import org.pgpainless.decryption_verification.OpenPgpMetadata; -import org.pgpainless.exception.NotYetImplementedException; import org.pgpainless.key.SubkeyIdentifier; import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.protection.SecretKeyRingProtector; @@ -50,21 +49,13 @@ public class DecryptImpl implements Decrypt { @Override public DecryptImpl verifyNotBefore(Date timestamp) throws SOPGPException.UnsupportedOption { - try { - consumerOptions.verifyNotBefore(timestamp); - } catch (NotYetImplementedException e) { - throw new SOPGPException.UnsupportedOption(); - } + consumerOptions.verifyNotBefore(timestamp); return this; } @Override public DecryptImpl verifyNotAfter(Date timestamp) throws SOPGPException.UnsupportedOption { - try { - consumerOptions.verifyNotAfter(timestamp); - } catch (NotYetImplementedException e) { - throw new SOPGPException.UnsupportedOption(); - } + consumerOptions.verifyNotAfter(timestamp); return this; } diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachInbandSignatureAndMessageImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachInbandSignatureAndMessageImpl.java new file mode 100644 index 00000000..418c9c36 --- /dev/null +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachInbandSignatureAndMessageImpl.java @@ -0,0 +1,69 @@ +/* + * 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.sop; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import org.bouncycastle.bcpg.ArmoredOutputStream; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.PGPSignatureList; +import org.pgpainless.signature.cleartext_signatures.ClearsignedMessageUtil; +import org.pgpainless.util.ArmoredOutputStreamFactory; +import sop.ReadyWithResult; +import sop.Signatures; +import sop.exception.SOPGPException; +import sop.operation.DetachInbandSignatureAndMessage; + +public class DetachInbandSignatureAndMessageImpl implements DetachInbandSignatureAndMessage { + + private boolean armor = true; + + @Override + public DetachInbandSignatureAndMessage noArmor() { + this.armor = false; + return this; + } + + @Override + public ReadyWithResult message(InputStream messageInputStream) { + + return new ReadyWithResult() { + @Override + public Signatures writeTo(OutputStream messageOutputStream) throws SOPGPException.NoSignature { + + return new Signatures() { + @Override + public void writeTo(OutputStream signatureOutputStream) throws IOException { + PGPSignatureList signatures = ClearsignedMessageUtil.detachSignaturesFromInbandClearsignedMessage(messageInputStream, messageOutputStream); + if (armor) { + ArmoredOutputStream armorOut = ArmoredOutputStreamFactory.get(signatureOutputStream); + for (PGPSignature signature : signatures) { + signature.encode(armorOut); + } + armorOut.close(); + } else { + for (PGPSignature signature : signatures) { + signature.encode(signatureOutputStream); + } + } + } + }; + } + }; + } +} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPImpl.java index fded2244..2561da91 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPImpl.java @@ -19,6 +19,7 @@ import sop.SOP; import sop.operation.Armor; import sop.operation.Dearmor; import sop.operation.Decrypt; +import sop.operation.DetachInbandSignatureAndMessage; import sop.operation.Encrypt; import sop.operation.ExtractCert; import sop.operation.GenerateKey; @@ -72,4 +73,9 @@ public class SOPImpl implements SOP { public Dearmor dearmor() { return new DearmorImpl(); } + + @Override + public DetachInbandSignatureAndMessage detachInbandSignatureAndMessage() { + return new DetachInbandSignatureAndMessageImpl(); + } } diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/VerifyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/VerifyImpl.java index 64aa41f8..b8cafba5 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/VerifyImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/VerifyImpl.java @@ -29,7 +29,6 @@ import org.pgpainless.PGPainless; import org.pgpainless.decryption_verification.ConsumerOptions; import org.pgpainless.decryption_verification.DecryptionStream; import org.pgpainless.decryption_verification.OpenPgpMetadata; -import org.pgpainless.exception.NotYetImplementedException; import org.pgpainless.key.SubkeyIdentifier; import sop.Verification; import sop.exception.SOPGPException; @@ -41,21 +40,13 @@ public class VerifyImpl implements Verify { @Override public Verify notBefore(Date timestamp) throws SOPGPException.UnsupportedOption { - try { - options.verifyNotBefore(timestamp); - } catch (NotYetImplementedException e) { - throw new SOPGPException.UnsupportedOption(); - } + options.verifyNotBefore(timestamp); return this; } @Override public Verify notAfter(Date timestamp) throws SOPGPException.UnsupportedOption { - try { - options.verifyNotAfter(timestamp); - } catch (NotYetImplementedException e) { - throw new SOPGPException.UnsupportedOption(); - } + options.verifyNotAfter(timestamp); return this; } diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/SopCLI.java b/sop-java-picocli/src/main/java/sop/cli/picocli/SopCLI.java index 745c7816..2f5806ec 100644 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/SopCLI.java +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/SopCLI.java @@ -20,6 +20,7 @@ import sop.SOP; import sop.cli.picocli.commands.ArmorCmd; import sop.cli.picocli.commands.DearmorCmd; import sop.cli.picocli.commands.DecryptCmd; +import sop.cli.picocli.commands.DetachInbandSignatureAndMessageCmd; import sop.cli.picocli.commands.EncryptCmd; import sop.cli.picocli.commands.ExtractCertCmd; import sop.cli.picocli.commands.GenerateKeyCmd; @@ -34,6 +35,7 @@ import sop.cli.picocli.commands.VersionCmd; ArmorCmd.class, DearmorCmd.class, DecryptCmd.class, + DetachInbandSignatureAndMessageCmd.class, EncryptCmd.class, ExtractCertCmd.class, GenerateKeyCmd.class, diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DetachInbandSignatureAndMessageCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DetachInbandSignatureAndMessageCmd.java index 94993cbc..0908a149 100644 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DetachInbandSignatureAndMessageCmd.java +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DetachInbandSignatureAndMessageCmd.java @@ -1,11 +1,66 @@ +/* + * 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 sop.cli.picocli.commands; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; + import picocli.CommandLine; +import sop.Signatures; +import sop.cli.picocli.SopCLI; import sop.exception.SOPGPException; +import sop.operation.DetachInbandSignatureAndMessage; @CommandLine.Command(name = "detach-inband-signature-and-message", description = "Split a clearsigned message", exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) public class DetachInbandSignatureAndMessageCmd implements Runnable { - + + @CommandLine.Option( + names = {"--signatures-out"}, + description = "Destination to which a detached signatures block will be written", + paramLabel = "SIGNATURES") + File signaturesOut; + + @CommandLine.Option(names = "--no-armor", + description = "ASCII armor the output", + negatable = true) + boolean armor = true; + + @Override + public void run() { + if (signaturesOut == null) { + throw new SOPGPException.MissingArg("--signatures-out is required."); + } + + DetachInbandSignatureAndMessage detach = SopCLI.getSop().detachInbandSignatureAndMessage(); + if (!armor) { + detach.noArmor(); + } + + try { + Signatures signatures = detach + .message(System.in).writeTo(System.out); + if (!signaturesOut.createNewFile()) { + throw new SOPGPException.OutputExists("Destination of --signatures-out already exists."); + } + signatures.writeTo(new FileOutputStream(signaturesOut)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } } diff --git a/sop-java/src/main/java/sop/Ready.java b/sop-java/src/main/java/sop/Ready.java index 4eea4bb6..104fae77 100644 --- a/sop-java/src/main/java/sop/Ready.java +++ b/sop-java/src/main/java/sop/Ready.java @@ -21,8 +21,21 @@ import java.io.OutputStream; public abstract class Ready { + /** + * Write the data to the provided output stream. + * + * @param outputStream output stream + * @throws IOException in case of an IO error + */ public abstract void writeTo(OutputStream outputStream) throws IOException; + /** + * Return the data as a byte array by writing it to a {@link ByteArrayOutputStream} first and then returning + * the array. + * + * @return data as byte array + * @throws IOException in case of an IO error + */ public byte[] getBytes() throws IOException { ByteArrayOutputStream bytes = new ByteArrayOutputStream(); writeTo(bytes); diff --git a/sop-java/src/main/java/sop/ReadyWithResult.java b/sop-java/src/main/java/sop/ReadyWithResult.java index 12744b32..2412b8f3 100644 --- a/sop-java/src/main/java/sop/ReadyWithResult.java +++ b/sop-java/src/main/java/sop/ReadyWithResult.java @@ -23,6 +23,16 @@ import sop.exception.SOPGPException; public abstract class ReadyWithResult { + /** + * Write the data eg. decrypted plaintext to the provided output stream and return the result of the + * processing operation. + * + * @param outputStream output stream + * @return result, eg. signatures + * + * @throws IOException in case of an IO error + * @throws SOPGPException.NoSignature + */ public abstract T writeTo(OutputStream outputStream) throws IOException, SOPGPException.NoSignature; public ByteArrayAndResult toBytes() throws IOException, SOPGPException.NoSignature { diff --git a/sop-java/src/main/java/sop/SOP.java b/sop-java/src/main/java/sop/SOP.java index 011129f3..64068650 100644 --- a/sop-java/src/main/java/sop/SOP.java +++ b/sop-java/src/main/java/sop/SOP.java @@ -18,6 +18,7 @@ package sop; import sop.operation.Armor; import sop.operation.Dearmor; import sop.operation.Decrypt; +import sop.operation.DetachInbandSignatureAndMessage; import sop.operation.Encrypt; import sop.operation.ExtractCert; import sop.operation.GenerateKey; @@ -100,4 +101,6 @@ public interface SOP { * @return builder instance */ Dearmor dearmor(); + + DetachInbandSignatureAndMessage detachInbandSignatureAndMessage(); } diff --git a/sop-java/src/main/java/sop/Signatures.java b/sop-java/src/main/java/sop/Signatures.java new file mode 100644 index 00000000..cacd198e --- /dev/null +++ b/sop-java/src/main/java/sop/Signatures.java @@ -0,0 +1,32 @@ +/* + * 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 sop; + +import java.io.IOException; +import java.io.OutputStream; + +public abstract class Signatures extends Ready { + + /** + * Write OpenPGP signatures to the provided output stream. + * + * @param signatureOutputStream output stream + * @throws IOException in case of an IO error + */ + @Override + public abstract void writeTo(OutputStream signatureOutputStream) throws IOException; + +} diff --git a/sop-java/src/main/java/sop/operation/DetachInbandSignatureAndMessage.java b/sop-java/src/main/java/sop/operation/DetachInbandSignatureAndMessage.java new file mode 100644 index 00000000..6ed4b828 --- /dev/null +++ b/sop-java/src/main/java/sop/operation/DetachInbandSignatureAndMessage.java @@ -0,0 +1,30 @@ +/* + * 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 sop.operation; + +import java.io.IOException; +import java.io.InputStream; + +import sop.ReadyWithResult; +import sop.Signatures; + +public interface DetachInbandSignatureAndMessage { + + DetachInbandSignatureAndMessage noArmor(); + + ReadyWithResult message(InputStream messageInputStream) throws IOException; + +}