mirror of
https://github.com/pgpainless/pgpainless.git
synced 2024-11-26 22:32:07 +01:00
Refactor CleartextSignatureProcessor to allow reuse in DetachInbandSignatureAndMessage
This commit is contained in:
parent
40dfa6528a
commit
772f69788b
14 changed files with 498 additions and 203 deletions
|
@ -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 == ' ';
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,36 +15,34 @@
|
||||||
*/
|
*/
|
||||||
package org.pgpainless.signature.cleartext_signatures;
|
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.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
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.bcpg.ArmoredInputStream;
|
||||||
import org.bouncycastle.openpgp.PGPException;
|
import org.bouncycastle.openpgp.PGPException;
|
||||||
import org.bouncycastle.openpgp.PGPObjectFactory;
|
|
||||||
import org.bouncycastle.openpgp.PGPPublicKey;
|
import org.bouncycastle.openpgp.PGPPublicKey;
|
||||||
import org.bouncycastle.openpgp.PGPPublicKeyRing;
|
import org.bouncycastle.openpgp.PGPPublicKeyRing;
|
||||||
import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
|
import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
|
||||||
import org.bouncycastle.openpgp.PGPSignature;
|
import org.bouncycastle.openpgp.PGPSignature;
|
||||||
import org.bouncycastle.openpgp.PGPSignatureGenerator;
|
|
||||||
import org.bouncycastle.openpgp.PGPSignatureList;
|
import org.bouncycastle.openpgp.PGPSignatureList;
|
||||||
import org.bouncycastle.util.Strings;
|
|
||||||
import org.pgpainless.PGPainless;
|
import org.pgpainless.PGPainless;
|
||||||
import org.pgpainless.exception.SignatureValidationException;
|
import org.pgpainless.exception.SignatureValidationException;
|
||||||
import org.pgpainless.implementation.ImplementationFactory;
|
|
||||||
import org.pgpainless.signature.CertificateValidator;
|
import org.pgpainless.signature.CertificateValidator;
|
||||||
import org.pgpainless.util.ArmoredInputStreamFactory;
|
import org.pgpainless.util.ArmoredInputStreamFactory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Processor for cleartext-signed messages.
|
* Processor for cleartext-signed messages.
|
||||||
* Based on Bouncycastle's {@link org.bouncycastle.openpgp.examples.ClearSignedFileProcessor}.
|
|
||||||
*/
|
*/
|
||||||
public class CleartextSignatureProcessor {
|
public class CleartextSignatureProcessor {
|
||||||
|
|
||||||
|
private static final Logger LOGGER = Logger.getLogger(CleartextSignatureProcessor.class.getName());
|
||||||
|
|
||||||
private final ArmoredInputStream in;
|
private final ArmoredInputStream in;
|
||||||
private final PGPPublicKeyRingCollection verificationKeys;
|
private final PGPPublicKeyRingCollection verificationKeys;
|
||||||
private final MultiPassStrategy multiPassStrategy;
|
private final MultiPassStrategy multiPassStrategy;
|
||||||
|
@ -76,182 +74,35 @@ public class CleartextSignatureProcessor {
|
||||||
* @throws SignatureValidationException if the signature is invalid.
|
* @throws SignatureValidationException if the signature is invalid.
|
||||||
*/
|
*/
|
||||||
public PGPSignature process() throws IOException, PGPException {
|
public PGPSignature process() throws IOException, PGPException {
|
||||||
if (!in.isClearText()) {
|
PGPSignatureList signatures = ClearsignedMessageUtil.detachSignaturesFromInbandClearsignedMessage(in, multiPassStrategy.getMessageOutputStream());
|
||||||
throw new IllegalStateException("Message is not cleartext.");
|
Map<PGPSignature, Exception> signatureValidationExceptions = new HashMap<>();
|
||||||
}
|
|
||||||
|
|
||||||
OutputStream out = new BufferedOutputStream(multiPassStrategy.getMessageOutputStream());
|
for (PGPSignature signature : signatures) {
|
||||||
|
PGPPublicKeyRing certificate = null;
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
PGPPublicKey signingKey = null;
|
||||||
for (PGPPublicKeyRing ring : verificationKeys) {
|
for (PGPPublicKeyRing cert : verificationKeys) {
|
||||||
signingKey = ring.getPublicKey(signature.getKeyID());
|
signingKey = cert.getPublicKey(signature.getKeyID());
|
||||||
if (signingKey != null) {
|
if (signingKey != null) {
|
||||||
signingKeyRing = ring;
|
certificate = cert;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (signingKey == null) {
|
if (signingKey == null) {
|
||||||
throw new SignatureValidationException("Missing public key " + Long.toHexString(signature.getKeyID()));
|
signatureValidationExceptions.put(signature, new NoSuchElementException("Missing verification key with key-id " + Long.toHexString(signature.getKeyID())));
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
signature.init(ImplementationFactory.getInstance().getPGPContentVerifierBuilderProvider(), signingKey);
|
try {
|
||||||
|
ClearsignedMessageUtil.initializeSignature(signature, signingKey, multiPassStrategy.getMessageInputStream());
|
||||||
InputStream sigIn = new BufferedInputStream(multiPassStrategy.getMessageInputStream());
|
CertificateValidator.validateCertificateAndVerifyInitializedSignature(signature, certificate, PGPainless.getPolicy());
|
||||||
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;
|
return signature;
|
||||||
}
|
} catch (SignatureValidationException e) {
|
||||||
|
LOGGER.log(Level.INFO, "Cannot verify signature made by key " + Long.toHexString(signature.getKeyID()) + ": " + e.getMessage());
|
||||||
private static int readInputLine(ByteArrayOutputStream bOut, InputStream fIn)
|
signatureValidationExceptions.put(signature, e);
|
||||||
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;
|
throw new SignatureValidationException("No valid signatures found.", signatureValidationExceptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
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 == ' ';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,6 +47,9 @@ public interface MultiPassStrategy {
|
||||||
* Provide an {@link InputStream} which contains the data that was previously written away in
|
* Provide an {@link InputStream} which contains the data that was previously written away in
|
||||||
* {@link #getMessageOutputStream()}.
|
* {@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
|
* @return input stream
|
||||||
* @throws IOException io error
|
* @throws IOException io error
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -32,7 +32,6 @@ import org.pgpainless.PGPainless;
|
||||||
import org.pgpainless.decryption_verification.ConsumerOptions;
|
import org.pgpainless.decryption_verification.ConsumerOptions;
|
||||||
import org.pgpainless.decryption_verification.DecryptionStream;
|
import org.pgpainless.decryption_verification.DecryptionStream;
|
||||||
import org.pgpainless.decryption_verification.OpenPgpMetadata;
|
import org.pgpainless.decryption_verification.OpenPgpMetadata;
|
||||||
import org.pgpainless.exception.NotYetImplementedException;
|
|
||||||
import org.pgpainless.key.SubkeyIdentifier;
|
import org.pgpainless.key.SubkeyIdentifier;
|
||||||
import org.pgpainless.key.info.KeyRingInfo;
|
import org.pgpainless.key.info.KeyRingInfo;
|
||||||
import org.pgpainless.key.protection.SecretKeyRingProtector;
|
import org.pgpainless.key.protection.SecretKeyRingProtector;
|
||||||
|
@ -50,21 +49,13 @@ public class DecryptImpl implements Decrypt {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public DecryptImpl verifyNotBefore(Date timestamp) throws SOPGPException.UnsupportedOption {
|
public DecryptImpl verifyNotBefore(Date timestamp) throws SOPGPException.UnsupportedOption {
|
||||||
try {
|
|
||||||
consumerOptions.verifyNotBefore(timestamp);
|
consumerOptions.verifyNotBefore(timestamp);
|
||||||
} catch (NotYetImplementedException e) {
|
|
||||||
throw new SOPGPException.UnsupportedOption();
|
|
||||||
}
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public DecryptImpl verifyNotAfter(Date timestamp) throws SOPGPException.UnsupportedOption {
|
public DecryptImpl verifyNotAfter(Date timestamp) throws SOPGPException.UnsupportedOption {
|
||||||
try {
|
|
||||||
consumerOptions.verifyNotAfter(timestamp);
|
consumerOptions.verifyNotAfter(timestamp);
|
||||||
} catch (NotYetImplementedException e) {
|
|
||||||
throw new SOPGPException.UnsupportedOption();
|
|
||||||
}
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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<Signatures> message(InputStream messageInputStream) {
|
||||||
|
|
||||||
|
return new ReadyWithResult<Signatures>() {
|
||||||
|
@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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,6 +19,7 @@ import sop.SOP;
|
||||||
import sop.operation.Armor;
|
import sop.operation.Armor;
|
||||||
import sop.operation.Dearmor;
|
import sop.operation.Dearmor;
|
||||||
import sop.operation.Decrypt;
|
import sop.operation.Decrypt;
|
||||||
|
import sop.operation.DetachInbandSignatureAndMessage;
|
||||||
import sop.operation.Encrypt;
|
import sop.operation.Encrypt;
|
||||||
import sop.operation.ExtractCert;
|
import sop.operation.ExtractCert;
|
||||||
import sop.operation.GenerateKey;
|
import sop.operation.GenerateKey;
|
||||||
|
@ -72,4 +73,9 @@ public class SOPImpl implements SOP {
|
||||||
public Dearmor dearmor() {
|
public Dearmor dearmor() {
|
||||||
return new DearmorImpl();
|
return new DearmorImpl();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DetachInbandSignatureAndMessage detachInbandSignatureAndMessage() {
|
||||||
|
return new DetachInbandSignatureAndMessageImpl();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,7 +29,6 @@ import org.pgpainless.PGPainless;
|
||||||
import org.pgpainless.decryption_verification.ConsumerOptions;
|
import org.pgpainless.decryption_verification.ConsumerOptions;
|
||||||
import org.pgpainless.decryption_verification.DecryptionStream;
|
import org.pgpainless.decryption_verification.DecryptionStream;
|
||||||
import org.pgpainless.decryption_verification.OpenPgpMetadata;
|
import org.pgpainless.decryption_verification.OpenPgpMetadata;
|
||||||
import org.pgpainless.exception.NotYetImplementedException;
|
|
||||||
import org.pgpainless.key.SubkeyIdentifier;
|
import org.pgpainless.key.SubkeyIdentifier;
|
||||||
import sop.Verification;
|
import sop.Verification;
|
||||||
import sop.exception.SOPGPException;
|
import sop.exception.SOPGPException;
|
||||||
|
@ -41,21 +40,13 @@ public class VerifyImpl implements Verify {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Verify notBefore(Date timestamp) throws SOPGPException.UnsupportedOption {
|
public Verify notBefore(Date timestamp) throws SOPGPException.UnsupportedOption {
|
||||||
try {
|
|
||||||
options.verifyNotBefore(timestamp);
|
options.verifyNotBefore(timestamp);
|
||||||
} catch (NotYetImplementedException e) {
|
|
||||||
throw new SOPGPException.UnsupportedOption();
|
|
||||||
}
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Verify notAfter(Date timestamp) throws SOPGPException.UnsupportedOption {
|
public Verify notAfter(Date timestamp) throws SOPGPException.UnsupportedOption {
|
||||||
try {
|
|
||||||
options.verifyNotAfter(timestamp);
|
options.verifyNotAfter(timestamp);
|
||||||
} catch (NotYetImplementedException e) {
|
|
||||||
throw new SOPGPException.UnsupportedOption();
|
|
||||||
}
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@ import sop.SOP;
|
||||||
import sop.cli.picocli.commands.ArmorCmd;
|
import sop.cli.picocli.commands.ArmorCmd;
|
||||||
import sop.cli.picocli.commands.DearmorCmd;
|
import sop.cli.picocli.commands.DearmorCmd;
|
||||||
import sop.cli.picocli.commands.DecryptCmd;
|
import sop.cli.picocli.commands.DecryptCmd;
|
||||||
|
import sop.cli.picocli.commands.DetachInbandSignatureAndMessageCmd;
|
||||||
import sop.cli.picocli.commands.EncryptCmd;
|
import sop.cli.picocli.commands.EncryptCmd;
|
||||||
import sop.cli.picocli.commands.ExtractCertCmd;
|
import sop.cli.picocli.commands.ExtractCertCmd;
|
||||||
import sop.cli.picocli.commands.GenerateKeyCmd;
|
import sop.cli.picocli.commands.GenerateKeyCmd;
|
||||||
|
@ -34,6 +35,7 @@ import sop.cli.picocli.commands.VersionCmd;
|
||||||
ArmorCmd.class,
|
ArmorCmd.class,
|
||||||
DearmorCmd.class,
|
DearmorCmd.class,
|
||||||
DecryptCmd.class,
|
DecryptCmd.class,
|
||||||
|
DetachInbandSignatureAndMessageCmd.class,
|
||||||
EncryptCmd.class,
|
EncryptCmd.class,
|
||||||
ExtractCertCmd.class,
|
ExtractCertCmd.class,
|
||||||
GenerateKeyCmd.class,
|
GenerateKeyCmd.class,
|
||||||
|
|
|
@ -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;
|
package sop.cli.picocli.commands;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
import picocli.CommandLine;
|
import picocli.CommandLine;
|
||||||
|
import sop.Signatures;
|
||||||
|
import sop.cli.picocli.SopCLI;
|
||||||
import sop.exception.SOPGPException;
|
import sop.exception.SOPGPException;
|
||||||
|
import sop.operation.DetachInbandSignatureAndMessage;
|
||||||
|
|
||||||
@CommandLine.Command(name = "detach-inband-signature-and-message",
|
@CommandLine.Command(name = "detach-inband-signature-and-message",
|
||||||
description = "Split a clearsigned message",
|
description = "Split a clearsigned message",
|
||||||
exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE)
|
exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE)
|
||||||
public class DetachInbandSignatureAndMessageCmd implements Runnable {
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,8 +21,21 @@ import java.io.OutputStream;
|
||||||
|
|
||||||
public abstract class Ready {
|
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;
|
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 {
|
public byte[] getBytes() throws IOException {
|
||||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||||
writeTo(bytes);
|
writeTo(bytes);
|
||||||
|
|
|
@ -23,6 +23,16 @@ import sop.exception.SOPGPException;
|
||||||
|
|
||||||
public abstract class ReadyWithResult<T> {
|
public abstract class ReadyWithResult<T> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 abstract T writeTo(OutputStream outputStream) throws IOException, SOPGPException.NoSignature;
|
||||||
|
|
||||||
public ByteArrayAndResult<T> toBytes() throws IOException, SOPGPException.NoSignature {
|
public ByteArrayAndResult<T> toBytes() throws IOException, SOPGPException.NoSignature {
|
||||||
|
|
|
@ -18,6 +18,7 @@ package sop;
|
||||||
import sop.operation.Armor;
|
import sop.operation.Armor;
|
||||||
import sop.operation.Dearmor;
|
import sop.operation.Dearmor;
|
||||||
import sop.operation.Decrypt;
|
import sop.operation.Decrypt;
|
||||||
|
import sop.operation.DetachInbandSignatureAndMessage;
|
||||||
import sop.operation.Encrypt;
|
import sop.operation.Encrypt;
|
||||||
import sop.operation.ExtractCert;
|
import sop.operation.ExtractCert;
|
||||||
import sop.operation.GenerateKey;
|
import sop.operation.GenerateKey;
|
||||||
|
@ -100,4 +101,6 @@ public interface SOP {
|
||||||
* @return builder instance
|
* @return builder instance
|
||||||
*/
|
*/
|
||||||
Dearmor dearmor();
|
Dearmor dearmor();
|
||||||
|
|
||||||
|
DetachInbandSignatureAndMessage detachInbandSignatureAndMessage();
|
||||||
}
|
}
|
||||||
|
|
32
sop-java/src/main/java/sop/Signatures.java
Normal file
32
sop-java/src/main/java/sop/Signatures.java
Normal file
|
@ -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;
|
||||||
|
|
||||||
|
}
|
|
@ -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<Signatures> message(InputStream messageInputStream) throws IOException;
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in a new issue