1
0
Fork 0
mirror of https://github.com/pgpainless/pgpainless.git synced 2025-12-11 06:41:09 +01:00

Base PGPainlessCLI on new sop-java module

* Rename pgpainless-sop -> pgpainless-cli
* Introduce sop-java (implementation-independent SOP API)
* Introduce sop-java-picocli (CLI frontend for sop-java)
* Introduce pgpainless-sop (implementation of sop-java using PGPainless)
* Rework pgpainless-cli (plugs pgpainless-sop into sop-java-picocli)
This commit is contained in:
Paul Schaub 2021-07-15 16:55:13 +02:00
parent 2ba782c451
commit 8cf5347b52
112 changed files with 6146 additions and 1303 deletions

View file

@ -0,0 +1,69 @@
/*
* Copyright 2020 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 java.io.PushbackInputStream;
import java.nio.charset.Charset;
import java.util.Arrays;
import org.bouncycastle.bcpg.ArmoredOutputStream;
import org.bouncycastle.util.io.Streams;
import org.pgpainless.util.ArmoredOutputStreamFactory;
import sop.Ready;
import sop.enums.ArmorLabel;
import sop.exception.SOPGPException;
import sop.operation.Armor;
public class ArmorImpl implements Armor {
public static final byte[] ARMOR_START = "-----BEGIN PGP".getBytes(Charset.forName("UTF8"));
boolean allowNested = false;
@Override
public Armor label(ArmorLabel label) throws SOPGPException.UnsupportedOption {
throw new SOPGPException.UnsupportedOption();
}
@Override
public Armor allowNested() throws SOPGPException.UnsupportedOption {
allowNested = true;
return this;
}
@Override
public Ready data(InputStream data) throws SOPGPException.BadData {
return new Ready() {
@Override
public void writeTo(OutputStream outputStream) throws IOException {
PushbackInputStream pbIn = new PushbackInputStream(data, ARMOR_START.length);
byte[] buffer = new byte[ARMOR_START.length];
int read = pbIn.read(buffer);
pbIn.unread(buffer, 0, read);
if (!allowNested && Arrays.equals(ARMOR_START, buffer)) {
Streams.pipeAll(pbIn, System.out);
} else {
ArmoredOutputStream armor = ArmoredOutputStreamFactory.get(System.out);
Streams.pipeAll(pbIn, armor);
armor.close();
}
}
};
}
}

View file

@ -0,0 +1,40 @@
/*
* 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.openpgp.PGPUtil;
import org.bouncycastle.util.io.Streams;
import sop.Ready;
import sop.operation.Dearmor;
public class DearmorImpl implements Dearmor {
@Override
public Ready data(InputStream data) throws IOException {
InputStream decoder = PGPUtil.getDecoderStream(data);
return new Ready() {
@Override
public void writeTo(OutputStream outputStream) throws IOException {
Streams.pipeAll(decoder, outputStream);
decoder.close();
}
};
}
}

View file

@ -0,0 +1,184 @@
/*
* 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 java.util.ArrayList;
import java.util.Date;
import java.util.List;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.bouncycastle.openpgp.PGPSecretKeyRingCollection;
import org.bouncycastle.openpgp.PGPSignature;
import org.bouncycastle.util.io.Streams;
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;
import org.pgpainless.util.Passphrase;
import sop.DecryptionResult;
import sop.ReadyWithResult;
import sop.SessionKey;
import sop.Verification;
import sop.exception.SOPGPException;
import sop.operation.Decrypt;
public class DecryptImpl implements Decrypt {
private final ConsumerOptions consumerOptions = new ConsumerOptions();
@Override
public DecryptImpl verifyNotBefore(Date timestamp) throws SOPGPException.UnsupportedOption {
try {
consumerOptions.verifyNotBefore(timestamp);
} catch (NotYetImplementedException e) {
// throw new SOPGPException.UnsupportedOption();
}
return this;
}
@Override
public DecryptImpl verifyNotAfter(Date timestamp) throws SOPGPException.UnsupportedOption {
try {
consumerOptions.verifyNotAfter(timestamp);
} catch (NotYetImplementedException e) {
// throw new SOPGPException.UnsupportedOption();
}
return this;
}
@Override
public DecryptImpl verifyWithCert(InputStream certIn) throws SOPGPException.BadData, IOException {
try {
PGPPublicKeyRingCollection certs = PGPainless.readKeyRing().keyRingCollection(certIn, false)
.getPgpPublicKeyRingCollection();
if (certs == null) {
throw new SOPGPException.BadData(new PGPException("No certificates provided."));
}
consumerOptions.addVerificationCerts(certs);
} catch (PGPException e) {
throw new SOPGPException.BadData(e);
}
return this;
}
@Override
public DecryptImpl withSessionKey(SessionKey sessionKey) throws SOPGPException.UnsupportedOption {
throw new SOPGPException.UnsupportedOption();
}
@Override
public DecryptImpl withPassword(String password) throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption {
consumerOptions.addDecryptionPassphrase(Passphrase.fromPassword(password));
String withoutTrailingWhitespace = removeTrailingWhitespace(password);
if (!password.equals(withoutTrailingWhitespace)) {
consumerOptions.addDecryptionPassphrase(Passphrase.fromPassword(withoutTrailingWhitespace));
}
return this;
}
private static String removeTrailingWhitespace(String passphrase) {
int i = passphrase.length() - 1;
// Find index of first non-whitespace character from the back
while (i > 0 && Character.isWhitespace(passphrase.charAt(i))) {
i--;
}
return passphrase.substring(0, i);
}
@Override
public DecryptImpl withKey(InputStream keyIn) throws SOPGPException.KeyIsProtected, SOPGPException.BadData, SOPGPException.UnsupportedAsymmetricAlgo {
try {
PGPSecretKeyRingCollection secretKeys = PGPainless.readKeyRing()
.keyRingCollection(keyIn, true)
.getPGPSecretKeyRingCollection();
for (PGPSecretKeyRing secretKey : secretKeys) {
KeyRingInfo info = new KeyRingInfo(secretKey);
if (!info.isFullyDecrypted()) {
throw new SOPGPException.KeyIsProtected();
}
}
consumerOptions.addDecryptionKeys(secretKeys, SecretKeyRingProtector.unprotectedKeys());
} catch (IOException | PGPException e) {
throw new SOPGPException.BadData(e);
}
return this;
}
@Override
public ReadyWithResult<DecryptionResult> ciphertext(InputStream ciphertext)
throws SOPGPException.BadData,
SOPGPException.MissingArg {
if (consumerOptions.getDecryptionKeys().isEmpty() && consumerOptions.getDecryptionPassphrases().isEmpty()) {
throw new SOPGPException.MissingArg("Missing decryption key or passphrase.");
}
DecryptionStream decryptionStream;
try {
decryptionStream = PGPainless.decryptAndOrVerify()
.onInputStream(ciphertext)
.withOptions(consumerOptions);
} catch (PGPException | IOException e) {
throw new SOPGPException.BadData(e);
}
return new ReadyWithResult<DecryptionResult>() {
@Override
public DecryptionResult writeTo(OutputStream outputStream) throws IOException, SOPGPException.NoSignature {
Streams.pipeAll(decryptionStream, outputStream);
decryptionStream.close();
OpenPgpMetadata metadata = decryptionStream.getResult();
List<Verification> verificationList = new ArrayList<>();
for (SubkeyIdentifier verifiedSigningKey : metadata.getVerifiedSignatures().keySet()) {
PGPSignature signature = metadata.getVerifiedSignatures().get(verifiedSigningKey);
Date verifyNotBefore = consumerOptions.getVerifyNotBefore();
Date verifyNotAfter = consumerOptions.getVerifyNotAfter();
if (verifyNotAfter == null || !signature.getCreationTime().after(verifyNotAfter)) {
if (verifyNotBefore == null || !signature.getCreationTime().before(verifyNotBefore)) {
verificationList.add(new Verification(
signature.getCreationTime(),
verifiedSigningKey.getSubkeyFingerprint().toString(),
verifiedSigningKey.getPrimaryKeyFingerprint().toString()));
}
}
}
if (!consumerOptions.getCertificates().isEmpty()) {
if (verificationList.isEmpty()) {
throw new SOPGPException.NoSignature();
}
}
return new DecryptionResult(null, verificationList);
}
};
}
}

View file

@ -0,0 +1,140 @@
/*
* 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.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
import org.bouncycastle.openpgp.PGPSecretKeyRingCollection;
import org.bouncycastle.util.io.Streams;
import org.pgpainless.PGPainless;
import org.pgpainless.algorithm.DocumentSignatureType;
import org.pgpainless.algorithm.StreamEncoding;
import org.pgpainless.encryption_signing.EncryptionOptions;
import org.pgpainless.encryption_signing.EncryptionStream;
import org.pgpainless.encryption_signing.ProducerOptions;
import org.pgpainless.encryption_signing.SigningOptions;
import org.pgpainless.exception.WrongPassphraseException;
import org.pgpainless.key.protection.SecretKeyRingProtector;
import org.pgpainless.util.Passphrase;
import sop.util.ProxyOutputStream;
import sop.Ready;
import sop.enums.EncryptAs;
import sop.exception.SOPGPException;
import sop.operation.Encrypt;
public class EncryptImpl implements Encrypt {
EncryptionOptions encryptionOptions = new EncryptionOptions();
SigningOptions signingOptions = null;
private EncryptAs encryptAs = EncryptAs.Binary;
boolean armor = true;
@Override
public Encrypt noArmor() {
armor = false;
return this;
}
@Override
public Encrypt mode(EncryptAs mode) throws SOPGPException.UnsupportedOption {
this.encryptAs = mode;
return this;
}
@Override
public Encrypt signWith(InputStream keyIn) throws SOPGPException.KeyIsProtected, SOPGPException.CertCannotSign, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData {
try {
PGPSecretKeyRingCollection keys = PGPainless.readKeyRing().secretKeyRingCollection(keyIn);
if (signingOptions == null) {
signingOptions = SigningOptions.get();
}
try {
signingOptions.addInlineSignatures(SecretKeyRingProtector.unprotectedKeys(), keys, DocumentSignatureType.BINARY_DOCUMENT);
} catch (IllegalArgumentException e) {
throw new SOPGPException.CertCannotSign();
} catch (WrongPassphraseException e) {
throw new SOPGPException.KeyIsProtected();
}
} catch (IOException | PGPException e) {
throw new SOPGPException.BadData(e);
}
return this;
}
@Override
public Encrypt withPassword(String password) throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption {
encryptionOptions.addPassphrase(Passphrase.fromPassword(password));
return this;
}
@Override
public Encrypt withCert(InputStream cert) throws SOPGPException.CertCannotEncrypt, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData {
try {
PGPPublicKeyRingCollection certificates = PGPainless.readKeyRing()
.keyRingCollection(cert, false)
.getPgpPublicKeyRingCollection();
encryptionOptions.addRecipients(certificates);
} catch (IOException | PGPException e) {
throw new SOPGPException.BadData(e);
}
return this;
}
@Override
public Ready plaintext(InputStream plaintext) throws IOException {
ProducerOptions producerOptions = signingOptions != null ?
ProducerOptions.signAndEncrypt(encryptionOptions, signingOptions) :
ProducerOptions.encrypt(encryptionOptions);
producerOptions.setAsciiArmor(armor);
producerOptions.setEncoding(encryptAsToStreamEncoding(encryptAs));
try {
ProxyOutputStream proxy = new ProxyOutputStream();
EncryptionStream encryptionStream = PGPainless.encryptAndOrSign()
.onOutputStream(proxy)
.withOptions(producerOptions);
return new Ready() {
@Override
public void writeTo(OutputStream outputStream) throws IOException {
proxy.replaceOutputStream(outputStream);
Streams.pipeAll(plaintext, encryptionStream);
encryptionStream.close();
}
};
} catch (PGPException e) {
throw new IOException();
}
}
private static StreamEncoding encryptAsToStreamEncoding(EncryptAs encryptAs) {
switch (encryptAs) {
case Binary:
return StreamEncoding.BINARY;
case Text:
return StreamEncoding.TEXT;
case MIME:
return StreamEncoding.UTF8;
}
throw new IllegalArgumentException("Invalid value encountered: " + encryptAs);
}
}

View file

@ -0,0 +1,63 @@
/*
* 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.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.pgpainless.PGPainless;
import org.pgpainless.key.util.KeyRingUtils;
import org.pgpainless.util.ArmorUtils;
import sop.operation.ExtractCert;
import sop.Ready;
import sop.exception.SOPGPException;
public class ExtractCertImpl implements ExtractCert {
private boolean armor = true;
@Override
public ExtractCert noArmor() {
armor = false;
return this;
}
@Override
public Ready key(InputStream keyInputStream) throws IOException, SOPGPException.BadData {
try {
PGPSecretKeyRing key = PGPainless.readKeyRing().secretKeyRing(keyInputStream);
PGPPublicKeyRing cert = KeyRingUtils.publicKeyRingFrom(key);
return new Ready() {
@Override
public void writeTo(OutputStream outputStream) throws IOException {
OutputStream out = armor ? ArmorUtils.createArmoredOutputStreamFor(cert, outputStream) : outputStream;
cert.encode(out);
if (armor) {
out.close();
}
}
};
} catch (PGPException e) {
throw new SOPGPException.BadData(e);
}
}
}

View file

@ -0,0 +1,95 @@
/*
* 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.OutputStream;
import java.security.InvalidAlgorithmParameterException;
import java.security.NoSuchAlgorithmException;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.Set;
import org.bouncycastle.bcpg.ArmoredOutputStream;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.pgpainless.PGPainless;
import org.pgpainless.key.modification.secretkeyring.SecretKeyRingEditorInterface;
import org.pgpainless.key.protection.SecretKeyRingProtector;
import org.pgpainless.util.ArmorUtils;
import sop.Ready;
import sop.exception.SOPGPException;
import sop.operation.GenerateKey;
public class GenerateKeyImpl implements GenerateKey {
private boolean armor = true;
private final Set<String> userIds = new LinkedHashSet<>();
@Override
public GenerateKey noArmor() {
this.armor = false;
return this;
}
@Override
public GenerateKey userId(String userId) {
this.userIds.add(userId);
return this;
}
@Override
public Ready generate() throws SOPGPException.MissingArg, SOPGPException.UnsupportedAsymmetricAlgo, IOException {
Iterator<String> userIdIterator = userIds.iterator();
if (!userIdIterator.hasNext()) {
throw new SOPGPException.MissingArg("Missing user-id.");
}
PGPSecretKeyRing key;
try {
key = PGPainless.generateKeyRing()
.modernKeyRing(userIdIterator.next(), null);
if (userIdIterator.hasNext()) {
SecretKeyRingEditorInterface editor = PGPainless.modifyKeyRing(key);
while (userIdIterator.hasNext()) {
editor.addUserId(userIdIterator.next(), SecretKeyRingProtector.unprotectedKeys());
}
key = editor.done();
}
PGPSecretKeyRing finalKey = key;
return new Ready() {
@Override
public void writeTo(OutputStream outputStream) throws IOException {
if (armor) {
ArmoredOutputStream armoredOutputStream = ArmorUtils.toAsciiArmoredStream(finalKey, outputStream);
finalKey.encode(armoredOutputStream);
armoredOutputStream.close();
} else {
finalKey.encode(outputStream);
}
}
};
} catch (InvalidAlgorithmParameterException | NoSuchAlgorithmException e) {
throw new SOPGPException.UnsupportedAsymmetricAlgo(e);
} catch (PGPException e) {
throw new RuntimeException(e);
}
}
}

View file

@ -1,58 +0,0 @@
/*
* Copyright 2020 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 org.pgpainless.sop.commands.Armor;
import org.pgpainless.sop.commands.Dearmor;
import org.pgpainless.sop.commands.Decrypt;
import org.pgpainless.sop.commands.Encrypt;
import org.pgpainless.sop.commands.ExtractCert;
import org.pgpainless.sop.commands.GenerateKey;
import org.pgpainless.sop.commands.Sign;
import org.pgpainless.sop.commands.Verify;
import org.pgpainless.sop.commands.Version;
import picocli.CommandLine;
@CommandLine.Command(exitCodeOnInvalidInput = 69,
subcommands = {
Armor.class,
Dearmor.class,
Decrypt.class,
Encrypt.class,
ExtractCert.class,
GenerateKey.class,
Sign.class,
Verify.class,
Version.class
}
)
public class PGPainlessCLI implements Runnable {
public PGPainlessCLI() {
}
public static void main(String[] args) {
int code = new CommandLine(new PGPainlessCLI())
.execute(args);
System.exit(code);
}
@Override
public void run() {
}
}

View file

@ -1,61 +0,0 @@
/*
* Copyright 2020 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 org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.pgpainless.util.ArmorUtils;
public class Print {
public static String toString(PGPSecretKeyRing keyRing, boolean armor) throws IOException {
if (armor) {
return ArmorUtils.toAsciiArmoredString(keyRing);
} else {
return new String(keyRing.getEncoded(), "UTF-8");
}
}
public static String toString(PGPPublicKeyRing keyRing, boolean armor) throws IOException {
if (armor) {
return ArmorUtils.toAsciiArmoredString(keyRing);
} else {
return new String(keyRing.getEncoded(), "UTF-8");
}
}
public static String toString(byte[] bytes, boolean armor) throws IOException {
if (armor) {
return ArmorUtils.toAsciiArmoredString(bytes);
} else {
return new String(bytes, "UTF-8");
}
}
public static void print_ln(String msg) {
// CHECKSTYLE:OFF
System.out.println(msg);
// CHECKSTYLE:ON
}
public static void err_ln(String msg) {
// CHECKSTYLE:OFF
System.err.println(msg);
// CHECKSTYLE:ON
}
}

View file

@ -0,0 +1,75 @@
/*
* 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 sop.SOP;
import sop.operation.Armor;
import sop.operation.Dearmor;
import sop.operation.Decrypt;
import sop.operation.Encrypt;
import sop.operation.ExtractCert;
import sop.operation.GenerateKey;
import sop.operation.Sign;
import sop.operation.Verify;
import sop.operation.Version;
public class SOPImpl implements SOP {
@Override
public Version version() {
return new VersionImpl();
}
@Override
public GenerateKey generateKey() {
return new GenerateKeyImpl();
}
@Override
public ExtractCert extractCert() {
return new ExtractCertImpl();
}
@Override
public Sign sign() {
return new SignImpl();
}
@Override
public Verify verify() {
return new VerifyImpl();
}
@Override
public Encrypt encrypt() {
return new EncryptImpl();
}
@Override
public Decrypt decrypt() {
return new DecryptImpl();
}
@Override
public Armor armor() {
return new ArmorImpl();
}
@Override
public Dearmor dearmor() {
return new DearmorImpl();
}
}

View file

@ -0,0 +1,128 @@
/*
* 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.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.bouncycastle.openpgp.PGPSignature;
import org.bouncycastle.util.io.Streams;
import org.pgpainless.PGPainless;
import org.pgpainless.algorithm.DocumentSignatureType;
import org.pgpainless.encryption_signing.EncryptionResult;
import org.pgpainless.encryption_signing.EncryptionStream;
import org.pgpainless.encryption_signing.ProducerOptions;
import org.pgpainless.encryption_signing.SigningOptions;
import org.pgpainless.key.SubkeyIdentifier;
import org.pgpainless.key.info.KeyRingInfo;
import org.pgpainless.key.protection.SecretKeyRingProtector;
import org.pgpainless.util.ArmoredOutputStreamFactory;
import sop.Ready;
import sop.enums.SignAs;
import sop.exception.SOPGPException;
import sop.operation.Sign;
public class SignImpl implements Sign {
private boolean armor = true;
private SignAs mode = SignAs.Binary;
private List<PGPSecretKeyRing> keys = new ArrayList<>();
private SigningOptions signingOptions = new SigningOptions();
@Override
public Sign noArmor() {
armor = false;
return this;
}
@Override
public Sign mode(SignAs mode) {
this.mode = mode;
return this;
}
@Override
public Sign key(InputStream keyIn) throws SOPGPException.KeyIsProtected, SOPGPException.BadData, IOException {
try {
PGPSecretKeyRing key = PGPainless.readKeyRing().secretKeyRing(keyIn);
KeyRingInfo info = new KeyRingInfo(key);
if (!info.isFullyDecrypted()) {
throw new SOPGPException.KeyIsProtected();
}
signingOptions.addDetachedSignature(SecretKeyRingProtector.unprotectedKeys(), key, modeToSigType(mode));
} catch (PGPException e) {
throw new SOPGPException.BadData(e);
}
return this;
}
@Override
public Ready data(InputStream data) throws IOException {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
try {
EncryptionStream signingStream = PGPainless.encryptAndOrSign()
.onOutputStream(buffer)
.withOptions(ProducerOptions.sign(signingOptions)
.setAsciiArmor(armor));
return new Ready() {
@Override
public void writeTo(OutputStream outputStream) throws IOException {
if (signingStream.isClosed()) {
throw new IllegalStateException("EncryptionStream is already closed.");
}
Streams.pipeAll(data, signingStream);
signingStream.close();
EncryptionResult encryptionResult = signingStream.getResult();
List<PGPSignature> signatures = new ArrayList<>();
for (SubkeyIdentifier key : encryptionResult.getDetachedSignatures().keySet()) {
signatures.addAll(encryptionResult.getDetachedSignatures().get(key));
}
OutputStream out;
if (armor) {
out = ArmoredOutputStreamFactory.get(outputStream);
} else {
out = outputStream;
}
for (PGPSignature sig : signatures) {
sig.encode(out);
}
out.close();
outputStream.close(); // armor out does not close underlying stream
}
};
} catch (PGPException e) {
throw new RuntimeException(e);
}
}
private static DocumentSignatureType modeToSigType(SignAs mode) {
return mode == SignAs.Binary ? DocumentSignatureType.BINARY_DOCUMENT
: DocumentSignatureType.CANONICAL_TEXT_DOCUMENT;
}
}

View file

@ -1,67 +0,0 @@
/*
* Copyright 2020 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 static org.pgpainless.sop.Print.err_ln;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.pgpainless.PGPainless;
public class SopKeyUtil {
public static List<PGPSecretKeyRing> loadKeysFromFiles(File... files) throws IOException, PGPException {
List<PGPSecretKeyRing> secretKeyRings = new ArrayList<>();
for (File file : files) {
try (FileInputStream in = new FileInputStream(file)) {
secretKeyRings.add(PGPainless.readKeyRing().secretKeyRing(in));
} catch (PGPException | IOException e) {
err_ln("Could not load secret key " + file.getName() + ": " + e.getMessage());
throw e;
}
}
return secretKeyRings;
}
public static List<PGPPublicKeyRing> loadCertificatesFromFile(File... files) throws IOException {
List<PGPPublicKeyRing> publicKeyRings = new ArrayList<>();
for (File file : files) {
try (FileInputStream in = new FileInputStream(file)) {
PGPPublicKeyRingCollection collection = PGPainless.readKeyRing()
.keyRingCollection(in, true)
.getPgpPublicKeyRingCollection();
if (collection == null) {
throw new PGPException("Provided file " + file.getName() + " does not contain a certificate.");
}
for (PGPPublicKeyRing keyRing : collection) {
publicKeyRings.add(keyRing);
}
} catch (IOException | PGPException e) {
err_ln("Could not read certificate from file " + file.getName() + ": " + e.getMessage());
throw new IOException(e);
}
}
return publicKeyRings;
}
}

View file

@ -0,0 +1,124 @@
/*
* 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.util.ArrayList;
import java.util.Date;
import java.util.List;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
import org.bouncycastle.openpgp.PGPSignature;
import org.bouncycastle.util.io.Streams;
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;
import sop.operation.Verify;
public class VerifyImpl implements Verify {
ConsumerOptions options = new ConsumerOptions();
@Override
public Verify notBefore(Date timestamp) throws SOPGPException.UnsupportedOption {
try {
options.verifyNotBefore(timestamp);
} catch (NotYetImplementedException e) {
// throw new SOPGPException.UnsupportedOption();
}
return this;
}
@Override
public Verify notAfter(Date timestamp) throws SOPGPException.UnsupportedOption {
try {
options.verifyNotAfter(timestamp);
} catch (NotYetImplementedException e) {
// throw new SOPGPException.UnsupportedOption();
}
return this;
}
@Override
public Verify cert(InputStream cert) throws SOPGPException.BadData {
PGPPublicKeyRingCollection certificates;
try {
certificates = PGPainless.readKeyRing().publicKeyRingCollection(cert);
} catch (IOException | PGPException e) {
throw new SOPGPException.BadData(e);
}
options.addVerificationCerts(certificates);
return this;
}
@Override
public VerifyImpl signatures(InputStream signatures) throws SOPGPException.BadData {
try {
options.addVerificationOfDetachedSignatures(signatures);
} catch (IOException | PGPException e) {
throw new SOPGPException.BadData(e);
}
return this;
}
@Override
public List<Verification> data(InputStream data) throws IOException, SOPGPException.NoSignature, SOPGPException.BadData {
DecryptionStream decryptionStream;
try {
decryptionStream = PGPainless.decryptAndOrVerify()
.onInputStream(data)
.withOptions(options);
Streams.drain(decryptionStream);
decryptionStream.close();
OpenPgpMetadata metadata = decryptionStream.getResult();
List<Verification> verificationList = new ArrayList<>();
for (SubkeyIdentifier verifiedSigningKey : metadata.getVerifiedSignatures().keySet()) {
PGPSignature signature = metadata.getVerifiedSignatures().get(verifiedSigningKey);
Date verifyNotBefore = options.getVerifyNotBefore();
Date verifyNotAfter = options.getVerifyNotAfter();
if (verifyNotAfter == null || !signature.getCreationTime().after(verifyNotAfter)) {
if (verifyNotBefore == null || !signature.getCreationTime().before(verifyNotBefore)) {
verificationList.add(new Verification(
signature.getCreationTime(),
verifiedSigningKey.getSubkeyFingerprint().toString(),
verifiedSigningKey.getPrimaryKeyFingerprint().toString()));
}
}
}
if (!options.getCertificates().isEmpty()) {
if (verificationList.isEmpty()) {
throw new SOPGPException.NoSignature();
}
}
return verificationList;
} catch (PGPException e) {
throw new SOPGPException.BadData(e);
}
}
}

View file

@ -1,5 +1,5 @@
/*
* Copyright 2020 Paul Schaub.
* 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.
@ -13,21 +13,21 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pgpainless.sop.commands;
import static org.pgpainless.sop.Print.print_ln;
package org.pgpainless.sop;
import java.io.IOException;
import java.util.Properties;
import picocli.CommandLine;
import sop.operation.Version;
@CommandLine.Command(name = "version", description = "Display version information about the tool",
exitCodeOnInvalidInput = 37)
public class Version implements Runnable {
public class VersionImpl implements Version {
@Override
public String getName() {
return "PGPainless-SOP";
}
@Override
public void run() {
public String getVersion() {
// See https://stackoverflow.com/a/50119235
String version;
try {
@ -37,6 +37,6 @@ public class Version implements Runnable {
} catch (IOException e) {
version = "DEVELOPMENT";
}
print_ln("PGPainlessCLI " + version);
return version;
}
}

View file

@ -1,75 +0,0 @@
/*
* Copyright 2020 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.commands;
import org.bouncycastle.bcpg.ArmoredOutputStream;
import org.bouncycastle.util.io.Streams;
import org.pgpainless.util.ArmoredOutputStreamFactory;
import picocli.CommandLine;
import java.io.IOException;
import java.io.PushbackInputStream;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import static org.pgpainless.sop.Print.err_ln;
@CommandLine.Command(name = "armor",
description = "Add ASCII Armor to standard input",
exitCodeOnInvalidInput = 37)
public class Armor implements Runnable {
private static final byte[] BEGIN_ARMOR = "-----BEGIN PGP".getBytes(StandardCharsets.UTF_8);
private enum Label {
auto,
sig,
key,
cert,
message
}
@CommandLine.Option(names = {"--label"}, description = "Label to be used in the header and tail of the armoring.", paramLabel = "{auto|sig|key|cert|message}")
Label label;
@CommandLine.Option(names = {"--allow-nested"}, description = "Allow additional armoring of already armored input")
boolean allowNested = false;
@Override
public void run() {
try (PushbackInputStream pbIn = new PushbackInputStream(System.in, BEGIN_ARMOR.length);
ArmoredOutputStream armoredOutputStream = ArmoredOutputStreamFactory.get(System.out)) {
// take a peek
byte[] firstBytes = new byte[BEGIN_ARMOR.length];
int readByteCount = pbIn.read(firstBytes);
if (readByteCount != -1) {
pbIn.unread(firstBytes, 0, readByteCount);
}
if (Arrays.equals(BEGIN_ARMOR, firstBytes) && !allowNested) {
Streams.pipeAll(pbIn, System.out);
} else {
Streams.pipeAll(pbIn, armoredOutputStream);
}
} catch (IOException e) {
err_ln("Input data cannot be ASCII armored.");
err_ln(e.getMessage());
System.exit(1);
}
}
}

View file

@ -1,41 +0,0 @@
/*
* Copyright 2020 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.commands;
import org.bouncycastle.bcpg.ArmoredInputStream;
import org.bouncycastle.util.io.Streams;
import picocli.CommandLine;
import java.io.IOException;
import static org.pgpainless.sop.Print.err_ln;
@CommandLine.Command(name = "dearmor",
description = "Remove ASCII Armor from standard input",
exitCodeOnInvalidInput = 37)
public class Dearmor implements Runnable {
@Override
public void run() {
try (ArmoredInputStream in = new ArmoredInputStream(System.in, true)) {
Streams.pipeAll(in, System.out);
} catch (IOException e) {
err_ln("Data cannot be dearmored.");
err_ln(e.getMessage());
System.exit(1);
}
}
}

View file

@ -1,178 +0,0 @@
/*
* Copyright 2020 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.commands;
import static org.pgpainless.sop.Print.err_ln;
import static org.pgpainless.sop.SopKeyUtil.loadKeysFromFiles;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.List;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.bouncycastle.openpgp.PGPSignature;
import org.bouncycastle.util.io.Streams;
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.key.SubkeyIdentifier;
import org.pgpainless.sop.SopKeyUtil;
import picocli.CommandLine;
@CommandLine.Command(name = "decrypt",
description = "Decrypt a message from standard input",
exitCodeOnInvalidInput = 37)
public class Decrypt implements Runnable {
private static final DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'");
@CommandLine.Option(
names = {"--session-key-out"},
description = "Can be used to learn the session key on successful decryption",
paramLabel = "SESSIONKEY")
File sessionKeyOut;
@CommandLine.Option(
names = {"--with-session-key"},
description = "Enables decryption of the \"CIPHERTEXT\" using the session key directly against the \"SEIPD\" packet",
paramLabel = "SESSIONKEY")
File[] withSessionKey;
@CommandLine.Option(
names = {"--with-password"},
description = "Enables decryption based on any \"SKESK\" packets in the \"CIPHERTEXT\"",
paramLabel = "PASSWORD")
String[] withPassword;
@CommandLine.Option(names = {"--verify-out"},
description = "Produces signature verification status to the designated file",
paramLabel = "VERIFICATIONS")
File verifyOut;
@CommandLine.Option(names = {"--verify-with"},
description = "Certificates whose signatures would be acceptable for signatures over this message",
paramLabel = "CERT")
File[] certs;
@CommandLine.Option(names = {"--not-before"},
description = "ISO-8601 formatted UTC date (eg. '2020-11-23T16:35Z)\n" +
"Reject signatures with a creation date not in range.\n" +
"Defaults to beginning of time (\"-\").",
paramLabel = "DATE")
String notBefore = "-";
@CommandLine.Option(names = {"--not-after"},
description = "ISO-8601 formatted UTC date (eg. '2020-11-23T16:35Z)\n" +
"Reject signatures with a creation date not in range.\n" +
"Defaults to current system time (\"now\").\n" +
"Accepts special value \"-\" for end of time.",
paramLabel = "DATE")
String notAfter = "now";
@CommandLine.Parameters(index = "0..*",
description = "Secret keys to attempt decryption with",
paramLabel = "KEY")
File[] keys;
@Override
public void run() {
if (verifyOut == null ^ certs == null) {
err_ln("To enable signature verification, both --verify-out and at least one --verify-with argument must be supplied.");
System.exit(23);
}
if (sessionKeyOut != null || withSessionKey != null) {
err_ln("session key in and out are not yet supported.");
System.exit(1);
}
ConsumerOptions options = new ConsumerOptions();
List<PGPPublicKeyRing> verifyWith = null;
try {
List<PGPSecretKeyRing> secretKeyRings = loadKeysFromFiles(keys);
for (PGPSecretKeyRing secretKey : secretKeyRings) {
options.addDecryptionKey(secretKey);
}
if (certs != null) {
verifyWith = SopKeyUtil.loadCertificatesFromFile(certs);
for (PGPPublicKeyRing cert : verifyWith) {
options.addVerificationCert(cert);
}
}
} catch (IOException | PGPException e) {
err_ln(e.getMessage());
System.exit(1);
return;
}
DecryptionStream decryptionStream;
try {
decryptionStream = PGPainless.decryptAndOrVerify()
.onInputStream(System.in)
.withOptions(options);
} catch (IOException | PGPException e) {
err_ln("Error constructing decryption stream: " + e.getMessage());
System.exit(1);
return;
}
try {
Streams.pipeAll(decryptionStream, System.out);
System.out.flush();
decryptionStream.close();
} catch (IOException e) {
err_ln("Unable to decrypt: " + e.getMessage());
System.exit(29);
}
if (verifyOut == null) {
return;
}
OpenPgpMetadata metadata = decryptionStream.getResult();
StringBuilder sb = new StringBuilder();
if (verifyWith != null) {
for (SubkeyIdentifier signingKey : metadata.getVerifiedSignatures().keySet()) {
PGPSignature signature = metadata.getVerifiedSignatures().get(signingKey);
sb.append(df.format(signature.getCreationTime())).append(' ')
.append(signingKey.getSubkeyFingerprint()).append(' ')
.append(signingKey.getPrimaryKeyFingerprint()).append('\n');
}
try {
verifyOut.createNewFile();
PrintStream verifyPrinter = new PrintStream(new FileOutputStream(verifyOut));
// CHECKSTYLE:OFF
verifyPrinter.println(sb);
// CHECKSTYLE:ON
verifyPrinter.close();
} catch (IOException e) {
err_ln("Error writing verifications file: " + e);
}
}
}
}

View file

@ -1,135 +0,0 @@
/*
* Copyright 2020 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.commands;
import static org.pgpainless.sop.Print.err_ln;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.List;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.bouncycastle.util.io.Streams;
import org.pgpainless.PGPainless;
import org.pgpainless.algorithm.DocumentSignatureType;
import org.pgpainless.encryption_signing.EncryptionOptions;
import org.pgpainless.encryption_signing.EncryptionStream;
import org.pgpainless.encryption_signing.ProducerOptions;
import org.pgpainless.encryption_signing.SigningOptions;
import org.pgpainless.key.protection.SecretKeyRingProtector;
import org.pgpainless.sop.SopKeyUtil;
import org.pgpainless.util.Passphrase;
import picocli.CommandLine;
@CommandLine.Command(name = "encrypt",
description = "Encrypt a message from standard input",
exitCodeOnInvalidInput = 37)
public class Encrypt implements Runnable {
public enum Type {
binary,
text,
mime
}
@CommandLine.Option(names = "--no-armor",
description = "ASCII armor the output",
negatable = true)
boolean armor = true;
@CommandLine.Option(names = {"--as"},
description = "Type of the input data. Defaults to 'binary'",
paramLabel = "{binary|text|mime}")
Type type;
@CommandLine.Option(names = "--with-password",
description = "Encrypt the message with a password",
paramLabel = "PASSWORD")
String[] withPassword = new String[0];
@CommandLine.Option(names = "--sign-with",
description = "Sign the output with a private key",
paramLabel = "KEY")
File[] signWith = new File[0];
@CommandLine.Parameters(description = "Certificates the message gets encrypted to",
index = "0..*",
paramLabel = "CERTS")
File[] certs = new File[0];
@Override
public void run() {
if (certs.length == 0 && withPassword.length == 0) {
err_ln("Please either provide --with-password or at least one CERT");
System.exit(19);
}
EncryptionOptions encOpt = new EncryptionOptions();
SigningOptions signOpt = new SigningOptions();
try {
List<PGPPublicKeyRing> encryptionKeys = SopKeyUtil.loadCertificatesFromFile(certs);
for (PGPPublicKeyRing key : encryptionKeys) {
encOpt.addRecipient(key);
}
} catch (IOException e) {
err_ln(e.getMessage());
System.exit(1);
return;
}
for (String s : withPassword) {
Passphrase passphrase = Passphrase.fromPassword(s);
encOpt.addPassphrase(passphrase);
}
SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys();
for (int i = 0; i < signWith.length; i++) {
try (FileInputStream fileIn = new FileInputStream(signWith[i])) {
PGPSecretKeyRing secretKey = PGPainless.readKeyRing().secretKeyRing(fileIn);
signOpt.addInlineSignature(protector, secretKey, parseType(type));
} catch (IOException | PGPException e) {
err_ln("Cannot read secret key from file " + signWith[i].getName());
err_ln(e.getMessage());
System.exit(1);
}
}
try {
EncryptionStream encryptionStream = PGPainless.encryptAndOrSign()
.onOutputStream(System.out)
.withOptions(ProducerOptions
.signAndEncrypt(encOpt, signOpt)
.setAsciiArmor(armor));
Streams.pipeAll(System.in, encryptionStream);
encryptionStream.close();
} catch (IOException | PGPException e) {
err_ln("An error happened.");
err_ln(e.getMessage());
System.exit(1);
}
}
private static DocumentSignatureType parseType(Type type) {
return type == Type.binary ? DocumentSignatureType.BINARY_DOCUMENT : DocumentSignatureType.CANONICAL_TEXT_DOCUMENT;
}
}

View file

@ -1,54 +0,0 @@
/*
* Copyright 2020 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.commands;
import static org.pgpainless.sop.Print.err_ln;
import static org.pgpainless.sop.Print.print_ln;
import java.io.IOException;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.pgpainless.PGPainless;
import org.pgpainless.key.util.KeyRingUtils;
import org.pgpainless.sop.Print;
import picocli.CommandLine;
@CommandLine.Command(name = "extract-cert",
description = "Extract a public key certificate from a secret key from standard input",
exitCodeOnInvalidInput = 37)
public class ExtractCert implements Runnable {
@CommandLine.Option(names = "--no-armor",
description = "ASCII armor the output",
negatable = true)
boolean armor = true;
@Override
public void run() {
try {
PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(System.in);
PGPPublicKeyRing publicKeys = KeyRingUtils.publicKeyRingFrom(secretKeys);
print_ln(Print.toString(publicKeys, armor));
} catch (IOException | PGPException e) {
err_ln("Error extracting certificate from keys;");
err_ln(e.getMessage());
System.exit(1);
}
}
}

View file

@ -1,95 +0,0 @@
/*
* Copyright 2020 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.commands;
import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.NoSuchAlgorithmException;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.TimeZone;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.pgpainless.PGPainless;
import org.pgpainless.algorithm.KeyFlag;
import org.pgpainless.key.generation.KeyRingBuilderInterface;
import org.pgpainless.key.generation.KeySpec;
import org.pgpainless.key.generation.type.KeyType;
import org.pgpainless.key.generation.type.eddsa.EdDSACurve;
import org.pgpainless.key.generation.type.xdh.XDHSpec;
import org.pgpainless.sop.Print;
import picocli.CommandLine;
import static org.pgpainless.sop.Print.err_ln;
import static org.pgpainless.sop.Print.print_ln;
@CommandLine.Command(name = "generate-key",
description = "Generate a secret key",
exitCodeOnInvalidInput = 37)
public class GenerateKey implements Runnable {
@CommandLine.Option(names = "--no-armor",
description = "ASCII armor the output",
negatable = true)
boolean armor = true;
@CommandLine.Parameters(description = "User-ID, eg. \"Alice <alice@example.com>\"")
List<String> userId;
@Override
public void run() {
if (userId.isEmpty()) {
print_ln("At least one user-id expected.");
System.exit(1);
return;
}
try {
KeyRingBuilderInterface.WithAdditionalUserIdOrPassphrase builder = PGPainless.generateKeyRing()
.withSubKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519))
.withKeyFlags(KeyFlag.SIGN_DATA)
.withDefaultAlgorithms())
.withSubKey(KeySpec.getBuilder(KeyType.XDH(XDHSpec._X25519))
.withKeyFlags(KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)
.withDefaultAlgorithms())
.withPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519))
.withKeyFlags(KeyFlag.CERTIFY_OTHER)
.withDefaultAlgorithms())
.withPrimaryUserId(userId.get(0));
for (int i = 1; i < userId.size(); i++) {
builder.withAdditionalUserId(userId.get(i));
}
Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
calendar.add(Calendar.YEAR, 3);
Date expiration = calendar.getTime();
PGPSecretKeyRing secretKeys = builder.setExpirationDate(expiration)
.withoutPassphrase()
.build();
print_ln(Print.toString(secretKeys, armor));
} catch (InvalidAlgorithmParameterException | NoSuchAlgorithmException | PGPException | IOException e) {
err_ln("Error creating OpenPGP key:");
err_ln(e.getMessage());
System.exit(1);
}
}
}

View file

@ -1,109 +0,0 @@
/*
* Copyright 2020 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.commands;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.bouncycastle.openpgp.PGPSignature;
import org.bouncycastle.util.io.Streams;
import org.pgpainless.PGPainless;
import org.pgpainless.algorithm.DocumentSignatureType;
import org.pgpainless.encryption_signing.EncryptionResult;
import org.pgpainless.encryption_signing.EncryptionStream;
import org.pgpainless.encryption_signing.ProducerOptions;
import org.pgpainless.encryption_signing.SigningOptions;
import org.pgpainless.key.SubkeyIdentifier;
import org.pgpainless.key.protection.SecretKeyRingProtector;
import org.pgpainless.sop.Print;
import picocli.CommandLine;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import static org.pgpainless.sop.Print.err_ln;
import static org.pgpainless.sop.Print.print_ln;
@CommandLine.Command(name = "sign",
description = "Create a detached signature on the data from standard input",
exitCodeOnInvalidInput = 37)
public class Sign implements Runnable {
public enum Type {
binary,
text
}
@CommandLine.Option(names = "--no-armor",
description = "ASCII armor the output",
negatable = true)
boolean armor = true;
@CommandLine.Option(names = "--as", description = "Defaults to 'binary'. If '--as=text' and the input data is not valid UTF-8, sign fails with return code 53.",
paramLabel = "{binary|text}")
Type type;
@CommandLine.Parameters(description = "Secret keys used for signing",
paramLabel = "KEY",
arity = "1..*")
File[] secretKeyFile;
@Override
public void run() {
PGPSecretKeyRing[] secretKeys = new PGPSecretKeyRing[secretKeyFile.length];
for (int i = 0, secretKeyFileLength = secretKeyFile.length; i < secretKeyFileLength; i++) {
File file = secretKeyFile[i];
try {
PGPSecretKeyRing secretKey = PGPainless.readKeyRing().secretKeyRing(new FileInputStream(file));
secretKeys[i] = secretKey;
} catch (IOException | PGPException e) {
err_ln("Error reading secret key ring " + file.getName());
err_ln(e.getMessage());
System.exit(1);
return;
}
}
try {
SigningOptions signOpt = new SigningOptions();
for (PGPSecretKeyRing signingKey : secretKeys) {
signOpt.addDetachedSignature(SecretKeyRingProtector.unprotectedKeys(), signingKey,
type == Type.text ? DocumentSignatureType.CANONICAL_TEXT_DOCUMENT : DocumentSignatureType.BINARY_DOCUMENT);
}
ByteArrayOutputStream out = new ByteArrayOutputStream();
EncryptionStream encryptionStream = PGPainless.encryptAndOrSign()
.onOutputStream(out)
.withOptions(ProducerOptions
.sign(signOpt)
.setAsciiArmor(armor));
Streams.pipeAll(System.in, encryptionStream);
encryptionStream.close();
EncryptionResult result = encryptionStream.getResult();
for (SubkeyIdentifier signingKey : result.getDetachedSignatures().keySet()) {
for (PGPSignature signature : result.getDetachedSignatures().get(signingKey)) {
print_ln(Print.toString(signature.getEncoded(), armor));
}
}
} catch (PGPException | IOException e) {
err_ln("Error signing data.");
err_ln(e.getMessage());
System.exit(1);
}
}
}

View file

@ -1,205 +0,0 @@
/*
* Copyright 2020 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.commands;
import static org.pgpainless.sop.Print.err_ln;
import static org.pgpainless.sop.Print.print_ln;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.TimeZone;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
import org.bouncycastle.openpgp.PGPSignature;
import org.bouncycastle.util.io.Streams;
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.key.SubkeyIdentifier;
import picocli.CommandLine;
@CommandLine.Command(name = "verify",
description = "Verify a detached signature over the data from standard input",
exitCodeOnInvalidInput = 37)
public class Verify implements Runnable {
private static final TimeZone tz = TimeZone.getTimeZone("UTC");
private static final DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'");
private static final Date beginningOfTime = new Date(0);
private static final Date endOfTime = new Date(8640000000000000L);
static {
df.setTimeZone(tz);
}
@CommandLine.Parameters(index = "0",
description = "Detached signature",
paramLabel = "SIGNATURE")
File signature;
@CommandLine.Parameters(index = "1..*",
arity = "1..*",
description = "Public key certificates",
paramLabel = "CERT")
File[] certificates;
@CommandLine.Option(names = {"--not-before"},
description = "ISO-8601 formatted UTC date (eg. '2020-11-23T16:35Z)\n" +
"Reject signatures with a creation date not in range.\n" +
"Defaults to beginning of time (\"-\").",
paramLabel = "DATE")
String notBefore = "-";
@CommandLine.Option(names = {"--not-after"},
description = "ISO-8601 formatted UTC date (eg. '2020-11-23T16:35Z)\n" +
"Reject signatures with a creation date not in range.\n" +
"Defaults to current system time (\"now\").\n" +
"Accepts special value \"-\" for end of time.",
paramLabel = "DATE")
String notAfter = "now";
@Override
public void run() {
Date notBeforeDate = parseNotBefore();
Date notAfterDate = parseNotAfter();
ConsumerOptions options = new ConsumerOptions();
try (FileInputStream sigIn = new FileInputStream(signature)) {
options.addVerificationOfDetachedSignatures(sigIn);
} catch (IOException | PGPException e) {
err_ln("Cannot read detached signature: " + e.getMessage());
System.exit(1);
}
Map<PGPPublicKeyRing, File> publicKeys = readCertificatesFromFiles();
if (publicKeys.isEmpty()) {
err_ln("No certificates supplied.");
System.exit(19);
}
for (PGPPublicKeyRing cert : publicKeys.keySet()) {
options.addVerificationCert(cert);
}
OpenPgpMetadata metadata;
try {
DecryptionStream verifier = PGPainless.decryptAndOrVerify()
.onInputStream(System.in)
.withOptions(options);
OutputStream out = new NullOutputStream();
Streams.pipeAll(verifier, out);
verifier.close();
metadata = verifier.getResult();
} catch (IOException | PGPException e) {
err_ln("Signature validation failed.");
err_ln(e.getMessage());
System.exit(1);
return;
}
Map<SubkeyIdentifier, PGPSignature> signaturesInTimeRange = new HashMap<>();
for (SubkeyIdentifier signingKey : metadata.getVerifiedSignatures().keySet()) {
PGPSignature signature = metadata.getVerifiedSignatures().get(signingKey);
Date creationTime = signature.getCreationTime();
if (!creationTime.before(notBeforeDate) && !creationTime.after(notAfterDate)) {
signaturesInTimeRange.put(signingKey, signature);
}
}
if (signaturesInTimeRange.isEmpty()) {
err_ln("No valid signatures found.");
System.exit(3);
}
printValidSignatures(signaturesInTimeRange, publicKeys);
}
private void printValidSignatures(Map<SubkeyIdentifier, PGPSignature> validSignatures, Map<PGPPublicKeyRing, File> publicKeys) {
for (SubkeyIdentifier signingKey : validSignatures.keySet()) {
PGPSignature signature = validSignatures.get(signingKey);
for (PGPPublicKeyRing ring : publicKeys.keySet()) {
// Search signing key ring
File file = publicKeys.get(ring);
if (ring.getPublicKey(signingKey.getKeyId()) == null) {
continue;
}
String utcSigDate = df.format(signature.getCreationTime());
print_ln(utcSigDate + " " + signingKey.getSubkeyFingerprint() + " " + signingKey.getPrimaryKeyFingerprint() +
" signed by " + file.getName());
}
}
}
private Map<PGPPublicKeyRing, File> readCertificatesFromFiles() {
Map<PGPPublicKeyRing, File> publicKeys = new HashMap<>();
for (File cert : certificates) {
try (FileInputStream in = new FileInputStream(cert)) {
PGPPublicKeyRingCollection collection = PGPainless.readKeyRing().publicKeyRingCollection(in);
for (PGPPublicKeyRing ring : collection) {
publicKeys.put(ring, cert);
}
} catch (IOException | PGPException e) {
err_ln("Cannot read certificate from file " + cert.getAbsolutePath() + ":");
err_ln(e.getMessage());
}
}
return publicKeys;
}
private Date parseNotAfter() {
try {
return notAfter.equals("now") ? new Date() : notAfter.equals("-") ? endOfTime : df.parse(notAfter);
} catch (ParseException e) {
err_ln("Invalid date string supplied as value of --not-after.");
System.exit(1);
return null;
}
}
private Date parseNotBefore() {
try {
return notBefore.equals("now") ? new Date() : notBefore.equals("-") ? beginningOfTime : df.parse(notBefore);
} catch (ParseException e) {
err_ln("Invalid date string supplied as value of --not-before.");
System.exit(1);
return null;
}
}
private static class NullOutputStream extends OutputStream {
@Override
public void write(int b) throws IOException {
// Nope
}
}
}

View file

@ -1,19 +0,0 @@
/*
* Copyright 2020 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.
*/
/**
* Subcommands of the PGPainless SOP.
*/
package org.pgpainless.sop.commands;

View file

@ -1,5 +1,5 @@
/*
* Copyright 2020 Paul Schaub.
* 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.
@ -14,9 +14,6 @@
* limitations under the License.
*/
/**
* PGPainless SOP implementing a Stateless OpenPGP Command Line Interface.
* @see <a href="https://tools.ietf.org/html/draft-dkg-openpgp-stateless-cli-01">
* Stateless OpenPGP Command Line Interface
* draft-dkg-openpgp-stateless-cli-01</a>
* Implementation of the java-sop package using pgpainless-core.
*/
package org.pgpainless.sop;