1
0
Fork 0
mirror of https://github.com/pgpainless/pgpainless.git synced 2025-09-10 18:59:39 +02: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;

View file

@ -1,39 +0,0 @@
/*
* 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 static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
import picocli.CommandLine;
public class ExitCodeTest {
@Test
public void testUnknownCommand_69() {
assertEquals(69, new CommandLine(new PGPainlessCLI()).execute("generate-kex"));
}
@Test
public void testCommandWithUnknownOption_37() {
assertEquals(37, new CommandLine(new PGPainlessCLI()).execute("generate-key", "-k", "\"k is unknown\""));
}
@Test
public void successfulVersion_0 () {
assertEquals(0, new CommandLine(new PGPainlessCLI()).execute("version"));
}
}

View file

@ -1,47 +0,0 @@
/*
* 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.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.Random;
public class TestUtils {
public static final String ALPHABET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
private static final Random RANDOM = new Random();
public static final String ARMOR_PRIVATE_KEY_HEADER = "-----BEGIN PGP PRIVATE KEY BLOCK-----";
public static final byte[] ARMOR_PRIVATE_KEY_HEADER_BYTES =
ARMOR_PRIVATE_KEY_HEADER.getBytes(StandardCharsets.UTF_8);
public static File createTempDirectory() throws IOException {
String name = randomString(10);
File dir = Files.createTempDirectory(name).toFile();
// dir.deleteOnExit();
return dir;
}
private static String randomString(int length) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < length; i++) {
sb.append(ALPHABET.charAt(RANDOM.nextInt(ALPHABET.length())));
}
return sb.toString();
}
}

View file

@ -15,13 +15,14 @@
*/
package org.pgpainless.sop;
import org.junit.jupiter.api.Test;
import picocli.CommandLine;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class DummyTest {
import org.junit.jupiter.api.Test;
public class VersionTest {
@Test
public void dummyTest() {
new CommandLine(new PGPainlessCLI()).execute("generate-key", "Ed Snowden <citizen4@lavabit.com>");
public void assertNameEqualsPGPainless() {
assertEquals("PGPainless-SOP", new SOPImpl().version().getName());
}
}

View file

@ -1,138 +0,0 @@
/*
* 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.commands;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.NoSuchAlgorithmException;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.pgpainless.PGPainless;
import org.pgpainless.sop.PGPainlessCLI;
import picocli.CommandLine;
public class ArmorTest {
private static PrintStream originalSout;
@BeforeEach
public void saveSout() {
originalSout = System.out;
}
@AfterEach
public void restoreSout() {
System.setOut(originalSout);
}
@Test
public void armorSecretKey() throws IOException, PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException {
PGPSecretKeyRing secretKey = PGPainless.generateKeyRing()
.modernKeyRing("alice@pgpainless.org", null);
byte[] bytes = secretKey.getEncoded();
System.setIn(new ByteArrayInputStream(bytes));
ByteArrayOutputStream armorOut = new ByteArrayOutputStream();
System.setOut(new PrintStream(armorOut));
new CommandLine(new PGPainlessCLI()).execute("armor");
PGPSecretKeyRing armored = PGPainless.readKeyRing().secretKeyRing(armorOut.toString());
assertArrayEquals(secretKey.getEncoded(), armored.getEncoded());
}
@Test
public void armorPublicKey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException {
PGPSecretKeyRing secretKey = PGPainless.generateKeyRing()
.modernKeyRing("alice@pgpainless.org", null);
PGPPublicKeyRing publicKey = PGPainless.extractCertificate(secretKey);
byte[] bytes = publicKey.getEncoded();
System.setIn(new ByteArrayInputStream(bytes));
ByteArrayOutputStream armorOut = new ByteArrayOutputStream();
System.setOut(new PrintStream(armorOut));
new CommandLine(new PGPainlessCLI()).execute("armor");
PGPPublicKeyRing armored = PGPainless.readKeyRing().publicKeyRing(armorOut.toString());
assertArrayEquals(publicKey.getEncoded(), armored.getEncoded());
}
@Test
public void armorMessage() {
String message = "Hello, World!\n";
System.setIn(new ByteArrayInputStream(message.getBytes(StandardCharsets.UTF_8)));
ByteArrayOutputStream armorOut = new ByteArrayOutputStream();
System.setOut(new PrintStream(armorOut));
new CommandLine(new PGPainlessCLI()).execute("armor");
String armored = armorOut.toString();
assertTrue(armored.startsWith("-----BEGIN PGP MESSAGE-----\n"));
assertTrue(armored.contains("SGVsbG8sIFdvcmxkIQo="));
}
@Test
public void doesNotNestArmorByDefault() {
String armored = "-----BEGIN PGP MESSAGE-----\n" +
"Version: BCPG v1.69\n" +
"\n" +
"SGVsbG8sIFdvcmxkCg==\n" +
"=fkLo\n" +
"-----END PGP MESSAGE-----";
System.setIn(new ByteArrayInputStream(armored.getBytes(StandardCharsets.UTF_8)));
ByteArrayOutputStream out = new ByteArrayOutputStream();
System.setOut(new PrintStream(out));
new CommandLine(new PGPainlessCLI()).execute("armor");
assertEquals(armored, out.toString());
}
@Test
public void testAllowNested() {
String armored = "-----BEGIN PGP MESSAGE-----\n" +
"Version: BCPG v1.69\n" +
"\n" +
"SGVsbG8sIFdvcmxkCg==\n" +
"=fkLo\n" +
"-----END PGP MESSAGE-----";
System.setIn(new ByteArrayInputStream(armored.getBytes(StandardCharsets.UTF_8)));
ByteArrayOutputStream out = new ByteArrayOutputStream();
System.setOut(new PrintStream(out));
new CommandLine(new PGPainlessCLI()).execute("armor", "--allow-nested");
assertNotEquals(armored, out.toString());
assertTrue(out.toString().contains(
"LS0tLS1CRUdJTiBQR1AgTUVTU0FHRS0tLS0tClZlcnNpb246IEJDUEcgdjEuNjkK\n" +
"ClNHVnNiRzhzSUZkdmNteGtDZz09Cj1ma0xvCi0tLS0tRU5EIFBHUCBNRVNTQUdF\n" +
"LS0tLS0="));
}
}

View file

@ -1,99 +0,0 @@
/*
* 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.commands;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.NoSuchAlgorithmException;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.pgpainless.PGPainless;
import org.pgpainless.sop.PGPainlessCLI;
import picocli.CommandLine;
public class DearmorTest {
private PrintStream originalSout;
@BeforeEach
public void saveSout() {
this.originalSout = System.out;
}
@AfterEach
public void restoreSout() {
System.setOut(originalSout);
}
@Test
public void dearmorSecretKey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException {
PGPSecretKeyRing secretKey = PGPainless.generateKeyRing()
.modernKeyRing("alice@pgpainless.org", null);
String armored = PGPainless.asciiArmor(secretKey);
System.setIn(new ByteArrayInputStream(armored.getBytes(StandardCharsets.UTF_8)));
ByteArrayOutputStream out = new ByteArrayOutputStream();
System.setOut(new PrintStream(out));
new CommandLine(new PGPainlessCLI()).execute("dearmor");
assertArrayEquals(secretKey.getEncoded(), out.toByteArray());
}
@Test
public void dearmorCertificate() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException {
PGPSecretKeyRing secretKey = PGPainless.generateKeyRing()
.modernKeyRing("alice@pgpainless.org", null);
PGPPublicKeyRing certificate = PGPainless.extractCertificate(secretKey);
String armored = PGPainless.asciiArmor(certificate);
System.setIn(new ByteArrayInputStream(armored.getBytes(StandardCharsets.UTF_8)));
ByteArrayOutputStream out = new ByteArrayOutputStream();
System.setOut(new PrintStream(out));
new CommandLine(new PGPainlessCLI()).execute("dearmor");
assertArrayEquals(certificate.getEncoded(), out.toByteArray());
}
@Test
public void dearmorMessage() {
String armored = "-----BEGIN PGP MESSAGE-----\n" +
"Version: BCPG v1.69\n" +
"\n" +
"SGVsbG8sIFdvcmxkCg==\n" +
"=fkLo\n" +
"-----END PGP MESSAGE-----";
System.setIn(new ByteArrayInputStream(armored.getBytes(StandardCharsets.UTF_8)));
ByteArrayOutputStream out = new ByteArrayOutputStream();
System.setOut(new PrintStream(out));
new CommandLine(new PGPainlessCLI()).execute("dearmor");
assertEquals("Hello, World\n", out.toString());
}
}

View file

@ -1,126 +0,0 @@
/*
* 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.commands;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.pgpainless.sop.PGPainlessCLI;
import org.pgpainless.sop.TestUtils;
import picocli.CommandLine;
public class EncryptDecryptTest {
private static File tempDir;
private static PrintStream originalSout;
@BeforeAll
public static void prepare() throws IOException {
tempDir = TestUtils.createTempDirectory();
}
@Test
public void test() throws IOException {
originalSout = System.out;
File julietKeyFile = new File(tempDir, "juliet.key");
assertTrue(julietKeyFile.createNewFile());
File julietCertFile = new File(tempDir, "juliet.asc");
assertTrue(julietCertFile.createNewFile());
File romeoKeyFile = new File(tempDir, "romeo.key");
assertTrue(romeoKeyFile.createNewFile());
File romeoCertFile = new File(tempDir, "romeo.asc");
assertTrue(romeoCertFile.createNewFile());
File msgAscFile = new File(tempDir, "msg.asc");
assertTrue(msgAscFile.createNewFile());
OutputStream julietKeyOut = new FileOutputStream(julietKeyFile);
System.setOut(new PrintStream(julietKeyOut));
new CommandLine(new PGPainlessCLI()).execute("generate-key", "Juliet Capulet <juliet@capulet.lit>");
julietKeyOut.close();
FileInputStream julietKeyIn = new FileInputStream(julietKeyFile);
System.setIn(julietKeyIn);
OutputStream julietCertOut = new FileOutputStream(julietCertFile);
System.setOut(new PrintStream(julietCertOut));
new CommandLine(new PGPainlessCLI()).execute("extract-cert");
julietKeyIn.close();
julietCertOut.close();
OutputStream romeoKeyOut = new FileOutputStream(romeoKeyFile);
System.setOut(new PrintStream(romeoKeyOut));
new CommandLine(new PGPainlessCLI()).execute("generate-key", "Romeo Montague <romeo@montague.lit>");
romeoKeyOut.close();
FileInputStream romeoKeyIn = new FileInputStream(romeoKeyFile);
System.setIn(romeoKeyIn);
OutputStream romeoCertOut = new FileOutputStream(romeoCertFile);
System.setOut(new PrintStream(romeoCertOut));
new CommandLine(new PGPainlessCLI()).execute("extract-cert");
romeoKeyIn.close();
romeoCertOut.close();
String msg = "Hello World!\n";
ByteArrayInputStream msgIn = new ByteArrayInputStream(msg.getBytes(StandardCharsets.UTF_8));
System.setIn(msgIn);
OutputStream msgAscOut = new FileOutputStream(msgAscFile);
System.setOut(new PrintStream(msgAscOut));
new CommandLine(new PGPainlessCLI()).execute("encrypt",
"--sign-with", romeoKeyFile.getAbsolutePath(),
julietCertFile.getAbsolutePath());
msgAscOut.close();
File verifyFile = new File(tempDir, "verify.txt");
assertTrue(verifyFile.createNewFile());
FileInputStream msgAscIn = new FileInputStream(msgAscFile);
System.setIn(msgAscIn);
ByteArrayOutputStream out = new ByteArrayOutputStream();
PrintStream pOut = new PrintStream(out);
System.setOut(pOut);
new CommandLine(new PGPainlessCLI()).execute("decrypt",
"--verify-out", verifyFile.getAbsolutePath(),
"--verify-with", romeoCertFile.getAbsolutePath(),
julietKeyFile.getAbsolutePath());
msgAscIn.close();
assertEquals(msg, out.toString());
}
@AfterAll
public static void after() {
System.setOut(originalSout);
// CHECKSTYLE:OFF
System.out.println(tempDir.getAbsolutePath());
// CHECKSTYLE:ON
}
}

View file

@ -1,55 +0,0 @@
/*
* 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.commands;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.security.InvalidAlgorithmParameterException;
import java.security.NoSuchAlgorithmException;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.junit.jupiter.api.Test;
import org.pgpainless.PGPainless;
import org.pgpainless.key.info.KeyRingInfo;
import org.pgpainless.sop.PGPainlessCLI;
import picocli.CommandLine;
public class ExtractCertTest {
@Test
public void testExtractCert() throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException, IOException {
PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing()
.simpleEcKeyRing("Juliet Capulet <juliet@capulet.lit>");
ByteArrayInputStream inputStream = new ByteArrayInputStream(secretKeys.getEncoded());
System.setIn(inputStream);
ByteArrayOutputStream out = new ByteArrayOutputStream();
System.setOut(new PrintStream(out));
new CommandLine(new PGPainlessCLI()).execute("extract-cert");
PGPPublicKeyRing publicKeys = PGPainless.readKeyRing().publicKeyRing(out.toByteArray());
KeyRingInfo info = PGPainless.inspectKeyRing(publicKeys);
assertFalse(info.isSecretKey());
assertTrue(info.isUserIdValid("Juliet Capulet <juliet@capulet.lit>"));
}
}

View file

@ -1,74 +0,0 @@
/*
* 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.commands;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.pgpainless.sop.TestUtils.ARMOR_PRIVATE_KEY_HEADER_BYTES;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.util.Arrays;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.pgpainless.PGPainless;
import org.pgpainless.key.info.KeyRingInfo;
import org.pgpainless.sop.PGPainlessCLI;
import org.pgpainless.sop.TestUtils;
import picocli.CommandLine;
public class GenerateCertTest {
private static File tempDir;
@BeforeAll
public static void setup() throws IOException {
tempDir = TestUtils.createTempDirectory();
}
@Test
public void testKeyGeneration() throws IOException, PGPException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
System.setOut(new PrintStream(out));
new CommandLine(new PGPainlessCLI()).execute("generate-key", "--armor", "Juliet Capulet <juliet@capulet.lit>");
PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(out.toByteArray());
KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys);
assertTrue(info.isUserIdValid("Juliet Capulet <juliet@capulet.lit>"));
byte[] outBegin = new byte[37];
System.arraycopy(out.toByteArray(), 0, outBegin, 0, 37);
assertArrayEquals(outBegin, ARMOR_PRIVATE_KEY_HEADER_BYTES);
}
@Test
public void testNoArmor() {
ByteArrayOutputStream out = new ByteArrayOutputStream();
System.setOut(new PrintStream(out));
new CommandLine(new PGPainlessCLI()).execute("generate-key", "--no-armor", "Test <test@test.test>");
byte[] outBegin = new byte[37];
System.arraycopy(out.toByteArray(), 0, outBegin, 0, 37);
assertFalse(Arrays.equals(outBegin, ARMOR_PRIVATE_KEY_HEADER_BYTES));
}
}

View file

@ -1,129 +0,0 @@
/*
* 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.commands;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.NoSuchAlgorithmException;
import java.util.Date;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.bouncycastle.util.io.Streams;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.pgpainless.PGPainless;
import org.pgpainless.key.OpenPgpV4Fingerprint;
import org.pgpainless.key.info.KeyRingInfo;
import org.pgpainless.key.util.KeyRingUtils;
import org.pgpainless.sop.PGPainlessCLI;
import org.pgpainless.sop.TestUtils;
import picocli.CommandLine;
public class SignVerifyTest {
private static File tempDir;
private static PrintStream originalSout;
private final String data = "If privacy is outlawed, only outlaws will have privacy.\n";
@BeforeAll
public static void prepare() throws IOException {
tempDir = TestUtils.createTempDirectory();
}
@Test
public void testSignatureCreationAndVerification() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException {
originalSout = System.out;
InputStream originalIn = System.in;
// Write alice key to disc
File aliceKeyFile = new File(tempDir, "alice.key");
assertTrue(aliceKeyFile.createNewFile());
PGPSecretKeyRing aliceKeys = PGPainless.generateKeyRing()
.modernKeyRing("alice", null);
OutputStream aliceKeyOut = new FileOutputStream(aliceKeyFile);
Streams.pipeAll(new ByteArrayInputStream(aliceKeys.getEncoded()), aliceKeyOut);
aliceKeyOut.close();
// Write alice pub key to disc
File aliceCertFile = new File(tempDir, "alice.pub");
assertTrue(aliceCertFile.createNewFile());
PGPPublicKeyRing alicePub = KeyRingUtils.publicKeyRingFrom(aliceKeys);
OutputStream aliceCertOut = new FileOutputStream(aliceCertFile);
Streams.pipeAll(new ByteArrayInputStream(alicePub.getEncoded()), aliceCertOut);
aliceCertOut.close();
// Write test data to disc
File dataFile = new File(tempDir, "data");
assertTrue(dataFile.createNewFile());
FileOutputStream dataOut = new FileOutputStream(dataFile);
Streams.pipeAll(new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8)), dataOut);
dataOut.close();
// Sign test data
FileInputStream dataIn = new FileInputStream(dataFile);
System.setIn(dataIn);
File sigFile = new File(tempDir, "sig.asc");
assertTrue(sigFile.createNewFile());
FileOutputStream sigOut = new FileOutputStream(sigFile);
System.setOut(new PrintStream(sigOut));
new CommandLine(new PGPainlessCLI()).execute("sign", "--armor", aliceKeyFile.getAbsolutePath());
sigOut.close();
// verify test data signature
ByteArrayOutputStream verifyOut = new ByteArrayOutputStream();
System.setOut(new PrintStream(verifyOut));
dataIn = new FileInputStream(dataFile);
System.setIn(dataIn);
new CommandLine(new PGPainlessCLI()).execute("verify", sigFile.getAbsolutePath(), aliceCertFile.getAbsolutePath());
dataIn.close();
// Test verification output
// [date] [signing-key-fp] [primary-key-fp] signed by [key.pub]
String verification = verifyOut.toString();
String[] split = verification.split(" ");
OpenPgpV4Fingerprint primaryKeyFingerprint = new OpenPgpV4Fingerprint(aliceKeys);
OpenPgpV4Fingerprint signingKeyFingerprint = new OpenPgpV4Fingerprint(new KeyRingInfo(alicePub, new Date()).getSigningSubkeys().get(0));
assertEquals(signingKeyFingerprint.toString(), split[1]);
assertEquals(primaryKeyFingerprint.toString(), split[2]);
System.setIn(originalIn);
}
@AfterAll
public static void after() {
System.setOut(originalSout);
// CHECKSTYLE:OFF
System.out.println(tempDir.getAbsolutePath());
// CHECKSTYLE:ON
}
}