mirror of
https://github.com/pgpainless/pgpainless.git
synced 2024-11-25 22:02:05 +01:00
Update SOP implementation to the latest spec version
See https://datatracker.ietf.org/doc/html/draft-dkg-openpgp-stateless-cli-03
This commit is contained in:
parent
5e0ca369bf
commit
1cb49f4b12
21 changed files with 348 additions and 112 deletions
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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,16 +32,28 @@ 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<PGPPublicKeyRing> 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 {
|
||||
|
||||
for (PGPPublicKeyRing cert : certs) {
|
||||
OutputStream out = armor ? ArmorUtils.createArmoredOutputStreamFor(cert, outputStream) : outputStream;
|
||||
cert.encode(out);
|
||||
|
||||
|
@ -47,6 +61,7 @@ public class ExtractCertImpl implements ExtractCert {
|
|||
out.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
for (PGPSecretKeyRing key : keys) {
|
||||
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);
|
||||
}
|
||||
|
@ -70,7 +69,7 @@ public class SignImpl implements Sign {
|
|||
}
|
||||
|
||||
@Override
|
||||
public Ready data(InputStream data) throws IOException {
|
||||
public ReadyWithResult<MicAlg> 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<MicAlg>() {
|
||||
@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<PGPSignature> 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;
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 <alice@unarmored.org>")
|
||||
.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" +
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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<File> 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<MicAlg> 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);
|
||||
|
|
|
@ -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()});
|
||||
|
|
|
@ -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<MicAlg>() {
|
||||
@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<MicAlg>() {
|
||||
@Override
|
||||
public void writeTo(OutputStream outputStream) throws IOException {
|
||||
public MicAlg writeTo(OutputStream outputStream) throws IOException {
|
||||
throw new IOException();
|
||||
}
|
||||
});
|
||||
|
|
55
sop-java/src/main/java/sop/MicAlg.java
Normal file
55
sop-java/src/main/java/sop/MicAlg.java
Normal file
|
@ -0,0 +1,55 @@
|
|||
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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<MicAlg> 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<MicAlg> data(byte[] data) throws IOException, SOPGPException.ExpectedText {
|
||||
return data(new ByteArrayInputStream(data));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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:
|
||||
* <pre>
|
||||
* "SOP 1.0
|
||||
* Awesome PGP!
|
||||
* Using Bouncycastle 1.70
|
||||
* LibFoo 1.2.2
|
||||
* See https://pgp.example.org/sop/ for more information"
|
||||
* </pre>
|
||||
*
|
||||
* @return extended version string
|
||||
*/
|
||||
String getExtendedVersion();
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue