1
0
Fork 0
mirror of https://github.com/pgpainless/pgpainless.git synced 2025-09-09 18:29: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,37 @@
plugins {
id 'application'
}
dependencies {
implementation(project(":sop-java"))
implementation 'info.picocli:picocli:4.6.1'
implementation 'com.google.inject:guice:5.0.1'
testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion"
// https://todd.ginsberg.com/post/testing-system-exit/
testImplementation 'com.ginsberg:junit5-system-exit:1.1.1'
testImplementation "org.mockito:mockito-core:3.11.2"
// https://mvnrepository.com/artifact/com.google.code.findbugs/jsr305
implementation group: 'com.google.code.findbugs', name: 'jsr305', version: '3.0.2'
}
mainClassName = 'sop.cli.picocli.SopCLI'
jar {
manifest {
attributes 'Main-Class': "$mainClassName"
}
from {
configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) }
} {
exclude "META-INF/*.SF"
exclude "META-INF/*.DSA"
exclude "META-INF/*.RSA"
}
}

View file

@ -0,0 +1,44 @@
/*
* Copyright 2021 Paul Schaub.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sop.cli.picocli;
import java.util.Date;
import sop.util.UTCUtil;
public class DateParser {
public static final Date BEGINNING_OF_TIME = new Date(0);
public static final Date END_OF_TIME = new Date(8640000000000000L);
public static Date parseNotAfter(String notAfter) {
Date date = notAfter.equals("now") ? new Date() : notAfter.equals("-") ? END_OF_TIME : UTCUtil.parseUTCDate(notAfter);
if (date == null) {
Print.errln("Invalid date string supplied as value of --not-after.");
System.exit(1);
}
return date;
}
public static Date parseNotBefore(String notBefore) {
Date date = notBefore.equals("now") ? new Date() : notBefore.equals("-") ? BEGINNING_OF_TIME : UTCUtil.parseUTCDate(notBefore);
if (date == null) {
Print.errln("Invalid date string supplied as value of --not-before.");
System.exit(1);
}
return date;
}
}

View file

@ -0,0 +1,37 @@
/*
* Copyright 2021 Paul Schaub.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sop.cli.picocli;
public class Print {
public static void errln(String string) {
// CHECKSTYLE:OFF
System.err.println(string);
// CHECKSTYLE:ON
}
public static void trace(Throwable e) {
// CHECKSTYLE:OFF
e.printStackTrace();
// CHECKSTYLE:ON
}
public static void outln(String string) {
// CHECKSTYLE:OFF
System.out.println(string);
// CHECKSTYLE:ON
}
}

View file

@ -0,0 +1,71 @@
/*
* Copyright 2021 Paul Schaub.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sop.cli.picocli;
import picocli.CommandLine;
import sop.SOP;
import sop.cli.picocli.commands.ArmorCmd;
import sop.cli.picocli.commands.DearmorCmd;
import sop.cli.picocli.commands.DecryptCmd;
import sop.cli.picocli.commands.EncryptCmd;
import sop.cli.picocli.commands.ExtractCertCmd;
import sop.cli.picocli.commands.GenerateKeyCmd;
import sop.cli.picocli.commands.SignCmd;
import sop.cli.picocli.commands.VerifyCmd;
import sop.cli.picocli.commands.VersionCmd;
@CommandLine.Command(
exitCodeOnInvalidInput = 69,
subcommands = {
ArmorCmd.class,
DearmorCmd.class,
DecryptCmd.class,
EncryptCmd.class,
ExtractCertCmd.class,
GenerateKeyCmd.class,
SignCmd.class,
VerifyCmd.class,
VersionCmd.class
}
)
public class SopCLI {
static SOP SOP_INSTANCE;
public static void main(String[] args) {
int exitCode = execute(args);
if (exitCode != 0) {
System.exit(exitCode);
}
}
public static int execute(String[] args) {
return new CommandLine(SopCLI.class)
.setCaseInsensitiveEnumValuesAllowed(true)
.execute(args);
}
public static SOP getSop() {
if (SOP_INSTANCE == null) {
throw new IllegalStateException("No SOP backend set.");
}
return SOP_INSTANCE;
}
public static void setSopInstance(SOP instance) {
SOP_INSTANCE = instance;
}
}

View file

@ -0,0 +1,73 @@
/*
* Copyright 2021 Paul Schaub.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sop.cli.picocli.commands;
import java.io.IOException;
import picocli.CommandLine;
import sop.Ready;
import sop.cli.picocli.Print;
import sop.cli.picocli.SopCLI;
import sop.enums.ArmorLabel;
import sop.exception.SOPGPException;
import sop.operation.Armor;
@CommandLine.Command(name = "armor",
description = "Add ASCII Armor to standard input",
exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE)
public class ArmorCmd implements Runnable {
@CommandLine.Option(names = {"--label"}, description = "Label to be used in the header and tail of the armoring.", paramLabel = "{auto|sig|key|cert|message}")
ArmorLabel label;
@CommandLine.Option(names = {"--allow-nested"}, description = "Allow additional armoring of already armored input")
boolean allowNested = false;
@Override
public void run() {
Armor armor = SopCLI.getSop().armor();
if (allowNested) {
try {
armor.allowNested();
} catch (SOPGPException.UnsupportedOption unsupportedOption) {
Print.errln("Option --allow-nested is not supported.");
Print.trace(unsupportedOption);
System.exit(unsupportedOption.getExitCode());
}
}
if (label != null) {
try {
armor.label(label);
} catch (SOPGPException.UnsupportedOption unsupportedOption) {
Print.errln("Armor labels not supported.");
System.exit(unsupportedOption.getExitCode());
}
}
try {
Ready ready = armor.data(System.in);
ready.writeTo(System.out);
} catch (SOPGPException.BadData badData) {
Print.errln("Bad data.");
Print.trace(badData);
System.exit(badData.getExitCode());
} catch (IOException e) {
Print.errln("IO Error.");
Print.trace(e);
System.exit(1);
}
}
}

View file

@ -0,0 +1,47 @@
/*
* 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 sop.cli.picocli.commands;
import java.io.IOException;
import picocli.CommandLine;
import sop.cli.picocli.Print;
import sop.cli.picocli.SopCLI;
import sop.exception.SOPGPException;
@CommandLine.Command(name = "dearmor",
description = "Remove ASCII Armor from standard input",
exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE)
public class DearmorCmd implements Runnable {
@Override
public void run() {
try {
SopCLI.getSop()
.dearmor()
.data(System.in)
.writeTo(System.out);
} catch (SOPGPException.BadData e) {
Print.errln("Bad data.");
Print.trace(e);
System.exit(e.getExitCode());
} catch (IOException e) {
Print.errln("IO Error.");
Print.trace(e);
System.exit(1);
}
}
}

View file

@ -0,0 +1,290 @@
/*
* 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 sop.cli.picocli.commands;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.regex.Pattern;
import picocli.CommandLine;
import sop.DecryptionResult;
import sop.ReadyWithResult;
import sop.SessionKey;
import sop.Verification;
import sop.cli.picocli.DateParser;
import sop.cli.picocli.Print;
import sop.cli.picocli.SopCLI;
import sop.exception.SOPGPException;
import sop.operation.Decrypt;
import sop.util.HexUtil;
@CommandLine.Command(name = "decrypt",
description = "Decrypt a message from standard input",
exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE)
public class DecryptCmd implements Runnable {
@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")
List<String> withSessionKey = new ArrayList<>();
@CommandLine.Option(
names = {"--with-password"},
description = "Enables decryption based on any \"SKESK\" packets in the \"CIPHERTEXT\"",
paramLabel = "PASSWORD")
List<String> withPassword = new ArrayList<>();
@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")
List<File> certs = new ArrayList<>();
@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")
List<File> keys = new ArrayList<>();
@Override
public void run() {
unlinkExistingVerifyOut(verifyOut);
Decrypt decrypt = SopCLI.getSop().decrypt();
setNotAfter(notAfter, decrypt);
setNotBefore(notBefore, decrypt);
setWithPasswords(withPassword, decrypt);
setWithSessionKeys(withSessionKey, decrypt);
setVerifyWith(certs, decrypt);
setDecryptWith(keys, decrypt);
if (verifyOut != null && certs.isEmpty()) {
Print.errln("--verify-out is requested, but no --verify-with was provided.");
System.exit(23);
}
try {
ReadyWithResult<DecryptionResult> ready = decrypt.ciphertext(System.in);
DecryptionResult result = ready.writeTo(System.out);
if (sessionKeyOut != null) {
if (sessionKeyOut.exists()) {
Print.errln("File " + sessionKeyOut.getAbsolutePath() + " already exists.");
Print.trace(new SOPGPException.OutputExists());
System.exit(1);
}
try (FileOutputStream outputStream = new FileOutputStream(sessionKeyOut)) {
if (!result.getSessionKey().isPresent()) {
Print.errln("Session key not extracted. Possibly the feature is not supported.");
System.exit(SOPGPException.UnsupportedOption.EXIT_CODE);
} else {
SessionKey sessionKey = result.getSessionKey().get();
outputStream.write(sessionKey.getAlgorithm());
outputStream.write(sessionKey.getKey());
}
}
}
if (verifyOut != null) {
if (!verifyOut.createNewFile()) {
throw new IOException("Cannot create file " + verifyOut.getAbsolutePath());
}
try (FileOutputStream outputStream = new FileOutputStream(verifyOut)) {
PrintWriter writer = new PrintWriter(outputStream);
for (Verification verification : result.getVerifications()) {
// CHECKSTYLE:OFF
writer.println(verification.toString());
// CHECKSTYLE:ON
}
writer.flush();
}
}
} catch (SOPGPException.BadData badData) {
Print.errln("No valid OpenPGP message found on Standard Input.");
Print.trace(badData);
System.exit(badData.getExitCode());
} catch (SOPGPException.MissingArg missingArg) {
Print.errln("Missing arguments.");
Print.trace(missingArg);
System.exit(missingArg.getExitCode());
} catch (IOException e) {
Print.errln("IO Error.");
Print.trace(e);
System.exit(1);
} catch (SOPGPException.NoSignature noSignature) {
Print.errln("No verifiable signature found.");
Print.trace(noSignature);
System.exit(noSignature.getExitCode());
} catch (SOPGPException.CannotDecrypt cannotDecrypt) {
Print.errln("Cannot decrypt.");
Print.trace(cannotDecrypt);
System.exit(cannotDecrypt.getExitCode());
}
}
private void setDecryptWith(List<File> keys, Decrypt decrypt) {
for (File key : keys) {
try (FileInputStream keyIn = new FileInputStream(key)) {
decrypt.withKey(keyIn);
} catch (SOPGPException.KeyIsProtected keyIsProtected) {
Print.errln("Key in file " + key.getAbsolutePath() + " is password protected.");
Print.trace(keyIsProtected);
System.exit(1);
} catch (SOPGPException.UnsupportedAsymmetricAlgo unsupportedAsymmetricAlgo) {
Print.errln("Key uses unsupported asymmetric algorithm.");
Print.trace(unsupportedAsymmetricAlgo);
System.exit(unsupportedAsymmetricAlgo.getExitCode());
} catch (SOPGPException.BadData badData) {
Print.errln("File " + key.getAbsolutePath() + " does not contain a private key.");
Print.trace(badData);
System.exit(badData.getExitCode());
} catch (FileNotFoundException e) {
Print.errln("File " + key.getAbsolutePath() + " does not exist.");
Print.trace(e);
System.exit(1);
} catch (IOException e) {
Print.errln("IO Error.");
Print.trace(e);
System.exit(1);
}
}
}
private void setVerifyWith(List<File> certs, Decrypt decrypt) {
for (File cert : certs) {
try (FileInputStream certIn = new FileInputStream(cert)) {
decrypt.verifyWithCert(certIn);
} catch (FileNotFoundException e) {
Print.errln("File " + cert.getAbsolutePath() + " does not exist.");
Print.trace(e);
System.exit(1);
} catch (IOException e) {
Print.errln("IO Error.");
Print.trace(e);
System.exit(1);
} catch (SOPGPException.BadData badData) {
Print.errln("File " + cert.getAbsolutePath() + " does not contain a valid certificate.");
Print.trace(badData);
System.exit(badData.getExitCode());
}
}
}
private void unlinkExistingVerifyOut(File verifyOut) {
if (verifyOut == null) {
return;
}
if (verifyOut.exists()) {
if (!verifyOut.delete()) {
Print.errln("Cannot delete existing verification file" + verifyOut.getAbsolutePath());
System.exit(1);
}
}
}
private void setWithSessionKeys(List<String> withSessionKey, Decrypt decrypt) {
Pattern sessionKeyPattern = Pattern.compile("^\\d+:[0-9A-F]+$");
for (String sessionKey : withSessionKey) {
if (!sessionKeyPattern.matcher(sessionKey).matches()) {
Print.errln("Invalid session key format.");
Print.errln("Session keys are expected in the format 'ALGONUM:HEXKEY'");
System.exit(1);
}
String[] split = sessionKey.split(":");
byte algorithm = (byte) Integer.parseInt(split[0]);
byte[] key = HexUtil.hexToBytes(split[1]);
try {
decrypt.withSessionKey(new SessionKey(algorithm, key));
} catch (SOPGPException.UnsupportedOption unsupportedOption) {
Print.errln("Unsupported option '--with-session-key'.");
Print.trace(unsupportedOption);
System.exit(unsupportedOption.getExitCode());
return;
}
}
}
private void setWithPasswords(List<String> withPassword, Decrypt decrypt) {
for (String password : withPassword) {
try {
decrypt.withPassword(password);
} catch (SOPGPException.PasswordNotHumanReadable passwordNotHumanReadable) {
Print.errln("Password not human readable.");
Print.trace(passwordNotHumanReadable);
System.exit(passwordNotHumanReadable.getExitCode());
} catch (SOPGPException.UnsupportedOption unsupportedOption) {
Print.errln("Unsupported option '--with-password'.");
Print.trace(unsupportedOption);
System.exit(unsupportedOption.getExitCode());
}
}
}
private void setNotAfter(String notAfter, Decrypt decrypt) {
Date notAfterDate = DateParser.parseNotAfter(notAfter);
try {
decrypt.verifyNotAfter(notAfterDate);
} catch (SOPGPException.UnsupportedOption unsupportedOption) {
Print.errln("Option '--not-after' not supported.");
Print.trace(unsupportedOption);
System.exit(unsupportedOption.getExitCode());
}
}
private void setNotBefore(String notBefore, Decrypt decrypt) {
Date notBeforeDate = DateParser.parseNotBefore(notBefore);
try {
decrypt.verifyNotBefore(notBeforeDate);
} catch (SOPGPException.UnsupportedOption unsupportedOption) {
Print.errln("Option '--not-before' not supported.");
Print.trace(unsupportedOption);
System.exit(unsupportedOption.getExitCode());
}
}
}

View file

@ -0,0 +1,164 @@
/*
* 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 sop.cli.picocli.commands;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import picocli.CommandLine;
import sop.Ready;
import sop.cli.picocli.Print;
import sop.cli.picocli.SopCLI;
import sop.enums.EncryptAs;
import sop.exception.SOPGPException;
import sop.operation.Encrypt;
@CommandLine.Command(name = "encrypt",
description = "Encrypt a message from standard input",
exitCodeOnInvalidInput = 37)
public class EncryptCmd implements Runnable {
@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}")
EncryptAs type;
@CommandLine.Option(names = "--with-password",
description = "Encrypt the message with a password",
paramLabel = "PASSWORD")
List<String> withPassword = new ArrayList<>();
@CommandLine.Option(names = "--sign-with",
description = "Sign the output with a private key",
paramLabel = "KEY")
List<File> signWith = new ArrayList<>();
@CommandLine.Parameters(description = "Certificates the message gets encrypted to",
index = "0..*",
paramLabel = "CERTS")
List<File> certs = new ArrayList<>();
@Override
public void run() {
Encrypt encrypt = SopCLI.getSop().encrypt();
if (type != null) {
try {
encrypt.mode(type);
} catch (SOPGPException.UnsupportedOption unsupportedOption) {
Print.errln("Unsupported option '--as'.");
Print.trace(unsupportedOption);
System.exit(unsupportedOption.getExitCode());
}
}
if (withPassword.isEmpty() && certs.isEmpty()) {
Print.errln("At least one password or cert file required for encryption.");
System.exit(19);
}
for (String password : withPassword) {
try {
encrypt.withPassword(password);
} catch (SOPGPException.PasswordNotHumanReadable passwordNotHumanReadable) {
Print.errln("Password is not human-readable.");
Print.trace(passwordNotHumanReadable);
System.exit(passwordNotHumanReadable.getExitCode());
} catch (SOPGPException.UnsupportedOption unsupportedOption) {
Print.errln("Unsupported option '--with-password'.");
Print.trace(unsupportedOption);
System.exit(unsupportedOption.getExitCode());
}
}
for (File keyFile : signWith) {
try (FileInputStream keyIn = new FileInputStream(keyFile)) {
encrypt.signWith(keyIn);
} catch (FileNotFoundException e) {
Print.errln("Key file " + keyFile.getAbsolutePath() + " not found.");
Print.trace(e);
System.exit(1);
} catch (IOException e) {
Print.errln("IO Error.");
Print.trace(e);
System.exit(1);
} catch (SOPGPException.KeyIsProtected keyIsProtected) {
Print.errln("Key from " + keyFile.getAbsolutePath() + " is password protected.");
Print.trace(keyIsProtected);
System.exit(1);
} catch (SOPGPException.UnsupportedAsymmetricAlgo unsupportedAsymmetricAlgo) {
Print.errln("Key from " + keyFile.getAbsolutePath() + " has unsupported asymmetric algorithm.");
Print.trace(unsupportedAsymmetricAlgo);
System.exit(unsupportedAsymmetricAlgo.getExitCode());
} catch (SOPGPException.CertCannotSign certCannotSign) {
Print.errln("Key from " + keyFile.getAbsolutePath() + " cannot sign.");
Print.trace(certCannotSign);
System.exit(1);
} catch (SOPGPException.BadData badData) {
Print.errln("Key file " + keyFile.getAbsolutePath() + " does not contain a valid OpenPGP private key.");
Print.trace(badData);
System.exit(badData.getExitCode());
}
}
for (File certFile : certs) {
try (FileInputStream certIn = new FileInputStream(certFile)) {
encrypt.withCert(certIn);
} catch (FileNotFoundException e) {
Print.errln("Certificate file " + certFile.getAbsolutePath() + " not found.");
Print.trace(e);
System.exit(1);
} catch (IOException e) {
Print.errln("IO Error.");
Print.trace(e);
System.exit(1);
} catch (SOPGPException.UnsupportedAsymmetricAlgo unsupportedAsymmetricAlgo) {
Print.errln("Certificate from " + certFile.getAbsolutePath() + " has unsupported asymmetric algorithm.");
Print.trace(unsupportedAsymmetricAlgo);
System.exit(unsupportedAsymmetricAlgo.getExitCode());
} catch (SOPGPException.CertCannotEncrypt certCannotEncrypt) {
Print.errln("Certificate from " + certFile.getAbsolutePath() + " is not capable of encryption.");
Print.trace(certCannotEncrypt);
System.exit(certCannotEncrypt.getExitCode());
} catch (SOPGPException.BadData badData) {
Print.errln("Certificate file " + certFile.getAbsolutePath() + " does not contain a valid OpenPGP certificate.");
Print.trace(badData);
System.exit(badData.getExitCode());
}
}
if (!armor) {
encrypt.noArmor();
}
try {
Ready ready = encrypt.plaintext(System.in);
ready.writeTo(System.out);
} catch (IOException e) {
Print.errln("IO Error.");
Print.trace(e);
System.exit(1);
}
}
}

View file

@ -0,0 +1,57 @@
/*
* 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 sop.cli.picocli.commands;
import java.io.IOException;
import picocli.CommandLine;
import sop.Ready;
import sop.cli.picocli.Print;
import sop.cli.picocli.SopCLI;
import sop.exception.SOPGPException;
import sop.operation.ExtractCert;
@CommandLine.Command(name = "extract-cert",
description = "Extract a public key certificate from a secret key from standard input",
exitCodeOnInvalidInput = 37)
public class ExtractCertCmd implements Runnable {
@CommandLine.Option(names = "--no-armor",
description = "ASCII armor the output",
negatable = true)
boolean armor = true;
@Override
public void run() {
ExtractCert extractCert = SopCLI.getSop().extractCert();
if (!armor) {
extractCert.noArmor();
}
try {
Ready ready = extractCert.key(System.in);
ready.writeTo(System.out);
} catch (IOException e) {
Print.errln("IO Error.");
Print.trace(e);
System.exit(1);
} catch (SOPGPException.BadData badData) {
Print.errln("Standard Input does not contain valid OpenPGP private key material.");
Print.trace(badData);
System.exit(badData.getExitCode());
}
}
}

View file

@ -0,0 +1,70 @@
/*
* 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 sop.cli.picocli.commands;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import picocli.CommandLine;
import sop.Ready;
import sop.cli.picocli.Print;
import sop.cli.picocli.SopCLI;
import sop.exception.SOPGPException;
import sop.operation.GenerateKey;
@CommandLine.Command(name = "generate-key",
description = "Generate a secret key",
exitCodeOnInvalidInput = 37)
public class GenerateKeyCmd 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 = new ArrayList<>();
@Override
public void run() {
GenerateKey generateKey = SopCLI.getSop().generateKey();
for (String userId : userId) {
generateKey.userId(userId);
}
if (!armor) {
generateKey.noArmor();
}
try {
Ready ready = generateKey.generate();
ready.writeTo(System.out);
} catch (SOPGPException.MissingArg missingArg) {
Print.errln("Missing argument.");
Print.trace(missingArg);
System.exit(missingArg.getExitCode());
} catch (SOPGPException.UnsupportedAsymmetricAlgo unsupportedAsymmetricAlgo) {
Print.errln("Unsupported asymmetric algorithm.");
Print.trace(unsupportedAsymmetricAlgo);
System.exit(unsupportedAsymmetricAlgo.getExitCode());
} catch (IOException e) {
Print.errln("IO Error.");
Print.trace(e);
System.exit(1);
}
}
}

View file

@ -0,0 +1,109 @@
/*
* 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 sop.cli.picocli.commands;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import picocli.CommandLine;
import sop.Ready;
import sop.cli.picocli.Print;
import sop.cli.picocli.SopCLI;
import sop.enums.SignAs;
import sop.exception.SOPGPException;
import sop.operation.Sign;
@CommandLine.Command(name = "sign",
description = "Create a detached signature on the data from standard input",
exitCodeOnInvalidInput = 37)
public class SignCmd implements Runnable {
@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}")
SignAs type;
@CommandLine.Parameters(description = "Secret keys used for signing",
paramLabel = "KEY")
List<File> secretKeyFile = new ArrayList<>();
@Override
public void run() {
Sign sign = SopCLI.getSop().sign();
if (type != null) {
try {
sign.mode(type);
} catch (SOPGPException.UnsupportedOption unsupportedOption) {
Print.errln("Unsupported option '--as'");
Print.trace(unsupportedOption);
System.exit(unsupportedOption.getExitCode());
}
}
if (secretKeyFile.isEmpty()) {
Print.errln("Missing required parameter 'KEY'.");
System.exit(19);
}
for (File keyFile : secretKeyFile) {
try (FileInputStream keyIn = new FileInputStream(keyFile)) {
sign.key(keyIn);
} catch (FileNotFoundException e) {
Print.errln("File " + keyFile.getAbsolutePath() + " does not exist.");
Print.trace(e);
System.exit(1);
} catch (IOException e) {
Print.errln("Cannot access file " + keyFile.getAbsolutePath());
Print.trace(e);
System.exit(1);
} catch (SOPGPException.KeyIsProtected e) {
Print.errln("Key " + keyFile.getName() + " is password protected.");
Print.trace(e);
System.exit(1);
} catch (SOPGPException.BadData badData) {
Print.errln("Bad data in key file " + keyFile.getAbsolutePath() + ":");
Print.trace(badData);
System.exit(badData.getExitCode());
}
}
if (!armor) {
sign.noArmor();
}
try {
Ready ready = sign.data(System.in);
ready.writeTo(System.out);
} catch (IOException e) {
Print.errln("IO Error.");
Print.trace(e);
System.exit(1);
} catch (SOPGPException.ExpectedText expectedText) {
Print.errln("Expected text input, but got binary data.");
Print.trace(expectedText);
System.exit(expectedText.getExitCode());
}
}
}

View file

@ -0,0 +1,143 @@
/*
* 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 sop.cli.picocli.commands;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import picocli.CommandLine;
import sop.Verification;
import sop.cli.picocli.DateParser;
import sop.cli.picocli.Print;
import sop.cli.picocli.SopCLI;
import sop.exception.SOPGPException;
import sop.operation.Verify;
@CommandLine.Command(name = "verify",
description = "Verify a detached signature over the data from standard input",
exitCodeOnInvalidInput = 37)
public class VerifyCmd implements Runnable {
@CommandLine.Parameters(index = "0",
description = "Detached signature",
paramLabel = "SIGNATURE")
File signature;
@CommandLine.Parameters(index = "1..*",
arity = "1..*",
description = "Public key certificates",
paramLabel = "CERT")
List<File> certificates = new ArrayList<>();
@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() {
Verify verify = SopCLI.getSop().verify();
if (notAfter != null) {
try {
verify.notAfter(DateParser.parseNotAfter(notAfter));
} catch (SOPGPException.UnsupportedOption unsupportedOption) {
Print.errln("Unsupported option '--not-after'.");
Print.trace(unsupportedOption);
System.exit(unsupportedOption.getExitCode());
}
}
if (notBefore != null) {
try {
verify.notBefore(DateParser.parseNotBefore(notBefore));
} catch (SOPGPException.UnsupportedOption unsupportedOption) {
Print.errln("Unsupported option '--not-before'.");
Print.trace(unsupportedOption);
System.exit(unsupportedOption.getExitCode());
}
}
for (File certFile : certificates) {
try (FileInputStream certIn = new FileInputStream(certFile)) {
verify.cert(certIn);
} catch (FileNotFoundException fileNotFoundException) {
Print.errln("Certificate file " + certFile.getAbsolutePath() + " not found.");
Print.trace(fileNotFoundException);
System.exit(1);
} catch (IOException ioException) {
Print.errln("IO Error.");
Print.trace(ioException);
System.exit(1);
} catch (SOPGPException.BadData badData) {
Print.errln("Certificate file " + certFile.getAbsolutePath() + " appears to not contain a valid OpenPGP certificate.");
Print.trace(badData);
System.exit(badData.getExitCode());
}
}
if (signature != null) {
try (FileInputStream sigIn = new FileInputStream(signature)) {
verify.signatures(sigIn);
} catch (FileNotFoundException e) {
Print.errln("Signature file " + signature.getAbsolutePath() + " does not exist.");
Print.trace(e);
System.exit(1);
} catch (IOException e) {
Print.errln("IO Error.");
Print.trace(e);
System.exit(1);
} catch (SOPGPException.BadData badData) {
Print.errln("File " + signature.getAbsolutePath() + " does not contain a valid OpenPGP signature.");
Print.trace(badData);
System.exit(badData.getExitCode());
}
}
List<Verification> verifications = null;
try {
verifications = verify.data(System.in);
} catch (SOPGPException.NoSignature e) {
Print.errln("No verifiable signature found.");
Print.trace(e);
System.exit(e.getExitCode());
} catch (IOException ioException) {
Print.errln("IO Error.");
Print.trace(ioException);
System.exit(1);
} catch (SOPGPException.BadData badData) {
Print.errln("Standard Input appears not to contain a valid OpenPGP message.");
Print.trace(badData);
System.exit(badData.getExitCode());
}
for (Verification verification : verifications) {
Print.outln(verification.toString());
}
}
}

View file

@ -0,0 +1,33 @@
/*
* 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 sop.cli.picocli.commands;
import picocli.CommandLine;
import sop.cli.picocli.Print;
import sop.cli.picocli.SopCLI;
import sop.operation.Version;
@CommandLine.Command(name = "version", description = "Display version information about the tool",
exitCodeOnInvalidInput = 37)
public class VersionCmd implements Runnable {
@Override
public void run() {
Version version = SopCLI.getSop().version();
Print.outln(version.getName() + " " + version.getVersion());
}
}

View file

@ -0,0 +1,19 @@
/*
* 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 sop.cli.picocli.commands;

View file

@ -0,0 +1,19 @@
/*
* 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.
*/
/**
* Implementation of the Stateless OpenPGP Command Line Interface using Picocli.
*/
package sop.cli.picocli;

