diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/SignVerifyTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/SignVerifyTest.java index 4c31f7ca..525e3e1d 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/SignVerifyTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/SignVerifyTest.java @@ -5,13 +5,16 @@ package org.pgpainless.cli.commands; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.io.BufferedReader; import java.io.ByteArrayInputStream; 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.InputStream; import java.io.OutputStream; @@ -77,6 +80,9 @@ public class SignVerifyTest { Streams.pipeAll(new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8)), dataOut); dataOut.close(); + // Define micalg output file + File micalgOut = new File(tempDir, "micalg"); + // Sign test data FileInputStream dataIn = new FileInputStream(dataFile); System.setIn(dataIn); @@ -84,7 +90,7 @@ public class SignVerifyTest { assertTrue(sigFile.createNewFile()); FileOutputStream sigOut = new FileOutputStream(sigFile); System.setOut(new PrintStream(sigOut)); - PGPainlessCLI.execute("sign", "--armor", aliceKeyFile.getAbsolutePath()); + PGPainlessCLI.execute("sign", "--armor", "--micalg-out", micalgOut.getAbsolutePath(), aliceKeyFile.getAbsolutePath()); sigOut.close(); // verify test data signature @@ -105,6 +111,15 @@ public class SignVerifyTest { assertEquals(signingKeyFingerprint.toString(), split[1].trim()); assertEquals(primaryKeyFingerprint.toString(), split[2].trim()); + // Test micalg output + assertTrue(micalgOut.exists()); + FileReader fileReader = new FileReader(micalgOut); + BufferedReader bufferedReader = new BufferedReader(fileReader); + String line = bufferedReader.readLine(); + assertNull(bufferedReader.readLine()); + bufferedReader.close(); + assertEquals("pgp-sha512", line); + System.setIn(originalIn); } diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java index 606f8673..bef260c4 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java @@ -100,10 +100,6 @@ public class DecryptImpl implements Decrypt { PGPSecretKeyRingCollection secretKeys = PGPainless.readKeyRing() .secretKeyRingCollection(keyIn); - if (secretKeys.size() != 1) { - throw new SOPGPException.BadData(new AssertionError("Exactly one single secret key expected. Got " + secretKeys.size())); - } - for (PGPSecretKeyRing secretKey : secretKeys) { KeyRingInfo info = new KeyRingInfo(secretKey); if (!info.isFullyDecrypted()) { diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java index b869d6ba..bb0af660 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java @@ -49,7 +49,7 @@ public class EncryptImpl implements Encrypt { } @Override - public Encrypt signWith(InputStream keyIn) throws SOPGPException.KeyIsProtected, SOPGPException.CertCannotSign, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData { + public Encrypt signWith(InputStream keyIn) throws SOPGPException.KeyIsProtected, SOPGPException.KeyCannotSign, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData { try { PGPSecretKeyRingCollection keys = PGPainless.readKeyRing().secretKeyRingCollection(keyIn); if (keys.size() != 1) { @@ -62,7 +62,7 @@ public class EncryptImpl implements Encrypt { try { signingOptions.addInlineSignatures(SecretKeyRingProtector.unprotectedKeys(), keys, DocumentSignatureType.BINARY_DOCUMENT); } catch (IllegalArgumentException e) { - throw new SOPGPException.CertCannotSign(); + throw new SOPGPException.KeyCannotSign(); } catch (WrongPassphraseException e) { throw new SOPGPException.KeyIsProtected(); } diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/ExtractCertImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/ExtractCertImpl.java index d22c71c3..6c3c825f 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/ExtractCertImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/ExtractCertImpl.java @@ -7,16 +7,18 @@ package org.pgpainless.sop; 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.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; 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; +import sop.operation.ExtractCert; public class ExtractCertImpl implements ExtractCert { @@ -30,21 +32,34 @@ public class ExtractCertImpl implements ExtractCert { @Override public Ready key(InputStream keyInputStream) throws IOException, SOPGPException.BadData { - PGPSecretKeyRing key = PGPainless.readKeyRing().secretKeyRing(keyInputStream); - if (key == null) { + PGPSecretKeyRingCollection keys; + try { + keys = PGPainless.readKeyRing().secretKeyRingCollection(keyInputStream); + } catch (PGPException e) { + throw new IOException("Cannot read keys.", e); + } + + if (keys == null || keys.size() == 0) { throw new SOPGPException.BadData(new PGPException("No key data found.")); } - PGPPublicKeyRing cert = KeyRingUtils.publicKeyRingFrom(key); + List certs = new ArrayList<>(); + for (PGPSecretKeyRing key : keys) { + PGPPublicKeyRing cert = PGPainless.extractCertificate(key); + certs.add(cert); + } 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(); + for (PGPPublicKeyRing cert : certs) { + OutputStream out = armor ? ArmorUtils.createArmoredOutputStreamFor(cert, outputStream) : outputStream; + cert.encode(out); + + if (armor) { + out.close(); + } } } }; diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/SignImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/SignImpl.java index 22b6ed32..068903d7 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/SignImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/SignImpl.java @@ -26,7 +26,8 @@ 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.MicAlg; +import sop.ReadyWithResult; import sop.enums.SignAs; import sop.exception.SOPGPException; import sop.operation.Sign; @@ -53,16 +54,14 @@ public class SignImpl implements Sign { public Sign key(InputStream keyIn) throws SOPGPException.KeyIsProtected, SOPGPException.BadData, IOException { try { PGPSecretKeyRingCollection keys = PGPainless.readKeyRing().secretKeyRingCollection(keyIn); - if (keys.size() != 1) { - throw new SOPGPException.BadData(new AssertionError("Exactly one secret key at a time expected. Got " + keys.size())); - } - PGPSecretKeyRing key = keys.iterator().next(); - KeyRingInfo info = new KeyRingInfo(key); - if (!info.isFullyDecrypted()) { - throw new SOPGPException.KeyIsProtected(); + for (PGPSecretKeyRing key : keys) { + KeyRingInfo info = new KeyRingInfo(key); + if (!info.isFullyDecrypted()) { + throw new SOPGPException.KeyIsProtected(); + } + signingOptions.addDetachedSignature(SecretKeyRingProtector.unprotectedKeys(), key, modeToSigType(mode)); } - signingOptions.addDetachedSignature(SecretKeyRingProtector.unprotectedKeys(), key, modeToSigType(mode)); } catch (PGPException e) { throw new SOPGPException.BadData(e); } @@ -70,7 +69,7 @@ public class SignImpl implements Sign { } @Override - public Ready data(InputStream data) throws IOException { + public ReadyWithResult data(InputStream data) throws IOException { ByteArrayOutputStream buffer = new ByteArrayOutputStream(); try { EncryptionStream signingStream = PGPainless.encryptAndOrSign() @@ -78,9 +77,9 @@ public class SignImpl implements Sign { .withOptions(ProducerOptions.sign(signingOptions) .setAsciiArmor(armor)); - return new Ready() { + return new ReadyWithResult() { @Override - public void writeTo(OutputStream outputStream) throws IOException { + public MicAlg writeTo(OutputStream outputStream) throws IOException { if (signingStream.isClosed()) { throw new IllegalStateException("EncryptionStream is already closed."); @@ -106,6 +105,8 @@ public class SignImpl implements Sign { } out.close(); outputStream.close(); // armor out does not close underlying stream + + return micAlgFromSignatures(signatures); } }; @@ -115,6 +116,19 @@ public class SignImpl implements Sign { } + private MicAlg micAlgFromSignatures(Iterable signatures) { + int algorithmId = 0; + for (PGPSignature signature : signatures) { + int sigAlg = signature.getHashAlgorithm(); + if (algorithmId == 0 || algorithmId == sigAlg) { + algorithmId = sigAlg; + } else { + return MicAlg.empty(); + } + } + return algorithmId == 0 ? MicAlg.empty() : MicAlg.fromHashAlgorithmId(algorithmId); + } + 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/VersionImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java index b08b2466..41fbd838 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java @@ -8,6 +8,7 @@ import java.io.IOException; import java.io.InputStream; import java.util.Properties; +import org.bouncycastle.jce.provider.BouncyCastleProvider; import sop.operation.Version; public class VersionImpl implements Version { @@ -33,4 +34,20 @@ public class VersionImpl implements Version { } return version; } + + @Override + public String getBackendVersion() { + double bcVersion = new BouncyCastleProvider().getVersion(); + return String.format("Bouncycastle %,.2f", bcVersion); + } + + @Override + public String getExtendedVersion() { + return getName() + " " + getVersion() + "\n" + + "Based on PGPainless " + getVersion() + "\n" + + "Using " + getBackendVersion() + "\n" + + "See https://pgpainless.org\n" + + "Implementing Stateless OpenPGP Protocol Version 3\n" + + "See https://datatracker.ietf.org/doc/html/draft-dkg-openpgp-stateless-cli-03"; + } } diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/EncryptDecryptRoundTripTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/EncryptDecryptRoundTripTest.java index 807c9dd3..d0699ae7 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/EncryptDecryptRoundTripTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/EncryptDecryptRoundTripTest.java @@ -51,7 +51,7 @@ public class EncryptDecryptRoundTripTest { } @Test - public void basicRoundTripWithKey() throws IOException, SOPGPException.CertCannotSign { + public void basicRoundTripWithKey() throws IOException, SOPGPException.KeyCannotSign { byte[] encrypted = sop.encrypt() .signWith(aliceKey) .withCert(aliceCert) @@ -74,7 +74,7 @@ public class EncryptDecryptRoundTripTest { } @Test - public void basicRoundTripWithoutArmorUsingKey() throws IOException, SOPGPException.CertCannotSign { + public void basicRoundTripWithoutArmorUsingKey() throws IOException, SOPGPException.KeyCannotSign { byte[] aliceKeyNoArmor = sop.generateKey() .userId("Alice ") .noArmor() @@ -189,16 +189,6 @@ public class EncryptDecryptRoundTripTest { .toByteArrayAndResult()); } - @Test - public void decrypt_withKeyWithMultipleKeysFails() { - byte[] keys = new byte[aliceKey.length + bobKey.length]; - System.arraycopy(aliceKey, 0, keys, 0 , aliceKey.length); - System.arraycopy(bobKey, 0, keys, aliceKey.length, bobKey.length); - - assertThrows(SOPGPException.BadData.class, () -> sop.decrypt() - .withKey(keys)); - } - @Test public void decrypt_withKeyWithPasswordProtectionFails() { String passwordProtectedKey = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/SignTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/SignTest.java index 3167618c..4736002f 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/SignTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/SignTest.java @@ -13,13 +13,11 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import java.security.InvalidAlgorithmParameterException; import java.security.NoSuchAlgorithmException; -import java.util.Arrays; import java.util.Date; import java.util.List; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; import org.bouncycastle.openpgp.PGPSignature; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -56,7 +54,7 @@ public class SignTest { byte[] signature = sop.sign() .key(key) .data(data) - .getBytes(); + .toByteArrayAndResult().getBytes(); assertTrue(new String(signature).startsWith("-----BEGIN PGP SIGNATURE-----")); @@ -76,7 +74,7 @@ public class SignTest { .key(key) .noArmor() .data(data) - .getBytes(); + .toByteArrayAndResult().getBytes(); assertFalse(new String(signature).startsWith("-----BEGIN PGP SIGNATURE-----")); @@ -95,7 +93,7 @@ public class SignTest { byte[] signature = sop.sign() .key(key) .data(data) - .getBytes(); + .toByteArrayAndResult().getBytes(); assertThrows(SOPGPException.NoSignature.class, () -> sop.verify() .cert(cert) @@ -109,7 +107,7 @@ public class SignTest { byte[] signature = sop.sign() .key(key) .data(data) - .getBytes(); + .toByteArrayAndResult().getBytes(); assertThrows(SOPGPException.NoSignature.class, () -> sop.verify() .cert(cert) @@ -124,22 +122,12 @@ public class SignTest { .mode(SignAs.Text) .key(key) .data(data) - .getBytes(); + .toByteArrayAndResult().getBytes(); PGPSignature sig = SignatureUtils.readSignatures(signature).get(0); assertEquals(SignatureType.CANONICAL_TEXT_DOCUMENT.getCode(), sig.getSignatureType()); } - @Test - public void rejectKeyRingCollection() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { - PGPSecretKeyRing key1 = PGPainless.generateKeyRing().modernKeyRing("Alice", null); - PGPSecretKeyRing key2 = PGPainless.generateKeyRing().modernKeyRing("Bob", null); - PGPSecretKeyRingCollection collection = new PGPSecretKeyRingCollection(Arrays.asList(key1, key2)); - byte[] keys = collection.getEncoded(); - - assertThrows(SOPGPException.BadData.class, () -> sop.sign().key(keys)); - } - @Test public void rejectEncryptedKey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { PGPSecretKeyRing key = PGPainless.generateKeyRing() diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/VersionTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/VersionTest.java index 712df550..c9739471 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/VersionTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/VersionTest.java @@ -5,19 +5,48 @@ package org.pgpainless.sop; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import sop.SOP; public class VersionTest { + private static SOP sop; + + @BeforeAll + public static void setup() { + sop = new SOPImpl(); + } + @Test public void testGetVersion() { - assertNotNull(new SOPImpl().version().getVersion()); + String version = sop.version().getVersion(); + assertNotNull(version); + assertFalse(version.isEmpty()); } @Test public void assertNameEqualsPGPainless() { - assertEquals("PGPainless-SOP", new SOPImpl().version().getName()); + assertEquals("PGPainless-SOP", sop.version().getName()); + } + + @Test + public void testGetBackendVersion() { + String backendVersion = sop.version().getBackendVersion(); + assertNotNull(backendVersion); + assertFalse(backendVersion.isEmpty()); + } + + @Test + public void testGetExtendedVersion() { + String extendedVersion = sop.version().getExtendedVersion(); + assertNotNull(extendedVersion); + assertFalse(extendedVersion.isEmpty()); + + String firstLine = extendedVersion.split("\n")[0]; + assertEquals(sop.version().getName() + " " + sop.version().getVersion(), firstLine); } } 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 index d1ee253c..2ccf0477 100644 --- 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 @@ -82,8 +82,8 @@ public class EncryptCmd implements Runnable { throw new SOPGPException.KeyIsProtected("Key from " + keyFile.getAbsolutePath() + " is password protected.", keyIsProtected); } catch (SOPGPException.UnsupportedAsymmetricAlgo unsupportedAsymmetricAlgo) { throw new SOPGPException.UnsupportedAsymmetricAlgo("Key from " + keyFile.getAbsolutePath() + " has unsupported asymmetric algorithm.", unsupportedAsymmetricAlgo); - } catch (SOPGPException.CertCannotSign certCannotSign) { - throw new RuntimeException("Key from " + keyFile.getAbsolutePath() + " cannot sign.", certCannotSign); + } catch (SOPGPException.KeyCannotSign keyCannotSign) { + throw new SOPGPException.KeyCannotSign("Key from " + keyFile.getAbsolutePath() + " cannot sign.", keyCannotSign); } catch (SOPGPException.BadData badData) { throw new SOPGPException.BadData("Key file " + keyFile.getAbsolutePath() + " does not contain a valid OpenPGP private key.", badData); } 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 index 961869ce..735e9664 100644 --- 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 @@ -7,12 +7,14 @@ 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.util.ArrayList; import java.util.List; import picocli.CommandLine; -import sop.Ready; +import sop.MicAlg; +import sop.ReadyWithResult; import sop.cli.picocli.Print; import sop.cli.picocli.SopCLI; import sop.enums.SignAs; @@ -34,9 +36,13 @@ public class SignCmd implements Runnable { SignAs type; @CommandLine.Parameters(description = "Secret keys used for signing", - paramLabel = "KEY") + paramLabel = "KEYS") List secretKeyFile = new ArrayList<>(); + @CommandLine.Option(names = "--micalg-out", description = "Emits the digest algorithm used to the specified file in a way that can be used to populate the micalg parameter for the PGP/MIME Content-Type (RFC3156)", + paramLabel = "MICALG") + File micAlgOut; + @Override public void run() { Sign sign = SopCLI.getSop().sign(); @@ -51,8 +57,12 @@ public class SignCmd implements Runnable { } } + if (micAlgOut != null && micAlgOut.exists()) { + throw new SOPGPException.OutputExists(String.format("Target %s of option %s already exists.", micAlgOut.getAbsolutePath(), "--micalg-out")); + } + if (secretKeyFile.isEmpty()) { - Print.errln("Missing required parameter 'KEY'."); + Print.errln("Missing required parameter 'KEYS'."); System.exit(19); } @@ -83,8 +93,16 @@ public class SignCmd implements Runnable { } try { - Ready ready = sign.data(System.in); - ready.writeTo(System.out); + ReadyWithResult ready = sign.data(System.in); + MicAlg micAlg = ready.writeTo(System.out); + + if (micAlgOut != null) { + // Write micalg out + micAlgOut.createNewFile(); + FileOutputStream micAlgOutStream = new FileOutputStream(micAlgOut); + micAlg.writeTo(micAlgOutStream); + micAlgOutStream.close(); + } } catch (IOException e) { Print.errln("IO Error."); Print.trace(e); 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 index cfa8a3f6..91f0a1e7 100644 --- 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 @@ -91,7 +91,7 @@ public class EncryptCmdTest { } @Test - public void signWith_multipleTimesGetPassedDown() throws IOException, SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotSign, SOPGPException.BadData { + public void signWith_multipleTimesGetPassedDown() throws IOException, SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData { File keyFile1 = File.createTempFile("sign-with-1-", ".asc"); File keyFile2 = File.createTempFile("sign-with-2-", ".asc"); @@ -107,7 +107,7 @@ public class EncryptCmdTest { @Test @ExpectSystemExitWithStatus(67) - public void signWith_keyIsProtectedCausesExit67() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotSign, SOPGPException.BadData, IOException { + public void signWith_keyIsProtectedCausesExit67() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException { when(encrypt.signWith((InputStream) any())).thenThrow(new SOPGPException.KeyIsProtected()); File keyFile = File.createTempFile("sign-with", ".asc"); SopCLI.main(new String[] {"encrypt", "--sign-with", keyFile.getAbsolutePath(), "--with-password", "starship"}); @@ -115,23 +115,23 @@ public class EncryptCmdTest { @Test @ExpectSystemExitWithStatus(13) - public void signWith_unsupportedAsymmetricAlgoCausesExit13() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotSign, SOPGPException.BadData, IOException { + public void signWith_unsupportedAsymmetricAlgoCausesExit13() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException { when(encrypt.signWith((InputStream) any())).thenThrow(new SOPGPException.UnsupportedAsymmetricAlgo("Unsupported asymmetric algorithm.", 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((InputStream) any())).thenThrow(new SOPGPException.CertCannotSign()); + @ExpectSystemExitWithStatus(79) + public void signWith_certCannotSignCausesExit1() throws IOException, SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData { + when(encrypt.signWith((InputStream) any())).thenThrow(new SOPGPException.KeyCannotSign()); 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 { + public void signWith_badDataCausesExit41() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException { when(encrypt.signWith((InputStream) 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()}); 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 index 8de61409..c5c6e201 100644 --- 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 @@ -19,7 +19,8 @@ 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.MicAlg; +import sop.ReadyWithResult; import sop.SOP; import sop.cli.picocli.SopCLI; import sop.exception.SOPGPException; @@ -33,10 +34,10 @@ public class SignCmdTest { @BeforeEach public void mockComponents() throws IOException, SOPGPException.ExpectedText { sign = mock(Sign.class); - when(sign.data((InputStream) any())).thenReturn(new Ready() { + when(sign.data((InputStream) any())).thenReturn(new ReadyWithResult() { @Override - public void writeTo(OutputStream outputStream) { - + public MicAlg writeTo(OutputStream outputStream) { + return MicAlg.fromHashAlgorithmId(10); } }); @@ -109,9 +110,9 @@ public class SignCmdTest { @Test @ExpectSystemExitWithStatus(1) public void data_ioExceptionCausesExit1() throws IOException, SOPGPException.ExpectedText { - when(sign.data((InputStream) any())).thenReturn(new Ready() { + when(sign.data((InputStream) any())).thenReturn(new ReadyWithResult() { @Override - public void writeTo(OutputStream outputStream) throws IOException { + public MicAlg writeTo(OutputStream outputStream) throws IOException { throw new IOException(); } }); diff --git a/sop-java/src/main/java/sop/MicAlg.java b/sop-java/src/main/java/sop/MicAlg.java new file mode 100644 index 00000000..5bee7875 --- /dev/null +++ b/sop-java/src/main/java/sop/MicAlg.java @@ -0,0 +1,55 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop; + +import java.io.OutputStream; +import java.io.PrintWriter; + +public class MicAlg { + + private final String micAlg; + + public MicAlg(String micAlg) { + if (micAlg == null) { + throw new IllegalArgumentException("MicAlg String cannot be null."); + } + this.micAlg = micAlg; + } + + public static MicAlg empty() { + return new MicAlg(""); + } + + public static MicAlg fromHashAlgorithmId(int id) { + switch (id) { + case 1: + return new MicAlg("pgp-md5"); + case 2: + return new MicAlg("pgp-sha1"); + case 3: + return new MicAlg("pgp-ripemd160"); + case 8: + return new MicAlg("pgp-sha256"); + case 9: + return new MicAlg("pgp-sha384"); + case 10: + return new MicAlg("pgp-sha512"); + case 11: + return new MicAlg("pgp-sha224"); + default: + throw new IllegalArgumentException("Unsupported hash algorithm ID: " + id); + } + } + + public String getMicAlg() { + return micAlg; + } + + public void writeTo(OutputStream outputStream) { + PrintWriter pw = new PrintWriter(outputStream); + pw.write(getMicAlg()); + pw.close(); + } +} diff --git a/sop-java/src/main/java/sop/exception/SOPGPException.java b/sop-java/src/main/java/sop/exception/SOPGPException.java index a8c98c9c..dfd09540 100644 --- a/sop-java/src/main/java/sop/exception/SOPGPException.java +++ b/sop-java/src/main/java/sop/exception/SOPGPException.java @@ -24,6 +24,9 @@ public abstract class SOPGPException extends RuntimeException { public abstract int getExitCode(); + /** + * No acceptable signatures found (sop verify). + */ public static class NoSignature extends SOPGPException { public static final int EXIT_CODE = 3; @@ -38,6 +41,9 @@ public abstract class SOPGPException extends RuntimeException { } } + /** + * Asymmetric algorithm unsupported (sop encrypt). + */ public static class UnsupportedAsymmetricAlgo extends SOPGPException { public static final int EXIT_CODE = 13; @@ -56,6 +62,9 @@ public abstract class SOPGPException extends RuntimeException { } } + /** + * Certificate not encryption capable (e,g, expired, revoked, unacceptable usage). + */ public static class CertCannotEncrypt extends SOPGPException { public static final int EXIT_CODE = 17; @@ -69,10 +78,9 @@ public abstract class SOPGPException extends RuntimeException { } } - public static class CertCannotSign extends Exception { - - } - + /** + * Missing required argument. + */ public static class MissingArg extends SOPGPException { public static final int EXIT_CODE = 19; @@ -87,6 +95,9 @@ public abstract class SOPGPException extends RuntimeException { } } + /** + * Incomplete verification instructions (sop decrypt). + */ public static class IncompleteVerification extends SOPGPException { public static final int EXIT_CODE = 23; @@ -101,6 +112,9 @@ public abstract class SOPGPException extends RuntimeException { } } + /** + * Unable to decrypt (sop decrypt). + */ public static class CannotDecrypt extends SOPGPException { public static final int EXIT_CODE = 29; @@ -111,6 +125,9 @@ public abstract class SOPGPException extends RuntimeException { } } + /** + * Non-UTF-8 or otherwise unreliable password (sop encrypt). + */ public static class PasswordNotHumanReadable extends SOPGPException { public static final int EXIT_CODE = 31; @@ -121,6 +138,9 @@ public abstract class SOPGPException extends RuntimeException { } } + /** + * Unsupported option. + */ public static class UnsupportedOption extends SOPGPException { public static final int EXIT_CODE = 37; @@ -139,6 +159,9 @@ public abstract class SOPGPException extends RuntimeException { } } + /** + * Invalid data type (no secret key where KEYS expected, etc.). + */ public static class BadData extends SOPGPException { public static final int EXIT_CODE = 41; @@ -157,6 +180,9 @@ public abstract class SOPGPException extends RuntimeException { } } + /** + * Non-Text input where text expected. + */ public static class ExpectedText extends SOPGPException { public static final int EXIT_CODE = 53; @@ -167,6 +193,9 @@ public abstract class SOPGPException extends RuntimeException { } } + /** + * Output file already exists. + */ public static class OutputExists extends SOPGPException { public static final int EXIT_CODE = 59; @@ -181,6 +210,9 @@ public abstract class SOPGPException extends RuntimeException { } } + /** + * Input file does not exist. + */ public static class MissingInput extends SOPGPException { public static final int EXIT_CODE = 61; @@ -195,6 +227,9 @@ public abstract class SOPGPException extends RuntimeException { } } + /** + * A KEYS input is protected (locked) with a password, and sop cannot unlock it. + */ public static class KeyIsProtected extends SOPGPException { public static final int EXIT_CODE = 67; @@ -213,6 +248,9 @@ public abstract class SOPGPException extends RuntimeException { } } + /** + * Unsupported subcommand. + */ public static class UnsupportedSubcommand extends SOPGPException { public static final int EXIT_CODE = 69; @@ -227,6 +265,9 @@ public abstract class SOPGPException extends RuntimeException { } } + /** + * An indirect parameter is a special designator (it starts with @), but sop does not know how to handle the prefix. + */ public static class UnsupportedSpecialPrefix extends SOPGPException { public static final int EXIT_CODE = 71; @@ -237,7 +278,10 @@ public abstract class SOPGPException extends RuntimeException { } } - + /** + * A indirect input parameter is a special designator (it starts with @), + * and a filename matching the designator is actually present. + */ public static class AmbiguousInput extends SOPGPException { public static final int EXIT_CODE = 73; @@ -251,4 +295,30 @@ public abstract class SOPGPException extends RuntimeException { return EXIT_CODE; } } + + /** + * Key not signature-capable (e.g. expired, revoked, unacceptable usage flags) + * (sop sign and sop encrypt with --sign-with). + */ + public static class KeyCannotSign extends SOPGPException { + + public static final int EXIT_CODE = 79; + + public KeyCannotSign() { + super(); + } + + public KeyCannotSign(String message) { + super(message); + } + + public KeyCannotSign(String s, KeyCannotSign keyCannotSign) { + super(s, keyCannotSign); + } + + @Override + public int getExitCode() { + return EXIT_CODE; + } + } } diff --git a/sop-java/src/main/java/sop/operation/Decrypt.java b/sop-java/src/main/java/sop/operation/Decrypt.java index 4cbd6f35..0811ac2d 100644 --- a/sop-java/src/main/java/sop/operation/Decrypt.java +++ b/sop-java/src/main/java/sop/operation/Decrypt.java @@ -35,9 +35,9 @@ public interface Decrypt { throws SOPGPException.UnsupportedOption; /** - * Adds the verification cert. + * Adds one or more verification cert. * - * @param cert input stream containing the cert + * @param cert input stream containing the cert(s) * @return builder instance */ Decrypt verifyWithCert(InputStream cert) @@ -45,9 +45,9 @@ public interface Decrypt { IOException; /** - * Adds the verification cert. + * Adds one or more verification cert. * - * @param cert byte array containing the cert + * @param cert byte array containing the cert(s) * @return builder instance */ default Decrypt verifyWithCert(byte[] cert) @@ -75,9 +75,9 @@ public interface Decrypt { SOPGPException.UnsupportedOption; /** - * Adds the decryption key. + * Adds one or more decryption key. * - * @param key input stream containing the key + * @param key input stream containing the key(s) * @return builder instance */ Decrypt withKey(InputStream key) @@ -86,9 +86,9 @@ public interface Decrypt { SOPGPException.UnsupportedAsymmetricAlgo; /** - * Adds the decryption key. + * Adds one or more decryption key. * - * @param key byte array containing the key + * @param key byte array containing the key(s) * @return builder instance */ default Decrypt withKey(byte[] key) diff --git a/sop-java/src/main/java/sop/operation/Encrypt.java b/sop-java/src/main/java/sop/operation/Encrypt.java index b5a92b25..784c07a0 100644 --- a/sop-java/src/main/java/sop/operation/Encrypt.java +++ b/sop-java/src/main/java/sop/operation/Encrypt.java @@ -38,7 +38,7 @@ public interface Encrypt { */ Encrypt signWith(InputStream key) throws SOPGPException.KeyIsProtected, - SOPGPException.CertCannotSign, + SOPGPException.KeyCannotSign, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData; @@ -50,7 +50,7 @@ public interface Encrypt { */ default Encrypt signWith(byte[] key) throws SOPGPException.KeyIsProtected, - SOPGPException.CertCannotSign, + SOPGPException.KeyCannotSign, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData { return signWith(new ByteArrayInputStream(key)); diff --git a/sop-java/src/main/java/sop/operation/ExtractCert.java b/sop-java/src/main/java/sop/operation/ExtractCert.java index 7a0de5c6..32491111 100644 --- a/sop-java/src/main/java/sop/operation/ExtractCert.java +++ b/sop-java/src/main/java/sop/operation/ExtractCert.java @@ -21,18 +21,18 @@ public interface ExtractCert { ExtractCert noArmor(); /** - * Extract the cert from the provided key. + * Extract the cert(s) from the provided key(s). * - * @param keyInputStream input stream containing the encoding of an OpenPGP key - * @return result containing the encoding of the keys cert + * @param keyInputStream input stream containing the encoding of one or more OpenPGP keys + * @return result containing the encoding of the keys certs */ Ready key(InputStream keyInputStream) throws IOException, SOPGPException.BadData; /** - * Extract the cert from the provided key. + * Extract the cert(s) from the provided key(s). * - * @param key byte array containing the encoding of an OpenPGP key - * @return result containing the encoding of the keys cert + * @param key byte array containing the encoding of one or more OpenPGP key + * @return result containing the encoding of the keys certs */ default Ready key(byte[] key) throws IOException, SOPGPException.BadData { return key(new ByteArrayInputStream(key)); diff --git a/sop-java/src/main/java/sop/operation/Sign.java b/sop-java/src/main/java/sop/operation/Sign.java index 9b9c3a6f..75f4e5a8 100644 --- a/sop-java/src/main/java/sop/operation/Sign.java +++ b/sop-java/src/main/java/sop/operation/Sign.java @@ -8,7 +8,8 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; -import sop.Ready; +import sop.MicAlg; +import sop.ReadyWithResult; import sop.enums.SignAs; import sop.exception.SOPGPException; @@ -31,17 +32,17 @@ public interface Sign { Sign mode(SignAs mode) throws SOPGPException.UnsupportedOption; /** - * Adds the signer key. + * Add one or more signing keys. * - * @param key input stream containing encoded key + * @param key input stream containing encoded keys * @return builder instance */ Sign key(InputStream key) throws SOPGPException.KeyIsProtected, SOPGPException.BadData, IOException; /** - * Adds the signer key. + * Add one or more signing keys. * - * @param key byte array containing encoded key + * @param key byte array containing encoded keys * @return builder instance */ default Sign key(byte[] key) throws SOPGPException.KeyIsProtected, SOPGPException.BadData, IOException { @@ -54,7 +55,7 @@ public interface Sign { * @param data input stream containing data * @return ready */ - Ready data(InputStream data) throws IOException, SOPGPException.ExpectedText; + ReadyWithResult data(InputStream data) throws IOException, SOPGPException.ExpectedText; /** * Signs data. @@ -62,7 +63,7 @@ public interface Sign { * @param data byte array containing data * @return ready */ - default Ready data(byte[] data) throws IOException, SOPGPException.ExpectedText { + default ReadyWithResult data(byte[] data) throws IOException, SOPGPException.ExpectedText { return data(new ByteArrayInputStream(data)); } } diff --git a/sop-java/src/main/java/sop/operation/Verify.java b/sop-java/src/main/java/sop/operation/Verify.java index 30905de2..1bf9fe09 100644 --- a/sop-java/src/main/java/sop/operation/Verify.java +++ b/sop-java/src/main/java/sop/operation/Verify.java @@ -29,17 +29,17 @@ public interface Verify extends VerifySignatures { Verify notAfter(Date timestamp) throws SOPGPException.UnsupportedOption; /** - * Adds the verification cert. + * Add one or more verification cert. * - * @param cert input stream containing the encoded cert + * @param cert input stream containing the encoded certs * @return builder instance */ Verify cert(InputStream cert) throws SOPGPException.BadData; /** - * Adds the verification cert. + * Add one or more verification cert. * - * @param cert byte array containing the encoded cert + * @param cert byte array containing the encoded certs * @return builder instance */ default Verify cert(byte[] cert) throws SOPGPException.BadData { diff --git a/sop-java/src/main/java/sop/operation/Version.java b/sop-java/src/main/java/sop/operation/Version.java index ab32099a..0b50993f 100644 --- a/sop-java/src/main/java/sop/operation/Version.java +++ b/sop-java/src/main/java/sop/operation/Version.java @@ -8,15 +8,42 @@ public interface Version { /** * Return the implementations name. + * e.g. "SOP", * * @return implementation name */ String getName(); /** - * Return the implementations version string. + * Return the implementations short version string. + * e.g. "1.0" * * @return version string */ String getVersion(); + + /** + * Return version information about the used OpenPGP backend. + * e.g. "Bouncycastle 1.70" + * + * @return backend version string + */ + String getBackendVersion(); + + /** + * Return an extended version string containing multiple lines of version information. + * The first line MUST match the information produced by {@link #getName()} and {@link #getVersion()}, but the rest of the text + * has no defined structure. + * Example: + *
+     *     "SOP 1.0
+     *     Awesome PGP!
+     *     Using Bouncycastle 1.70
+     *     LibFoo 1.2.2
+     *     See https://pgp.example.org/sop/ for more information"
+     * 
+ * + * @return extended version string + */ + String getExtendedVersion(); }