diff --git a/build.gradle b/build.gradle
index dbcd1038..5ae633c7 100644
--- a/build.gradle
+++ b/build.gradle
@@ -27,7 +27,7 @@ allprojects {
apply plugin: 'checkstyle'
// For non-sop modules, enable android api compatibility check
- if (!it.name.equals('pgpainless-sop')) {
+ if (it.name.equals('pgpainless-core') || it.name.equals('sop-java') || it.name.equals('pgpainless-sop')) {
// animalsniffer
apply plugin: 'ru.vyarus.animalsniffer'
dependencies {
diff --git a/pgpainless-sop/README.md b/pgpainless-cli/README.md
similarity index 100%
rename from pgpainless-sop/README.md
rename to pgpainless-cli/README.md
diff --git a/pgpainless-cli/build.gradle b/pgpainless-cli/build.gradle
new file mode 100644
index 00000000..cae9c78f
--- /dev/null
+++ b/pgpainless-cli/build.gradle
@@ -0,0 +1,66 @@
+plugins {
+ id 'application'
+}
+def generatedVersionDir = "${buildDir}/generated-version"
+
+sourceSets {
+ main {
+ output.dir(generatedVersionDir, builtBy: 'generateVersionProperties')
+ }
+}
+
+task generateVersionProperties {
+ doLast {
+ def propertiesFile = file "$generatedVersionDir/version.properties"
+ propertiesFile.parentFile.mkdirs()
+ propertiesFile.createNewFile()
+ // Instead of using a Properties object here, we directly write to the file
+ // since Properties adds a timestamp, ruining reproducibility
+ propertiesFile.write("version="+rootProject.version.toString())
+ }
+}
+processResources.dependsOn generateVersionProperties
+
+dependencies {
+ implementation(project(":pgpainless-sop"))
+ implementation(project(":sop-java"))
+
+ implementation(project(":sop-java-picocli"))
+ implementation 'info.picocli:picocli:4.5.2'
+
+ testImplementation(project(":pgpainless-core"))
+
+ 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'
+
+ /*
+ implementation "org.bouncycastle:bcprov-debug-jdk15on:$bouncyCastleVersion"
+ /*/
+ implementation "org.bouncycastle:bcprov-jdk15on:$bouncyCastleVersion"
+ //*/
+ implementation "org.bouncycastle:bcpg-jdk15on:$bouncyCastleVersion"
+
+ // https://mvnrepository.com/artifact/com.google.code.findbugs/jsr305
+ implementation group: 'com.google.code.findbugs', name: 'jsr305', version: '3.0.2'
+}
+
+mainClassName = 'org.pgpainless.cli.PGPainlessCLI'
+
+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"
+ }
+}
+
+tasks."jar".dependsOn(":pgpainless-core:assemble")
diff --git a/pgpainless-sop/pgpainless b/pgpainless-cli/pgpainless
similarity index 100%
rename from pgpainless-sop/pgpainless
rename to pgpainless-cli/pgpainless
diff --git a/pgpainless-cli/src/main/java/org/pgpainless/cli/PGPainlessCLI.java b/pgpainless-cli/src/main/java/org/pgpainless/cli/PGPainlessCLI.java
new file mode 100644
index 00000000..736607eb
--- /dev/null
+++ b/pgpainless-cli/src/main/java/org/pgpainless/cli/PGPainlessCLI.java
@@ -0,0 +1,37 @@
+/*
+ * 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.cli;
+
+import org.pgpainless.sop.SOPImpl;
+import sop.cli.picocli.SopCLI;
+
+public class PGPainlessCLI {
+
+ static {
+ SopCLI.setSopInstance(new SOPImpl());
+ }
+
+ public static void main(String[] args) {
+ int result = execute(args);
+ if (result != 0) {
+ System.exit(result);
+ }
+ }
+
+ public static int execute(String... args) {
+ return SopCLI.execute(args);
+ }
+}
diff --git a/pgpainless-cli/src/main/java/org/pgpainless/cli/package-info.java b/pgpainless-cli/src/main/java/org/pgpainless/cli/package-info.java
new file mode 100644
index 00000000..2eabeaba
--- /dev/null
+++ b/pgpainless-cli/src/main/java/org/pgpainless/cli/package-info.java
@@ -0,0 +1,22 @@
+/*
+ * 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.
+ */
+/**
+ * PGPainless SOP implementing a Stateless OpenPGP Command Line Interface.
+ * @see
+ * Stateless OpenPGP Command Line Interface
+ * draft-dkg-openpgp-stateless-cli-01
+ */
+package org.pgpainless.cli;
diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/ExitCodeTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/ExitCodeTest.java
similarity index 62%
rename from pgpainless-sop/src/test/java/org/pgpainless/sop/ExitCodeTest.java
rename to pgpainless-cli/src/test/java/org/pgpainless/cli/ExitCodeTest.java
index e8933b19..c9f88cca 100644
--- a/pgpainless-sop/src/test/java/org/pgpainless/sop/ExitCodeTest.java
+++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/ExitCodeTest.java
@@ -13,27 +13,29 @@
* 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;
+package org.pgpainless.cli;
+import com.ginsberg.junit.exit.ExpectSystemExitWithStatus;
+import com.ginsberg.junit.exit.FailOnSystemExit;
import org.junit.jupiter.api.Test;
-import picocli.CommandLine;
public class ExitCodeTest {
@Test
+ @ExpectSystemExitWithStatus(69)
public void testUnknownCommand_69() {
- assertEquals(69, new CommandLine(new PGPainlessCLI()).execute("generate-kex"));
+ PGPainlessCLI.main(new String[] {"generate-kex"});
}
@Test
+ @ExpectSystemExitWithStatus(37)
public void testCommandWithUnknownOption_37() {
- assertEquals(37, new CommandLine(new PGPainlessCLI()).execute("generate-key", "-k", "\"k is unknown\""));
+ PGPainlessCLI.main(new String[] {"generate-key", "-k", "\"k is unknown\""});
}
@Test
- public void successfulVersion_0 () {
- assertEquals(0, new CommandLine(new PGPainlessCLI()).execute("version"));
+ @FailOnSystemExit
+ public void successfulExecutionDoesNotTerminateJVM() {
+ PGPainlessCLI.main(new String[] {"version"});
}
}
diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/TestUtils.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/TestUtils.java
similarity index 98%
rename from pgpainless-sop/src/test/java/org/pgpainless/sop/TestUtils.java
rename to pgpainless-cli/src/test/java/org/pgpainless/cli/TestUtils.java
index 9f5e74a2..ca525814 100644
--- a/pgpainless-sop/src/test/java/org/pgpainless/sop/TestUtils.java
+++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/TestUtils.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.pgpainless.sop;
+package org.pgpainless.cli;
import java.io.File;
import java.io.IOException;
diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/commands/ArmorTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/ArmorTest.java
similarity index 91%
rename from pgpainless-sop/src/test/java/org/pgpainless/sop/commands/ArmorTest.java
rename to pgpainless-cli/src/test/java/org/pgpainless/cli/commands/ArmorTest.java
index 8df3ea87..0e938fa8 100644
--- a/pgpainless-sop/src/test/java/org/pgpainless/sop/commands/ArmorTest.java
+++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/ArmorTest.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.pgpainless.sop.commands;
+package org.pgpainless.cli.commands;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -28,6 +28,7 @@ import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.NoSuchAlgorithmException;
+import com.ginsberg.junit.exit.FailOnSystemExit;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
@@ -35,8 +36,7 @@ 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;
+import org.pgpainless.cli.PGPainlessCLI;
public class ArmorTest {
@@ -53,6 +53,7 @@ public class ArmorTest {
}
@Test
+ @FailOnSystemExit
public void armorSecretKey() throws IOException, PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException {
PGPSecretKeyRing secretKey = PGPainless.generateKeyRing()
.modernKeyRing("alice@pgpainless.org", null);
@@ -61,13 +62,14 @@ public class ArmorTest {
System.setIn(new ByteArrayInputStream(bytes));
ByteArrayOutputStream armorOut = new ByteArrayOutputStream();
System.setOut(new PrintStream(armorOut));
- new CommandLine(new PGPainlessCLI()).execute("armor");
+ PGPainlessCLI.execute("armor");
PGPSecretKeyRing armored = PGPainless.readKeyRing().secretKeyRing(armorOut.toString());
assertArrayEquals(secretKey.getEncoded(), armored.getEncoded());
}
@Test
+ @FailOnSystemExit
public void armorPublicKey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException {
PGPSecretKeyRing secretKey = PGPainless.generateKeyRing()
.modernKeyRing("alice@pgpainless.org", null);
@@ -77,20 +79,21 @@ public class ArmorTest {
System.setIn(new ByteArrayInputStream(bytes));
ByteArrayOutputStream armorOut = new ByteArrayOutputStream();
System.setOut(new PrintStream(armorOut));
- new CommandLine(new PGPainlessCLI()).execute("armor");
+ PGPainlessCLI.execute("armor");
PGPPublicKeyRing armored = PGPainless.readKeyRing().publicKeyRing(armorOut.toString());
assertArrayEquals(publicKey.getEncoded(), armored.getEncoded());
}
@Test
+ @FailOnSystemExit
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");
+ PGPainlessCLI.execute("armor");
String armored = armorOut.toString();
@@ -99,6 +102,7 @@ public class ArmorTest {
}
@Test
+ @FailOnSystemExit
public void doesNotNestArmorByDefault() {
String armored = "-----BEGIN PGP MESSAGE-----\n" +
"Version: BCPG v1.69\n" +
@@ -110,12 +114,13 @@ public class ArmorTest {
System.setIn(new ByteArrayInputStream(armored.getBytes(StandardCharsets.UTF_8)));
ByteArrayOutputStream out = new ByteArrayOutputStream();
System.setOut(new PrintStream(out));
- new CommandLine(new PGPainlessCLI()).execute("armor");
+ PGPainlessCLI.execute("armor");
assertEquals(armored, out.toString());
}
@Test
+ @FailOnSystemExit
public void testAllowNested() {
String armored = "-----BEGIN PGP MESSAGE-----\n" +
"Version: BCPG v1.69\n" +
@@ -127,7 +132,7 @@ public class ArmorTest {
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");
+ PGPainlessCLI.execute("armor", "--allow-nested");
assertNotEquals(armored, out.toString());
assertTrue(out.toString().contains(
diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/commands/DearmorTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/DearmorTest.java
similarity index 91%
rename from pgpainless-sop/src/test/java/org/pgpainless/sop/commands/DearmorTest.java
rename to pgpainless-cli/src/test/java/org/pgpainless/cli/commands/DearmorTest.java
index 0703693d..d735adab 100644
--- a/pgpainless-sop/src/test/java/org/pgpainless/sop/commands/DearmorTest.java
+++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/DearmorTest.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.pgpainless.sop.commands;
+package org.pgpainless.cli.commands;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -26,6 +26,7 @@ import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.NoSuchAlgorithmException;
+import com.ginsberg.junit.exit.FailOnSystemExit;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
@@ -33,8 +34,7 @@ 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;
+import org.pgpainless.cli.PGPainlessCLI;
public class DearmorTest {
@@ -51,6 +51,7 @@ public class DearmorTest {
}
@Test
+ @FailOnSystemExit
public void dearmorSecretKey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException {
PGPSecretKeyRing secretKey = PGPainless.generateKeyRing()
.modernKeyRing("alice@pgpainless.org", null);
@@ -59,13 +60,14 @@ public class DearmorTest {
System.setIn(new ByteArrayInputStream(armored.getBytes(StandardCharsets.UTF_8)));
ByteArrayOutputStream out = new ByteArrayOutputStream();
System.setOut(new PrintStream(out));
- new CommandLine(new PGPainlessCLI()).execute("dearmor");
+ PGPainlessCLI.execute("dearmor");
assertArrayEquals(secretKey.getEncoded(), out.toByteArray());
}
@Test
+ @FailOnSystemExit
public void dearmorCertificate() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException {
PGPSecretKeyRing secretKey = PGPainless.generateKeyRing()
.modernKeyRing("alice@pgpainless.org", null);
@@ -75,12 +77,13 @@ public class DearmorTest {
System.setIn(new ByteArrayInputStream(armored.getBytes(StandardCharsets.UTF_8)));
ByteArrayOutputStream out = new ByteArrayOutputStream();
System.setOut(new PrintStream(out));
- new CommandLine(new PGPainlessCLI()).execute("dearmor");
+ PGPainlessCLI.execute("dearmor");
assertArrayEquals(certificate.getEncoded(), out.toByteArray());
}
@Test
+ @FailOnSystemExit
public void dearmorMessage() {
String armored = "-----BEGIN PGP MESSAGE-----\n" +
"Version: BCPG v1.69\n" +
@@ -92,7 +95,7 @@ public class DearmorTest {
System.setIn(new ByteArrayInputStream(armored.getBytes(StandardCharsets.UTF_8)));
ByteArrayOutputStream out = new ByteArrayOutputStream();
System.setOut(new PrintStream(out));
- new CommandLine(new PGPainlessCLI()).execute("dearmor");
+ PGPainlessCLI.execute("dearmor");
assertEquals("Hello, World\n", out.toString());
}
diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/commands/EncryptDecryptTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/EncryptDecryptTest.java
similarity index 85%
rename from pgpainless-sop/src/test/java/org/pgpainless/sop/commands/EncryptDecryptTest.java
rename to pgpainless-cli/src/test/java/org/pgpainless/cli/commands/EncryptDecryptTest.java
index a521faf2..96d7ef70 100644
--- a/pgpainless-sop/src/test/java/org/pgpainless/sop/commands/EncryptDecryptTest.java
+++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/EncryptDecryptTest.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.pgpainless.sop.commands;
+package org.pgpainless.cli.commands;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -28,12 +28,12 @@ import java.io.OutputStream;
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
+import com.ginsberg.junit.exit.FailOnSystemExit;
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;
+import org.pgpainless.cli.PGPainlessCLI;
+import org.pgpainless.cli.TestUtils;
public class EncryptDecryptTest {
@@ -46,7 +46,8 @@ public class EncryptDecryptTest {
}
@Test
- public void test() throws IOException {
+ @FailOnSystemExit
+ public void encryptAndDecryptAMessage() throws IOException {
originalSout = System.out;
File julietKeyFile = new File(tempDir, "juliet.key");
assertTrue(julietKeyFile.createNewFile());
@@ -65,27 +66,27 @@ public class EncryptDecryptTest {
OutputStream julietKeyOut = new FileOutputStream(julietKeyFile);
System.setOut(new PrintStream(julietKeyOut));
- new CommandLine(new PGPainlessCLI()).execute("generate-key", "Juliet Capulet ");
+ PGPainlessCLI.execute("generate-key", "Juliet Capulet ");
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");
+ 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 ");
+ PGPainlessCLI.execute("generate-key", "Romeo Montague ");
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");
+ PGPainlessCLI.execute("extract-cert");
romeoKeyIn.close();
romeoCertOut.close();
@@ -94,7 +95,7 @@ public class EncryptDecryptTest {
System.setIn(msgIn);
OutputStream msgAscOut = new FileOutputStream(msgAscFile);
System.setOut(new PrintStream(msgAscOut));
- new CommandLine(new PGPainlessCLI()).execute("encrypt",
+ PGPainlessCLI.execute("encrypt",
"--sign-with", romeoKeyFile.getAbsolutePath(),
julietCertFile.getAbsolutePath());
msgAscOut.close();
@@ -107,7 +108,7 @@ public class EncryptDecryptTest {
ByteArrayOutputStream out = new ByteArrayOutputStream();
PrintStream pOut = new PrintStream(out);
System.setOut(pOut);
- new CommandLine(new PGPainlessCLI()).execute("decrypt",
+ PGPainlessCLI.execute("decrypt",
"--verify-out", verifyFile.getAbsolutePath(),
"--verify-with", romeoCertFile.getAbsolutePath(),
julietKeyFile.getAbsolutePath());
diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/commands/ExtractCertTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/ExtractCertTest.java
similarity index 91%
rename from pgpainless-sop/src/test/java/org/pgpainless/sop/commands/ExtractCertTest.java
rename to pgpainless-cli/src/test/java/org/pgpainless/cli/commands/ExtractCertTest.java
index e9e52e75..e3d0175b 100644
--- a/pgpainless-sop/src/test/java/org/pgpainless/sop/commands/ExtractCertTest.java
+++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/ExtractCertTest.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.pgpainless.sop.commands;
+package org.pgpainless.cli.commands;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -25,18 +25,19 @@ import java.io.PrintStream;
import java.security.InvalidAlgorithmParameterException;
import java.security.NoSuchAlgorithmException;
+import com.ginsberg.junit.exit.FailOnSystemExit;
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.cli.PGPainlessCLI;
import org.pgpainless.key.info.KeyRingInfo;
-import org.pgpainless.sop.PGPainlessCLI;
-import picocli.CommandLine;
public class ExtractCertTest {
@Test
+ @FailOnSystemExit
public void testExtractCert() throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException, IOException {
PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing()
.simpleEcKeyRing("Juliet Capulet ");
@@ -46,7 +47,7 @@ public class ExtractCertTest {
ByteArrayOutputStream out = new ByteArrayOutputStream();
System.setOut(new PrintStream(out));
- new CommandLine(new PGPainlessCLI()).execute("extract-cert");
+ PGPainlessCLI.execute("extract-cert");
PGPPublicKeyRing publicKeys = PGPainless.readKeyRing().publicKeyRing(out.toByteArray());
KeyRingInfo info = PGPainless.inspectKeyRing(publicKeys);
assertFalse(info.isSecretKey());
diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/commands/GenerateCertTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/GenerateCertTest.java
similarity index 83%
rename from pgpainless-sop/src/test/java/org/pgpainless/sop/commands/GenerateCertTest.java
rename to pgpainless-cli/src/test/java/org/pgpainless/cli/commands/GenerateCertTest.java
index 91e7d335..e693c719 100644
--- a/pgpainless-sop/src/test/java/org/pgpainless/sop/commands/GenerateCertTest.java
+++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/GenerateCertTest.java
@@ -13,12 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.pgpainless.sop.commands;
+package org.pgpainless.cli.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 static org.pgpainless.cli.TestUtils.ARMOR_PRIVATE_KEY_HEADER_BYTES;
import java.io.ByteArrayOutputStream;
import java.io.File;
@@ -26,15 +26,15 @@ import java.io.IOException;
import java.io.PrintStream;
import java.util.Arrays;
+import com.ginsberg.junit.exit.FailOnSystemExit;
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.cli.PGPainlessCLI;
+import org.pgpainless.cli.TestUtils;
import org.pgpainless.key.info.KeyRingInfo;
-import org.pgpainless.sop.PGPainlessCLI;
-import org.pgpainless.sop.TestUtils;
-import picocli.CommandLine;
public class GenerateCertTest {
@@ -47,10 +47,11 @@ public class GenerateCertTest {
}
@Test
+ @FailOnSystemExit
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 ");
+ PGPainlessCLI.execute("generate-key", "--armor", "Juliet Capulet ");
PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(out.toByteArray());
KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys);
@@ -62,10 +63,11 @@ public class GenerateCertTest {
}
@Test
+ @FailOnSystemExit
public void testNoArmor() {
ByteArrayOutputStream out = new ByteArrayOutputStream();
System.setOut(new PrintStream(out));
- new CommandLine(new PGPainlessCLI()).execute("generate-key", "--no-armor", "Test ");
+ PGPainlessCLI.execute("generate-key", "--no-armor", "Test ");
byte[] outBegin = new byte[37];
System.arraycopy(out.toByteArray(), 0, outBegin, 0, 37);
diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/commands/SignVerifyTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/SignVerifyTest.java
similarity index 90%
rename from pgpainless-sop/src/test/java/org/pgpainless/sop/commands/SignVerifyTest.java
rename to pgpainless-cli/src/test/java/org/pgpainless/cli/commands/SignVerifyTest.java
index 76c28ba7..27fedbec 100644
--- a/pgpainless-sop/src/test/java/org/pgpainless/sop/commands/SignVerifyTest.java
+++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/SignVerifyTest.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.pgpainless.sop.commands;
+package org.pgpainless.cli.commands;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -32,6 +32,7 @@ import java.security.InvalidAlgorithmParameterException;
import java.security.NoSuchAlgorithmException;
import java.util.Date;
+import com.ginsberg.junit.exit.FailOnSystemExit;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
@@ -40,12 +41,11 @@ 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.cli.PGPainlessCLI;
+import org.pgpainless.cli.TestUtils;
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 {
@@ -60,6 +60,7 @@ public class SignVerifyTest {
}
@Test
+ @FailOnSystemExit
public void testSignatureCreationAndVerification() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException {
originalSout = System.out;
InputStream originalIn = System.in;
@@ -95,7 +96,7 @@ public class SignVerifyTest {
assertTrue(sigFile.createNewFile());
FileOutputStream sigOut = new FileOutputStream(sigFile);
System.setOut(new PrintStream(sigOut));
- new CommandLine(new PGPainlessCLI()).execute("sign", "--armor", aliceKeyFile.getAbsolutePath());
+ PGPainlessCLI.execute("sign", "--armor", aliceKeyFile.getAbsolutePath());
sigOut.close();
// verify test data signature
@@ -103,7 +104,7 @@ public class SignVerifyTest {
System.setOut(new PrintStream(verifyOut));
dataIn = new FileInputStream(dataFile);
System.setIn(dataIn);
- new CommandLine(new PGPainlessCLI()).execute("verify", sigFile.getAbsolutePath(), aliceCertFile.getAbsolutePath());
+ PGPainlessCLI.execute("verify", sigFile.getAbsolutePath(), aliceCertFile.getAbsolutePath());
dataIn.close();
// Test verification output
@@ -113,8 +114,8 @@ public class SignVerifyTest {
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]);
+ assertEquals(signingKeyFingerprint.toString(), split[1].trim());
+ assertEquals(primaryKeyFingerprint.toString(), split[2].trim());
System.setIn(originalIn);
}
diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java
index 15695a6d..cdf79fa9 100644
--- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java
+++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java
@@ -16,6 +16,7 @@
package org.pgpainless.decryption_verification;
import java.io.BufferedInputStream;
+import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
@@ -76,8 +77,10 @@ public final class DecryptionStreamFactory {
private final ConsumerOptions options;
private final OpenPgpMetadata.Builder resultBuilder = OpenPgpMetadata.getBuilder();
- private static final PGPContentVerifierBuilderProvider verifierBuilderProvider = ImplementationFactory.getInstance().getPGPContentVerifierBuilderProvider();
- private static final KeyFingerPrintCalculator keyFingerprintCalculator = ImplementationFactory.getInstance().getKeyFingerprintCalculator();
+ private static final PGPContentVerifierBuilderProvider verifierBuilderProvider =
+ ImplementationFactory.getInstance().getPGPContentVerifierBuilderProvider();
+ private static final KeyFingerPrintCalculator keyFingerprintCalculator =
+ ImplementationFactory.getInstance().getKeyFingerprintCalculator();
private final Map verifiableOnePassSignatures = new HashMap<>();
private final List integrityProtectedStreams = new ArrayList<>();
@@ -109,11 +112,23 @@ public final class DecryptionStreamFactory {
try {
// Parse OpenPGP message
inputStream = factory.processPGPPackets(objectFactory, 1);
- } catch (MissingLiteralDataException e) {
- // Not an OpenPGP message. Reset the buffered stream to parse the message as arbitrary binary data
+ } catch (EOFException e) {
+ throw e;
+ }
+ catch (MissingLiteralDataException e) {
+ // Not an OpenPGP message.
+ // Reset the buffered stream to parse the message as arbitrary binary data
// to allow for detached signature verification.
bufferedIn.reset();
inputStream = bufferedIn;
+ } catch (IOException e) {
+ if (e.getMessage().contains("invalid armor")) {
+ // We falsely assumed the data to be armored.
+ bufferedIn.reset();
+ inputStream = bufferedIn;
+ } else {
+ throw e;
+ }
}
return new DecryptionStream(inputStream, factory.resultBuilder, factory.integrityProtectedStreams);
@@ -146,7 +161,9 @@ public final class DecryptionStreamFactory {
throws PGPException, IOException {
LOGGER.log(LEVEL, "Encountered PGPEncryptedDataList");
InputStream decryptedDataStream = decrypt(pgpEncryptedDataList);
- return processPGPPackets(new PGPObjectFactory(PGPUtil.getDecoderStream(decryptedDataStream), keyFingerprintCalculator), ++depth);
+ InputStream decodedDataStream = PGPUtil.getDecoderStream(decryptedDataStream);
+ PGPObjectFactory factory = new PGPObjectFactory(decodedDataStream, keyFingerprintCalculator);
+ return processPGPPackets(factory, ++depth);
}
private InputStream processPGPCompressedData(PGPCompressedData pgpCompressedData, int depth)
@@ -155,8 +172,9 @@ public final class DecryptionStreamFactory {
LOGGER.log(LEVEL, "Encountered PGPCompressedData: " + compressionAlgorithm);
resultBuilder.setCompressionAlgorithm(compressionAlgorithm);
- InputStream dataStream = pgpCompressedData.getDataStream();
- PGPObjectFactory objectFactory = new PGPObjectFactory(PGPUtil.getDecoderStream(dataStream), keyFingerprintCalculator);
+ InputStream inflatedDataStream = pgpCompressedData.getDataStream();
+ InputStream decodedDataStream = PGPUtil.getDecoderStream(inflatedDataStream);
+ PGPObjectFactory objectFactory = new PGPObjectFactory(decodedDataStream, keyFingerprintCalculator);
return processPGPPackets(objectFactory, ++depth);
}
@@ -171,11 +189,10 @@ public final class DecryptionStreamFactory {
private InputStream processPGPLiteralData(@Nonnull PGPObjectFactory objectFactory, PGPLiteralData pgpLiteralData) {
LOGGER.log(LEVEL, "Found PGPLiteralData");
InputStream literalDataInputStream = pgpLiteralData.getInputStream();
- OpenPgpMetadata.FileInfo fileInfo = new OpenPgpMetadata.FileInfo(
- pgpLiteralData.getFileName(),
- pgpLiteralData.getModificationTime(),
- StreamEncoding.fromCode(pgpLiteralData.getFormat()));
- resultBuilder.setFileInfo(fileInfo);
+
+ resultBuilder.setFileName(pgpLiteralData.getFileName())
+ .setModificationDate(pgpLiteralData.getModificationTime())
+ .setFileEncoding(StreamEncoding.fromCode(pgpLiteralData.getFormat()));
if (verifiableOnePassSignatures.isEmpty()) {
LOGGER.log(LEVEL, "No OnePassSignatures found -> We are done");
@@ -226,9 +243,6 @@ public final class DecryptionStreamFactory {
// data is public key encrypted
else if (encryptedData instanceof PGPPublicKeyEncryptedData) {
- if (options.getDecryptionKeys().isEmpty()) {
-
- }
PGPPublicKeyEncryptedData publicKeyEncryptedData = (PGPPublicKeyEncryptedData) encryptedData;
long keyId = publicKeyEncryptedData.getKeyID();
if (!options.getDecryptionKeys().isEmpty()) {
diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMetadata.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMetadata.java
index 624ae3ca..cc658723 100644
--- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMetadata.java
+++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMetadata.java
@@ -24,6 +24,8 @@ import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
+import javax.annotation.Nonnull;
+
import org.bouncycastle.openpgp.PGPLiteralData;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
@@ -44,7 +46,9 @@ public class OpenPgpMetadata {
private final List detachedSignatures;
private final SymmetricKeyAlgorithm symmetricKeyAlgorithm;
private final CompressionAlgorithm compressionAlgorithm;
- private final FileInfo fileInfo;
+ private final String fileName;
+ private final Date modificationDate;
+ private final StreamEncoding fileEncoding;
public OpenPgpMetadata(Set recipientKeyIds,
SubkeyIdentifier decryptionKey,
@@ -52,7 +56,9 @@ public class OpenPgpMetadata {
CompressionAlgorithm algorithm,
List onePassSignatures,
List detachedSignatures,
- FileInfo fileInfo) {
+ String fileName,
+ Date modificationDate,
+ StreamEncoding fileEncoding) {
this.recipientKeyIds = Collections.unmodifiableSet(recipientKeyIds);
this.decryptionKey = decryptionKey;
@@ -60,7 +66,9 @@ public class OpenPgpMetadata {
this.compressionAlgorithm = algorithm;
this.detachedSignatures = Collections.unmodifiableList(detachedSignatures);
this.onePassSignatures = Collections.unmodifiableList(onePassSignatures);
- this.fileInfo = fileInfo;
+ this.fileName = fileName;
+ this.modificationDate = modificationDate;
+ this.fileEncoding = fileEncoding;
}
public Set getRecipientKeyIds() {
@@ -148,12 +156,59 @@ public class OpenPgpMetadata {
}
}
+ /**
+ * Return information about the encrypted/signed file.
+ *
+ * @deprecated use {@link #getFileName()}, {@link #getModificationDate()} and {@link #getFileEncoding()} instead.
+ * @return file info
+ */
+ @Deprecated
public FileInfo getFileInfo() {
- return fileInfo;
+ return new FileInfo(
+ getFileName(),
+ getModificationDate(),
+ getFileEncoding()
+ );
}
+ /**
+ * Return the name of the encrypted / signed file.
+ *
+ * @return file name
+ */
+ public String getFileName() {
+ return fileName;
+ }
+
+ /**
+ * Return true, if the encrypted data is intended for your eyes only.
+ *
+ * @return true if for-your-eyes-only
+ */
+ public boolean isForYourEyesOnly() {
+ return PGPLiteralData.CONSOLE.equals(getFileName());
+ }
+
+ /**
+ * Return the modification date of the encrypted / signed file.
+ *
+ * @return modification date
+ */
+ public Date getModificationDate() {
+ return modificationDate;
+ }
+
+ /**
+ * Return the encoding format of the encrypted / signed file.
+ *
+ * @return encoding
+ */
+ public StreamEncoding getFileEncoding() {
+ return fileEncoding;
+ }
+
+ @Deprecated
public static class FileInfo {
- public static final String FOR_YOUR_EYES_ONLY = PGPLiteralData.CONSOLE;
protected final String fileName;
protected final Date modificationDate;
@@ -165,22 +220,10 @@ public class OpenPgpMetadata {
this.streamEncoding = streamEncoding;
}
- public static FileInfo binaryStream() {
- return new FileInfo("", null, StreamEncoding.BINARY);
- }
-
- public static FileInfo forYourEyesOnly() {
- return new FileInfo(FOR_YOUR_EYES_ONLY, null, StreamEncoding.BINARY);
- }
-
public String getFileName() {
return fileName;
}
- public boolean isForYourEyesOnly() {
- return FOR_YOUR_EYES_ONLY.equals(fileName);
- }
-
public Date getModificationDate() {
return modificationDate;
}
@@ -243,7 +286,9 @@ public class OpenPgpMetadata {
private final List onePassSignatures = new ArrayList<>();
private SymmetricKeyAlgorithm symmetricKeyAlgorithm = SymmetricKeyAlgorithm.NULL;
private CompressionAlgorithm compressionAlgorithm = CompressionAlgorithm.UNCOMPRESSED;
- private FileInfo fileInfo;
+ private String fileName;
+ private StreamEncoding fileEncoding;
+ private Date modificationDate;
public Builder addRecipientKeyId(Long keyId) {
this.recipientFingerprints.add(keyId);
@@ -269,6 +314,21 @@ public class OpenPgpMetadata {
return this;
}
+ public Builder setFileName(@Nonnull String fileName) {
+ this.fileName = fileName;
+ return this;
+ }
+
+ public Builder setModificationDate(Date modificationDate) {
+ this.modificationDate = modificationDate;
+ return this;
+ }
+
+ public Builder setFileEncoding(StreamEncoding encoding) {
+ this.fileEncoding = encoding;
+ return this;
+ }
+
public void addDetachedSignature(DetachedSignature signature) {
this.detachedSignatures.add(signature);
}
@@ -277,15 +337,10 @@ public class OpenPgpMetadata {
this.onePassSignatures.add(onePassSignature);
}
- public Builder setFileInfo(FileInfo fileInfo) {
- this.fileInfo = fileInfo;
- return this;
- }
-
public OpenPgpMetadata build() {
return new OpenPgpMetadata(recipientFingerprints, decryptionKey,
symmetricKeyAlgorithm, compressionAlgorithm,
- onePassSignatures, detachedSignatures, fileInfo);
+ onePassSignatures, detachedSignatures, fileName, modificationDate, fileEncoding);
}
}
}
diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionResult.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionResult.java
index e050ad04..d488e76b 100644
--- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionResult.java
+++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionResult.java
@@ -16,11 +16,16 @@
package org.pgpainless.encryption_signing;
import java.util.Collections;
+import java.util.Date;
import java.util.HashSet;
import java.util.Set;
+import javax.annotation.Nonnull;
+
+import org.bouncycastle.openpgp.PGPLiteralData;
import org.bouncycastle.openpgp.PGPSignature;
import org.pgpainless.algorithm.CompressionAlgorithm;
+import org.pgpainless.algorithm.StreamEncoding;
import org.pgpainless.algorithm.SymmetricKeyAlgorithm;
import org.pgpainless.decryption_verification.OpenPgpMetadata;
import org.pgpainless.key.SubkeyIdentifier;
@@ -33,18 +38,24 @@ public final class EncryptionResult {
private final MultiMap detachedSignatures;
private final Set recipients;
- private final OpenPgpMetadata.FileInfo fileInfo;
+ private final String fileName;
+ private final Date modificationDate;
+ private final StreamEncoding fileEncoding;
private EncryptionResult(SymmetricKeyAlgorithm encryptionAlgorithm,
CompressionAlgorithm compressionAlgorithm,
MultiMap detachedSignatures,
Set recipients,
- OpenPgpMetadata.FileInfo fileInfo) {
+ String fileName,
+ Date modificationDate,
+ StreamEncoding encoding) {
this.encryptionAlgorithm = encryptionAlgorithm;
this.compressionAlgorithm = compressionAlgorithm;
this.detachedSignatures = detachedSignatures;
this.recipients = Collections.unmodifiableSet(recipients);
- this.fileInfo = fileInfo;
+ this.fileName = fileName;
+ this.modificationDate = modificationDate;
+ this.fileEncoding = encoding;
}
@Deprecated
@@ -68,14 +79,37 @@ public final class EncryptionResult {
return recipients;
}
+ /**
+ * Return information about the encrypted / signed data.
+ *
+ * @deprecated use {@link #getFileName()}, {@link #getModificationDate()} and {@link #getFileEncoding()} instead.
+ * @return info
+ */
+ @Deprecated
public OpenPgpMetadata.FileInfo getFileInfo() {
- return fileInfo;
+ return new OpenPgpMetadata.FileInfo(getFileName(), getModificationDate(), getFileEncoding());
+ }
+
+ public String getFileName() {
+ return fileName;
+ }
+
+ public Date getModificationDate() {
+ return modificationDate;
+ }
+
+ public StreamEncoding getFileEncoding() {
+ return fileEncoding;
}
public static Builder builder() {
return new Builder();
}
+ public boolean isForYourEyesOnly() {
+ return PGPLiteralData.CONSOLE.equals(getFileName());
+ }
+
public static class Builder {
private SymmetricKeyAlgorithm encryptionAlgorithm;
@@ -83,7 +117,9 @@ public final class EncryptionResult {
private final MultiMap detachedSignatures = new MultiMap<>();
private Set recipients = new HashSet<>();
- private OpenPgpMetadata.FileInfo fileInfo;
+ private String fileName = "";
+ private Date modificationDate = new Date(0L); // NOW
+ private StreamEncoding encoding = StreamEncoding.BINARY;
public Builder setEncryptionAlgorithm(SymmetricKeyAlgorithm encryptionAlgorithm) {
this.encryptionAlgorithm = encryptionAlgorithm;
@@ -105,8 +141,18 @@ public final class EncryptionResult {
return this;
}
- public Builder setFileInfo(OpenPgpMetadata.FileInfo fileInfo) {
- this.fileInfo = fileInfo;
+ public Builder setFileName(@Nonnull String fileName) {
+ this.fileName = fileName;
+ return this;
+ }
+
+ public Builder setModificationDate(@Nonnull Date modificationDate) {
+ this.modificationDate = modificationDate;
+ return this;
+ }
+
+ public Builder setFileEncoding(StreamEncoding fileEncoding) {
+ this.encoding = fileEncoding;
return this;
}
@@ -117,11 +163,9 @@ public final class EncryptionResult {
if (compressionAlgorithm == null) {
throw new IllegalStateException("Compression algorithm not set.");
}
- if (fileInfo == null) {
- throw new IllegalStateException("File info not set.");
- }
- return new EncryptionResult(encryptionAlgorithm, compressionAlgorithm, detachedSignatures, recipients, fileInfo);
+ return new EncryptionResult(encryptionAlgorithm, compressionAlgorithm, detachedSignatures, recipients,
+ fileName, modificationDate, encoding);
}
}
}
diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java
index 8b9b9a19..1b00856c 100644
--- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java
+++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java
@@ -33,7 +33,6 @@ import org.bouncycastle.openpgp.operator.PGPDataEncryptorBuilder;
import org.bouncycastle.openpgp.operator.PGPKeyEncryptionMethodGenerator;
import org.pgpainless.algorithm.CompressionAlgorithm;
import org.pgpainless.algorithm.SymmetricKeyAlgorithm;
-import org.pgpainless.decryption_verification.OpenPgpMetadata;
import org.pgpainless.implementation.ImplementationFactory;
import org.pgpainless.key.SubkeyIdentifier;
import org.pgpainless.util.ArmoredOutputStreamFactory;
@@ -154,8 +153,9 @@ public final class EncryptionStream extends OutputStream {
new byte[BUFFER_SIZE]);
outermostStream = literalDataStream;
- resultBuilder.setFileInfo(new OpenPgpMetadata.FileInfo(
- options.getFileName(), options.getModificationDate(), options.getEncoding()));
+ resultBuilder.setFileName(options.getFileName())
+ .setModificationDate(options.getModificationDate())
+ .setFileEncoding(options.getEncoding());
}
@Override
@@ -256,4 +256,8 @@ public final class EncryptionStream extends OutputStream {
}
return resultBuilder.build();
}
+
+ public boolean isClosed() {
+ return closed;
+ }
}
diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SigningOptions.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SigningOptions.java
index 099dcd17..8f19ad4b 100644
--- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SigningOptions.java
+++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SigningOptions.java
@@ -33,11 +33,14 @@ import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder;
import org.pgpainless.PGPainless;
import org.pgpainless.algorithm.DocumentSignatureType;
import org.pgpainless.algorithm.HashAlgorithm;
+import org.pgpainless.exception.KeyCannotSignException;
import org.pgpainless.exception.KeyValidationException;
import org.pgpainless.implementation.ImplementationFactory;
+import org.pgpainless.key.OpenPgpV4Fingerprint;
import org.pgpainless.key.SubkeyIdentifier;
import org.pgpainless.key.info.KeyRingInfo;
import org.pgpainless.key.protection.SecretKeyRingProtector;
+import org.pgpainless.key.protection.UnlockSecretKey;
import org.pgpainless.policy.Policy;
public final class SigningOptions {
@@ -161,12 +164,12 @@ public final class SigningOptions {
List signingPubKeys = keyRingInfo.getSigningSubkeys();
if (signingPubKeys.isEmpty()) {
- throw new AssertionError("Key has no valid signing key.");
+ throw new KeyCannotSignException("Key " + new OpenPgpV4Fingerprint(secretKey) + " has no valid signing key.");
}
for (PGPPublicKey signingPubKey : signingPubKeys) {
PGPSecretKey signingSecKey = secretKey.getSecretKey(signingPubKey.getKeyID());
- PGPPrivateKey signingSubkey = signingSecKey.extractPrivateKey(secretKeyDecryptor.getDecryptor(signingPubKey.getKeyID()));
+ PGPPrivateKey signingSubkey = UnlockSecretKey.unlockSecretKey(signingSecKey, secretKeyDecryptor);
Set hashAlgorithms = keyRingInfo.getPreferredHashAlgorithms(userId, signingPubKey.getKeyID());
HashAlgorithm hashAlgorithm = negotiateHashAlgorithm(hashAlgorithms, PGPainless.getPolicy());
addSigningMethod(secretKey, signingSubkey, hashAlgorithm, signatureType, false);
@@ -242,7 +245,7 @@ public final class SigningOptions {
List signingPubKeys = keyRingInfo.getSigningSubkeys();
if (signingPubKeys.isEmpty()) {
- throw new AssertionError("Key has no valid signing key.");
+ throw new KeyCannotSignException("Key has no valid signing key.");
}
for (PGPPublicKey signingPubKey : signingPubKeys) {
diff --git a/pgpainless-core/src/main/java/org/pgpainless/exception/KeyCannotSignException.java b/pgpainless-core/src/main/java/org/pgpainless/exception/KeyCannotSignException.java
new file mode 100644
index 00000000..bb3d6574
--- /dev/null
+++ b/pgpainless-core/src/main/java/org/pgpainless/exception/KeyCannotSignException.java
@@ -0,0 +1,24 @@
+/*
+ * 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.exception;
+
+import org.bouncycastle.openpgp.PGPException;
+
+public class KeyCannotSignException extends PGPException {
+ public KeyCannotSignException(String message) {
+ super(message);
+ }
+}
diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java b/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java
index bb6bad9e..10855877 100644
--- a/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java
+++ b/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java
@@ -19,6 +19,7 @@ import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
+import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
@@ -68,6 +69,23 @@ public class ArmorUtils {
return sb.toString();
}
+ public static ArmoredOutputStream toAsciiArmoredStream(PGPKeyRing keyRing, OutputStream outputStream) {
+ MultiMap header = keyToHeader(keyRing);
+ return toAsciiArmoredStream(outputStream, header);
+ }
+
+ public static ArmoredOutputStream toAsciiArmoredStream(OutputStream outputStream, MultiMap header) {
+ ArmoredOutputStream armoredOutputStream = ArmoredOutputStreamFactory.get(outputStream);
+ if (header != null) {
+ for (String headerKey : header.keySet()) {
+ for (String headerValue : header.get(headerKey)) {
+ armoredOutputStream.addHeader(headerKey, headerValue);
+ }
+ }
+ }
+ return armoredOutputStream;
+ }
+
public static String toAsciiArmoredString(PGPPublicKeyRingCollection publicKeyRings) throws IOException {
StringBuilder sb = new StringBuilder();
for (Iterator iterator = publicKeyRings.iterator(); iterator.hasNext(); ) {
@@ -124,20 +142,25 @@ public class ArmorUtils {
public static String toAsciiArmoredString(InputStream inputStream, MultiMap additionalHeaderValues) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
- ArmoredOutputStream armor = ArmoredOutputStreamFactory.get(out);
- if (additionalHeaderValues != null) {
- for (String header : additionalHeaderValues.keySet()) {
- for (String value : additionalHeaderValues.get(header)) {
- armor.addHeader(header, value);
- }
- }
- }
+ ArmoredOutputStream armor = toAsciiArmoredStream(out, additionalHeaderValues);
Streams.pipeAll(inputStream, armor);
armor.close();
return out.toString();
}
+ public static ArmoredOutputStream createArmoredOutputStreamFor(PGPKeyRing keyRing, OutputStream outputStream) {
+ ArmoredOutputStream armor = ArmoredOutputStreamFactory.get(outputStream);
+ MultiMap headerMap = keyToHeader(keyRing);
+ for (String header : headerMap.keySet()) {
+ for (String value : headerMap.get(header)) {
+ armor.addHeader(header, value);
+ }
+ }
+
+ return armor;
+ }
+
public static List getCommendHeaderValues(ArmoredInputStream armor) {
return getArmorHeaderValues(armor, HEADER_COMMENT);
}
diff --git a/pgpainless-core/src/test/java/org/junit/JUtils.java b/pgpainless-core/src/test/java/org/junit/JUtils.java
index c053027f..6fced9c7 100644
--- a/pgpainless-core/src/test/java/org/junit/JUtils.java
+++ b/pgpainless-core/src/test/java/org/junit/JUtils.java
@@ -17,9 +17,17 @@ package org.junit;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import java.util.Date;
+
+import org.pgpainless.util.DateUtil;
+
public class JUtils {
public static void assertEquals(long a, long b, long delta) {
assertTrue(a - delta <= b && a + delta >= b);
}
+
+ public static void assertDateEquals(Date a, Date b) {
+ org.junit.jupiter.api.Assertions.assertEquals(DateUtil.formatUTCDate(a), DateUtil.formatUTCDate(b));
+ }
}
diff --git a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/FileInfoTest.java b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/FileInfoTest.java
deleted file mode 100644
index ad83cdc6..00000000
--- a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/FileInfoTest.java
+++ /dev/null
@@ -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.encryption_signing;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-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.Test;
-import org.pgpainless.PGPainless;
-import org.pgpainless.algorithm.StreamEncoding;
-import org.pgpainless.decryption_verification.ConsumerOptions;
-import org.pgpainless.decryption_verification.DecryptionStream;
-import org.pgpainless.decryption_verification.OpenPgpMetadata;
-import org.pgpainless.key.util.KeyRingUtils;
-
-public class FileInfoTest {
-
- @Test
- public void textFile() throws NoSuchAlgorithmException, PGPException, InvalidAlgorithmParameterException, IOException {
- OpenPgpMetadata.FileInfo fileInfo = new OpenPgpMetadata.FileInfo("message.txt", new Date(), StreamEncoding.TEXT);
- executeWith(fileInfo);
- }
-
- @Test
- public void binaryStream() throws NoSuchAlgorithmException, PGPException, InvalidAlgorithmParameterException, IOException {
- OpenPgpMetadata.FileInfo fileInfo = OpenPgpMetadata.FileInfo.binaryStream();
- executeWith(fileInfo);
- }
-
- @Test
- public void forYourEyesOnly() throws NoSuchAlgorithmException, PGPException, InvalidAlgorithmParameterException, IOException {
- OpenPgpMetadata.FileInfo fileInfo = OpenPgpMetadata.FileInfo.forYourEyesOnly();
- executeWith(fileInfo);
- }
-
- public void executeWith(OpenPgpMetadata.FileInfo fileInfo) throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException, IOException {
- PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().simpleEcKeyRing("alice@wonderland.lit");
- PGPPublicKeyRing publicKeys = KeyRingUtils.publicKeyRingFrom(secretKeys);
-
- String data = "Hello, World!";
-
- ByteArrayInputStream dataIn = new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8));
- ByteArrayOutputStream dataOut = new ByteArrayOutputStream();
- EncryptionStream encryptionStream = PGPainless.encryptAndOrSign()
- .onOutputStream(dataOut)
- .withOptions(ProducerOptions.encrypt(
- EncryptionOptions
- .encryptCommunications()
- .addRecipient(publicKeys))
- .setEncoding(fileInfo.getStreamFormat())
- .setFileName(fileInfo.getFileName())
- .setModificationDate(fileInfo.getModificationDate())
- );
-
- Streams.pipeAll(dataIn, encryptionStream);
- encryptionStream.close();
-
- OpenPgpMetadata.FileInfo cryptInfo = encryptionStream.getResult().getFileInfo();
- assertEquals(fileInfo, cryptInfo);
-
- ByteArrayInputStream cryptIn = new ByteArrayInputStream(dataOut.toByteArray());
- ByteArrayOutputStream plainOut = new ByteArrayOutputStream();
-
- DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify()
- .onInputStream(cryptIn)
- .withOptions(new ConsumerOptions()
- .addDecryptionKey(secretKeys));
- Streams.pipeAll(decryptionStream, plainOut);
-
- decryptionStream.close();
-
- OpenPgpMetadata.FileInfo decryptInfo = decryptionStream.getResult().getFileInfo();
- assertEquals(fileInfo, decryptInfo);
- }
-}
diff --git a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/FileInformationTest.java b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/FileInformationTest.java
new file mode 100644
index 00000000..9e825e46
--- /dev/null
+++ b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/FileInformationTest.java
@@ -0,0 +1,181 @@
+/*
+ * 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.encryption_signing;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+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.nio.charset.StandardCharsets;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.NoSuchAlgorithmException;
+import java.util.Date;
+
+import org.bouncycastle.openpgp.PGPException;
+import org.bouncycastle.openpgp.PGPLiteralData;
+import org.bouncycastle.openpgp.PGPPublicKeyRing;
+import org.bouncycastle.openpgp.PGPSecretKeyRing;
+import org.bouncycastle.util.io.Streams;
+import org.junit.JUtils;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.pgpainless.PGPainless;
+import org.pgpainless.algorithm.StreamEncoding;
+import org.pgpainless.decryption_verification.ConsumerOptions;
+import org.pgpainless.decryption_verification.DecryptionStream;
+import org.pgpainless.decryption_verification.OpenPgpMetadata;
+
+public class FileInformationTest {
+
+ private static final String data = "Hello, World!\n";
+ private static PGPSecretKeyRing secretKey;
+ private static PGPPublicKeyRing certificate;
+
+ @BeforeAll
+ public static void generateKey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException {
+ secretKey = PGPainless.generateKeyRing().modernKeyRing("alice@wonderland.lit", null);
+ certificate = PGPainless.extractCertificate(secretKey);
+ }
+
+ @Test
+ public void testTextFile() throws PGPException, IOException {
+ String fileName = "message.txt";
+ Date modificationDate = new Date();
+ StreamEncoding encoding = StreamEncoding.TEXT;
+
+ ByteArrayInputStream dataIn = new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8));
+ ByteArrayOutputStream dataOut = new ByteArrayOutputStream();
+ EncryptionStream encryptionStream = PGPainless.encryptAndOrSign()
+ .onOutputStream(dataOut)
+ .withOptions(ProducerOptions.encrypt(
+ EncryptionOptions
+ .encryptCommunications()
+ .addRecipient(certificate))
+ .setFileName(fileName)
+ .setModificationDate(modificationDate)
+ .setEncoding(encoding)
+ );
+
+ Streams.pipeAll(dataIn, encryptionStream);
+ encryptionStream.close();
+
+ EncryptionResult encResult = encryptionStream.getResult();
+ assertEquals(fileName, encResult.getFileName());
+ JUtils.assertDateEquals(modificationDate, encResult.getModificationDate());
+ assertEquals(encoding, encResult.getFileEncoding());
+
+ ByteArrayInputStream cryptIn = new ByteArrayInputStream(dataOut.toByteArray());
+ ByteArrayOutputStream plainOut = new ByteArrayOutputStream();
+
+ DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify()
+ .onInputStream(cryptIn)
+ .withOptions(new ConsumerOptions()
+ .addDecryptionKey(secretKey));
+ Streams.pipeAll(decryptionStream, plainOut);
+
+ decryptionStream.close();
+
+ OpenPgpMetadata decResult = decryptionStream.getResult();
+
+ assertEquals(fileName, decResult.getFileName());
+ JUtils.assertDateEquals(modificationDate, decResult.getModificationDate());
+ assertEquals(encoding, decResult.getFileEncoding());
+ }
+
+ @Test
+ public void testDefaults() throws PGPException, IOException {
+ ByteArrayInputStream dataIn = new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8));
+ ByteArrayOutputStream dataOut = new ByteArrayOutputStream();
+ EncryptionStream encryptionStream = PGPainless.encryptAndOrSign()
+ .onOutputStream(dataOut)
+ .withOptions(ProducerOptions.encrypt(
+ EncryptionOptions
+ .encryptCommunications()
+ .addRecipient(certificate))
+ );
+
+ Streams.pipeAll(dataIn, encryptionStream);
+ encryptionStream.close();
+
+ EncryptionResult encResult = encryptionStream.getResult();
+ assertEquals("", encResult.getFileName());
+ JUtils.assertDateEquals(PGPLiteralData.NOW, encResult.getModificationDate());
+ assertEquals(PGPLiteralData.BINARY, encResult.getFileEncoding().getCode());
+ assertFalse(encResult.isForYourEyesOnly());
+
+ ByteArrayInputStream cryptIn = new ByteArrayInputStream(dataOut.toByteArray());
+ ByteArrayOutputStream plainOut = new ByteArrayOutputStream();
+
+ DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify()
+ .onInputStream(cryptIn)
+ .withOptions(new ConsumerOptions()
+ .addDecryptionKey(secretKey));
+ Streams.pipeAll(decryptionStream, plainOut);
+
+ decryptionStream.close();
+
+ OpenPgpMetadata decResult = decryptionStream.getResult();
+
+ assertEquals("", decResult.getFileName());
+ JUtils.assertDateEquals(PGPLiteralData.NOW, decResult.getModificationDate());
+ assertEquals(PGPLiteralData.BINARY, decResult.getFileEncoding().getCode());
+ assertFalse(decResult.isForYourEyesOnly());
+ }
+
+ @Test
+ public void testForYourEyesOnly() throws PGPException, IOException {
+ ByteArrayInputStream dataIn = new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8));
+ ByteArrayOutputStream dataOut = new ByteArrayOutputStream();
+ EncryptionStream encryptionStream = PGPainless.encryptAndOrSign()
+ .onOutputStream(dataOut)
+ .withOptions(ProducerOptions.encrypt(
+ EncryptionOptions
+ .encryptCommunications()
+ .addRecipient(certificate))
+ .setForYourEyesOnly()
+ );
+
+ Streams.pipeAll(dataIn, encryptionStream);
+ encryptionStream.close();
+
+ EncryptionResult encResult = encryptionStream.getResult();
+ assertEquals(PGPLiteralData.CONSOLE, encResult.getFileName());
+ JUtils.assertDateEquals(PGPLiteralData.NOW, encResult.getModificationDate());
+ assertEquals(PGPLiteralData.BINARY, encResult.getFileEncoding().getCode());
+ assertTrue(encResult.isForYourEyesOnly());
+
+ ByteArrayInputStream cryptIn = new ByteArrayInputStream(dataOut.toByteArray());
+ ByteArrayOutputStream plainOut = new ByteArrayOutputStream();
+
+ DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify()
+ .onInputStream(cryptIn)
+ .withOptions(new ConsumerOptions()
+ .addDecryptionKey(secretKey));
+ Streams.pipeAll(decryptionStream, plainOut);
+
+ decryptionStream.close();
+
+ OpenPgpMetadata decResult = decryptionStream.getResult();
+
+ assertEquals(PGPLiteralData.CONSOLE, decResult.getFileName());
+ JUtils.assertDateEquals(PGPLiteralData.NOW, decResult.getModificationDate());
+ assertEquals(PGPLiteralData.BINARY, decResult.getFileEncoding().getCode());
+ assertTrue(decResult.isForYourEyesOnly());
+ }
+}
diff --git a/pgpainless-sop/build.gradle b/pgpainless-sop/build.gradle
index 054e1fdf..02f6adb8 100644
--- a/pgpainless-sop/build.gradle
+++ b/pgpainless-sop/build.gradle
@@ -1,59 +1,23 @@
plugins {
- id 'application'
+ id 'java'
+}
+
+group 'org.pgpainless'
+version '0.2.1-SNAPSHOT'
+
+repositories {
+ mavenCentral()
}
dependencies {
+
implementation(project(":pgpainless-core"))
+ implementation(project(":sop-java"))
- implementation 'info.picocli:picocli:4.5.2'
-
- testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion"
- testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion"
-
- /*
- implementation "org.bouncycastle:bcprov-debug-jdk15on:$bouncyCastleVersion"
- /*/
- implementation "org.bouncycastle:bcprov-jdk15on:$bouncyCastleVersion"
- //*/
- implementation "org.bouncycastle:bcpg-jdk15on:$bouncyCastleVersion"
-
- // https://mvnrepository.com/artifact/com.google.code.findbugs/jsr305
- implementation group: 'com.google.code.findbugs', name: 'jsr305', version: '3.0.2'
-}
-
-mainClassName = 'org.pgpainless.sop.PGPainlessCLI'
-
-def generatedVersionDir = "${buildDir}/generated-version"
-
-sourceSets {
- main {
- output.dir(generatedVersionDir, builtBy: 'generateVersionProperties')
- }
-}
-
-task generateVersionProperties {
- doLast {
- def propertiesFile = file "$generatedVersionDir/version.properties"
- propertiesFile.parentFile.mkdirs()
- propertiesFile.createNewFile()
- // Instead of using a Properties object here, we directly write to the file
- // since Properties adds a timestamp, ruining reproducibility
- propertiesFile.write("version="+rootProject.version.toString())
- }
-}
-processResources.dependsOn generateVersionProperties
-
-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"
- }
+ testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.0'
+ testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.0'
}
+test {
+ useJUnitPlatform()
+}
\ No newline at end of file
diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/ArmorImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/ArmorImpl.java
new file mode 100644
index 00000000..2d2b93c4
--- /dev/null
+++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/ArmorImpl.java
@@ -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();
+ }
+ }
+ };
+ }
+}
diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/DearmorImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DearmorImpl.java
new file mode 100644
index 00000000..e4b05a4b
--- /dev/null
+++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/DearmorImpl.java
@@ -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();
+ }
+ };
+ }
+}
diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java
new file mode 100644
index 00000000..94535e90
--- /dev/null
+++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java
@@ -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 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() {
+ @Override
+ public DecryptionResult writeTo(OutputStream outputStream) throws IOException, SOPGPException.NoSignature {
+ Streams.pipeAll(decryptionStream, outputStream);
+ decryptionStream.close();
+ OpenPgpMetadata metadata = decryptionStream.getResult();
+
+ List 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);
+ }
+ };
+ }
+}
diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java
new file mode 100644
index 00000000..548129fa
--- /dev/null
+++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java
@@ -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);
+ }
+}
diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/ExtractCertImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/ExtractCertImpl.java
new file mode 100644
index 00000000..20431664
--- /dev/null
+++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/ExtractCertImpl.java
@@ -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);
+ }
+ }
+}
diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java
new file mode 100644
index 00000000..2ff2273a
--- /dev/null
+++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java
@@ -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 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 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);
+ }
+ }
+}
diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/PGPainlessCLI.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/PGPainlessCLI.java
deleted file mode 100644
index b1d85dae..00000000
--- a/pgpainless-sop/src/main/java/org/pgpainless/sop/PGPainlessCLI.java
+++ /dev/null
@@ -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() {
-
- }
-}
diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/Print.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/Print.java
deleted file mode 100644
index 24721870..00000000
--- a/pgpainless-sop/src/main/java/org/pgpainless/sop/Print.java
+++ /dev/null
@@ -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
- }
-}
diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPImpl.java
new file mode 100644
index 00000000..fded2244
--- /dev/null
+++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPImpl.java
@@ -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();
+ }
+}
diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/SignImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/SignImpl.java
new file mode 100644
index 00000000..cd6bff94
--- /dev/null
+++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/SignImpl.java
@@ -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 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 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;
+ }
+}
diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/SopKeyUtil.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/SopKeyUtil.java
deleted file mode 100644
index f0082c10..00000000
--- a/pgpainless-sop/src/main/java/org/pgpainless/sop/SopKeyUtil.java
+++ /dev/null
@@ -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 loadKeysFromFiles(File... files) throws IOException, PGPException {
- List 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 loadCertificatesFromFile(File... files) throws IOException {
- List 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;
- }
-}
diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/VerifyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/VerifyImpl.java
new file mode 100644
index 00000000..047a7ab1
--- /dev/null
+++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/VerifyImpl.java
@@ -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 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 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);
+ }
+ }
+}
diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Version.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java
similarity index 71%
rename from pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Version.java
rename to pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java
index e3974739..adee014f 100644
--- a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Version.java
+++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java
@@ -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;
}
}
diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Armor.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Armor.java
deleted file mode 100644
index a70556ef..00000000
--- a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Armor.java
+++ /dev/null
@@ -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);
- }
- }
-}
diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Decrypt.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Decrypt.java
deleted file mode 100644
index 8ea4c964..00000000
--- a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Decrypt.java
+++ /dev/null
@@ -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 verifyWith = null;
- try {
-
- List 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);
- }
- }
- }
-}
diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Encrypt.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Encrypt.java
deleted file mode 100644
index 7c2a9a99..00000000
--- a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Encrypt.java
+++ /dev/null
@@ -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 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;
- }
-}
diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/GenerateKey.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/GenerateKey.java
deleted file mode 100644
index 99a3b0d1..00000000
--- a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/GenerateKey.java
+++ /dev/null
@@ -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 \"")
- List 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);
- }
- }
-}
diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Sign.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Sign.java
deleted file mode 100644
index c7cd874a..00000000
--- a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Sign.java
+++ /dev/null
@@ -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);
- }
- }
-}
diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Verify.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Verify.java
deleted file mode 100644
index 460f83dc..00000000
--- a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Verify.java
+++ /dev/null
@@ -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 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 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 validSignatures, Map 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 readCertificatesFromFiles() {
- Map 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
- }
- }
-}
diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/package-info.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/package-info.java
index 43afb989..a480c702 100644
--- a/pgpainless-sop/src/main/java/org/pgpainless/sop/package-info.java
+++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/package-info.java
@@ -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
- * Stateless OpenPGP Command Line Interface
- * draft-dkg-openpgp-stateless-cli-01
+ * Implementation of the java-sop package using pgpainless-core.
*/
package org.pgpainless.sop;
diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/DummyTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/VersionTest.java
similarity index 76%
rename from pgpainless-sop/src/test/java/org/pgpainless/sop/DummyTest.java
rename to pgpainless-sop/src/test/java/org/pgpainless/sop/VersionTest.java
index e73fba3f..02a38217 100644
--- a/pgpainless-sop/src/test/java/org/pgpainless/sop/DummyTest.java
+++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/VersionTest.java
@@ -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 ");
+ public void assertNameEqualsPGPainless() {
+ assertEquals("PGPainless-SOP", new SOPImpl().version().getName());
}
}
diff --git a/settings.gradle b/settings.gradle
index 5cc5fd5c..2fb62820 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1,5 +1,8 @@
rootProject.name = 'PGPainless'
include 'pgpainless-core',
- 'pgpainless-sop'
+ 'sop-java',
+ 'pgpainless-sop',
+ 'sop-java-picocli',
+ 'pgpainless-cli'
diff --git a/sop-java-picocli/build.gradle b/sop-java-picocli/build.gradle
new file mode 100644
index 00000000..f48a891d
--- /dev/null
+++ b/sop-java-picocli/build.gradle
@@ -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"
+ }
+}
+
diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/DateParser.java b/sop-java-picocli/src/main/java/sop/cli/picocli/DateParser.java
new file mode 100644
index 00000000..cfcdb870
--- /dev/null
+++ b/sop-java-picocli/src/main/java/sop/cli/picocli/DateParser.java
@@ -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;
+ }
+}
diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/Print.java b/sop-java-picocli/src/main/java/sop/cli/picocli/Print.java
new file mode 100644
index 00000000..fd8d98ea
--- /dev/null
+++ b/sop-java-picocli/src/main/java/sop/cli/picocli/Print.java
@@ -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
+ }
+}
diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/SopCLI.java b/sop-java-picocli/src/main/java/sop/cli/picocli/SopCLI.java
new file mode 100644
index 00000000..f57b2833
--- /dev/null
+++ b/sop-java-picocli/src/main/java/sop/cli/picocli/SopCLI.java
@@ -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;
+ }
+}
diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ArmorCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ArmorCmd.java
new file mode 100644
index 00000000..4087b60c
--- /dev/null
+++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ArmorCmd.java
@@ -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);
+ }
+ }
+}
diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Dearmor.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DearmorCmd.java
similarity index 58%
rename from pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Dearmor.java
rename to sop-java-picocli/src/main/java/sop/cli/picocli/commands/DearmorCmd.java
index 5de54b8c..c4d1d009 100644
--- a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Dearmor.java
+++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DearmorCmd.java
@@ -13,28 +13,34 @@
* 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;
+package sop.cli.picocli.commands;
import java.io.IOException;
-import static org.pgpainless.sop.Print.err_ln;
+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 = 37)
-public class Dearmor implements Runnable {
+ exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE)
+public class DearmorCmd implements Runnable {
@Override
public void run() {
- try (ArmoredInputStream in = new ArmoredInputStream(System.in, true)) {
- Streams.pipeAll(in, System.out);
+ 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) {
- err_ln("Data cannot be dearmored.");
- err_ln(e.getMessage());
+ Print.errln("IO Error.");
+ Print.trace(e);
System.exit(1);
}
}
diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DecryptCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DecryptCmd.java
new file mode 100644
index 00000000..75ea2bca
--- /dev/null
+++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DecryptCmd.java
@@ -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 withSessionKey = new ArrayList<>();
+
+ @CommandLine.Option(
+ names = {"--with-password"},
+ description = "Enables decryption based on any \"SKESK\" packets in the \"CIPHERTEXT\"",
+ paramLabel = "PASSWORD")
+ List 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 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 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 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 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 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 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 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());
+ }
+ }
+}
diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/EncryptCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/EncryptCmd.java
new file mode 100644
index 00000000..bdc6b1b4
--- /dev/null
+++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/EncryptCmd.java
@@ -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 withPassword = new ArrayList<>();
+
+ @CommandLine.Option(names = "--sign-with",
+ description = "Sign the output with a private key",
+ paramLabel = "KEY")
+ List signWith = new ArrayList<>();
+
+ @CommandLine.Parameters(description = "Certificates the message gets encrypted to",
+ index = "0..*",
+ paramLabel = "CERTS")
+ List 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);
+ }
+ }
+}
diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/ExtractCert.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ExtractCertCmd.java
similarity index 56%
rename from pgpainless-sop/src/main/java/org/pgpainless/sop/commands/ExtractCert.java
rename to sop-java-picocli/src/main/java/sop/cli/picocli/commands/ExtractCertCmd.java
index 52480575..16d9ac24 100644
--- a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/ExtractCert.java
+++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ExtractCertCmd.java
@@ -13,25 +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.err_ln;
-import static org.pgpainless.sop.Print.print_ln;
+package sop.cli.picocli.commands;
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;
+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 ExtractCert implements Runnable {
+public class ExtractCertCmd implements Runnable {
@CommandLine.Option(names = "--no-armor",
description = "ASCII armor the output",
@@ -40,15 +36,22 @@ public class ExtractCert implements Runnable {
@Override
public void run() {
- try {
- PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(System.in);
- PGPPublicKeyRing publicKeys = KeyRingUtils.publicKeyRingFrom(secretKeys);
+ ExtractCert extractCert = SopCLI.getSop().extractCert();
+ if (!armor) {
+ extractCert.noArmor();
+ }
- print_ln(Print.toString(publicKeys, armor));
- } catch (IOException | PGPException e) {
- err_ln("Error extracting certificate from keys;");
- err_ln(e.getMessage());
+ 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());
}
}
}
diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/GenerateKeyCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/GenerateKeyCmd.java
new file mode 100644
index 00000000..b9ca045f
--- /dev/null
+++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/GenerateKeyCmd.java
@@ -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 \"")
+ List 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);
+ }
+ }
+}
diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/SignCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/SignCmd.java
new file mode 100644
index 00000000..6140bfce
--- /dev/null
+++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/SignCmd.java
@@ -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 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());
+ }
+ }
+}
diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/VerifyCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/VerifyCmd.java
new file mode 100644
index 00000000..18e1c03d
--- /dev/null
+++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/VerifyCmd.java
@@ -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 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 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());
+ }
+ }
+}
diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/VersionCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/VersionCmd.java
new file mode 100644
index 00000000..a0a93e2a
--- /dev/null
+++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/VersionCmd.java
@@ -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());
+ }
+}
diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/package-info.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/package-info.java
similarity index 94%
rename from pgpainless-sop/src/main/java/org/pgpainless/sop/commands/package-info.java
rename to sop-java-picocli/src/main/java/sop/cli/picocli/commands/package-info.java
index 633d7afb..28684d5a 100644
--- a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/package-info.java
+++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/package-info.java
@@ -16,4 +16,4 @@
/**
* Subcommands of the PGPainless SOP.
*/
-package org.pgpainless.sop.commands;
+package sop.cli.picocli.commands;
diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/package-info.java b/sop-java-picocli/src/main/java/sop/cli/picocli/package-info.java
new file mode 100644
index 00000000..af01095d
--- /dev/null
+++ b/sop-java-picocli/src/main/java/sop/cli/picocli/package-info.java
@@ -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;
diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/DateParserTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/DateParserTest.java
new file mode 100644
index 00000000..0be6f7f2
--- /dev/null
+++ b/sop-java-picocli/src/test/java/sop/cli/picocli/DateParserTest.java
@@ -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));
+ }
+}
diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/SOPTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/SOPTest.java
new file mode 100644
index 00000000..3ee1461a
--- /dev/null
+++ b/sop-java-picocli/src/test/java/sop/cli/picocli/SOPTest.java
@@ -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"});
+ }
+}
diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/ArmorCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/ArmorCmdTest.java
new file mode 100644
index 00000000..e4a5d84a
--- /dev/null
+++ b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/ArmorCmdTest.java
@@ -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) {
+ }
+ };
+ }
+}
diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/DearmorCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/DearmorCmdTest.java
new file mode 100644
index 00000000..d83f747e
--- /dev/null
+++ b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/DearmorCmdTest.java
@@ -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"});
+ }
+}
diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/DecryptCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/DecryptCmdTest.java
new file mode 100644
index 00000000..4e0b2497
--- /dev/null
+++ b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/DecryptCmdTest.java
@@ -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 nopReadyWithResult() {
+ return new ReadyWithResult() {
+ @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 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() {
+ @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() {
+ @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() {
+ @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"});
+ }
+}
diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/EncryptCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/EncryptCmdTest.java
new file mode 100644
index 00000000..ce56bbcd
--- /dev/null
+++ b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/EncryptCmdTest.java
@@ -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"});
+ }
+}
diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/ExtractCertCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/ExtractCertCmdTest.java
new file mode 100644
index 00000000..6e453894
--- /dev/null
+++ b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/ExtractCertCmdTest.java
@@ -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"});
+ }
+}
diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/GenerateKeyCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/GenerateKeyCmdTest.java
new file mode 100644
index 00000000..4c061d43
--- /dev/null
+++ b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/GenerateKeyCmdTest.java
@@ -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 ", "Bob "});
+
+ InOrder inOrder = Mockito.inOrder(generateKey);
+ inOrder.verify(generateKey).userId("Alice ");
+ inOrder.verify(generateKey).userId("Bob ");
+
+ 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"});
+ }
+}
diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/SignCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/SignCmdTest.java
new file mode 100644
index 00000000..98220c79
--- /dev/null
+++ b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/SignCmdTest.java
@@ -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()});
+ }
+}
diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/VerifyCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/VerifyCmdTest.java
new file mode 100644
index 00000000..98d742e2
--- /dev/null
+++ b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/VerifyCmdTest.java
@@ -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());
+ }
+}
diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/VersionCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/VersionCmdTest.java
new file mode 100644
index 00000000..1aa1440e
--- /dev/null
+++ b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/VersionCmdTest.java
@@ -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"});
+ }
+}
diff --git a/sop-java/build.gradle b/sop-java/build.gradle
new file mode 100644
index 00000000..3da940c6
--- /dev/null
+++ b/sop-java/build.gradle
@@ -0,0 +1,18 @@
+plugins {
+ id 'java'
+}
+
+group 'org.pgpainless'
+
+repositories {
+ mavenCentral()
+}
+
+dependencies {
+ testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion"
+ testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion"
+}
+
+test {
+ useJUnitPlatform()
+}
\ No newline at end of file
diff --git a/sop-java/src/main/java/sop/ByteArrayAndResult.java b/sop-java/src/main/java/sop/ByteArrayAndResult.java
new file mode 100644
index 00000000..b462926e
--- /dev/null
+++ b/sop-java/src/main/java/sop/ByteArrayAndResult.java
@@ -0,0 +1,35 @@
+/*
+ * 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;
+
+public class ByteArrayAndResult {
+
+ private final byte[] bytes;
+ private final T result;
+
+ public ByteArrayAndResult(byte[] bytes, T result) {
+ this.bytes = bytes;
+ this.result = result;
+ }
+
+ public byte[] getBytes() {
+ return bytes;
+ }
+
+ public T getResult() {
+ return result;
+ }
+}
diff --git a/sop-java/src/main/java/sop/DecryptionResult.java b/sop-java/src/main/java/sop/DecryptionResult.java
new file mode 100644
index 00000000..3fa32d9c
--- /dev/null
+++ b/sop-java/src/main/java/sop/DecryptionResult.java
@@ -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 sop;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import sop.util.Optional;
+
+public class DecryptionResult {
+
+ private final Optional sessionKey;
+ private final List verifications;
+
+ public DecryptionResult(SessionKey sessionKey, List verifications) {
+ this.sessionKey = Optional.ofNullable(sessionKey);
+ this.verifications = Collections.unmodifiableList(verifications);
+ }
+
+ public Optional getSessionKey() {
+ return sessionKey;
+ }
+
+ public List getVerifications() {
+ return new ArrayList<>(verifications);
+ }
+}
diff --git a/sop-java/src/main/java/sop/Ready.java b/sop-java/src/main/java/sop/Ready.java
new file mode 100644
index 00000000..4eea4bb6
--- /dev/null
+++ b/sop-java/src/main/java/sop/Ready.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2021 Paul Schaub.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package sop;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+public abstract class Ready {
+
+ public abstract void writeTo(OutputStream outputStream) throws IOException;
+
+ public byte[] getBytes() throws IOException {
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+ writeTo(bytes);
+ return bytes.toByteArray();
+ }
+}
diff --git a/sop-java/src/main/java/sop/ReadyWithResult.java b/sop-java/src/main/java/sop/ReadyWithResult.java
new file mode 100644
index 00000000..12744b32
--- /dev/null
+++ b/sop-java/src/main/java/sop/ReadyWithResult.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2021 Paul Schaub.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package sop;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+import sop.exception.SOPGPException;
+
+public abstract class ReadyWithResult {
+
+ public abstract T writeTo(OutputStream outputStream) throws IOException, SOPGPException.NoSignature;
+
+ public ByteArrayAndResult toBytes() throws IOException, SOPGPException.NoSignature {
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+ T result = writeTo(bytes);
+ return new ByteArrayAndResult<>(bytes.toByteArray(), result);
+ }
+}
diff --git a/sop-java/src/main/java/sop/SOP.java b/sop-java/src/main/java/sop/SOP.java
new file mode 100644
index 00000000..011129f3
--- /dev/null
+++ b/sop-java/src/main/java/sop/SOP.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2021 Paul Schaub.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package sop;
+
+import 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;
+
+/**
+ * Stateless OpenPGP Interface.
+ */
+public interface SOP {
+
+ /**
+ * Get information about the implementations name and version.
+ *
+ * @return version
+ */
+ Version version();
+
+ /**
+ * Generate a secret key.
+ * Customize the operation using the builder {@link GenerateKey}.
+ *
+ * @return builder instance
+ */
+ GenerateKey generateKey();
+
+ /**
+ * Extract a certificate (public key) from a secret key.
+ * Customize the operation using the builder {@link ExtractCert}.
+ *
+ * @return builder instance
+ */
+ ExtractCert extractCert();
+
+ /**
+ * Create detached signatures.
+ * Customize the operation using the builder {@link Sign}.
+ *
+ * @return builder instance
+ */
+ Sign sign();
+
+ /**
+ * Verify detached signatures.
+ * Customize the operation using the builder {@link Verify}.
+ *
+ * @return builder instance
+ */
+ Verify verify();
+
+ /**
+ * Encrypt a message.
+ * Customize the operation using the builder {@link Encrypt}.
+ *
+ * @return builder instance
+ */
+ Encrypt encrypt();
+
+ /**
+ * Decrypt a message.
+ * Customize the operation using the builder {@link Decrypt}.
+ *
+ * @return builder instance
+ */
+ Decrypt decrypt();
+
+ /**
+ * Convert binary OpenPGP data to ASCII.
+ * Customize the operation using the builder {@link Armor}.
+ *
+ * @return builder instance
+ */
+ Armor armor();
+
+ /**
+ * Converts ASCII armored OpenPGP data to binary.
+ * Customize the operation using the builder {@link Dearmor}.
+ *
+ * @return builder instance
+ */
+ Dearmor dearmor();
+}
diff --git a/sop-java/src/main/java/sop/SessionKey.java b/sop-java/src/main/java/sop/SessionKey.java
new file mode 100644
index 00000000..86e0edf5
--- /dev/null
+++ b/sop-java/src/main/java/sop/SessionKey.java
@@ -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 sop;
+
+import java.util.Arrays;
+
+import sop.util.HexUtil;
+
+public class SessionKey {
+
+ private final byte algorithm;
+ private final byte[] sessionKey;
+
+ public SessionKey(byte algorithm, byte[] sessionKey) {
+ this.algorithm = algorithm;
+ this.sessionKey = sessionKey;
+ }
+
+ /**
+ * Return the symmetric algorithm octet.
+ *
+ * @return algorithm id
+ */
+ public byte getAlgorithm() {
+ return algorithm;
+ }
+
+ /**
+ * Return the session key.
+ *
+ * @return session key
+ */
+ public byte[] getKey() {
+ return sessionKey;
+ }
+
+ @Override
+ public int hashCode() {
+ return getAlgorithm() * 17 + Arrays.hashCode(getKey());
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == null) {
+ return false;
+ }
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof SessionKey)) {
+ return false;
+ }
+
+ SessionKey otherKey = (SessionKey) other;
+ return getAlgorithm() == otherKey.getAlgorithm() && Arrays.equals(getKey(), otherKey.getKey());
+ }
+
+ @Override
+ public String toString() {
+ return "" + (int) getAlgorithm() + ':' + HexUtil.bytesToHex(sessionKey);
+ }
+}
diff --git a/sop-java/src/main/java/sop/Verification.java b/sop-java/src/main/java/sop/Verification.java
new file mode 100644
index 00000000..5dd60cd9
--- /dev/null
+++ b/sop-java/src/main/java/sop/Verification.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2021 Paul Schaub.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package sop;
+
+import java.util.Date;
+
+import sop.util.UTCUtil;
+
+public class Verification {
+
+ private final Date creationTime;
+ private final String signingKeyFingerprint;
+ private final String signingCertFingerprint;
+
+ public Verification(Date creationTime, String signingKeyFingerprint, String signingCertFingerprint) {
+ this.creationTime = creationTime;
+ this.signingKeyFingerprint = signingKeyFingerprint;
+ this.signingCertFingerprint = signingCertFingerprint;
+ }
+
+ /**
+ * Return the signatures creation time.
+ *
+ * @return signature creation time
+ */
+ public Date getCreationTime() {
+ return creationTime;
+ }
+
+ /**
+ * Return the fingerprint of the signing (sub)key.
+ *
+ * @return signing key fingerprint
+ */
+ public String getSigningKeyFingerprint() {
+ return signingKeyFingerprint;
+ }
+
+ /**
+ * Return the fingerprint fo the signing certificate.
+ *
+ * @return signing certificate fingerprint
+ */
+ public String getSigningCertFingerprint() {
+ return signingCertFingerprint;
+ }
+
+ @Override
+ public String toString() {
+ return UTCUtil.formatUTCDate(getCreationTime()) +
+ ' ' +
+ getSigningKeyFingerprint() +
+ ' ' +
+ getSigningCertFingerprint();
+ }
+}
diff --git a/sop-java/src/main/java/sop/enums/ArmorLabel.java b/sop-java/src/main/java/sop/enums/ArmorLabel.java
new file mode 100644
index 00000000..f5c1eea2
--- /dev/null
+++ b/sop-java/src/main/java/sop/enums/ArmorLabel.java
@@ -0,0 +1,24 @@
+/*
+ * 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.enums;
+
+public enum ArmorLabel {
+ Auto,
+ Sig,
+ Key,
+ Cert,
+ Message
+}
diff --git a/sop-java/src/main/java/sop/enums/EncryptAs.java b/sop-java/src/main/java/sop/enums/EncryptAs.java
new file mode 100644
index 00000000..590333da
--- /dev/null
+++ b/sop-java/src/main/java/sop/enums/EncryptAs.java
@@ -0,0 +1,22 @@
+/*
+ * 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.enums;
+
+public enum EncryptAs {
+ Binary,
+ Text,
+ MIME
+}
diff --git a/sop-java/src/main/java/sop/enums/SignAs.java b/sop-java/src/main/java/sop/enums/SignAs.java
new file mode 100644
index 00000000..7c4e0249
--- /dev/null
+++ b/sop-java/src/main/java/sop/enums/SignAs.java
@@ -0,0 +1,21 @@
+/*
+ * 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.enums;
+
+public enum SignAs {
+ Binary,
+ Text
+}
diff --git a/sop-java/src/main/java/sop/enums/package-info.java b/sop-java/src/main/java/sop/enums/package-info.java
new file mode 100644
index 00000000..1406a899
--- /dev/null
+++ b/sop-java/src/main/java/sop/enums/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
+
+/**
+ * Stateless OpenPGP Interface for Java.
+ * Enumerations.
+ */
+package sop.enums;
diff --git a/sop-java/src/main/java/sop/exception/SOPGPException.java b/sop-java/src/main/java/sop/exception/SOPGPException.java
new file mode 100644
index 00000000..c86d8d39
--- /dev/null
+++ b/sop-java/src/main/java/sop/exception/SOPGPException.java
@@ -0,0 +1,157 @@
+/*
+ * 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.exception;
+
+public class SOPGPException extends Exception {
+
+ public SOPGPException() {
+ super();
+ }
+
+ public SOPGPException(String message) {
+ super(message);
+ }
+
+ public SOPGPException(Throwable e) {
+ super(e);
+ }
+
+ public static class NoSignature extends SOPGPException {
+
+ public static final int EXIT_CODE = 3;
+
+ public int getExitCode() {
+ return EXIT_CODE;
+ }
+ }
+
+ public static class UnsupportedAsymmetricAlgo extends SOPGPException {
+
+ public static final int EXIT_CODE = 13;
+
+ public UnsupportedAsymmetricAlgo(Throwable e) {
+ super(e);
+ }
+
+ public int getExitCode() {
+ return EXIT_CODE;
+ }
+ }
+
+ public static class CertCannotEncrypt extends SOPGPException {
+ public static final int EXIT_CODE = 17;
+
+ public int getExitCode() {
+ return EXIT_CODE;
+ }
+ }
+
+ public static class CertCannotSign extends SOPGPException {
+
+ }
+
+ public static class MissingArg extends SOPGPException {
+
+ public static final int EXIT_CODE = 19;
+
+ public MissingArg(String s) {
+ super(s);
+ }
+
+ public int getExitCode() {
+ return EXIT_CODE;
+ }
+ }
+
+ public static class IncompleteVerification extends SOPGPException {
+
+ public static final int EXIT_CODE = 23;
+
+ public int getExitCode() {
+ return EXIT_CODE;
+ }
+ }
+
+ public static class CannotDecrypt extends SOPGPException {
+
+ public static final int EXIT_CODE = 29;
+
+ public int getExitCode() {
+ return EXIT_CODE;
+ }
+ }
+
+ public static class PasswordNotHumanReadable extends SOPGPException {
+
+ public static final int EXIT_CODE = 31;
+
+ public int getExitCode() {
+ return EXIT_CODE;
+ }
+ }
+
+ public static class UnsupportedOption extends SOPGPException {
+
+ public static final int EXIT_CODE = 37;
+
+ public int getExitCode() {
+ return EXIT_CODE;
+ }
+ }
+
+ public static class BadData extends SOPGPException {
+
+ public static final int EXIT_CODE = 41;
+
+ public BadData(Throwable e) {
+ super(e);
+ }
+
+ public int getExitCode() {
+ return EXIT_CODE;
+ }
+ }
+
+ public static class ExpectedText extends SOPGPException {
+
+ public static final int EXIT_CODE = 53;
+
+ public int getExitCode() {
+ return EXIT_CODE;
+ }
+ }
+
+ public static class OutputExists extends SOPGPException {
+
+ }
+
+ public static class KeyIsProtected extends SOPGPException {
+
+ }
+
+ public static class AmbiguousInput extends SOPGPException {
+
+ }
+
+ public static class NotImplemented extends SOPGPException {
+
+ public static final int EXIT_CODE = 69;
+
+ public int getExitCode() {
+ return EXIT_CODE;
+ }
+ }
+}
diff --git a/sop-java/src/main/java/sop/exception/package-info.java b/sop-java/src/main/java/sop/exception/package-info.java
new file mode 100644
index 00000000..0c65aecd
--- /dev/null
+++ b/sop-java/src/main/java/sop/exception/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
+
+/**
+ * Stateless OpenPGP Interface for Java.
+ * Exception classes.
+ */
+package sop.exception;
diff --git a/sop-java/src/main/java/sop/operation/Armor.java b/sop-java/src/main/java/sop/operation/Armor.java
new file mode 100644
index 00000000..59db198a
--- /dev/null
+++ b/sop-java/src/main/java/sop/operation/Armor.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2021 Paul Schaub.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package sop.operation;
+
+import java.io.InputStream;
+
+import sop.Ready;
+import sop.enums.ArmorLabel;
+import sop.exception.SOPGPException;
+
+public interface Armor {
+
+ /**
+ * Overrides automatic detection of label.
+ *
+ * @param label armor label
+ * @return builder instance
+ */
+ Armor label(ArmorLabel label) throws SOPGPException.UnsupportedOption;
+
+ /**
+ * Allow nested Armoring.
+ *
+ * @return builder instance
+ */
+ Armor allowNested() throws SOPGPException.UnsupportedOption;
+
+ /**
+ * Armor the provided data.
+ *
+ * @param data input stream of unarmored OpenPGP data
+ * @return armored data
+ */
+ Ready data(InputStream data) throws SOPGPException.BadData;
+}
diff --git a/sop-java/src/main/java/sop/operation/Dearmor.java b/sop-java/src/main/java/sop/operation/Dearmor.java
new file mode 100644
index 00000000..43a55bc4
--- /dev/null
+++ b/sop-java/src/main/java/sop/operation/Dearmor.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2021 Paul Schaub.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package sop.operation;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import sop.Ready;
+import sop.exception.SOPGPException;
+
+public interface Dearmor {
+
+ /**
+ * Dearmor armored OpenPGP data.
+ *
+ * @param data armored OpenPGP data
+ * @return input stream of unarmored data
+ */
+ Ready data(InputStream data) throws SOPGPException.BadData, IOException;
+}
diff --git a/sop-java/src/main/java/sop/operation/Decrypt.java b/sop-java/src/main/java/sop/operation/Decrypt.java
new file mode 100644
index 00000000..1ed1e700
--- /dev/null
+++ b/sop-java/src/main/java/sop/operation/Decrypt.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2021 Paul Schaub.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package sop.operation;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Date;
+
+import sop.DecryptionResult;
+import sop.ReadyWithResult;
+import sop.SessionKey;
+import sop.exception.SOPGPException;
+
+public interface Decrypt {
+
+ /**
+ * Makes the SOP consider signatures before this date invalid.
+ *
+ * @param timestamp timestamp
+ * @return builder instance
+ */
+ Decrypt verifyNotBefore(Date timestamp)
+ throws SOPGPException.UnsupportedOption;
+
+ /**
+ * Makes the SOP consider signatures after this date invalid.
+ *
+ * @param timestamp timestamp
+ * @return builder instance
+ */
+ Decrypt verifyNotAfter(Date timestamp)
+ throws SOPGPException.UnsupportedOption;
+
+ /**
+ * Adds the verification cert.
+ *
+ * @param cert input stream containing the cert
+ * @return builder instance
+ */
+ Decrypt verifyWithCert(InputStream cert)
+ throws SOPGPException.BadData,
+ IOException;
+
+ /**
+ * Tries to decrypt with the given session key.
+ *
+ * @param sessionKey session key
+ * @return builder instance
+ */
+ Decrypt withSessionKey(SessionKey sessionKey)
+ throws SOPGPException.UnsupportedOption;
+
+ /**
+ * Tries to decrypt with the given password.
+ *
+ * @param password password
+ * @return builder instance
+ */
+ Decrypt withPassword(String password)
+ throws SOPGPException.PasswordNotHumanReadable,
+ SOPGPException.UnsupportedOption;
+
+ /**
+ * Adds the decryption key.
+ *
+ * @param key input stream containing the key
+ * @return builder instance
+ */
+ Decrypt withKey(InputStream key)
+ throws SOPGPException.KeyIsProtected,
+ SOPGPException.BadData,
+ SOPGPException.UnsupportedAsymmetricAlgo;
+
+ /**
+ * Decrypts the given ciphertext, returning verification results and plaintext.
+ * @param ciphertext ciphertext
+ * @return ready with result
+ */
+ ReadyWithResult ciphertext(InputStream ciphertext)
+ throws SOPGPException.BadData, SOPGPException.MissingArg, SOPGPException.CannotDecrypt;
+}
diff --git a/sop-java/src/main/java/sop/operation/Encrypt.java b/sop-java/src/main/java/sop/operation/Encrypt.java
new file mode 100644
index 00000000..2722ed71
--- /dev/null
+++ b/sop-java/src/main/java/sop/operation/Encrypt.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2021 Paul Schaub.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package sop.operation;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import sop.Ready;
+import sop.enums.EncryptAs;
+import sop.exception.SOPGPException;
+
+public interface Encrypt {
+
+ /**
+ * Disable ASCII armor encoding.
+ *
+ * @return builder instance
+ */
+ Encrypt noArmor();
+
+ /**
+ * Sets encryption mode.
+ *
+ * @param mode mode
+ * @return builder instance
+ */
+ Encrypt mode(EncryptAs mode)
+ throws SOPGPException.UnsupportedOption;
+
+ /**
+ * Adds the signer key.
+ *
+ * @param key input stream containing the encoded signer key
+ * @return builder instance
+ */
+ Encrypt signWith(InputStream key)
+ throws SOPGPException.KeyIsProtected,
+ SOPGPException.CertCannotSign,
+ SOPGPException.UnsupportedAsymmetricAlgo,
+ SOPGPException.BadData;
+
+ /**
+ * Encrypt with the given password.
+ *
+ * @param password password
+ * @return builder instance
+ */
+ Encrypt withPassword(String password)
+ throws SOPGPException.PasswordNotHumanReadable,
+ SOPGPException.UnsupportedOption;
+
+ /**
+ * Encrypt with the given cert.
+ *
+ * @param cert input stream containing the encoded cert.
+ * @return builder instance
+ */
+ Encrypt withCert(InputStream cert)
+ throws SOPGPException.CertCannotEncrypt,
+ SOPGPException.UnsupportedAsymmetricAlgo,
+ SOPGPException.BadData;
+
+ /**
+ * Encrypt the given data yielding the ciphertext.
+ * @param plaintext plaintext
+ * @return input stream containing the ciphertext
+ */
+ Ready plaintext(InputStream plaintext)
+ throws IOException;
+}
diff --git a/sop-java/src/main/java/sop/operation/ExtractCert.java b/sop-java/src/main/java/sop/operation/ExtractCert.java
new file mode 100644
index 00000000..4c427701
--- /dev/null
+++ b/sop-java/src/main/java/sop/operation/ExtractCert.java
@@ -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 sop.operation;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import sop.Ready;
+import sop.exception.SOPGPException;
+
+public interface ExtractCert {
+
+ /**
+ * Disable ASCII armor encoding.
+ *
+ * @return builder instance
+ */
+ ExtractCert noArmor();
+
+ /**
+ * Extract the cert from the provided key.
+ *
+ * @param keyInputStream input stream containing the encoding of an OpenPGP key
+ * @return input stream containing the encoding of the keys cert
+ */
+ Ready key(InputStream keyInputStream) throws IOException, SOPGPException.BadData;
+}
diff --git a/sop-java/src/main/java/sop/operation/GenerateKey.java b/sop-java/src/main/java/sop/operation/GenerateKey.java
new file mode 100644
index 00000000..7447d924
--- /dev/null
+++ b/sop-java/src/main/java/sop/operation/GenerateKey.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2021 Paul Schaub.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package sop.operation;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import sop.Ready;
+import sop.exception.SOPGPException;
+
+public interface GenerateKey {
+
+ /**
+ * Disable ASCII armor encoding.
+ *
+ * @return builder instance
+ */
+ GenerateKey noArmor();
+
+ /**
+ * Adds a user-id.
+ *
+ * @param userId user-id
+ * @return builder instance
+ */
+ GenerateKey userId(String userId);
+
+ /**
+ * Generate the OpenPGP key and return it encoded as an {@link InputStream}.
+ *
+ * @return key
+ */
+ Ready generate() throws SOPGPException.MissingArg, SOPGPException.UnsupportedAsymmetricAlgo, IOException;
+}
diff --git a/sop-java/src/main/java/sop/operation/Sign.java b/sop-java/src/main/java/sop/operation/Sign.java
new file mode 100644
index 00000000..1a3f7418
--- /dev/null
+++ b/sop-java/src/main/java/sop/operation/Sign.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2021 Paul Schaub.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package sop.operation;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import sop.Ready;
+import sop.enums.SignAs;
+import sop.exception.SOPGPException;
+
+public interface Sign {
+
+ /**
+ * Disable ASCII armor encoding.
+ *
+ * @return builder instance
+ */
+ Sign noArmor();
+
+ /**
+ * Sets the signature mode.
+ *
+ * @param mode signature mode
+ * @return builder instance
+ */
+ Sign mode(SignAs mode) throws SOPGPException.UnsupportedOption;
+
+ /**
+ * Adds the signer key.
+ *
+ * @param key input stream containing encoded key
+ * @return builder instance
+ */
+ Sign key(InputStream key) throws SOPGPException.KeyIsProtected, SOPGPException.BadData, IOException;
+
+ /**
+ * Signs data.
+ *
+ * @param data input stream containing data
+ * @return ready
+ */
+ Ready data(InputStream data) throws IOException, SOPGPException.ExpectedText;
+}
diff --git a/sop-java/src/main/java/sop/operation/Verify.java b/sop-java/src/main/java/sop/operation/Verify.java
new file mode 100644
index 00000000..ae515e1e
--- /dev/null
+++ b/sop-java/src/main/java/sop/operation/Verify.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2021 Paul Schaub.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package sop.operation;
+
+import java.io.InputStream;
+import java.util.Date;
+
+import sop.exception.SOPGPException;
+
+public interface Verify extends VerifySignatures {
+
+ /**
+ * Makes the SOP implementation consider signatures before this date invalid.
+ *
+ * @param timestamp timestamp
+ * @return builder instance
+ */
+ Verify notBefore(Date timestamp) throws SOPGPException.UnsupportedOption;
+
+ /**
+ * Makes the SOP implementation consider signatures after this date invalid.
+ *
+ * @param timestamp timestamp
+ * @return builder instance
+ */
+ Verify notAfter(Date timestamp) throws SOPGPException.UnsupportedOption;
+
+ /**
+ * Adds the verification cert.
+ *
+ * @param cert input stream containing the encoded cert
+ * @return builder instance
+ */
+ Verify cert(InputStream cert) throws SOPGPException.BadData;
+
+ /**
+ * Provides the signatures.
+ * @param signatures input stream containing encoded, detached signatures.
+ *
+ * @return builder instance
+ */
+ VerifySignatures signatures(InputStream signatures) throws SOPGPException.BadData;
+
+}
diff --git a/sop-java/src/main/java/sop/operation/VerifySignatures.java b/sop-java/src/main/java/sop/operation/VerifySignatures.java
new file mode 100644
index 00000000..5fc01b31
--- /dev/null
+++ b/sop-java/src/main/java/sop/operation/VerifySignatures.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2021 Paul Schaub.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package sop.operation;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.List;
+
+import sop.Verification;
+import sop.exception.SOPGPException;
+
+public interface VerifySignatures {
+
+ List data(InputStream data) throws IOException, SOPGPException.NoSignature, SOPGPException.BadData;
+}
diff --git a/sop-java/src/main/java/sop/operation/Version.java b/sop-java/src/main/java/sop/operation/Version.java
new file mode 100644
index 00000000..db89e121
--- /dev/null
+++ b/sop-java/src/main/java/sop/operation/Version.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2021 Paul Schaub.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package sop.operation;
+
+public interface Version {
+
+ /**
+ * Return the implementations name.
+ *
+ * @return implementation name
+ */
+ String getName();
+
+ /**
+ * Return the implementations version string.
+ *
+ * @return version string
+ */
+ String getVersion();
+}
diff --git a/sop-java/src/main/java/sop/operation/package-info.java b/sop-java/src/main/java/sop/operation/package-info.java
new file mode 100644
index 00000000..6156f3d1
--- /dev/null
+++ b/sop-java/src/main/java/sop/operation/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
+
+/**
+ * Stateless OpenPGP Interface for Java.
+ * Different cryptographic operations.
+ */
+package sop.operation;
diff --git a/sop-java/src/main/java/sop/package-info.java b/sop-java/src/main/java/sop/package-info.java
new file mode 100644
index 00000000..8caf9bb9
--- /dev/null
+++ b/sop-java/src/main/java/sop/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * 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.
+ */
+
+/**
+ * Stateless OpenPGP Interface for Java.
+ */
+package sop;
diff --git a/sop-java/src/main/java/sop/util/HexUtil.java b/sop-java/src/main/java/sop/util/HexUtil.java
new file mode 100644
index 00000000..c1cb3580
--- /dev/null
+++ b/sop-java/src/main/java/sop/util/HexUtil.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2021 Paul Schaub, @maybeWeCouldStealAVan, @Dave L.
+ *
+ * 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.util;
+
+public class HexUtil {
+
+ private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray();
+
+ /**
+ * Encode a byte array to a hex string.
+ *
+ * @see
+ * How to convert a byte array to a hex string in Java?
+ * @param bytes
+ * @return
+ */
+ public static String bytesToHex(byte[] bytes) {
+ char[] hexChars = new char[bytes.length * 2];
+ for (int j = 0; j < bytes.length; j++) {
+ int v = bytes[j] & 0xFF;
+ hexChars[j * 2] = HEX_ARRAY[v >>> 4];
+ hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F];
+ }
+ return new String(hexChars);
+ }
+
+ /**
+ * Decode a hex string into a byte array.
+ *
+ * @see
+ * Convert a string representation of a hex dump to a byte array using Java?
+ * @param s hex string
+ * @return decoded byte array
+ */
+ public static byte[] hexToBytes(String s) {
+ int len = s.length();
+ byte[] data = new byte[len / 2];
+ for (int i = 0; i < len; i += 2) {
+ data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
+ + Character.digit(s.charAt(i + 1), 16));
+ }
+ return data;
+ }
+}
diff --git a/sop-java/src/main/java/sop/util/Optional.java b/sop-java/src/main/java/sop/util/Optional.java
new file mode 100644
index 00000000..7ee61df5
--- /dev/null
+++ b/sop-java/src/main/java/sop/util/Optional.java
@@ -0,0 +1,61 @@
+/*
+ * 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.util;
+
+/**
+ * Backport of java.util.Optional for older Android versions.
+ *
+ * @param item type
+ */
+public class Optional {
+
+ private final T item;
+
+ public Optional() {
+ this(null);
+ }
+
+ public Optional(T item) {
+ this.item = item;
+ }
+
+ public static Optional of(T item) {
+ if (item == null) {
+ throw new NullPointerException("Item cannot be null.");
+ }
+ return new Optional<>(item);
+ }
+
+ public static Optional ofNullable(T item) {
+ return new Optional<>(item);
+ }
+
+ public static Optional ofEmpty() {
+ return new Optional<>(null);
+ }
+
+ public T get() {
+ return item;
+ }
+
+ public boolean isPresent() {
+ return item != null;
+ }
+
+ public boolean isEmpty() {
+ return item == null;
+ }
+}
diff --git a/sop-java/src/main/java/sop/util/ProxyOutputStream.java b/sop-java/src/main/java/sop/util/ProxyOutputStream.java
new file mode 100644
index 00000000..16fd04f6
--- /dev/null
+++ b/sop-java/src/main/java/sop/util/ProxyOutputStream.java
@@ -0,0 +1,91 @@
+/*
+ * 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.util;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * {@link OutputStream} that buffers data being written into it, until its underlying output stream is being replaced.
+ * At that point, first all the buffered data is being written to the underlying stream, followed by any successive
+ * data that may get written to the {@link ProxyOutputStream}.
+ *
+ * This class is useful if we need to provide an {@link OutputStream} at one point in time where the final
+ * target output stream is not yet known.
+ */
+public class ProxyOutputStream extends OutputStream {
+
+ private final ByteArrayOutputStream buffer;
+ private OutputStream swapped;
+
+ public ProxyOutputStream() {
+ this.buffer = new ByteArrayOutputStream();
+ }
+
+ public synchronized void replaceOutputStream(OutputStream underlying) throws IOException {
+ if (underlying == null) {
+ throw new NullPointerException("Underlying OutputStream cannot be null.");
+ }
+ this.swapped = underlying;
+
+ byte[] bufferBytes = buffer.toByteArray();
+ swapped.write(bufferBytes);
+ }
+
+ @Override
+ public synchronized void write(byte[] b) throws IOException {
+ if (swapped == null) {
+ buffer.write(b);
+ } else {
+ swapped.write(b);
+ }
+ }
+
+ @Override
+ public synchronized void write(byte[] b, int off, int len) throws IOException {
+ if (swapped == null) {
+ buffer.write(b, off, len);
+ } else {
+ swapped.write(b, off, len);
+ }
+ }
+
+ @Override
+ public synchronized void flush() throws IOException {
+ buffer.flush();
+ if (swapped != null) {
+ swapped.flush();
+ }
+ }
+
+ @Override
+ public synchronized void close() throws IOException {
+ buffer.close();
+ if (swapped != null) {
+ swapped.close();
+ }
+ }
+
+ @Override
+ public synchronized void write(int i) throws IOException {
+ if (swapped == null) {
+ buffer.write(i);
+ } else {
+ swapped.write(i);
+ }
+ }
+}
diff --git a/sop-java/src/main/java/sop/util/UTCUtil.java b/sop-java/src/main/java/sop/util/UTCUtil.java
new file mode 100644
index 00000000..c847863e
--- /dev/null
+++ b/sop-java/src/main/java/sop/util/UTCUtil.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2021 Paul Schaub.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package sop.util;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.TimeZone;
+
+/**
+ * Utility class to parse and format dates as ISO-8601 UTC timestamps.
+ */
+public class UTCUtil {
+
+ public static SimpleDateFormat UTC_FORMATTER = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
+ public static SimpleDateFormat[] UTC_PARSERS = new SimpleDateFormat[] {
+ UTC_FORMATTER,
+ new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssX"),
+ new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'"),
+ new SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'")
+ };
+
+ static {
+ for (SimpleDateFormat f : UTC_PARSERS) {
+ f.setTimeZone(TimeZone.getTimeZone("UTC"));
+ }
+ }
+ /**
+ * Parse an ISO-8601 UTC timestamp from a string.
+ *
+ * @param dateString string
+ * @return date
+ */
+ public static Date parseUTCDate(String dateString) {
+ for (SimpleDateFormat parser : UTC_PARSERS) {
+ try {
+ return parser.parse(dateString);
+ } catch (ParseException e) {
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Format a date as ISO-8601 UTC timestamp.
+ *
+ * @param date date
+ * @return timestamp string
+ */
+ public static String formatUTCDate(Date date) {
+ return UTC_FORMATTER.format(date);
+ }
+}
diff --git a/sop-java/src/main/java/sop/util/package-info.java b/sop-java/src/main/java/sop/util/package-info.java
new file mode 100644
index 00000000..11a87c66
--- /dev/null
+++ b/sop-java/src/main/java/sop/util/package-info.java
@@ -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.
+ */
+/**
+ * Utility classes.
+ */
+package sop.util;
diff --git a/sop-java/src/test/java/sop/util/ByteArrayAndResultTest.java b/sop-java/src/test/java/sop/util/ByteArrayAndResultTest.java
new file mode 100644
index 00000000..8b7c2d78
--- /dev/null
+++ b/sop-java/src/test/java/sop/util/ByteArrayAndResultTest.java
@@ -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.util;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+import sop.ByteArrayAndResult;
+import sop.Verification;
+
+public class ByteArrayAndResultTest {
+
+ @Test
+ public void testCreationAndGetters() {
+ byte[] bytes = "Hello, World!\n".getBytes(StandardCharsets.UTF_8);
+ List result = Collections.singletonList(
+ new Verification(UTCUtil.parseUTCDate("2019-10-24T23:48:29Z"),
+ "C90E6D36200A1B922A1509E77618196529AE5FF8",
+ "C4BC2DDB38CCE96485EBE9C2F20691179038E5C6")
+ );
+ ByteArrayAndResult> bytesAndResult = new ByteArrayAndResult<>(bytes, result);
+
+ assertArrayEquals(bytes, bytesAndResult.getBytes());
+ assertEquals(result, bytesAndResult.getResult());
+ }
+}
diff --git a/sop-java/src/test/java/sop/util/HexUtilTest.java b/sop-java/src/test/java/sop/util/HexUtilTest.java
new file mode 100644
index 00000000..6d229aad
--- /dev/null
+++ b/sop-java/src/test/java/sop/util/HexUtilTest.java
@@ -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.util;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.nio.charset.Charset;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test using some test vectors from RFC4648.
+ *
+ * @see RFC-4648 §10: Test Vectors
+ */
+public class HexUtilTest {
+
+ private static final Charset ASCII = Charset.forName("US-ASCII");
+
+ @Test
+ public void emptyHexEncodeTest() {
+ assertHexEquals("", "");
+ }
+
+ @Test
+ public void encodeF() {
+ assertHexEquals("66", "f");
+ }
+
+ @Test
+ public void encodeFo() {
+ assertHexEquals("666F", "fo");
+ }
+
+ @Test
+ public void encodeFoo() {
+ assertHexEquals("666F6F", "foo");
+ }
+
+ @Test
+ public void encodeFoob() {
+ assertHexEquals("666F6F62", "foob");
+ }
+
+ @Test
+ public void encodeFooba() {
+ assertHexEquals("666F6F6261", "fooba");
+ }
+
+ @Test
+ public void encodeFoobar() {
+ assertHexEquals("666F6F626172", "foobar");
+ }
+
+ private void assertHexEquals(String hex, String ascii) {
+ assertEquals(hex, HexUtil.bytesToHex(ascii.getBytes(ASCII)));
+ assertArrayEquals(ascii.getBytes(ASCII), HexUtil.hexToBytes(hex));
+ }
+}
diff --git a/sop-java/src/test/java/sop/util/OptionalTest.java b/sop-java/src/test/java/sop/util/OptionalTest.java
new file mode 100644
index 00000000..edcb7aa5
--- /dev/null
+++ b/sop-java/src/test/java/sop/util/OptionalTest.java
@@ -0,0 +1,89 @@
+/*
+ * 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.util;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+
+public class OptionalTest {
+
+ @Test
+ public void testEmpty() {
+ Optional optional = new Optional<>();
+ assertEmpty(optional);
+ }
+
+ @Test
+ public void testArg() {
+ String string = "foo";
+ Optional optional = new Optional<>(string);
+ assertFalse(optional.isEmpty());
+ assertTrue(optional.isPresent());
+ assertEquals(string, optional.get());
+ }
+
+ @Test
+ public void testOfEmpty() {
+ Optional optional = Optional.ofEmpty();
+ assertEmpty(optional);
+ }
+
+ @Test
+ public void testNullArg() {
+ Optional optional = new Optional<>(null);
+ assertEmpty(optional);
+ }
+
+ @Test
+ public void testOfWithNullArgThrows() {
+ assertThrows(NullPointerException.class, () -> Optional.of(null));
+ }
+
+ @Test
+ public void testOf() {
+ String string = "Hello, World!";
+ Optional optional = Optional.of(string);
+ assertFalse(optional.isEmpty());
+ assertTrue(optional.isPresent());
+ assertEquals(string, optional.get());
+ }
+
+ @Test
+ public void testOfNullableWithNull() {
+ Optional optional = Optional.ofNullable(null);
+ assertEmpty(optional);
+ }
+
+ @Test
+ public void testOfNullableWithArg() {
+ Optional optional = Optional.ofNullable("bar");
+ assertEquals("bar", optional.get());
+ assertFalse(optional.isEmpty());
+ assertTrue(optional.isPresent());
+ }
+
+ private void assertEmpty(Optional optional) {
+ assertTrue(optional.isEmpty());
+ assertFalse(optional.isPresent());
+
+ assertNull(optional.get());
+ }
+}
diff --git a/sop-java/src/test/java/sop/util/ProxyOutputStreamTest.java b/sop-java/src/test/java/sop/util/ProxyOutputStreamTest.java
new file mode 100644
index 00000000..b9c203b3
--- /dev/null
+++ b/sop-java/src/test/java/sop/util/ProxyOutputStreamTest.java
@@ -0,0 +1,51 @@
+/*
+ * 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.util;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+import org.junit.jupiter.api.Test;
+
+public class ProxyOutputStreamTest {
+
+ @Test
+ public void replaceOutputStreamThrowsNPEForNull() {
+ ProxyOutputStream proxy = new ProxyOutputStream();
+ assertThrows(NullPointerException.class, () -> proxy.replaceOutputStream(null));
+ }
+
+ @Test
+ public void testSwappingStreamPreservesWrittenBytes() throws IOException {
+ byte[] firstSection = "Foo\nBar\n".getBytes(StandardCharsets.UTF_8);
+ byte[] secondSection = "Baz\n".getBytes(StandardCharsets.UTF_8);
+
+ ProxyOutputStream proxy = new ProxyOutputStream();
+ proxy.write(firstSection);
+
+ ByteArrayOutputStream swappedStream = new ByteArrayOutputStream();
+ proxy.replaceOutputStream(swappedStream);
+
+ proxy.write(secondSection);
+ proxy.close();
+
+ assertEquals("Foo\nBar\nBaz\n", swappedStream.toString());
+ }
+}
diff --git a/sop-java/src/test/java/sop/util/ReadyTest.java b/sop-java/src/test/java/sop/util/ReadyTest.java
new file mode 100644
index 00000000..64a08a2c
--- /dev/null
+++ b/sop-java/src/test/java/sop/util/ReadyTest.java
@@ -0,0 +1,41 @@
+/*
+ * 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.util;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+
+import org.junit.jupiter.api.Test;
+import sop.Ready;
+
+public class ReadyTest {
+
+ @Test
+ public void readyTest() throws IOException {
+ byte[] data = "Hello, World!\n".getBytes(StandardCharsets.UTF_8);
+ Ready ready = new Ready() {
+ @Override
+ public void writeTo(OutputStream outputStream) throws IOException {
+ outputStream.write(data);
+ }
+ };
+
+ assertArrayEquals(data, ready.getBytes());
+ }
+}
diff --git a/sop-java/src/test/java/sop/util/ReadyWithResultTest.java b/sop-java/src/test/java/sop/util/ReadyWithResultTest.java
new file mode 100644
index 00000000..51d1eeb7
--- /dev/null
+++ b/sop-java/src/test/java/sop/util/ReadyWithResultTest.java
@@ -0,0 +1,55 @@
+/*
+ * 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.util;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+import sop.ByteArrayAndResult;
+import sop.ReadyWithResult;
+import sop.Verification;
+import sop.exception.SOPGPException;
+
+public class ReadyWithResultTest {
+
+ @Test
+ public void testReadyWithResult() throws SOPGPException.NoSignature, IOException {
+ byte[] data = "Hello, World!\n".getBytes(StandardCharsets.UTF_8);
+ List result = Collections.singletonList(
+ new Verification(UTCUtil.parseUTCDate("2019-10-24T23:48:29Z"),
+ "C90E6D36200A1B922A1509E77618196529AE5FF8",
+ "C4BC2DDB38CCE96485EBE9C2F20691179038E5C6")
+ );
+ ReadyWithResult> readyWithResult = new ReadyWithResult>() {
+ @Override
+ public List writeTo(OutputStream outputStream) throws IOException, SOPGPException.NoSignature {
+ outputStream.write(data);
+ return result;
+ }
+ };
+
+ ByteArrayAndResult> bytesAndResult = readyWithResult.toBytes();
+ assertArrayEquals(data, bytesAndResult.getBytes());
+ assertEquals(result, bytesAndResult.getResult());
+ }
+}
diff --git a/sop-java/src/test/java/sop/util/SessionKeyTest.java b/sop-java/src/test/java/sop/util/SessionKeyTest.java
new file mode 100644
index 00000000..712e6342
--- /dev/null
+++ b/sop-java/src/test/java/sop/util/SessionKeyTest.java
@@ -0,0 +1,52 @@
+/*
+ * 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.util;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+
+import org.junit.jupiter.api.Test;
+import sop.SessionKey;
+
+public class SessionKeyTest {
+
+ @Test
+ public void toStringTest() {
+ SessionKey sessionKey = new SessionKey((byte) 9, HexUtil.hexToBytes("FCA4BEAF687F48059CACC14FB019125CD57392BAB7037C707835925CBF9F7BCD"));
+ assertEquals("9:FCA4BEAF687F48059CACC14FB019125CD57392BAB7037C707835925CBF9F7BCD", sessionKey.toString());
+ }
+
+ @Test
+ public void equalsTest() {
+ SessionKey s1 = new SessionKey((byte) 9, HexUtil.hexToBytes("FCA4BEAF687F48059CACC14FB019125CD57392BAB7037C707835925CBF9F7BCD"));
+ SessionKey s2 = new SessionKey((byte) 9, HexUtil.hexToBytes("FCA4BEAF687F48059CACC14FB019125CD57392BAB7037C707835925CBF9F7BCD"));
+ SessionKey s3 = new SessionKey((byte) 4, HexUtil.hexToBytes("FCA4BEAF687F48059CACC14FB019125CD57392BAB7037C707835925CBF9F7BCD"));
+ SessionKey s4 = new SessionKey((byte) 9, HexUtil.hexToBytes("19125CD57392BAB7037C7078359FCA4BEAF687F4025CBF9F7BCD8059CACC14FB"));
+ SessionKey s5 = new SessionKey((byte) 4, HexUtil.hexToBytes("19125CD57392BAB7037C7078359FCA4BEAF687F4025CBF9F7BCD8059CACC14FB"));
+
+ assertEquals(s1, s1);
+ assertEquals(s1, s2);
+ assertEquals(s1.hashCode(), s2.hashCode());
+ assertNotEquals(s1, s3);
+ assertNotEquals(s1.hashCode(), s3.hashCode());
+ assertNotEquals(s1, s4);
+ assertNotEquals(s1.hashCode(), s4.hashCode());
+ assertNotEquals(s4, s5);
+ assertNotEquals(s4.hashCode(), s5.hashCode());
+ assertNotEquals(s1, null);
+ assertNotEquals(s1, "FCA4BEAF687F48059CACC14FB019125CD57392BAB7037C707835925CBF9F7BCD");
+ }
+}
diff --git a/sop-java/src/test/java/sop/util/UTCUtilTest.java b/sop-java/src/test/java/sop/util/UTCUtilTest.java
new file mode 100644
index 00000000..56c77f9f
--- /dev/null
+++ b/sop-java/src/test/java/sop/util/UTCUtilTest.java
@@ -0,0 +1,59 @@
+/*
+ * 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.util;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import java.util.Date;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test parsing some date examples from the stateless OpenPGP CLI spec.
+ *
+ * @see OpenPGP Stateless CLI §4.1. Date
+ */
+public class UTCUtilTest {
+
+ @Test
+ public void parseExample1() {
+ String timestamp = "2019-10-29T12:11:04+00:00";
+ Date date = UTCUtil.parseUTCDate(timestamp);
+ assertEquals("2019-10-29T12:11:04Z", UTCUtil.formatUTCDate(date));
+ }
+
+ @Test
+ public void parseExample2() {
+ String timestamp = "2019-10-24T23:48:29Z";
+ Date date = UTCUtil.parseUTCDate(timestamp);
+ assertEquals("2019-10-24T23:48:29Z", UTCUtil.formatUTCDate(date));
+ }
+
+ @Test
+ public void parseExample3() {
+ String timestamp = "20191029T121104Z";
+ Date date = UTCUtil.parseUTCDate(timestamp);
+ assertEquals("2019-10-29T12:11:04Z", UTCUtil.formatUTCDate(date));
+ }
+
+ @Test
+ public void invalidDateReturnsNull() {
+ String invalidTimestamp = "foobar";
+ Date expectNull = UTCUtil.parseUTCDate(invalidTimestamp);
+ assertNull(expectNull);
+ }
+}