View file

@ -0,0 +1,60 @@
/*
* Copyright 2021 Paul Schaub.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sop.cli.picocli;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.Date;
import org.junit.jupiter.api.Test;
import sop.util.UTCUtil;
public class DateParserTest {
@Test
public void parseNotAfterDashReturnsEndOfTime() {
assertEquals(DateParser.END_OF_TIME, DateParser.parseNotAfter("-"));
}
@Test
public void parseNotBeforeDashReturnsBeginningOfTime() {
assertEquals(DateParser.BEGINNING_OF_TIME, DateParser.parseNotBefore("-"));
}
@Test
public void parseNotAfterNowReturnsNow() {
assertEquals(new Date().getTime(), DateParser.parseNotAfter("now").getTime(), 1000);
}
@Test
public void parseNotBeforeNowReturnsNow() {
assertEquals(new Date().getTime(), DateParser.parseNotBefore("now").getTime(), 1000);
}
@Test
public void parseNotAfterTimestamp() {
String timestamp = "2019-10-24T23:48:29Z";
Date date = DateParser.parseNotAfter(timestamp);
assertEquals(timestamp, UTCUtil.formatUTCDate(date));
}
@Test
public void parseNotBeforeTimestamp() {
String timestamp = "2019-10-29T18:36:45Z";
Date date = DateParser.parseNotBefore(timestamp);
assertEquals(timestamp, UTCUtil.formatUTCDate(date));
}
}

View file

@ -0,0 +1,42 @@
/*
* Copyright 2021 Paul Schaub.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sop.cli.picocli;
import static org.mockito.Mockito.mock;
import com.ginsberg.junit.exit.ExpectSystemExitWithStatus;
import org.junit.jupiter.api.Test;
import sop.SOP;
public class SOPTest {
@Test
@ExpectSystemExitWithStatus(69)
public void assertExitOnInvalidSubcommand() {
SOP sop = mock(SOP.class);
SopCLI.setSopInstance(sop);
SopCLI.main(new String[] {"invalid"});
}
@Test
@ExpectSystemExitWithStatus(1)
public void assertThrowsIfNoSOPBackendSet() {
SopCLI.SOP_INSTANCE = null;
// At this point, no SOP backend is set, so an InvalidStateException triggers exit(1)
SopCLI.main(new String[] {"armor"});
}
}

View file

@ -0,0 +1,131 @@
/*
* Copyright 2021 Paul Schaub.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sop.cli.picocli.commands;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.io.IOException;
import java.io.OutputStream;
import com.ginsberg.junit.exit.ExpectSystemExitWithStatus;
import com.ginsberg.junit.exit.FailOnSystemExit;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import sop.Ready;
import sop.SOP;
import sop.cli.picocli.SopCLI;
import sop.enums.ArmorLabel;
import sop.exception.SOPGPException;
import sop.operation.Armor;
public class ArmorCmdTest {
private Armor armor;
private SOP sop;
@BeforeEach
public void mockComponents() throws SOPGPException.BadData {
armor = mock(Armor.class);
sop = mock(SOP.class);
when(sop.armor()).thenReturn(armor);
when(armor.data(any())).thenReturn(nopReady());
SopCLI.setSopInstance(sop);
}
@Test
public void assertAllowNestedIsCalledWhenFlagged() throws SOPGPException.UnsupportedOption {
SopCLI.main(new String[] {"armor", "--allow-nested"});
verify(armor, times(1)).allowNested();
}
@Test
public void assertAllowNestedIsNotCalledByDefault() throws SOPGPException.UnsupportedOption {
SopCLI.main(new String[] {"armor"});
verify(armor, never()).allowNested();
}
@Test
public void assertLabelIsNotCalledByDefault() throws SOPGPException.UnsupportedOption {
SopCLI.main(new String[] {"armor"});
verify(armor, never()).label(any());
}
@Test
public void assertLabelIsCalledWhenFlaggedWithArgument() throws SOPGPException.UnsupportedOption {
for (ArmorLabel label : ArmorLabel.values()) {
SopCLI.main(new String[] {"armor", "--label", label.name()});
verify(armor, times(1)).label(label);
}
}
@Test
public void assertDataIsAlwaysCalled() throws SOPGPException.BadData {
SopCLI.main(new String[] {"armor"});
verify(armor, times(1)).data(any());
}
@Test
@ExpectSystemExitWithStatus(37)
public void assertThrowsForInvalidLabel() {
SopCLI.main(new String[] {"armor", "--label", "Invalid"});
}
@Test
@ExpectSystemExitWithStatus(37)
public void ifLabelsUnsupportedExit37() throws SOPGPException.UnsupportedOption {
when(armor.label(any())).thenThrow(new SOPGPException.UnsupportedOption());
SopCLI.main(new String[] {"armor", "--label", "Sig"});
}
@Test
@ExpectSystemExitWithStatus(37)
public void ifAllowNestedUnsupportedExit37() throws SOPGPException.UnsupportedOption {
when(armor.allowNested()).thenThrow(new SOPGPException.UnsupportedOption());
SopCLI.main(new String[] {"armor", "--allow-nested"});
}
@Test
@ExpectSystemExitWithStatus(41)
public void ifBadDataExit41() throws SOPGPException.BadData {
when(armor.data(any())).thenThrow(new SOPGPException.BadData(new IOException()));
SopCLI.main(new String[] {"armor"});
}
@Test
@FailOnSystemExit
public void ifNoErrorsNoExit() {
when(sop.armor()).thenReturn(armor);
SopCLI.main(new String[] {"armor"});
}
private static Ready nopReady() {
return new Ready() {
@Override
public void writeTo(OutputStream outputStream) {
}
};
}
}

View file

@ -0,0 +1,71 @@
/*
* Copyright 2021 Paul Schaub.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sop.cli.picocli.commands;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.io.IOException;
import java.io.OutputStream;
import com.ginsberg.junit.exit.ExpectSystemExitWithStatus;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import sop.Ready;
import sop.SOP;
import sop.cli.picocli.SopCLI;
import sop.exception.SOPGPException;
import sop.operation.Dearmor;
public class DearmorCmdTest {
private SOP sop;
private Dearmor dearmor;
@BeforeEach
public void mockComponents() throws IOException, SOPGPException.BadData {
sop = mock(SOP.class);
dearmor = mock(Dearmor.class);
when(dearmor.data(any())).thenReturn(nopReady());
when(sop.dearmor()).thenReturn(dearmor);
SopCLI.setSopInstance(sop);
}
private static Ready nopReady() {
return new Ready() {
@Override
public void writeTo(OutputStream outputStream) {
}
};
}
@Test
public void assertDataIsCalled() throws IOException, SOPGPException.BadData {
SopCLI.main(new String[] {"dearmor"});
verify(dearmor, times(1)).data(any());
}
@Test
@ExpectSystemExitWithStatus(41)
public void assertBadDataCausesExit41() throws IOException, SOPGPException.BadData {
when(dearmor.data(any())).thenThrow(new SOPGPException.BadData(new IOException("invalid armor")));
SopCLI.main(new String[] {"dearmor"});
}
}

View file

@ -0,0 +1,346 @@
/*
* Copyright 2021 Paul Schaub.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sop.cli.picocli.commands;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.Date;
import com.ginsberg.junit.exit.ExpectSystemExitWithStatus;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentMatcher;
import org.mockito.ArgumentMatchers;
import sop.DecryptionResult;
import sop.ReadyWithResult;
import sop.SOP;
import sop.SessionKey;
import sop.Verification;
import sop.cli.picocli.DateParser;
import sop.cli.picocli.SopCLI;
import sop.exception.SOPGPException;
import sop.operation.Decrypt;
import sop.util.HexUtil;
import sop.util.UTCUtil;
public class DecryptCmdTest {
private Decrypt decrypt;
@BeforeEach
public void mockComponents() throws SOPGPException.UnsupportedOption, SOPGPException.MissingArg, SOPGPException.BadData, SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.PasswordNotHumanReadable, SOPGPException.CannotDecrypt {
SOP sop = mock(SOP.class);
decrypt = mock(Decrypt.class);
when(decrypt.verifyNotAfter(any())).thenReturn(decrypt);
when(decrypt.verifyNotBefore(any())).thenReturn(decrypt);
when(decrypt.withPassword(any())).thenReturn(decrypt);
when(decrypt.withSessionKey(any())).thenReturn(decrypt);
when(decrypt.withKey(any())).thenReturn(decrypt);
when(decrypt.ciphertext(any())).thenReturn(nopReadyWithResult());
when(sop.decrypt()).thenReturn(decrypt);
SopCLI.setSopInstance(sop);
}
private static ReadyWithResult<DecryptionResult> nopReadyWithResult() {
return new ReadyWithResult<DecryptionResult>() {
@Override
public DecryptionResult writeTo(OutputStream outputStream) {
return new DecryptionResult(null, Collections.emptyList());
}
};
}
@Test
@ExpectSystemExitWithStatus(19)
public void missingArgumentsExceptionCausesExit19() throws SOPGPException.MissingArg, SOPGPException.BadData, SOPGPException.CannotDecrypt {
when(decrypt.ciphertext(any())).thenThrow(new SOPGPException.MissingArg("Missing arguments."));
SopCLI.main(new String[] {"decrypt"});
}
@Test
@ExpectSystemExitWithStatus(41)
public void badDataExceptionCausesExit41() throws SOPGPException.MissingArg, SOPGPException.BadData, SOPGPException.CannotDecrypt {
when(decrypt.ciphertext(any())).thenThrow(new SOPGPException.BadData(new IOException()));
SopCLI.main(new String[] {"decrypt"});
}
@Test
@ExpectSystemExitWithStatus(31)
public void assertNotHumanReadablePasswordCausesExit31() throws SOPGPException.PasswordNotHumanReadable,
SOPGPException.UnsupportedOption {
when(decrypt.withPassword(any())).thenThrow(new SOPGPException.PasswordNotHumanReadable());
SopCLI.main(new String[] {"decrypt", "--with-password", "pretendThisIsNotReadable"});
}
@Test
public void assertWithPasswordPassesPasswordDown() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption {
SopCLI.main(new String[] {"decrypt", "--with-password", "orange"});
verify(decrypt, times(1)).withPassword("orange");
}
@Test
@ExpectSystemExitWithStatus(37)
public void assertUnsupportedWithPasswordCausesExit37() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption {
when(decrypt.withPassword(any())).thenThrow(new SOPGPException.UnsupportedOption());
SopCLI.main(new String[] {"decrypt", "--with-password", "swordfish"});
}
@Test
public void assertDefaultTimeRangesAreUsedIfNotOverwritten() throws SOPGPException.UnsupportedOption {
Date now = new Date();
SopCLI.main(new String[] {"decrypt"});
verify(decrypt, times(1)).verifyNotBefore(DateParser.BEGINNING_OF_TIME);
verify(decrypt, times(1)).verifyNotAfter(
ArgumentMatchers.argThat(argument -> {
// allow 1 second difference
return Math.abs(now.getTime() - argument.getTime()) <= 1000;
}));
}
@Test
public void assertVerifyNotAfterAndBeforeDashResultsInMaxTimeRange() throws SOPGPException.UnsupportedOption {
SopCLI.main(new String[] {"decrypt", "--not-before", "-", "--not-after", "-"});
verify(decrypt, times(1)).verifyNotBefore(DateParser.BEGINNING_OF_TIME);
verify(decrypt, times(1)).verifyNotAfter(DateParser.END_OF_TIME);
}
@Test
public void assertVerifyNotAfterAndBeforeNowResultsInMinTimeRange() throws SOPGPException.UnsupportedOption {
Date now = new Date();
ArgumentMatcher<Date> isMaxOneSecOff = argument -> {
// Allow less than 1 second difference
return Math.abs(now.getTime() - argument.getTime()) <= 1000;
};
SopCLI.main(new String[] {"decrypt", "--not-before", "now", "--not-after", "now"});
verify(decrypt, times(1)).verifyNotAfter(ArgumentMatchers.argThat(isMaxOneSecOff));
verify(decrypt, times(1)).verifyNotBefore(ArgumentMatchers.argThat(isMaxOneSecOff));
}
@Test
@ExpectSystemExitWithStatus(1)
public void assertMalformedDateInNotBeforeCausesExit1() {
// ParserException causes exit(1)
SopCLI.main(new String[] {"decrypt", "--not-before", "invalid"});
}
@Test
@ExpectSystemExitWithStatus(1)
public void assertMalformedDateInNotAfterCausesExit1() {
// ParserException causes exit(1)
SopCLI.main(new String[] {"decrypt", "--not-after", "invalid"});
}
@Test
@ExpectSystemExitWithStatus(37)
public void assertUnsupportedNotAfterCausesExit37() throws SOPGPException.UnsupportedOption {
when(decrypt.verifyNotAfter(any())).thenThrow(new SOPGPException.UnsupportedOption());
SopCLI.main(new String[] {"decrypt", "--not-after", "now"});
}
@Test
@ExpectSystemExitWithStatus(37)
public void assertUnsupportedNotBeforeCausesExit37() throws SOPGPException.UnsupportedOption {
when(decrypt.verifyNotBefore(any())).thenThrow(new SOPGPException.UnsupportedOption());
SopCLI.main(new String[] {"decrypt", "--not-before", "now"});
}
@Test
@ExpectSystemExitWithStatus(1)
public void assertExistingSessionKeyOutFileCausesExit1() throws IOException {
File tempFile = File.createTempFile("existing-session-key-", ".tmp");
tempFile.deleteOnExit();
SopCLI.main(new String[] {"decrypt", "--session-key-out", tempFile.getAbsolutePath()});
}
@Test
@ExpectSystemExitWithStatus(37)
public void assertWhenSessionKeyCannotBeExtractedExit37() throws IOException {
Path tempDir = Files.createTempDirectory("session-key-out-dir");
File tempFile = new File(tempDir.toFile(), "session-key");
tempFile.deleteOnExit();
SopCLI.main(new String[] {"decrypt", "--session-key-out", tempFile.getAbsolutePath()});
}
@Test
public void assertSessionKeyIsProperlyWrittenToSessionKeyFile() throws SOPGPException.CannotDecrypt, SOPGPException.MissingArg, SOPGPException.BadData, IOException {
byte[] key = "C7CBDAF42537776F12509B5168793C26B93294E5ABDFA73224FB0177123E9137".getBytes(StandardCharsets.UTF_8);
when(decrypt.ciphertext(any())).thenReturn(new ReadyWithResult<DecryptionResult>() {
@Override
public DecryptionResult writeTo(OutputStream outputStream) {
return new DecryptionResult(
new SessionKey((byte) 9, key),
Collections.emptyList()
);
}
});
Path tempDir = Files.createTempDirectory("session-key-out-dir");
File tempFile = new File(tempDir.toFile(), "session-key");
tempFile.deleteOnExit();
SopCLI.main(new String[] {"decrypt", "--session-key-out", tempFile.getAbsolutePath()});
ByteArrayOutputStream bytesInFile = new ByteArrayOutputStream();
try (FileInputStream fileIn = new FileInputStream(tempFile)) {
byte[] buf = new byte[32];
int read = fileIn.read(buf);
while (read != -1) {
bytesInFile.write(buf, 0, read);
read = fileIn.read(buf);
}
}
byte[] algAndKey = new byte[key.length + 1];
algAndKey[0] = (byte) 9;
System.arraycopy(key, 0, algAndKey, 1, key.length);
assertArrayEquals(algAndKey, bytesInFile.toByteArray());
}
@Test
@ExpectSystemExitWithStatus(29)
public void assertUnableToDecryptExceptionResultsInExit29() throws SOPGPException.CannotDecrypt, SOPGPException.MissingArg, SOPGPException.BadData {
when(decrypt.ciphertext(any())).thenThrow(new SOPGPException.CannotDecrypt());
SopCLI.main(new String[] {"decrypt"});
}
@Test
@ExpectSystemExitWithStatus(3)
public void assertNoSignatureExceptionCausesExit3() throws SOPGPException.CannotDecrypt, SOPGPException.MissingArg, SOPGPException.BadData {
when(decrypt.ciphertext(any())).thenReturn(new ReadyWithResult<DecryptionResult>() {
@Override
public DecryptionResult writeTo(OutputStream outputStream) throws SOPGPException.NoSignature {
throw new SOPGPException.NoSignature();
}
});
SopCLI.main(new String[] {"decrypt"});
}
@Test
@ExpectSystemExitWithStatus(41)
public void badDataInVerifyWithCausesExit41() throws IOException, SOPGPException.BadData {
when(decrypt.verifyWithCert(any())).thenThrow(new SOPGPException.BadData(new IOException()));
File tempFile = File.createTempFile("verify-with-", ".tmp");
SopCLI.main(new String[] {"decrypt", "--verify-with", tempFile.getAbsolutePath()});
}
@Test
@ExpectSystemExitWithStatus(1)
public void unexistentCertFileCausesExit1() {
SopCLI.main(new String[] {"decrypt", "--verify-with", "invalid"});
}
@Test
public void existingVerifyOutFileIsUnlinkedBeforeVerification() throws IOException, SOPGPException.CannotDecrypt, SOPGPException.MissingArg, SOPGPException.BadData {
File certFile = File.createTempFile("existing-verify-out-cert", ".asc");
File existingVerifyOut = File.createTempFile("existing-verify-out", ".tmp");
byte[] data = "some data".getBytes(StandardCharsets.UTF_8);
try (FileOutputStream out = new FileOutputStream(existingVerifyOut)) {
out.write(data);
}
Date date = UTCUtil.parseUTCDate("2021-07-11T20:58:23Z");
when(decrypt.ciphertext(any())).thenReturn(new ReadyWithResult<DecryptionResult>() {
@Override
public DecryptionResult writeTo(OutputStream outputStream) {
return new DecryptionResult(null, Collections.singletonList(
new Verification(
date,
"1B66A707819A920925BC6777C3E0AFC0B2DFF862",
"C8CD564EBF8D7BBA90611D8D071773658BF6BF86"))
);
}
});
SopCLI.main(new String[] {"decrypt", "--verify-out", existingVerifyOut.getAbsolutePath(), "--verify-with", certFile.getAbsolutePath()});
try (BufferedReader reader = new BufferedReader(new FileReader(existingVerifyOut))) {
String line = reader.readLine();
assertEquals("2021-07-11T20:58:23Z 1B66A707819A920925BC6777C3E0AFC0B2DFF862 C8CD564EBF8D7BBA90611D8D071773658BF6BF86", line);
}
}
@Test
public void assertWithSessionKeyIsPassedDown() throws SOPGPException.UnsupportedOption {
SessionKey key1 = new SessionKey((byte) 9, HexUtil.hexToBytes("C7CBDAF42537776F12509B5168793C26B93294E5ABDFA73224FB0177123E9137"));
SessionKey key2 = new SessionKey((byte) 9, HexUtil.hexToBytes("FCA4BEAF687F48059CACC14FB019125CD57392BAB7037C707835925CBF9F7BCD"));
SopCLI.main(new String[] {"decrypt",
"--with-session-key", "9:C7CBDAF42537776F12509B5168793C26B93294E5ABDFA73224FB0177123E9137",
"--with-session-key", "9:FCA4BEAF687F48059CACC14FB019125CD57392BAB7037C707835925CBF9F7BCD"});
verify(decrypt).withSessionKey(key1);
verify(decrypt).withSessionKey(key2);
}
@Test
@ExpectSystemExitWithStatus(1)
public void assertMalformedSessionKeysResultInExit1() {
SopCLI.main(new String[] {"decrypt",
"--with-session-key", "C7CBDAF42537776F12509B5168793C26B93294E5ABDFA73224FB0177123E9137"});
}
@Test
@ExpectSystemExitWithStatus(41)
public void assertBadDataInKeysResultsInExit41() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData, IOException {
when(decrypt.withKey(any())).thenThrow(new SOPGPException.BadData(new IOException()));
File tempKeyFile = File.createTempFile("key-", ".tmp");
SopCLI.main(new String[] {"decrypt", tempKeyFile.getAbsolutePath()});
}
@Test
@ExpectSystemExitWithStatus(1)
public void assertKeyFileNotFoundCausesExit1() {
SopCLI.main(new String[] {"decrypt", "nonexistent-key"});
}
@Test
@ExpectSystemExitWithStatus(1)
public void assertProtectedKeyCausesExit1() throws IOException, SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData {
when(decrypt.withKey(any())).thenThrow(new SOPGPException.KeyIsProtected());
File tempKeyFile = File.createTempFile("key-", ".tmp");
SopCLI.main(new String[] {"decrypt", tempKeyFile.getAbsolutePath()});
}
@Test
@ExpectSystemExitWithStatus(13)
public void assertUnsupportedAlgorithmExceptionCausesExit13() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData, IOException {
when(decrypt.withKey(any())).thenThrow(new SOPGPException.UnsupportedAsymmetricAlgo(new IOException()));
File tempKeyFile = File.createTempFile("key-", ".tmp");
SopCLI.main(new String[] {"decrypt", tempKeyFile.getAbsolutePath()});
}
@Test
@ExpectSystemExitWithStatus(23)
public void verifyOutWithoutVerifyWithCausesExit23() {
SopCLI.main(new String[] {"decrypt", "--verify-out", "out.file"});
}
}

View file

@ -0,0 +1,204 @@
/*
* Copyright 2021 Paul Schaub.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sop.cli.picocli.commands;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import com.ginsberg.junit.exit.ExpectSystemExitWithStatus;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import sop.Ready;
import sop.SOP;
import sop.cli.picocli.SopCLI;
import sop.enums.EncryptAs;
import sop.exception.SOPGPException;
import sop.operation.Encrypt;
public class EncryptCmdTest {
Encrypt encrypt;
@BeforeEach
public void mockComponents() throws IOException {
encrypt = mock(Encrypt.class);
when(encrypt.plaintext(any())).thenReturn(new Ready() {
@Override
public void writeTo(OutputStream outputStream) {
}
});
SOP sop = mock(SOP.class);
when(sop.encrypt()).thenReturn(encrypt);
SopCLI.setSopInstance(sop);
}
@Test
@ExpectSystemExitWithStatus(19)
public void missingPasswordAndCertFileCauseExit19() {
SopCLI.main(new String[] {"encrypt", "--no-armor"});
}
@Test
@ExpectSystemExitWithStatus(37)
public void as_unsupportedEncryptAsCausesExit37() throws SOPGPException.UnsupportedOption {
when(encrypt.mode(any())).thenThrow(new SOPGPException.UnsupportedOption());
SopCLI.main(new String[] {"encrypt", "--as", "Binary"});
}
@Test
@ExpectSystemExitWithStatus(37)
public void as_invalidModeOptionCausesExit37() {
SopCLI.main(new String[] {"encrypt", "--as", "invalid"});
}
@Test
public void as_modeIsPassedDown() throws SOPGPException.UnsupportedOption {
for (EncryptAs mode : EncryptAs.values()) {
SopCLI.main(new String[] {"encrypt", "--as", mode.name(), "--with-password", "0rbit"});
verify(encrypt, times(1)).mode(mode);
}
}
@Test
@ExpectSystemExitWithStatus(31)
public void withPassword_notHumanReadablePasswordCausesExit31() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption {
when(encrypt.withPassword("pretendThisIsNotReadable")).thenThrow(new SOPGPException.PasswordNotHumanReadable());
SopCLI.main(new String[] {"encrypt", "--with-password", "pretendThisIsNotReadable"});
}
@Test
@ExpectSystemExitWithStatus(37)
public void withPassword_unsupportedWithPasswordCausesExit37() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption {
when(encrypt.withPassword(any())).thenThrow(new SOPGPException.UnsupportedOption());
SopCLI.main(new String[] {"encrypt", "--with-password", "orange"});
}
@Test
public void signWith_multipleTimesGetPassedDown() throws IOException, SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotSign, SOPGPException.BadData {
File keyFile1 = File.createTempFile("sign-with-1-", ".asc");
File keyFile2 = File.createTempFile("sign-with-2-", ".asc");
SopCLI.main(new String[] {"encrypt", "--with-password", "password", "--sign-with", keyFile1.getAbsolutePath(), "--sign-with", keyFile2.getAbsolutePath()});
verify(encrypt, times(2)).signWith(any());
}
@Test
@ExpectSystemExitWithStatus(1)
public void signWith_nonExistentKeyFileCausesExit1() {
SopCLI.main(new String[] {"encrypt", "--with-password", "admin", "--sign-with", "nonExistent.asc"});
}
@Test
@ExpectSystemExitWithStatus(1)
public void signWith_keyIsProtectedCausesExit1() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotSign, SOPGPException.BadData, IOException {
when(encrypt.signWith(any())).thenThrow(new SOPGPException.KeyIsProtected());
File keyFile = File.createTempFile("sign-with", ".asc");
SopCLI.main(new String[] {"encrypt", "--sign-with", keyFile.getAbsolutePath(), "--with-password", "starship"});
}
@Test
@ExpectSystemExitWithStatus(13)
public void signWith_unsupportedAsymmetricAlgoCausesExit13() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotSign, SOPGPException.BadData, IOException {
when(encrypt.signWith(any())).thenThrow(new SOPGPException.UnsupportedAsymmetricAlgo(new Exception()));
File keyFile = File.createTempFile("sign-with", ".asc");
SopCLI.main(new String[] {"encrypt", "--with-password", "123456", "--sign-with", keyFile.getAbsolutePath()});
}
@Test
@ExpectSystemExitWithStatus(1)
public void signWith_certCannotSignCausesExit1() throws IOException, SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotSign, SOPGPException.BadData {
when(encrypt.signWith(any())).thenThrow(new SOPGPException.CertCannotSign());
File keyFile = File.createTempFile("sign-with", ".asc");
SopCLI.main(new String[] {"encrypt", "--with-password", "dragon", "--sign-with", keyFile.getAbsolutePath()});
}
@Test
@ExpectSystemExitWithStatus(41)
public void signWith_badDataCausesExit41() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotSign, SOPGPException.BadData, IOException {
when(encrypt.signWith(any())).thenThrow(new SOPGPException.BadData(new IOException()));
File keyFile = File.createTempFile("sign-with", ".asc");
SopCLI.main(new String[] {"encrypt", "--with-password", "orange", "--sign-with", keyFile.getAbsolutePath()});
}
@Test
@ExpectSystemExitWithStatus(1)
public void cert_nonExistentCertFileCausesExit1() {
SopCLI.main(new String[] {"encrypt", "invalid.asc"});
}
@Test
@ExpectSystemExitWithStatus(13)
public void cert_unsupportedAsymmetricAlgorithmCausesExit13() throws IOException, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotEncrypt, SOPGPException.BadData {
when(encrypt.withCert(any())).thenThrow(new SOPGPException.UnsupportedAsymmetricAlgo(new Exception()));
File certFile = File.createTempFile("cert", ".asc");
SopCLI.main(new String[] {"encrypt", certFile.getAbsolutePath()});
}
@Test
@ExpectSystemExitWithStatus(17)
public void cert_certCannotEncryptCausesExit17() throws IOException, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotEncrypt, SOPGPException.BadData {
when(encrypt.withCert(any())).thenThrow(new SOPGPException.CertCannotEncrypt());
File certFile = File.createTempFile("cert", ".asc");
SopCLI.main(new String[] {"encrypt", certFile.getAbsolutePath()});
}
@Test
@ExpectSystemExitWithStatus(41)
public void cert_badDataCausesExit41() throws IOException, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotEncrypt, SOPGPException.BadData {
when(encrypt.withCert(any())).thenThrow(new SOPGPException.BadData(new IOException()));
File certFile = File.createTempFile("cert", ".asc");
SopCLI.main(new String[] {"encrypt", certFile.getAbsolutePath()});
}
@Test
public void noArmor_notCalledByDefault() {
SopCLI.main(new String[] {"encrypt", "--with-password", "clownfish"});
verify(encrypt, never()).noArmor();
}
@Test
public void noArmor_callGetsPassedDown() {
SopCLI.main(new String[] {"encrypt", "--with-password", "monkey", "--no-armor"});
verify(encrypt, times(1)).noArmor();
}
@Test
@ExpectSystemExitWithStatus(1)
public void writeTo_ioExceptionCausesExit1() throws IOException {
when(encrypt.plaintext(any())).thenReturn(new Ready() {
@Override
public void writeTo(OutputStream outputStream) throws IOException {
throw new IOException();
}
});
SopCLI.main(new String[] {"encrypt", "--with-password", "wildcat"});
}
}

View file

@ -0,0 +1,86 @@
/*
* 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 sop.cli.picocli.commands;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.io.IOException;
import java.io.OutputStream;
import com.ginsberg.junit.exit.ExpectSystemExitWithStatus;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import sop.Ready;
import sop.SOP;
import sop.cli.picocli.SopCLI;
import sop.exception.SOPGPException;
import sop.operation.ExtractCert;
public class ExtractCertCmdTest {
ExtractCert extractCert;
@BeforeEach
public void mockComponents() throws IOException, SOPGPException.BadData {
extractCert = mock(ExtractCert.class);
when(extractCert.key(any())).thenReturn(new Ready() {
@Override
public void writeTo(OutputStream outputStream) {
}
});
SOP sop = mock(SOP.class);
when(sop.extractCert()).thenReturn(extractCert);
SopCLI.setSopInstance(sop);
}
@Test
public void noArmor_notCalledByDefault() {
SopCLI.main(new String[] {"extract-cert"});
verify(extractCert, never()).noArmor();
}
@Test
public void noArmor_passedDown() {
SopCLI.main(new String[] {"extract-cert", "--no-armor"});
verify(extractCert, times(1)).noArmor();
}
@Test
@ExpectSystemExitWithStatus(1)
public void key_ioExceptionCausesExit1() throws IOException, SOPGPException.BadData {
when(extractCert.key(any())).thenReturn(new Ready() {
@Override
public void writeTo(OutputStream outputStream) throws IOException {
throw new IOException();
}
});
SopCLI.main(new String[] {"extract-cert"});
}
@Test
@ExpectSystemExitWithStatus(41)
public void key_badDataCausesExit41() throws IOException, SOPGPException.BadData {
when(extractCert.key(any())).thenThrow(new SOPGPException.BadData(new IOException()));
SopCLI.main(new String[] {"extract-cert"});
}
}

View file

@ -0,0 +1,109 @@
/*
* 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 sop.cli.picocli.commands;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.io.IOException;
import java.io.OutputStream;
import com.ginsberg.junit.exit.ExpectSystemExitWithStatus;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InOrder;
import org.mockito.Mockito;
import sop.Ready;
import sop.SOP;
import sop.cli.picocli.SopCLI;
import sop.exception.SOPGPException;
import sop.operation.GenerateKey;
public class GenerateKeyCmdTest {
GenerateKey generateKey;
@BeforeEach
public void mockComponents() throws SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.MissingArg, IOException {
generateKey = mock(GenerateKey.class);
when(generateKey.generate()).thenReturn(new Ready() {
@Override
public void writeTo(OutputStream outputStream) {
}
});
SOP sop = mock(SOP.class);
when(sop.generateKey()).thenReturn(generateKey);
SopCLI.setSopInstance(sop);
}
@Test
public void noArmor_notCalledByDefault() {
SopCLI.main(new String[] {"generate-key", "Alice"});
verify(generateKey, never()).noArmor();
}
@Test
public void noArmor_passedDown() {
SopCLI.main(new String[] {"generate-key", "--no-armor", "Alice"});
verify(generateKey, times(1)).noArmor();
}
@Test
public void userId_multipleUserIdsPassedDownInProperOrder() {
SopCLI.main(new String[] {"generate-key", "Alice <alice@pgpainless.org>", "Bob <bob@pgpainless.org>"});
InOrder inOrder = Mockito.inOrder(generateKey);
inOrder.verify(generateKey).userId("Alice <alice@pgpainless.org>");
inOrder.verify(generateKey).userId("Bob <bob@pgpainless.org>");
verify(generateKey, times(2)).userId(any());
}
@Test
@ExpectSystemExitWithStatus(19)
public void missingArgumentCausesExit19() throws SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.MissingArg, IOException {
// TODO: RFC4880-bis and the current Stateless OpenPGP CLI spec allow keys to have no user-ids,
// so we might want to change this test in the future.
when(generateKey.generate()).thenThrow(new SOPGPException.MissingArg("Missing user-id."));
SopCLI.main(new String[] {"generate-key"});
}
@Test
@ExpectSystemExitWithStatus(13)
public void unsupportedAsymmetricAlgorithmCausesExit13() throws SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.MissingArg, IOException {
when(generateKey.generate()).thenThrow(new SOPGPException.UnsupportedAsymmetricAlgo(new Exception()));
SopCLI.main(new String[] {"generate-key", "Alice"});
}
@Test
@ExpectSystemExitWithStatus(1)
public void ioExceptionCausesExit1() throws SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.MissingArg, IOException {
when(generateKey.generate()).thenReturn(new Ready() {
@Override
public void writeTo(OutputStream outputStream) throws IOException {
throw new IOException();
}
});
SopCLI.main(new String[] {"generate-key", "Alice"});
}
}

View file

@ -0,0 +1,137 @@
/*
* Copyright 2021 Paul Schaub.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sop.cli.picocli.commands;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import com.ginsberg.junit.exit.ExpectSystemExitWithStatus;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import sop.Ready;
import sop.SOP;
import sop.cli.picocli.SopCLI;
import sop.exception.SOPGPException;
import sop.operation.Sign;
public class SignCmdTest {
Sign sign;
File keyFile;
@BeforeEach
public void mockComponents() throws IOException, SOPGPException.ExpectedText {
sign = mock(Sign.class);
when(sign.data(any())).thenReturn(new Ready() {
@Override
public void writeTo(OutputStream outputStream) {
}
});
SOP sop = mock(SOP.class);
when(sop.sign()).thenReturn(sign);
SopCLI.setSopInstance(sop);
keyFile = File.createTempFile("sign-", ".asc");
}
@Test
public void as_optionsAreCaseInsensitive() {
SopCLI.main(new String[] {"sign", "--as", "Binary", keyFile.getAbsolutePath()});
SopCLI.main(new String[] {"sign", "--as", "binary", keyFile.getAbsolutePath()});
SopCLI.main(new String[] {"sign", "--as", "BINARY", keyFile.getAbsolutePath()});
}
@Test
@ExpectSystemExitWithStatus(37)
public void as_invalidOptionCausesExit37() {
SopCLI.main(new String[] {"sign", "--as", "Invalid", keyFile.getAbsolutePath()});
}
@Test
@ExpectSystemExitWithStatus(37)
public void as_unsupportedOptionCausesExit37() throws SOPGPException.UnsupportedOption {
when(sign.mode(any())).thenThrow(new SOPGPException.UnsupportedOption());
SopCLI.main(new String[] {"sign", "--as", "binary", keyFile.getAbsolutePath()});
}
@Test
@ExpectSystemExitWithStatus(1)
public void key_nonExistentKeyFileCausesExit1() {
SopCLI.main(new String[] {"sign", "invalid.asc"});
}
@Test
@ExpectSystemExitWithStatus(1)
public void key_keyIsProtectedCausesExit1() throws SOPGPException.KeyIsProtected, IOException, SOPGPException.BadData {
when(sign.key(any())).thenThrow(new SOPGPException.KeyIsProtected());
SopCLI.main(new String[] {"sign", keyFile.getAbsolutePath()});
}
@Test
@ExpectSystemExitWithStatus(41)
public void key_badDataCausesExit41() throws SOPGPException.KeyIsProtected, IOException, SOPGPException.BadData {
when(sign.key(any())).thenThrow(new SOPGPException.BadData(new IOException()));
SopCLI.main(new String[] {"sign", keyFile.getAbsolutePath()});
}
@Test
@ExpectSystemExitWithStatus(19)
public void key_missingKeyFileCausesExit19() {
SopCLI.main(new String[] {"sign"});
}
@Test
public void noArmor_notCalledByDefault() {
SopCLI.main(new String[] {"sign", keyFile.getAbsolutePath()});
verify(sign, never()).noArmor();
}
@Test
public void noArmor_passedDown() {
SopCLI.main(new String[] {"sign", "--no-armor", keyFile.getAbsolutePath()});
verify(sign, times(1)).noArmor();
}
@Test
@ExpectSystemExitWithStatus(1)
public void data_ioExceptionCausesExit1() throws IOException, SOPGPException.ExpectedText {
when(sign.data(any())).thenReturn(new Ready() {
@Override
public void writeTo(OutputStream outputStream) throws IOException {
throw new IOException();
}
});
SopCLI.main(new String[] {"sign", keyFile.getAbsolutePath()});
}
@Test
@ExpectSystemExitWithStatus(53)
public void data_expectedTextExceptionCausesExit53() throws IOException, SOPGPException.ExpectedText {
when(sign.data(any())).thenThrow(new SOPGPException.ExpectedText());
SopCLI.main(new String[] {"sign", keyFile.getAbsolutePath()});
}
}

View file

@ -0,0 +1,214 @@
/*
* 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 sop.cli.picocli.commands;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import com.ginsberg.junit.exit.ExpectSystemExitWithStatus;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentMatchers;
import sop.SOP;
import sop.Verification;
import sop.cli.picocli.DateParser;
import sop.cli.picocli.SopCLI;
import sop.exception.SOPGPException;
import sop.operation.Verify;
import sop.util.UTCUtil;
public class VerifyCmdTest {
Verify verify;
File signature;
File cert;
PrintStream originalSout;
@BeforeEach
public void prepare() throws SOPGPException.UnsupportedOption, SOPGPException.BadData, SOPGPException.NoSignature, IOException {
originalSout = System.out;
verify = mock(Verify.class);
when(verify.notBefore(any())).thenReturn(verify);
when(verify.notAfter(any())).thenReturn(verify);
when(verify.cert(any())).thenReturn(verify);
when(verify.signatures(any())).thenReturn(verify);
when(verify.data(any())).thenReturn(
Collections.singletonList(
new Verification(
UTCUtil.parseUTCDate("2019-10-29T18:36:45Z"),
"EB85BB5FA33A75E15E944E63F231550C4F47E38E",
"EB85BB5FA33A75E15E944E63F231550C4F47E38E")
)
);
SOP sop = mock(SOP.class);
when(sop.verify()).thenReturn(verify);
SopCLI.setSopInstance(sop);
signature = File.createTempFile("signature-", ".asc");
cert = File.createTempFile("cert-", ".asc");
}
@AfterEach
public void restoreSout() {
System.setOut(originalSout);
}
@Test
public void notAfter_passedDown() throws SOPGPException.UnsupportedOption {
Date date = UTCUtil.parseUTCDate("2019-10-29T18:36:45Z");
SopCLI.main(new String[] {"verify", "--not-after", "2019-10-29T18:36:45Z", signature.getAbsolutePath(), cert.getAbsolutePath()});
verify(verify, times(1)).notAfter(date);
}
@Test
public void notAfter_now() throws SOPGPException.UnsupportedOption {
Date now = new Date();
SopCLI.main(new String[] {"verify", "--not-after", "now", signature.getAbsolutePath(), cert.getAbsolutePath()});
verify(verify, times(1)).notAfter(dateMatcher(now));
}
@Test
public void notAfter_dashCountsAsEndOfTime() throws SOPGPException.UnsupportedOption {
SopCLI.main(new String[] {"verify", "--not-after", "-", signature.getAbsolutePath(), cert.getAbsolutePath()});
verify(verify, times(1)).notAfter(DateParser.END_OF_TIME);
}
@Test
@ExpectSystemExitWithStatus(37)
public void notAfter_unsupportedOptionCausesExit37() throws SOPGPException.UnsupportedOption {
when(verify.notAfter(any())).thenThrow(new SOPGPException.UnsupportedOption());
SopCLI.main(new String[] {"verify", "--not-after", "2019-10-29T18:36:45Z", signature.getAbsolutePath(), cert.getAbsolutePath()});
}
@Test
public void notBefore_passedDown() throws SOPGPException.UnsupportedOption {
Date date = UTCUtil.parseUTCDate("2019-10-29T18:36:45Z");
SopCLI.main(new String[] {"verify", "--not-before", "2019-10-29T18:36:45Z", signature.getAbsolutePath(), cert.getAbsolutePath()});
verify(verify, times(1)).notBefore(date);
}
@Test
public void notBefore_now() throws SOPGPException.UnsupportedOption {
Date now = new Date();
SopCLI.main(new String[] {"verify", "--not-before", "now", signature.getAbsolutePath(), cert.getAbsolutePath()});
verify(verify, times(1)).notBefore(dateMatcher(now));
}
@Test
public void notBefore_dashCountsAsBeginningOfTime() throws SOPGPException.UnsupportedOption {
SopCLI.main(new String[] {"verify", "--not-before", "-", signature.getAbsolutePath(), cert.getAbsolutePath()});
verify(verify, times(1)).notBefore(DateParser.BEGINNING_OF_TIME);
}
@Test
@ExpectSystemExitWithStatus(37)
public void notBefore_unsupportedOptionCausesExit37() throws SOPGPException.UnsupportedOption {
when(verify.notBefore(any())).thenThrow(new SOPGPException.UnsupportedOption());
SopCLI.main(new String[] {"verify", "--not-before", "2019-10-29T18:36:45Z", signature.getAbsolutePath(), cert.getAbsolutePath()});
}
@Test
public void notBeforeAndNotAfterAreCalledWithDefaultValues() throws SOPGPException.UnsupportedOption {
SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), cert.getAbsolutePath()});
verify(verify, times(1)).notAfter(dateMatcher(new Date()));
verify(verify, times(1)).notBefore(DateParser.BEGINNING_OF_TIME);
}
private static Date dateMatcher(Date date) {
return ArgumentMatchers.argThat(argument -> Math.abs(argument.getTime() - date.getTime()) < 1000);
}
@Test
@ExpectSystemExitWithStatus(1)
public void cert_fileNotFoundCausesExit1() {
SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), "invalid.asc"});
}
@Test
@ExpectSystemExitWithStatus(41)
public void cert_badDataCausesExit41() throws SOPGPException.BadData {
when(verify.cert(any())).thenThrow(new SOPGPException.BadData(new IOException()));
SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), cert.getAbsolutePath()});
}
@Test
@ExpectSystemExitWithStatus(1)
public void signature_fileNotFoundCausesExit1() {
SopCLI.main(new String[] {"verify", "invalid.sig", cert.getAbsolutePath()});
}
@Test
@ExpectSystemExitWithStatus(41)
public void signature_badDataCausesExit41() throws SOPGPException.BadData {
when(verify.signatures(any())).thenThrow(new SOPGPException.BadData(new IOException()));
SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), cert.getAbsolutePath()});
}
@Test
@ExpectSystemExitWithStatus(3)
public void data_noSignaturesCausesExit3() throws SOPGPException.NoSignature, IOException, SOPGPException.BadData {
when(verify.data(any())).thenThrow(new SOPGPException.NoSignature());
SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), cert.getAbsolutePath()});
}
@Test
@ExpectSystemExitWithStatus(41)
public void data_badDataCausesExit41() throws SOPGPException.NoSignature, IOException, SOPGPException.BadData {
when(verify.data(any())).thenThrow(new SOPGPException.BadData(new IOException()));
SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), cert.getAbsolutePath()});
}
@Test
public void resultIsPrintedProperly() throws SOPGPException.NoSignature, IOException, SOPGPException.BadData {
when(verify.data(any())).thenReturn(Arrays.asList(
new Verification(UTCUtil.parseUTCDate("2019-10-29T18:36:45Z"),
"EB85BB5FA33A75E15E944E63F231550C4F47E38E",
"EB85BB5FA33A75E15E944E63F231550C4F47E38E"),
new Verification(UTCUtil.parseUTCDate("2019-10-24T23:48:29Z"),
"C90E6D36200A1B922A1509E77618196529AE5FF8",
"C4BC2DDB38CCE96485EBE9C2F20691179038E5C6")
));
ByteArrayOutputStream out = new ByteArrayOutputStream();
System.setOut(new PrintStream(out));
SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), cert.getAbsolutePath()});
System.setOut(originalSout);
String expected = "2019-10-29T18:36:45Z EB85BB5FA33A75E15E944E63F231550C4F47E38E EB85BB5FA33A75E15E944E63F231550C4F47E38E\n" +
"2019-10-24T23:48:29Z C90E6D36200A1B922A1509E77618196529AE5FF8 C4BC2DDB38CCE96485EBE9C2F20691179038E5C6\n";
assertEquals(expected, out.toString());
}
}

View file

@ -0,0 +1,58 @@
/*
* Copyright 2021 Paul Schaub.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sop.cli.picocli.commands;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.ginsberg.junit.exit.ExpectSystemExitWithStatus;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import sop.SOP;
import sop.cli.picocli.SopCLI;
import sop.operation.Version;
public class VersionCmdTest {
private SOP sop;
private Version version;
@BeforeEach
public void mockComponents() {
sop = mock(SOP.class);
version = mock(Version.class);
when(version.getName()).thenReturn("MockSop");
when(version.getVersion()).thenReturn("1.0");
when(sop.version()).thenReturn(version);
SopCLI.setSopInstance(sop);
}
@Test
public void assertVersionCommandWorks() {
SopCLI.main(new String[] {"version"});
verify(version, times(1)).getVersion();
verify(version, times(1)).getName();
}
@Test
@ExpectSystemExitWithStatus(37)
public void assertInvalidOptionResultsInExit37() {
SopCLI.main(new String[] {"version", "--invalid"});
}
}