From b393a90da47ddf6eb30beddc96c4a8e7ef7ecc97 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 24 Mar 2024 16:16:29 +0100 Subject: [PATCH] Port pgpainless-sop to Kotlin --- .../java/org/pgpainless/sop/ArmorImpl.java | 63 ------ .../pgpainless/sop/ChangeKeyPasswordImpl.java | 96 --------- .../java/org/pgpainless/sop/DearmorImpl.java | 45 ---- .../java/org/pgpainless/sop/DecryptImpl.java | 176 --------------- .../org/pgpainless/sop/DetachedSignImpl.java | 168 --------------- .../pgpainless/sop/DetachedVerifyImpl.java | 100 --------- .../java/org/pgpainless/sop/EncryptImpl.java | 201 ------------------ .../org/pgpainless/sop/ExtractCertImpl.java | 64 ------ .../org/pgpainless/sop/GenerateKeyImpl.java | 154 -------------- .../org/pgpainless/sop/InlineDetachImpl.java | 156 -------------- .../org/pgpainless/sop/InlineSignImpl.java | 135 ------------ .../org/pgpainless/sop/InlineVerifyImpl.java | 101 --------- .../java/org/pgpainless/sop/KeyReader.java | 69 ------ .../org/pgpainless/sop/ListProfilesImpl.java | 36 ---- .../MatchMakingSecretKeyRingProtector.java | 119 ----------- .../org/pgpainless/sop/NullOutputStream.java | 17 -- .../org/pgpainless/sop/RevokeKeyImpl.java | 123 ----------- .../main/java/org/pgpainless/sop/SOPImpl.java | 149 ------------- .../java/org/pgpainless/sop/SOPVImpl.java | 40 ---- .../pgpainless/sop/VerificationHelper.java | 52 ----- .../java/org/pgpainless/sop/VersionImpl.java | 98 --------- .../java/org/pgpainless/sop/package-info.java | 8 - .../kotlin/org/pgpainless/sop/ArmorImpl.kt | 54 +++++ .../pgpainless/sop/ChangeKeyPasswordImpl.kt | 81 +++++++ .../kotlin/org/pgpainless/sop/DearmorImpl.kt | 38 ++++ .../kotlin/org/pgpainless/sop/DecryptImpl.kt | 125 +++++++++++ .../org/pgpainless/sop/DetachedSignImpl.kt | 130 +++++++++++ .../org/pgpainless/sop/DetachedVerifyImpl.kt | 71 +++++++ .../kotlin/org/pgpainless/sop/EncryptImpl.kt | 155 ++++++++++++++ .../org/pgpainless/sop/ExtractCertImpl.kt | 47 ++++ .../org/pgpainless/sop/GenerateKeyImpl.kt | 136 ++++++++++++ .../org/pgpainless/sop/InlineDetachImpl.kt | 134 ++++++++++++ .../org/pgpainless/sop/InlineSignImpl.kt | 114 ++++++++++ .../org/pgpainless/sop/InlineVerifyImpl.kt | 73 +++++++ .../kotlin/org/pgpainless/sop/KeyReader.kt | 77 +++++++ .../org/pgpainless/sop/ListProfilesImpl.kt | 20 ++ .../sop/MatchMakingSecretKeyRingProtector.kt | 88 ++++++++ .../org/pgpainless/sop/NullOutputStream.kt | 31 +++ .../org/pgpainless/sop/RevokeKeyImpl.kt | 95 +++++++++ .../main/kotlin/org/pgpainless/sop/SOPImpl.kt | 56 +++++ .../kotlin/org/pgpainless/sop/SOPVImpl.kt | 24 +++ .../org/pgpainless/sop/VerificationHelper.kt | 48 +++++ .../kotlin/org/pgpainless/sop/VersionImpl.kt | 63 ++++++ 43 files changed, 1660 insertions(+), 2170 deletions(-) delete mode 100644 pgpainless-sop/src/main/java/org/pgpainless/sop/ArmorImpl.java delete mode 100644 pgpainless-sop/src/main/java/org/pgpainless/sop/ChangeKeyPasswordImpl.java delete mode 100644 pgpainless-sop/src/main/java/org/pgpainless/sop/DearmorImpl.java delete mode 100644 pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java delete mode 100644 pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedSignImpl.java delete mode 100644 pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java delete mode 100644 pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java delete mode 100644 pgpainless-sop/src/main/java/org/pgpainless/sop/ExtractCertImpl.java delete mode 100644 pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java delete mode 100644 pgpainless-sop/src/main/java/org/pgpainless/sop/InlineDetachImpl.java delete mode 100644 pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java delete mode 100644 pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java delete mode 100644 pgpainless-sop/src/main/java/org/pgpainless/sop/KeyReader.java delete mode 100644 pgpainless-sop/src/main/java/org/pgpainless/sop/ListProfilesImpl.java delete mode 100644 pgpainless-sop/src/main/java/org/pgpainless/sop/MatchMakingSecretKeyRingProtector.java delete mode 100644 pgpainless-sop/src/main/java/org/pgpainless/sop/NullOutputStream.java delete mode 100644 pgpainless-sop/src/main/java/org/pgpainless/sop/RevokeKeyImpl.java delete mode 100644 pgpainless-sop/src/main/java/org/pgpainless/sop/SOPImpl.java delete mode 100644 pgpainless-sop/src/main/java/org/pgpainless/sop/SOPVImpl.java delete mode 100644 pgpainless-sop/src/main/java/org/pgpainless/sop/VerificationHelper.java delete mode 100644 pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java delete mode 100644 pgpainless-sop/src/main/java/org/pgpainless/sop/package-info.java create mode 100644 pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ArmorImpl.kt create mode 100644 pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ChangeKeyPasswordImpl.kt create mode 100644 pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DearmorImpl.kt create mode 100644 pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DecryptImpl.kt create mode 100644 pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DetachedSignImpl.kt create mode 100644 pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DetachedVerifyImpl.kt create mode 100644 pgpainless-sop/src/main/kotlin/org/pgpainless/sop/EncryptImpl.kt create mode 100644 pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ExtractCertImpl.kt create mode 100644 pgpainless-sop/src/main/kotlin/org/pgpainless/sop/GenerateKeyImpl.kt create mode 100644 pgpainless-sop/src/main/kotlin/org/pgpainless/sop/InlineDetachImpl.kt create mode 100644 pgpainless-sop/src/main/kotlin/org/pgpainless/sop/InlineSignImpl.kt create mode 100644 pgpainless-sop/src/main/kotlin/org/pgpainless/sop/InlineVerifyImpl.kt create mode 100644 pgpainless-sop/src/main/kotlin/org/pgpainless/sop/KeyReader.kt create mode 100644 pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ListProfilesImpl.kt create mode 100644 pgpainless-sop/src/main/kotlin/org/pgpainless/sop/MatchMakingSecretKeyRingProtector.kt create mode 100644 pgpainless-sop/src/main/kotlin/org/pgpainless/sop/NullOutputStream.kt create mode 100644 pgpainless-sop/src/main/kotlin/org/pgpainless/sop/RevokeKeyImpl.kt create mode 100644 pgpainless-sop/src/main/kotlin/org/pgpainless/sop/SOPImpl.kt create mode 100644 pgpainless-sop/src/main/kotlin/org/pgpainless/sop/SOPVImpl.kt create mode 100644 pgpainless-sop/src/main/kotlin/org/pgpainless/sop/VerificationHelper.kt create mode 100644 pgpainless-sop/src/main/kotlin/org/pgpainless/sop/VersionImpl.kt diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/ArmorImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/ArmorImpl.java deleted file mode 100644 index 421dc7a7..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/ArmorImpl.java +++ /dev/null @@ -1,63 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.sop; - -import java.io.BufferedOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; - -import org.bouncycastle.bcpg.ArmoredOutputStream; -import org.bouncycastle.util.io.Streams; -import org.pgpainless.decryption_verification.OpenPgpInputStream; -import org.pgpainless.util.ArmoredOutputStreamFactory; -import sop.Ready; -import sop.enums.ArmorLabel; -import sop.exception.SOPGPException; -import sop.operation.Armor; - -import javax.annotation.Nonnull; - -/** - * Implementation of the
armor
operation using PGPainless. - */ -public class ArmorImpl implements Armor { - - @Nonnull - @Override - @Deprecated - public Armor label(@Nonnull ArmorLabel label) throws SOPGPException.UnsupportedOption { - throw new SOPGPException.UnsupportedOption("Setting custom Armor labels not supported."); - } - - @Nonnull - @Override - public Ready data(@Nonnull InputStream data) throws SOPGPException.BadData { - return new Ready() { - @Override - public void writeTo(@Nonnull OutputStream outputStream) throws IOException { - // By buffering the output stream, we can improve performance drastically - BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream); - - // Determine nature of the given data - OpenPgpInputStream openPgpIn = new OpenPgpInputStream(data); - openPgpIn.reset(); - - if (openPgpIn.isAsciiArmored()) { - // armoring already-armored data is an idempotent operation - Streams.pipeAll(openPgpIn, bufferedOutputStream); - bufferedOutputStream.flush(); - openPgpIn.close(); - return; - } - - ArmoredOutputStream armor = ArmoredOutputStreamFactory.get(bufferedOutputStream); - Streams.pipeAll(openPgpIn, armor); - bufferedOutputStream.flush(); - armor.close(); - } - }; - } -} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/ChangeKeyPasswordImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/ChangeKeyPasswordImpl.java deleted file mode 100644 index 56613720..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/ChangeKeyPasswordImpl.java +++ /dev/null @@ -1,96 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -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.bcpg.ArmoredOutputStream; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; -import org.pgpainless.exception.MissingPassphraseException; -import org.pgpainless.key.OpenPgpFingerprint; -import org.pgpainless.key.protection.SecretKeyRingProtector; -import org.pgpainless.key.util.KeyRingUtils; -import org.pgpainless.util.ArmoredOutputStreamFactory; -import org.pgpainless.util.Passphrase; -import sop.Ready; -import sop.exception.SOPGPException; -import sop.operation.ChangeKeyPassword; - -import javax.annotation.Nonnull; - -public class ChangeKeyPasswordImpl implements ChangeKeyPassword { - - private final MatchMakingSecretKeyRingProtector oldProtector = new MatchMakingSecretKeyRingProtector(); - private Passphrase newPassphrase = Passphrase.emptyPassphrase(); - private boolean armor = true; - - @Nonnull - @Override - public ChangeKeyPassword noArmor() { - armor = false; - return this; - } - - @Nonnull - @Override - public ChangeKeyPassword oldKeyPassphrase(@Nonnull String oldPassphrase) { - oldProtector.addPassphrase(Passphrase.fromPassword(oldPassphrase)); - return this; - } - - @Nonnull - @Override - public ChangeKeyPassword newKeyPassphrase(@Nonnull String newPassphrase) { - this.newPassphrase = Passphrase.fromPassword(newPassphrase); - return this; - } - - @Nonnull - @Override - public Ready keys(@Nonnull InputStream inputStream) throws SOPGPException.KeyIsProtected { - SecretKeyRingProtector newProtector = SecretKeyRingProtector.unlockAnyKeyWith(newPassphrase); - PGPSecretKeyRingCollection secretKeyRingCollection; - try { - secretKeyRingCollection = KeyReader.readSecretKeys(inputStream, true); - } catch (IOException e) { - throw new SOPGPException.BadData(e); - } - - List updatedSecretKeys = new ArrayList<>(); - for (PGPSecretKeyRing secretKeys : secretKeyRingCollection) { - oldProtector.addSecretKey(secretKeys); - try { - PGPSecretKeyRing changed = KeyRingUtils.changePassphrase(null, secretKeys, oldProtector, newProtector); - updatedSecretKeys.add(changed); - } catch (MissingPassphraseException e) { - throw new SOPGPException.KeyIsProtected("Cannot unlock key " + OpenPgpFingerprint.of(secretKeys), e); - } catch (PGPException e) { - if (e.getMessage().contains("Exception decrypting key")) { - throw new SOPGPException.KeyIsProtected("Cannot unlock key " + OpenPgpFingerprint.of(secretKeys), e); - } - throw new RuntimeException("Cannot change passphrase of key " + OpenPgpFingerprint.of(secretKeys), e); - } - } - final PGPSecretKeyRingCollection changedSecretKeyCollection = new PGPSecretKeyRingCollection(updatedSecretKeys); - return new Ready() { - @Override - public void writeTo(@Nonnull OutputStream outputStream) throws IOException { - if (armor) { - ArmoredOutputStream armorOut = ArmoredOutputStreamFactory.get(outputStream); - changedSecretKeyCollection.encode(armorOut); - armorOut.close(); - } else { - changedSecretKeyCollection.encode(outputStream); - } - } - }; - } -} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/DearmorImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DearmorImpl.java deleted file mode 100644 index 5fd0a03b..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DearmorImpl.java +++ /dev/null @@ -1,45 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.sop; - -import java.io.BufferedOutputStream; -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.exception.SOPGPException; -import sop.operation.Dearmor; - -import javax.annotation.Nonnull; - -/** - * Implementation of the
dearmor
operation using PGPainless. - */ -public class DearmorImpl implements Dearmor { - - @Nonnull - @Override - public Ready data(@Nonnull InputStream data) { - InputStream decoder; - try { - decoder = PGPUtil.getDecoderStream(data); - } catch (IOException e) { - throw new SOPGPException.BadData(e); - } - return new Ready() { - - @Override - public void writeTo(@Nonnull OutputStream outputStream) throws IOException { - BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream); - Streams.pipeAll(decoder, bufferedOutputStream); - bufferedOutputStream.flush(); - 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 deleted file mode 100644 index bc5081d7..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java +++ /dev/null @@ -1,176 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -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.util.io.Streams; -import org.pgpainless.PGPainless; -import org.pgpainless.algorithm.SymmetricKeyAlgorithm; -import org.pgpainless.decryption_verification.ConsumerOptions; -import org.pgpainless.decryption_verification.DecryptionStream; -import org.pgpainless.decryption_verification.MessageMetadata; -import org.pgpainless.decryption_verification.SignatureVerification; -import org.pgpainless.exception.MalformedOpenPgpMessageException; -import org.pgpainless.exception.MissingDecryptionMethodException; -import org.pgpainless.exception.WrongPassphraseException; -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; -import sop.util.UTF8Util; - -import javax.annotation.Nonnull; - -/** - * Implementation of the
decrypt
operation using PGPainless. - */ -public class DecryptImpl implements Decrypt { - - private final ConsumerOptions consumerOptions = ConsumerOptions.get(); - private final MatchMakingSecretKeyRingProtector protector = new MatchMakingSecretKeyRingProtector(); - - @Nonnull - @Override - public DecryptImpl verifyNotBefore(@Nonnull Date timestamp) throws SOPGPException.UnsupportedOption { - consumerOptions.verifyNotBefore(timestamp); - return this; - } - - @Nonnull - @Override - public DecryptImpl verifyNotAfter(@Nonnull Date timestamp) throws SOPGPException.UnsupportedOption { - consumerOptions.verifyNotAfter(timestamp); - return this; - } - - @Nonnull - @Override - public DecryptImpl verifyWithCert(@Nonnull InputStream certIn) throws SOPGPException.BadData, IOException { - PGPPublicKeyRingCollection certs = KeyReader.readPublicKeys(certIn, true); - if (certs != null) { - consumerOptions.addVerificationCerts(certs); - } - return this; - } - - @Nonnull - @Override - public DecryptImpl withSessionKey(@Nonnull SessionKey sessionKey) throws SOPGPException.UnsupportedOption { - consumerOptions.setSessionKey( - new org.pgpainless.util.SessionKey( - SymmetricKeyAlgorithm.requireFromId(sessionKey.getAlgorithm()), - sessionKey.getKey())); - return this; - } - - @Nonnull - @Override - public DecryptImpl withPassword(@Nonnull String password) { - 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); - } - - @Nonnull - @Override - public DecryptImpl withKey(@Nonnull InputStream keyIn) throws SOPGPException.BadData, IOException, SOPGPException.UnsupportedAsymmetricAlgo { - PGPSecretKeyRingCollection secretKeyCollection = KeyReader.readSecretKeys(keyIn, true); - - for (PGPSecretKeyRing key : secretKeyCollection) { - protector.addSecretKey(key); - consumerOptions.addDecryptionKey(key, protector); - } - return this; - } - - @Nonnull - @Override - public Decrypt withKeyPassword(@Nonnull byte[] password) { - String string = new String(password, UTF8Util.UTF8); - protector.addPassphrase(Passphrase.fromPassword(string)); - return this; - } - - @Nonnull - @Override - public ReadyWithResult ciphertext(@Nonnull InputStream ciphertext) - throws SOPGPException.BadData, - SOPGPException.MissingArg { - - if (consumerOptions.getDecryptionKeys().isEmpty() && consumerOptions.getDecryptionPassphrases().isEmpty() && consumerOptions.getSessionKey() == null) { - throw new SOPGPException.MissingArg("Missing decryption key, passphrase or session key."); - } - - DecryptionStream decryptionStream; - try { - decryptionStream = PGPainless.decryptAndOrVerify() - .onInputStream(ciphertext) - .withOptions(consumerOptions); - } catch (MissingDecryptionMethodException e) { - throw new SOPGPException.CannotDecrypt("No usable decryption key or password provided.", e); - } catch (WrongPassphraseException e) { - throw new SOPGPException.KeyIsProtected(); - } catch (MalformedOpenPgpMessageException | PGPException | IOException e) { - throw new SOPGPException.BadData(e); - } finally { - // Forget passphrases after decryption - protector.clear(); - } - - return new ReadyWithResult() { - @Override - public DecryptionResult writeTo(@Nonnull OutputStream outputStream) throws IOException, SOPGPException.NoSignature { - Streams.pipeAll(decryptionStream, outputStream); - decryptionStream.close(); - MessageMetadata metadata = decryptionStream.getMetadata(); - - if (!metadata.isEncrypted()) { - throw new SOPGPException.BadData("Data is not encrypted."); - } - - List verificationList = new ArrayList<>(); - for (SignatureVerification signatureVerification : metadata.getVerifiedInlineSignatures()) { - verificationList.add(VerificationHelper.mapVerification(signatureVerification)); - } - - SessionKey sessionKey = null; - if (metadata.getSessionKey() != null) { - org.pgpainless.util.SessionKey sk = metadata.getSessionKey(); - sessionKey = new SessionKey( - (byte) sk.getAlgorithm().getAlgorithmId(), - sk.getKey() - ); - } - - return new DecryptionResult(sessionKey, verificationList); - } - }; - } -} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedSignImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedSignImpl.java deleted file mode 100644 index 400d2324..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedSignImpl.java +++ /dev/null @@ -1,168 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -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.PGPSecretKeyRing; -import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; -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.exception.KeyException; -import org.pgpainless.key.OpenPgpFingerprint; -import org.pgpainless.key.SubkeyIdentifier; -import org.pgpainless.key.info.KeyRingInfo; -import org.pgpainless.util.ArmoredOutputStreamFactory; -import org.pgpainless.util.Passphrase; -import sop.MicAlg; -import sop.ReadyWithResult; -import sop.SigningResult; -import sop.enums.SignAs; -import sop.exception.SOPGPException; -import sop.operation.DetachedSign; -import sop.util.UTF8Util; - -import javax.annotation.Nonnull; - -/** - * Implementation of the
sign
operation using PGPainless. - */ -public class DetachedSignImpl implements DetachedSign { - - private boolean armor = true; - private SignAs mode = SignAs.binary; - private final SigningOptions signingOptions = SigningOptions.get(); - private final MatchMakingSecretKeyRingProtector protector = new MatchMakingSecretKeyRingProtector(); - private final List signingKeys = new ArrayList<>(); - - @Override - public DetachedSign noArmor() { - armor = false; - return this; - } - - @Override - @Nonnull - public DetachedSign mode(@Nonnull SignAs mode) { - this.mode = mode; - return this; - } - - @Override - @Nonnull - public DetachedSign key(@Nonnull InputStream keyIn) throws SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException { - PGPSecretKeyRingCollection keys = KeyReader.readSecretKeys(keyIn, true); - for (PGPSecretKeyRing key : keys) { - KeyRingInfo info = PGPainless.inspectKeyRing(key); - if (!info.isUsableForSigning()) { - throw new SOPGPException.KeyCannotSign("Key " + info.getFingerprint() + " does not have valid, signing capable subkeys."); - } - protector.addSecretKey(key); - signingKeys.add(key); - } - return this; - } - - @Override - @Nonnull - public DetachedSign withKeyPassword(@Nonnull byte[] password) { - String string = new String(password, UTF8Util.UTF8); - protector.addPassphrase(Passphrase.fromPassword(string)); - return this; - } - - @Override - @Nonnull - public ReadyWithResult data(@Nonnull InputStream data) throws IOException { - for (PGPSecretKeyRing key : signingKeys) { - try { - signingOptions.addDetachedSignature(protector, key, modeToSigType(mode)); - } catch (KeyException.UnacceptableSigningKeyException | KeyException.MissingSecretKeyException e) { - throw new SOPGPException.KeyCannotSign("Key " + OpenPgpFingerprint.of(key) + " cannot sign.", e); - } catch (PGPException e) { - throw new SOPGPException.KeyIsProtected("Key " + OpenPgpFingerprint.of(key) + " cannot be unlocked.", e); - } - } - - OutputStream sink = new NullOutputStream(); - try { - EncryptionStream signingStream = PGPainless.encryptAndOrSign() - .onOutputStream(sink) - .withOptions(ProducerOptions.sign(signingOptions) - .setAsciiArmor(armor)); - - return new ReadyWithResult() { - @Override - public SigningResult writeTo(@Nonnull OutputStream outputStream) throws IOException { - - if (signingStream.isClosed()) { - throw new IllegalStateException("EncryptionStream is already closed."); - } - - Streams.pipeAll(data, signingStream); - signingStream.close(); - EncryptionResult encryptionResult = signingStream.getResult(); - - // forget passphrases - protector.clear(); - - 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 - - return SigningResult.builder() - .setMicAlg(micAlgFromSignatures(signatures)) - .build(); - } - }; - - } catch (PGPException e) { - throw new RuntimeException(e); - } - - } - - 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/DetachedVerifyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java deleted file mode 100644 index 4c475bd0..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java +++ /dev/null @@ -1,100 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -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.util.io.Streams; -import org.pgpainless.PGPainless; -import org.pgpainless.decryption_verification.ConsumerOptions; -import org.pgpainless.decryption_verification.DecryptionStream; -import org.pgpainless.decryption_verification.MessageMetadata; -import org.pgpainless.decryption_verification.SignatureVerification; -import org.pgpainless.exception.MalformedOpenPgpMessageException; -import sop.Verification; -import sop.exception.SOPGPException; -import sop.operation.DetachedVerify; - -import javax.annotation.Nonnull; - -/** - * Implementation of the
verify
operation using PGPainless. - */ -public class DetachedVerifyImpl implements DetachedVerify { - - private final ConsumerOptions options = ConsumerOptions.get(); - - @Override - @Nonnull - public DetachedVerify notBefore(@Nonnull Date timestamp) throws SOPGPException.UnsupportedOption { - options.verifyNotBefore(timestamp); - return this; - } - - @Override - @Nonnull - public DetachedVerify notAfter(@Nonnull Date timestamp) throws SOPGPException.UnsupportedOption { - options.verifyNotAfter(timestamp); - return this; - } - - @Override - @Nonnull - public DetachedVerify cert(@Nonnull InputStream cert) throws SOPGPException.BadData, IOException { - PGPPublicKeyRingCollection certificates = KeyReader.readPublicKeys(cert, true); - options.addVerificationCerts(certificates); - return this; - } - - @Override - @Nonnull - public DetachedVerifyImpl signatures(@Nonnull InputStream signatures) throws SOPGPException.BadData { - try { - options.addVerificationOfDetachedSignatures(signatures); - } catch (IOException | PGPException e) { - throw new SOPGPException.BadData(e); - } - return this; - } - - @Override - @Nonnull - public List data(@Nonnull InputStream data) throws IOException, SOPGPException.NoSignature, SOPGPException.BadData { - options.forceNonOpenPgpData(); - - DecryptionStream decryptionStream; - try { - decryptionStream = PGPainless.decryptAndOrVerify() - .onInputStream(data) - .withOptions(options); - - Streams.drain(decryptionStream); - decryptionStream.close(); - - MessageMetadata metadata = decryptionStream.getMetadata(); - List verificationList = new ArrayList<>(); - - for (SignatureVerification signatureVerification : metadata.getVerifiedDetachedSignatures()) { - verificationList.add(VerificationHelper.mapVerification(signatureVerification)); - } - - if (!options.getCertificateSource().getExplicitCertificates().isEmpty()) { - if (verificationList.isEmpty()) { - throw new SOPGPException.NoSignature(); - } - } - - return verificationList; - } catch (MalformedOpenPgpMessageException | PGPException e) { - throw new SOPGPException.BadData(e); - } - } -} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java deleted file mode 100644 index cbd5a108..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java +++ /dev/null @@ -1,201 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.sop; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -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.KeyException; -import org.pgpainless.exception.WrongPassphraseException; -import org.pgpainless.key.OpenPgpFingerprint; -import org.pgpainless.key.info.KeyRingInfo; -import org.pgpainless.util.Passphrase; -import sop.EncryptionResult; -import sop.Profile; -import sop.ReadyWithResult; -import sop.enums.EncryptAs; -import sop.exception.SOPGPException; -import sop.operation.Encrypt; -import sop.util.ProxyOutputStream; -import sop.util.UTF8Util; - -import javax.annotation.Nonnull; - -/** - * Implementation of the
encrypt
operation using PGPainless. - */ -public class EncryptImpl implements Encrypt { - - private static final Profile RFC4880_PROFILE = new Profile("rfc4880", "Follow the packet format of rfc4880"); - - public static final List SUPPORTED_PROFILES = Arrays.asList(RFC4880_PROFILE); - - EncryptionOptions encryptionOptions = EncryptionOptions.get(); - SigningOptions signingOptions = null; - MatchMakingSecretKeyRingProtector protector = new MatchMakingSecretKeyRingProtector(); - private final Set signingKeys = new HashSet<>(); - private String profile = RFC4880_PROFILE.getName(); // TODO: Use in future releases - - private EncryptAs encryptAs = EncryptAs.binary; - boolean armor = true; - - @Nonnull - @Override - public Encrypt noArmor() { - armor = false; - return this; - } - - @Nonnull - @Override - public Encrypt mode(@Nonnull EncryptAs mode) throws SOPGPException.UnsupportedOption { - this.encryptAs = mode; - return this; - } - - @Nonnull - @Override - public Encrypt signWith(@Nonnull InputStream keyIn) - throws SOPGPException.KeyCannotSign, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData, IOException { - if (signingOptions == null) { - signingOptions = SigningOptions.get(); - } - PGPSecretKeyRingCollection keys = KeyReader.readSecretKeys(keyIn, true); - if (keys.size() != 1) { - throw new SOPGPException.BadData(new AssertionError("Exactly one secret key at a time expected. Got " + keys.size())); - } - PGPSecretKeyRing signingKey = keys.iterator().next(); - - KeyRingInfo info = PGPainless.inspectKeyRing(signingKey); - if (info.getSigningSubkeys().isEmpty()) { - throw new SOPGPException.KeyCannotSign("Key " + OpenPgpFingerprint.of(signingKey) + " cannot sign."); - } - - protector.addSecretKey(signingKey); - signingKeys.add(signingKey); - return this; - } - - @Nonnull - @Override - public Encrypt withKeyPassword(@Nonnull byte[] password) { - String passphrase = new String(password, UTF8Util.UTF8); - protector.addPassphrase(Passphrase.fromPassword(passphrase)); - return this; - } - - @Nonnull - @Override - public Encrypt withPassword(@Nonnull String password) throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption { - encryptionOptions.addPassphrase(Passphrase.fromPassword(password)); - return this; - } - - @Nonnull - @Override - public Encrypt withCert(@Nonnull InputStream cert) throws SOPGPException.CertCannotEncrypt, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData { - try { - PGPPublicKeyRingCollection certificates = KeyReader.readPublicKeys(cert, true); - encryptionOptions.addRecipients(certificates); - } catch (KeyException.UnacceptableEncryptionKeyException e) { - throw new SOPGPException.CertCannotEncrypt(e.getMessage(), e); - } catch (IOException e) { - throw new SOPGPException.BadData(e); - } - return this; - } - - @Nonnull - @Override - public Encrypt profile(@Nonnull String profileName) { - // sanitize profile name to make sure we only accept supported profiles - for (Profile profile : SUPPORTED_PROFILES) { - if (profile.getName().equals(profileName)) { - // profile is supported, return - this.profile = profile.getName(); - return this; - } - } - - // Profile is not supported, throw - throw new SOPGPException.UnsupportedProfile("encrypt", profileName); - } - - @Nonnull - @Override - public ReadyWithResult plaintext(@Nonnull InputStream plaintext) throws IOException { - if (!encryptionOptions.hasEncryptionMethod()) { - throw new SOPGPException.MissingArg("Missing encryption method."); - } - ProducerOptions producerOptions = signingOptions != null ? - ProducerOptions.signAndEncrypt(encryptionOptions, signingOptions) : - ProducerOptions.encrypt(encryptionOptions); - producerOptions.setAsciiArmor(armor); - producerOptions.setEncoding(encryptAsToStreamEncoding(encryptAs)); - - for (PGPSecretKeyRing signingKey : signingKeys) { - try { - signingOptions.addInlineSignature( - protector, - signingKey, - (encryptAs == EncryptAs.binary ? DocumentSignatureType.BINARY_DOCUMENT : DocumentSignatureType.CANONICAL_TEXT_DOCUMENT) - ); - } catch (KeyException.UnacceptableSigningKeyException e) { - throw new SOPGPException.KeyCannotSign(); - } catch (WrongPassphraseException e) { - throw new SOPGPException.KeyIsProtected(); - } catch (PGPException e) { - throw new SOPGPException.BadData(e); - } - } - - try { - ProxyOutputStream proxy = new ProxyOutputStream(); - EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() - .onOutputStream(proxy) - .withOptions(producerOptions); - - return new ReadyWithResult() { - @Override - public EncryptionResult writeTo(@Nonnull OutputStream outputStream) throws IOException { - proxy.replaceOutputStream(outputStream); - Streams.pipeAll(plaintext, encryptionStream); - encryptionStream.close(); - // TODO: Extract and emit SessionKey - return new EncryptionResult(null); - } - }; - } catch (PGPException e) { - throw new IOException(); - } - } - - private static StreamEncoding encryptAsToStreamEncoding(EncryptAs encryptAs) { - switch (encryptAs) { - case binary: - return StreamEncoding.BINARY; - case text: - 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 deleted file mode 100644 index 6dce2578..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/ExtractCertImpl.java +++ /dev/null @@ -1,64 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -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.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; -import org.pgpainless.PGPainless; -import org.pgpainless.util.ArmorUtils; -import sop.Ready; -import sop.exception.SOPGPException; -import sop.operation.ExtractCert; - -import javax.annotation.Nonnull; - -/** - * Implementation of the
extract-cert
operation using PGPainless. - */ -public class ExtractCertImpl implements ExtractCert { - - private boolean armor = true; - - @Override - @Nonnull - public ExtractCert noArmor() { - armor = false; - return this; - } - - @Override - @Nonnull - public Ready key(@Nonnull InputStream keyInputStream) throws IOException, SOPGPException.BadData { - PGPSecretKeyRingCollection keys = KeyReader.readSecretKeys(keyInputStream, true); - - List certs = new ArrayList<>(); - for (PGPSecretKeyRing key : keys) { - PGPPublicKeyRing cert = PGPainless.extractCertificate(key); - certs.add(cert); - } - - return new Ready() { - @Override - public void writeTo(@Nonnull OutputStream outputStream) throws IOException { - - for (PGPPublicKeyRing cert : certs) { - OutputStream out = armor ? ArmorUtils.toAsciiArmoredStream(cert, outputStream) : outputStream; - cert.encode(out); - - if (armor) { - out.close(); - } - } - } - }; - } -} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java deleted file mode 100644 index 03583891..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java +++ /dev/null @@ -1,154 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.sop; - -import java.io.IOException; -import java.io.OutputStream; -import java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; -import java.util.Arrays; -import java.util.LinkedHashSet; -import java.util.List; -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.algorithm.KeyFlag; -import org.pgpainless.key.generation.KeyRingBuilder; -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.rsa.RsaLength; -import org.pgpainless.key.generation.type.xdh.XDHSpec; -import org.pgpainless.util.ArmorUtils; -import org.pgpainless.util.Passphrase; -import sop.Profile; -import sop.Ready; -import sop.exception.SOPGPException; -import sop.operation.GenerateKey; - -import javax.annotation.Nonnull; - -/** - * Implementation of the
generate-key
operation using PGPainless. - */ -public class GenerateKeyImpl implements GenerateKey { - - public static final Profile CURVE25519_PROFILE = new Profile("draft-koch-eddsa-for-openpgp-00", "Generate EdDSA / ECDH keys using Curve25519"); - public static final Profile RSA4096_PROFILE = new Profile("rfc4880", "Generate 4096-bit RSA keys"); - - public static final List SUPPORTED_PROFILES = Arrays.asList(CURVE25519_PROFILE, RSA4096_PROFILE); - - private boolean armor = true; - private boolean signingOnly = false; - private final Set userIds = new LinkedHashSet<>(); - private Passphrase passphrase = Passphrase.emptyPassphrase(); - private String profile = CURVE25519_PROFILE.getName(); - - @Override - @Nonnull - public GenerateKey noArmor() { - this.armor = false; - return this; - } - - @Override - @Nonnull - public GenerateKey userId(@Nonnull String userId) { - this.userIds.add(userId); - return this; - } - - @Override - @Nonnull - public GenerateKey withKeyPassword(@Nonnull String password) { - this.passphrase = Passphrase.fromPassword(password); - return this; - } - - @Override - @Nonnull - public GenerateKey profile(@Nonnull String profileName) { - // Sanitize the profile name to make sure we support the given profile - for (Profile profile : SUPPORTED_PROFILES) { - if (profile.getName().equals(profileName)) { - this.profile = profileName; - // return if we found the profile - return this; - } - } - - // profile not found, throw - throw new SOPGPException.UnsupportedProfile("generate-key", profileName); - } - - @Override - @Nonnull - public GenerateKey signingOnly() { - signingOnly = true; - return this; - } - - @Override - @Nonnull - public Ready generate() throws SOPGPException.MissingArg, SOPGPException.UnsupportedAsymmetricAlgo { - try { - final PGPSecretKeyRing key = generateKeyWithProfile(profile, userIds, passphrase, signingOnly); - return new Ready() { - @Override - public void writeTo(@Nonnull OutputStream outputStream) throws IOException { - if (armor) { - ArmoredOutputStream armoredOutputStream = ArmorUtils.toAsciiArmoredStream(key, outputStream); - key.encode(armoredOutputStream); - armoredOutputStream.close(); - } else { - key.encode(outputStream); - } - } - }; - } catch (InvalidAlgorithmParameterException | NoSuchAlgorithmException e) { - throw new SOPGPException.UnsupportedAsymmetricAlgo("Unsupported asymmetric algorithm.", e); - } catch (PGPException e) { - throw new RuntimeException(e); - } - } - - private PGPSecretKeyRing generateKeyWithProfile(String profile, Set userIds, Passphrase passphrase, boolean signingOnly) - throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - KeyRingBuilder keyBuilder; - // XDH + EdDSA - if (profile.equals(CURVE25519_PROFILE.getName())) { - keyBuilder = PGPainless.buildKeyRing() - .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER)) - .addSubkey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.SIGN_DATA)); - if (!signingOnly) { - keyBuilder.addSubkey(KeySpec.getBuilder(KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)); - } - } - // RSA 4096 - else if (profile.equals(RSA4096_PROFILE.getName())) { - keyBuilder = PGPainless.buildKeyRing() - .setPrimaryKey(KeySpec.getBuilder(KeyType.RSA(RsaLength._4096), KeyFlag.CERTIFY_OTHER)) - .addSubkey(KeySpec.getBuilder(KeyType.RSA(RsaLength._4096), KeyFlag.SIGN_DATA)); - if (!signingOnly) { - keyBuilder.addSubkey(KeySpec.getBuilder(KeyType.RSA(RsaLength._4096), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)); - } - } - else { - // Missing else-if branch for profile. Oops. - throw new SOPGPException.UnsupportedProfile("generate-key", profile); - } - - for (String userId : userIds) { - keyBuilder.addUserId(userId); - } - if (!passphrase.isEmpty()) { - keyBuilder.setPassphrase(passphrase); - } - return keyBuilder.build(); - } -} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineDetachImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineDetachImpl.java deleted file mode 100644 index 85c06d25..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineDetachImpl.java +++ /dev/null @@ -1,156 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.sop; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; - -import org.bouncycastle.bcpg.ArmoredInputStream; -import org.bouncycastle.bcpg.ArmoredOutputStream; -import org.bouncycastle.openpgp.PGPCompressedData; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPLiteralData; -import org.bouncycastle.openpgp.PGPObjectFactory; -import org.bouncycastle.openpgp.PGPOnePassSignatureList; -import org.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.openpgp.PGPSignatureList; -import org.bouncycastle.util.io.Streams; -import org.pgpainless.decryption_verification.OpenPgpInputStream; -import org.pgpainless.decryption_verification.cleartext_signatures.ClearsignedMessageUtil; -import org.pgpainless.exception.WrongConsumingMethodException; -import org.pgpainless.implementation.ImplementationFactory; -import org.pgpainless.util.ArmoredOutputStreamFactory; -import sop.ReadyWithResult; -import sop.Signatures; -import sop.exception.SOPGPException; -import sop.operation.InlineDetach; - -import javax.annotation.Nonnull; - -/** - * Implementation of the
inline-detach
operation using PGPainless. - */ -public class InlineDetachImpl implements InlineDetach { - - private boolean armor = true; - - @Override - @Nonnull - public InlineDetach noArmor() { - this.armor = false; - return this; - } - - @Override - @Nonnull - public ReadyWithResult message(@Nonnull InputStream messageInputStream) { - - return new ReadyWithResult() { - - private final ByteArrayOutputStream sigOut = new ByteArrayOutputStream(); - - @Override - public Signatures writeTo(@Nonnull OutputStream messageOutputStream) - throws SOPGPException.NoSignature, IOException { - - PGPSignatureList signatures = null; - OpenPgpInputStream pgpIn = new OpenPgpInputStream(messageInputStream); - - if (pgpIn.isNonOpenPgp()) { - throw new SOPGPException.BadData("Data appears to be non-OpenPGP."); - } - - // handle ASCII armor - if (pgpIn.isAsciiArmored()) { - ArmoredInputStream armorIn = new ArmoredInputStream(pgpIn); - - // Handle cleartext signature framework - if (armorIn.isClearText()) { - try { - signatures = ClearsignedMessageUtil.detachSignaturesFromInbandClearsignedMessage(armorIn, messageOutputStream); - if (signatures.isEmpty()) { - throw new SOPGPException.BadData("Data did not contain OpenPGP signatures."); - } - } catch (WrongConsumingMethodException e) { - throw new SOPGPException.BadData(e); - } - } - // else just dearmor - pgpIn = new OpenPgpInputStream(armorIn); - } - - // if data was not using cleartext signatures framework - if (signatures == null) { - - if (!pgpIn.isBinaryOpenPgp()) { - throw new SOPGPException.BadData("Data was containing ASCII armored non-OpenPGP data."); - } - - // handle binary OpenPGP data - PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(pgpIn); - Object next; - while ((next = objectFactory.nextObject()) != null) { - - if (next instanceof PGPOnePassSignatureList) { - // skip over ops - continue; - } - - if (next instanceof PGPLiteralData) { - // write out contents of literal data packet - PGPLiteralData literalData = (PGPLiteralData) next; - InputStream literalIn = literalData.getDataStream(); - Streams.pipeAll(literalIn, messageOutputStream); - literalIn.close(); - continue; - } - - if (next instanceof PGPCompressedData) { - // decompress compressed data - PGPCompressedData compressedData = (PGPCompressedData) next; - try { - objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(compressedData.getDataStream()); - } catch (PGPException e) { - throw new SOPGPException.BadData("Cannot decompress PGPCompressedData", e); - } - continue; - } - - if (next instanceof PGPSignatureList) { - signatures = (PGPSignatureList) next; - } - } - } - - if (signatures == null) { - throw new SOPGPException.BadData("Data did not contain OpenPGP signatures."); - } - - // write out signatures - if (armor) { - ArmoredOutputStream armorOut = ArmoredOutputStreamFactory.get(sigOut); - for (PGPSignature signature : signatures) { - signature.encode(armorOut); - } - armorOut.close(); - } else { - for (PGPSignature signature : signatures) { - signature.encode(sigOut); - } - } - - return new Signatures() { - @Override - public void writeTo(@Nonnull OutputStream signatureOutputStream) throws IOException { - Streams.pipeAll(new ByteArrayInputStream(sigOut.toByteArray()), signatureOutputStream); - } - }; - } - }; - } -} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java deleted file mode 100644 index 6ed1d471..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java +++ /dev/null @@ -1,135 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -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.PGPSecretKeyRing; -import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; -import org.bouncycastle.util.io.Streams; -import org.pgpainless.PGPainless; -import org.pgpainless.algorithm.DocumentSignatureType; -import org.pgpainless.encryption_signing.EncryptionStream; -import org.pgpainless.encryption_signing.ProducerOptions; -import org.pgpainless.encryption_signing.SigningOptions; -import org.pgpainless.exception.KeyException; -import org.pgpainless.key.OpenPgpFingerprint; -import org.pgpainless.key.info.KeyRingInfo; -import org.pgpainless.util.Passphrase; -import sop.Ready; -import sop.enums.InlineSignAs; -import sop.exception.SOPGPException; -import sop.operation.InlineSign; -import sop.util.UTF8Util; - -import javax.annotation.Nonnull; - -/** - * Implementation of the
inline-sign
operation using PGPainless. - */ -public class InlineSignImpl implements InlineSign { - - private boolean armor = true; - private InlineSignAs mode = InlineSignAs.binary; - private final SigningOptions signingOptions = new SigningOptions(); - private final MatchMakingSecretKeyRingProtector protector = new MatchMakingSecretKeyRingProtector(); - private final List signingKeys = new ArrayList<>(); - - @Override - @Nonnull - public InlineSign mode(@Nonnull InlineSignAs mode) throws SOPGPException.UnsupportedOption { - this.mode = mode; - return this; - } - - @Override - @Nonnull - public InlineSign noArmor() { - this.armor = false; - return this; - } - - @Override - @Nonnull - public InlineSign key(@Nonnull InputStream keyIn) throws SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException { - PGPSecretKeyRingCollection keys = KeyReader.readSecretKeys(keyIn, true); - for (PGPSecretKeyRing key : keys) { - KeyRingInfo info = PGPainless.inspectKeyRing(key); - if (!info.isUsableForSigning()) { - throw new SOPGPException.KeyCannotSign("Key " + info.getFingerprint() + " does not have valid, signing capable subkeys."); - } - protector.addSecretKey(key); - signingKeys.add(key); - } - return this; - } - - @Override - @Nonnull - public InlineSign withKeyPassword(@Nonnull byte[] password) { - String string = new String(password, UTF8Util.UTF8); - protector.addPassphrase(Passphrase.fromPassword(string)); - return this; - } - - @Override - @Nonnull - public Ready data(@Nonnull InputStream data) throws SOPGPException.KeyIsProtected, SOPGPException.ExpectedText { - for (PGPSecretKeyRing key : signingKeys) { - try { - if (mode == InlineSignAs.clearsigned) { - signingOptions.addDetachedSignature(protector, key, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT); - } else { - signingOptions.addInlineSignature(protector, key, modeToSigType(mode)); - } - } catch (KeyException.UnacceptableSigningKeyException | KeyException.MissingSecretKeyException e) { - throw new SOPGPException.KeyCannotSign("Key " + OpenPgpFingerprint.of(key) + " cannot sign.", e); - } catch (PGPException e) { - throw new SOPGPException.KeyIsProtected("Key " + OpenPgpFingerprint.of(key) + " cannot be unlocked.", e); - } - } - - ProducerOptions producerOptions = ProducerOptions.sign(signingOptions); - if (mode == InlineSignAs.clearsigned) { - producerOptions.setCleartextSigned(); - producerOptions.setAsciiArmor(true); - } else { - producerOptions.setAsciiArmor(armor); - } - - return new Ready() { - @Override - public void writeTo(@Nonnull OutputStream outputStream) throws IOException, SOPGPException.NoSignature { - try { - EncryptionStream signingStream = PGPainless.encryptAndOrSign() - .onOutputStream(outputStream) - .withOptions(producerOptions); - - if (signingStream.isClosed()) { - throw new IllegalStateException("EncryptionStream is already closed."); - } - - Streams.pipeAll(data, signingStream); - signingStream.close(); - - // forget passphrases - protector.clear(); - } catch (PGPException e) { - throw new RuntimeException(e); - } - } - }; - } - - private static DocumentSignatureType modeToSigType(InlineSignAs mode) { - return mode == InlineSignAs.binary ? DocumentSignatureType.BINARY_DOCUMENT - : DocumentSignatureType.CANONICAL_TEXT_DOCUMENT; - } -} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java deleted file mode 100644 index 352bd7c8..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java +++ /dev/null @@ -1,101 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -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.util.io.Streams; -import org.pgpainless.PGPainless; -import org.pgpainless.decryption_verification.ConsumerOptions; -import org.pgpainless.decryption_verification.DecryptionStream; -import org.pgpainless.decryption_verification.MessageMetadata; -import org.pgpainless.decryption_verification.SignatureVerification; -import org.pgpainless.exception.MalformedOpenPgpMessageException; -import org.pgpainless.exception.MissingDecryptionMethodException; -import sop.ReadyWithResult; -import sop.Verification; -import sop.exception.SOPGPException; -import sop.operation.InlineVerify; - -import javax.annotation.Nonnull; - -/** - * Implementation of the
inline-verify
operation using PGPainless. - */ -public class InlineVerifyImpl implements InlineVerify { - - private final ConsumerOptions options = ConsumerOptions.get(); - - @Override - @Nonnull - public InlineVerify notBefore(@Nonnull Date timestamp) throws SOPGPException.UnsupportedOption { - options.verifyNotBefore(timestamp); - return this; - } - - @Override - @Nonnull - public InlineVerify notAfter(@Nonnull Date timestamp) throws SOPGPException.UnsupportedOption { - options.verifyNotAfter(timestamp); - return this; - } - - @Override - @Nonnull - public InlineVerify cert(@Nonnull InputStream cert) throws SOPGPException.BadData, IOException { - PGPPublicKeyRingCollection certificates = KeyReader.readPublicKeys(cert, true); - options.addVerificationCerts(certificates); - return this; - } - - @Override - @Nonnull - public ReadyWithResult> data(@Nonnull InputStream data) throws SOPGPException.NoSignature, SOPGPException.BadData { - return new ReadyWithResult>() { - @Override - public List writeTo(@Nonnull OutputStream outputStream) throws IOException, SOPGPException.NoSignature { - DecryptionStream decryptionStream; - try { - decryptionStream = PGPainless.decryptAndOrVerify() - .onInputStream(data) - .withOptions(options); - - Streams.pipeAll(decryptionStream, outputStream); - decryptionStream.close(); - - MessageMetadata metadata = decryptionStream.getMetadata(); - List verificationList = new ArrayList<>(); - - List verifications = metadata.isUsingCleartextSignatureFramework() ? - metadata.getVerifiedDetachedSignatures() : - metadata.getVerifiedInlineSignatures(); - - for (SignatureVerification signatureVerification : verifications) { - verificationList.add(VerificationHelper.mapVerification(signatureVerification)); - } - - if (!options.getCertificateSource().getExplicitCertificates().isEmpty()) { - if (verificationList.isEmpty()) { - throw new SOPGPException.NoSignature(); - } - } - - return verificationList; - } catch (MissingDecryptionMethodException e) { - throw new SOPGPException.BadData("Cannot verify encrypted message.", e); - } catch (MalformedOpenPgpMessageException | PGPException e) { - throw new SOPGPException.BadData(e); - } - } - }; - } -} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/KeyReader.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/KeyReader.java deleted file mode 100644 index 4d676b33..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/KeyReader.java +++ /dev/null @@ -1,69 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.sop; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; -import org.bouncycastle.openpgp.PGPRuntimeOperationException; -import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; -import org.pgpainless.PGPainless; -import org.pgpainless.key.collection.PGPKeyRingCollection; -import sop.exception.SOPGPException; - -import java.io.IOException; -import java.io.InputStream; - -/** - * Reader for OpenPGP keys and certificates with error matching according to the SOP spec. - */ -class KeyReader { - - static PGPSecretKeyRingCollection readSecretKeys(InputStream keyInputStream, boolean requireContent) - throws IOException, SOPGPException.BadData { - PGPSecretKeyRingCollection keys; - try { - keys = PGPainless.readKeyRing().secretKeyRingCollection(keyInputStream); - } catch (IOException e) { - String message = e.getMessage(); - if (message == null) { - throw e; - } - if (message.startsWith("unknown object in stream:") || - message.startsWith("invalid header encountered")) { - throw new SOPGPException.BadData(e); - } - throw e; - } - - if (requireContent && keys.size() == 0) { - throw new SOPGPException.BadData(new PGPException("No key data found.")); - } - - return keys; - } - - static PGPPublicKeyRingCollection readPublicKeys(InputStream certIn, boolean requireContent) - throws IOException { - PGPKeyRingCollection certs; - try { - certs = PGPainless.readKeyRing().keyRingCollection(certIn, false); - } catch (IOException e) { - String msg = e.getMessage(); - if (msg != null && (msg.startsWith("unknown object in stream:") || msg.startsWith("invalid header encountered"))) { - throw new SOPGPException.BadData(e); - } - throw e; - } catch (PGPRuntimeOperationException e) { - throw new SOPGPException.BadData(e); - } - if (certs.getPgpSecretKeyRingCollection().size() != 0) { - throw new SOPGPException.BadData("Secret key components encountered, while certificates were expected."); - } - if (requireContent && certs.getPgpPublicKeyRingCollection().size() == 0) { - throw new SOPGPException.BadData(new PGPException("No cert data found.")); - } - return certs.getPgpPublicKeyRingCollection(); - } -} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/ListProfilesImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/ListProfilesImpl.java deleted file mode 100644 index e39c080d..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/ListProfilesImpl.java +++ /dev/null @@ -1,36 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.sop; - -import java.util.List; - -import sop.Profile; -import sop.exception.SOPGPException; -import sop.operation.ListProfiles; - -import javax.annotation.Nonnull; - -/** - * Implementation of the
list-profiles
operation using PGPainless. - * - */ -public class ListProfilesImpl implements ListProfiles { - - @Override - @Nonnull - public List subcommand(@Nonnull String command) { - - switch (command) { - case "generate-key": - return GenerateKeyImpl.SUPPORTED_PROFILES; - - case "encrypt": - return EncryptImpl.SUPPORTED_PROFILES; - - default: - throw new SOPGPException.UnsupportedProfile(command); - } - } -} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/MatchMakingSecretKeyRingProtector.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/MatchMakingSecretKeyRingProtector.java deleted file mode 100644 index 67386bde..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/MatchMakingSecretKeyRingProtector.java +++ /dev/null @@ -1,119 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.sop; - -import java.util.HashSet; -import java.util.Set; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPSecretKey; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; -import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor; -import org.pgpainless.implementation.ImplementationFactory; -import org.pgpainless.key.info.KeyInfo; -import org.pgpainless.key.protection.CachingSecretKeyRingProtector; -import org.pgpainless.key.protection.SecretKeyRingProtector; -import org.pgpainless.key.protection.UnlockSecretKey; -import org.pgpainless.util.Passphrase; - -import javax.annotation.Nullable; - -/** - * Implementation of the {@link SecretKeyRingProtector} which can be handed passphrases and keys separately, - * and which then matches up passphrases and keys when needed. - */ -public class MatchMakingSecretKeyRingProtector implements SecretKeyRingProtector { - - private final Set passphrases = new HashSet<>(); - private final Set keys = new HashSet<>(); - private final CachingSecretKeyRingProtector protector = new CachingSecretKeyRingProtector(); - - /** - * Add a single passphrase to the protector. - * - * @param passphrase passphrase - */ - public void addPassphrase(Passphrase passphrase) { - if (passphrase.isEmpty()) { - return; - } - - if (!passphrases.add(passphrase)) { - return; - } - - for (PGPSecretKeyRing key : keys) { - for (PGPSecretKey subkey : key) { - if (protector.hasPassphrase(subkey.getKeyID())) { - continue; - } - - testPassphrase(passphrase, subkey); - } - } - } - - /** - * Add a single {@link PGPSecretKeyRing} to the protector. - * - * @param key secret keys - */ - public void addSecretKey(PGPSecretKeyRing key) { - if (!keys.add(key)) { - return; - } - - for (PGPSecretKey subkey : key) { - if (KeyInfo.isDecrypted(subkey)) { - protector.addPassphrase(subkey.getKeyID(), Passphrase.emptyPassphrase()); - } else { - for (Passphrase passphrase : passphrases) { - testPassphrase(passphrase, subkey); - } - } - } - } - - private void testPassphrase(Passphrase passphrase, PGPSecretKey subkey) { - try { - PBESecretKeyDecryptor decryptor = ImplementationFactory.getInstance().getPBESecretKeyDecryptor(passphrase); - UnlockSecretKey.unlockSecretKey(subkey, decryptor); - protector.addPassphrase(subkey.getKeyID(), passphrase); - } catch (PGPException e) { - // wrong password - } - } - - @Override - public boolean hasPassphraseFor(long keyId) { - return protector.hasPassphrase(keyId); - } - - @Nullable - @Override - public PBESecretKeyDecryptor getDecryptor(long keyId) throws PGPException { - return protector.getDecryptor(keyId); - } - - @Nullable - @Override - public PBESecretKeyEncryptor getEncryptor(long keyId) throws PGPException { - return protector.getEncryptor(keyId); - } - - /** - * Clear all known passphrases from the protector. - */ - public void clear() { - for (Passphrase passphrase : passphrases) { - passphrase.clear(); - } - - for (PGPSecretKeyRing key : keys) { - protector.forgetPassphrase(key); - } - } -} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/NullOutputStream.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/NullOutputStream.java deleted file mode 100644 index 9977ba28..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/NullOutputStream.java +++ /dev/null @@ -1,17 +0,0 @@ -// SPDX-FileCopyrightText: 2024 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.sop; - -import java.io.OutputStream; - -/** - * {@link OutputStream} that simply discards bytes written to it. - */ -public class NullOutputStream extends OutputStream { - @Override - public void write(int b) { - // NOP - } -} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/RevokeKeyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/RevokeKeyImpl.java deleted file mode 100644 index 6b11b73a..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/RevokeKeyImpl.java +++ /dev/null @@ -1,123 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.sop; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.nio.charset.CharacterCodingException; -import java.util.ArrayList; -import java.util.List; - -import org.bouncycastle.bcpg.ArmoredOutputStream; -import org.bouncycastle.bcpg.PublicKeyPacket; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; -import org.bouncycastle.openpgp.PGPSignature; -import org.pgpainless.PGPainless; -import org.pgpainless.exception.WrongPassphraseException; -import org.pgpainless.key.OpenPgpFingerprint; -import org.pgpainless.key.modification.secretkeyring.SecretKeyRingEditorInterface; -import org.pgpainless.key.util.KeyRingUtils; -import org.pgpainless.key.util.RevocationAttributes; -import org.pgpainless.util.ArmoredOutputStreamFactory; -import org.pgpainless.util.Passphrase; -import sop.Ready; -import sop.exception.SOPGPException; -import sop.operation.RevokeKey; -import sop.util.UTF8Util; - -import javax.annotation.Nonnull; - -public class RevokeKeyImpl implements RevokeKey { - - private final MatchMakingSecretKeyRingProtector protector = new MatchMakingSecretKeyRingProtector(); - private boolean armor = true; - - @Override - @Nonnull - public RevokeKey noArmor() { - this.armor = false; - return this; - } - - /** - * Provide the decryption password for the secret key. - * - * @param password password - * @return builder instance - * @throws sop.exception.SOPGPException.UnsupportedOption if the implementation does not support key passwords - * @throws sop.exception.SOPGPException.PasswordNotHumanReadable if the password is not human-readable - */ - @Override - @Nonnull - public RevokeKey withKeyPassword(@Nonnull byte[] password) - throws SOPGPException.UnsupportedOption, - SOPGPException.PasswordNotHumanReadable { - String string; - try { - string = UTF8Util.decodeUTF8(password); - } catch (CharacterCodingException e) { - throw new SOPGPException.PasswordNotHumanReadable("Cannot UTF8-decode password."); - } - protector.addPassphrase(Passphrase.fromPassword(string)); - return this; - } - - @Override - @Nonnull - public Ready keys(@Nonnull InputStream keys) throws SOPGPException.BadData { - PGPSecretKeyRingCollection secretKeyRings; - try { - secretKeyRings = KeyReader.readSecretKeys(keys, true); - } catch (IOException e) { - throw new SOPGPException.BadData("Cannot decode secret keys.", e); - } - for (PGPSecretKeyRing secretKeys : secretKeyRings) { - protector.addSecretKey(secretKeys); - } - - final List revocationCertificates = new ArrayList<>(); - for (PGPSecretKeyRing secretKeys : secretKeyRings) { - SecretKeyRingEditorInterface editor = PGPainless.modifyKeyRing(secretKeys); - try { - RevocationAttributes revocationAttributes = RevocationAttributes.createKeyRevocation() - .withReason(RevocationAttributes.Reason.NO_REASON) - .withoutDescription(); - if (secretKeys.getPublicKey().getVersion() == PublicKeyPacket.VERSION_6) { - PGPPublicKeyRing revocation = editor.createMinimalRevocationCertificate(protector, revocationAttributes); - revocationCertificates.add(revocation); - } else { - PGPPublicKeyRing certificate = PGPainless.extractCertificate(secretKeys); - PGPSignature revocation = editor.createRevocation(protector, revocationAttributes); - certificate = KeyRingUtils.injectCertification(certificate, revocation); - revocationCertificates.add(certificate); - } - } catch (WrongPassphraseException e) { - throw new SOPGPException.KeyIsProtected("Missing or wrong passphrase for key " + OpenPgpFingerprint.of(secretKeys), e); - } - catch (PGPException e) { - throw new RuntimeException("Cannot generate revocation certificate.", e); - } - } - - return new Ready() { - @Override - public void writeTo(@Nonnull OutputStream outputStream) throws IOException { - PGPPublicKeyRingCollection certificateCollection = new PGPPublicKeyRingCollection(revocationCertificates); - if (armor) { - ArmoredOutputStream out = ArmoredOutputStreamFactory.get(outputStream); - certificateCollection.encode(out); - out.close(); - } else { - certificateCollection.encode(outputStream); - } - } - }; - } -} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPImpl.java deleted file mode 100644 index 932175d3..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPImpl.java +++ /dev/null @@ -1,149 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.sop; - -import org.pgpainless.util.ArmoredOutputStreamFactory; -import sop.SOP; -import sop.SOPV; -import sop.operation.Armor; -import sop.operation.ChangeKeyPassword; -import sop.operation.Dearmor; -import sop.operation.Decrypt; -import sop.operation.DetachedSign; -import sop.operation.DetachedVerify; -import sop.operation.InlineDetach; -import sop.operation.Encrypt; -import sop.operation.ExtractCert; -import sop.operation.GenerateKey; -import sop.operation.InlineSign; -import sop.operation.InlineVerify; -import sop.operation.ListProfiles; -import sop.operation.RevokeKey; -import sop.operation.Version; - -import javax.annotation.Nonnull; - -/** - * Implementation of the
sop
API using PGPainless. - *
 {@code
- * SOP sop = new SOPImpl();
- * }
- * - * For a slimmed down interface that merely focuses on signature verification, see {@link SOPVImpl}. - */ -public class SOPImpl implements SOP { - - static { - ArmoredOutputStreamFactory.setVersionInfo(null); - } - - // Delegate for sig verification operations - private final SOPV sopv = new SOPVImpl(); - - @Override - @Nonnull - public Version version() { - // Delegate to SOPV - return sopv.version(); - } - - @Override - @Nonnull - public GenerateKey generateKey() { - return new GenerateKeyImpl(); - } - - @Override - @Nonnull - public ExtractCert extractCert() { - return new ExtractCertImpl(); - } - - @Override - @Nonnull - public DetachedSign sign() { - return detachedSign(); - } - - @Override - @Nonnull - public DetachedSign detachedSign() { - return new DetachedSignImpl(); - } - - @Override - @Nonnull - public InlineSign inlineSign() { - return new InlineSignImpl(); - } - - @Override - @Nonnull - public DetachedVerify verify() { - return detachedVerify(); - } - - @Override - @Nonnull - public DetachedVerify detachedVerify() { - // Delegate to SOPV - return sopv.detachedVerify(); - } - - @Override - @Nonnull - public InlineVerify inlineVerify() { - // Delegate to SOPV - return sopv.inlineVerify(); - } - - @Override - @Nonnull - public Encrypt encrypt() { - return new EncryptImpl(); - } - - @Override - @Nonnull - public Decrypt decrypt() { - return new DecryptImpl(); - } - - @Override - @Nonnull - public Armor armor() { - return new ArmorImpl(); - } - - @Override - @Nonnull - public Dearmor dearmor() { - return new DearmorImpl(); - } - - @Override - @Nonnull - public ListProfiles listProfiles() { - return new ListProfilesImpl(); - } - - @Override - @Nonnull - public RevokeKey revokeKey() { - return new RevokeKeyImpl(); - } - - @Override - @Nonnull - public ChangeKeyPassword changeKeyPassword() { - return new ChangeKeyPasswordImpl(); - } - - @Override - @Nonnull - public InlineDetach inlineDetach() { - return new InlineDetachImpl(); - } -} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPVImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPVImpl.java deleted file mode 100644 index c1167a3b..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPVImpl.java +++ /dev/null @@ -1,40 +0,0 @@ -// SPDX-FileCopyrightText: 2024 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.sop; - -import org.jetbrains.annotations.NotNull; -import org.pgpainless.util.ArmoredOutputStreamFactory; -import sop.SOPV; -import sop.operation.DetachedVerify; -import sop.operation.InlineVerify; -import sop.operation.Version; - -/** - * Implementation of the
sopv
interface subset using PGPainless. - */ -public class SOPVImpl implements SOPV { - - static { - ArmoredOutputStreamFactory.setVersionInfo(null); - } - - @NotNull - @Override - public DetachedVerify detachedVerify() { - return new DetachedVerifyImpl(); - } - - @NotNull - @Override - public InlineVerify inlineVerify() { - return new InlineVerifyImpl(); - } - - @NotNull - @Override - public Version version() { - return new VersionImpl(); - } -} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/VerificationHelper.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/VerificationHelper.java deleted file mode 100644 index 126a5e3b..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/VerificationHelper.java +++ /dev/null @@ -1,52 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.sop; - -import org.bouncycastle.openpgp.PGPSignature; -import org.pgpainless.decryption_verification.SignatureVerification; -import sop.Verification; -import sop.enums.SignatureMode; - -/** - * Helper class for shared methods related to {@link Verification Verifications}. - */ -public class VerificationHelper { - - /** - * Map a {@link SignatureVerification} object to a {@link Verification}. - * - * @param sigVerification signature verification - * @return verification - */ - public static Verification mapVerification(SignatureVerification sigVerification) { - return new Verification( - sigVerification.getSignature().getCreationTime(), - sigVerification.getSigningKey().getSubkeyFingerprint().toString(), - sigVerification.getSigningKey().getPrimaryKeyFingerprint().toString(), - getMode(sigVerification.getSignature()), - null); - } - - /** - * Map an OpenPGP signature type to a {@link SignatureMode} enum. - * Note: This method only maps {@link PGPSignature#BINARY_DOCUMENT} and {@link PGPSignature#CANONICAL_TEXT_DOCUMENT}. - * Other values are mapped to
null
. - * - * @param signature signature - * @return signature mode enum or null - */ - private static SignatureMode getMode(PGPSignature signature) { - - if (signature.getSignatureType() == PGPSignature.BINARY_DOCUMENT) { - return SignatureMode.binary; - } - - if (signature.getSignatureType() == PGPSignature.CANONICAL_TEXT_DOCUMENT) { - return SignatureMode.text; - } - - return null; - } -} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java deleted file mode 100644 index 3d3dd597..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java +++ /dev/null @@ -1,98 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.sop; - -import java.io.IOException; -import java.io.InputStream; -import java.util.Locale; -import java.util.Properties; - -import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.jetbrains.annotations.NotNull; -import sop.exception.SOPGPException; -import sop.operation.Version; - -import javax.annotation.Nonnull; - -/** - * Implementation of the
version
operation using PGPainless. - */ -public class VersionImpl implements Version { - - // draft version - private static final int SOP_VERSION = 10; - - private static final String SOPV_VERSION = "1.0"; - - @Override - @Nonnull - public String getName() { - return "PGPainless-SOP"; - } - - @Override - @Nonnull - public String getVersion() { - // See https://stackoverflow.com/a/50119235 - String version; - try { - Properties properties = new Properties(); - InputStream propertiesFileIn = getClass().getResourceAsStream("/version.properties"); - if (propertiesFileIn == null) { - throw new IOException("File version.properties not found."); - } - properties.load(propertiesFileIn); - version = properties.getProperty("version"); - } catch (IOException e) { - version = "DEVELOPMENT"; - } - return version; - } - - @Override - @Nonnull - public String getBackendVersion() { - return "PGPainless " + getVersion(); - } - - @Override - @Nonnull - public String getExtendedVersion() { - double bcVersion = new BouncyCastleProvider().getVersion(); - String FORMAT_VERSION = String.format("%02d", SOP_VERSION); - return getName() + " " + getVersion() + "\n" + - "https://codeberg.org/PGPainless/pgpainless/src/branch/master/pgpainless-sop\n" + - "\n" + - "Implementation of the Stateless OpenPGP Protocol Version " + FORMAT_VERSION + "\n" + - "https://datatracker.ietf.org/doc/html/draft-dkg-openpgp-stateless-cli-" + FORMAT_VERSION + "\n" + - "\n" + - "Based on pgpainless-core " + getVersion() + "\n" + - "https://pgpainless.org\n" + - "\n" + - "Using " + String.format(Locale.US, "Bouncy Castle %.2f", bcVersion) + "\n" + - "https://www.bouncycastle.org/java.html"; - } - - @Override - public int getSopSpecRevisionNumber() { - return SOP_VERSION; - } - - @Override - public boolean isSopSpecImplementationIncomplete() { - return false; - } - - @Override - public String getSopSpecImplementationRemarks() { - return null; - } - - @NotNull - @Override - public String getSopVVersion() throws SOPGPException.UnsupportedOption { - return SOPV_VERSION; - } -} 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 deleted file mode 100644 index c0ce9cda..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Implementation of the java-sop package using pgpainless-core. - */ -package org.pgpainless.sop; diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ArmorImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ArmorImpl.kt new file mode 100644 index 00000000..bf7e63f1 --- /dev/null +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ArmorImpl.kt @@ -0,0 +1,54 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop + +import java.io.BufferedOutputStream +import java.io.InputStream +import java.io.OutputStream +import kotlin.jvm.Throws +import org.bouncycastle.util.io.Streams +import org.pgpainless.decryption_verification.OpenPgpInputStream +import org.pgpainless.util.ArmoredOutputStreamFactory +import sop.Ready +import sop.enums.ArmorLabel +import sop.exception.SOPGPException +import sop.operation.Armor + +/** Implementation of the `armor` operation using PGPainless. */ +class ArmorImpl : Armor { + + @Throws(SOPGPException.BadData::class) + override fun data(data: InputStream): Ready { + return object : Ready() { + override fun writeTo(outputStream: OutputStream) { + // By buffering the output stream, we can improve performance drastically + val bufferedOutputStream = BufferedOutputStream(outputStream) + + // Determine the nature of the given data + val openPgpIn = OpenPgpInputStream(data) + openPgpIn.reset() + + if (openPgpIn.isAsciiArmored) { + // armoring already-armored data is an idempotent operation + Streams.pipeAll(openPgpIn, bufferedOutputStream) + bufferedOutputStream.flush() + openPgpIn.close() + return + } + + val armor = ArmoredOutputStreamFactory.get(bufferedOutputStream) + Streams.pipeAll(openPgpIn, armor) + bufferedOutputStream.flush() + armor.close() + openPgpIn.close() + } + } + } + + @Deprecated("Setting custom labels is not supported.") + override fun label(label: ArmorLabel): Armor { + throw SOPGPException.UnsupportedOption("Setting custom Armor labels not supported.") + } +} diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ChangeKeyPasswordImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ChangeKeyPasswordImpl.kt new file mode 100644 index 00000000..a9aaf1e4 --- /dev/null +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ChangeKeyPasswordImpl.kt @@ -0,0 +1,81 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +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.PGPSecretKeyRingCollection +import org.pgpainless.bouncycastle.extensions.openPgpFingerprint +import org.pgpainless.exception.MissingPassphraseException +import org.pgpainless.key.protection.SecretKeyRingProtector +import org.pgpainless.key.util.KeyRingUtils +import org.pgpainless.util.ArmoredOutputStreamFactory +import org.pgpainless.util.Passphrase +import sop.Ready +import sop.exception.SOPGPException +import sop.operation.ChangeKeyPassword + +/** Implementation of the `change-key-password` operation using PGPainless. */ +class ChangeKeyPasswordImpl : ChangeKeyPassword { + + private val oldProtector = MatchMakingSecretKeyRingProtector() + private var newPassphrase = Passphrase.emptyPassphrase() + private var armor = true + + override fun keys(keys: InputStream): Ready { + val newProtector = SecretKeyRingProtector.unlockAnyKeyWith(newPassphrase) + val secretKeysCollection = + try { + KeyReader.readSecretKeys(keys, true) + } catch (e: IOException) { + throw SOPGPException.BadData(e) + } + + val updatedSecretKeys = + secretKeysCollection + .map { secretKeys -> + oldProtector.addSecretKey(secretKeys) + try { + return@map KeyRingUtils.changePassphrase( + null, secretKeys, oldProtector, newProtector) + } catch (e: MissingPassphraseException) { + throw SOPGPException.KeyIsProtected( + "Cannot unlock key ${secretKeys.openPgpFingerprint}", e) + } catch (e: PGPException) { + if (e.message?.contains("Exception decrypting key") == true) { + throw SOPGPException.KeyIsProtected( + "Cannot unlock key ${secretKeys.openPgpFingerprint}", e) + } + throw RuntimeException( + "Cannot change passphrase of key ${secretKeys.openPgpFingerprint}", e) + } + } + .let { PGPSecretKeyRingCollection(it) } + + return object : Ready() { + override fun writeTo(outputStream: OutputStream) { + if (armor) { + ArmoredOutputStreamFactory.get(outputStream).use { + updatedSecretKeys.encode(it) + } + } else { + updatedSecretKeys.encode(outputStream) + } + } + } + } + + override fun newKeyPassphrase(newPassphrase: String): ChangeKeyPassword = apply { + this.newPassphrase = Passphrase.fromPassword(newPassphrase) + } + + override fun noArmor(): ChangeKeyPassword = apply { armor = false } + + override fun oldKeyPassphrase(oldPassphrase: String): ChangeKeyPassword = apply { + oldProtector.addPassphrase(Passphrase.fromPassword(oldPassphrase)) + } +} diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DearmorImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DearmorImpl.kt new file mode 100644 index 00000000..9d196004 --- /dev/null +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DearmorImpl.kt @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop + +import java.io.BufferedOutputStream +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.exception.SOPGPException +import sop.operation.Dearmor + +/** Implementation of the `dearmor` operation using PGPainless. */ +class DearmorImpl : Dearmor { + + override fun data(data: InputStream): Ready { + val decoder = + try { + PGPUtil.getDecoderStream(data) + } catch (e: IOException) { + throw SOPGPException.BadData(e) + } + + return object : Ready() { + override fun writeTo(outputStream: OutputStream) { + BufferedOutputStream(outputStream).use { + Streams.pipeAll(decoder, it) + it.flush() + decoder.close() + } + } + } + } +} diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DecryptImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DecryptImpl.kt new file mode 100644 index 00000000..f6a9fb1d --- /dev/null +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DecryptImpl.kt @@ -0,0 +1,125 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop + +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream +import java.util.* +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.util.io.Streams +import org.pgpainless.PGPainless +import org.pgpainless.algorithm.SymmetricKeyAlgorithm +import org.pgpainless.decryption_verification.ConsumerOptions +import org.pgpainless.exception.MalformedOpenPgpMessageException +import org.pgpainless.exception.MissingDecryptionMethodException +import org.pgpainless.exception.WrongPassphraseException +import org.pgpainless.util.Passphrase +import sop.DecryptionResult +import sop.ReadyWithResult +import sop.SessionKey +import sop.exception.SOPGPException +import sop.operation.Decrypt +import sop.util.UTF8Util + +/** Implementation of the `decrypt` operation using PGPainless. */ +class DecryptImpl : Decrypt { + + private val consumerOptions = ConsumerOptions.get() + private val protector = MatchMakingSecretKeyRingProtector() + + override fun ciphertext(ciphertext: InputStream): ReadyWithResult { + if (consumerOptions.getDecryptionKeys().isEmpty() && + consumerOptions.getDecryptionPassphrases().isEmpty() && + consumerOptions.getSessionKey() == null) { + throw SOPGPException.MissingArg("Missing decryption key, passphrase or session key.") + } + + val decryptionStream = + try { + PGPainless.decryptAndOrVerify() + .onInputStream(ciphertext) + .withOptions(consumerOptions) + } catch (e: MissingDecryptionMethodException) { + throw SOPGPException.CannotDecrypt( + "No usable decryption key or password provided.", e) + } catch (e: WrongPassphraseException) { + throw SOPGPException.KeyIsProtected() + } catch (e: MalformedOpenPgpMessageException) { + throw SOPGPException.BadData(e) + } catch (e: PGPException) { + throw SOPGPException.BadData(e) + } catch (e: IOException) { + throw SOPGPException.BadData(e) + } finally { + // Forget passphrases after decryption + protector.clear() + } + + return object : ReadyWithResult() { + override fun writeTo(outputStream: OutputStream): DecryptionResult { + Streams.pipeAll(decryptionStream, outputStream) + decryptionStream.close() + + val metadata = decryptionStream.metadata + if (!metadata.isEncrypted) { + throw SOPGPException.BadData("Data is not encrypted.") + } + + val verificationList = + metadata.verifiedInlineSignatures.map { VerificationHelper.mapVerification(it) } + + var sessionKey: SessionKey? = null + if (metadata.sessionKey != null) { + sessionKey = + SessionKey( + metadata.sessionKey!!.algorithm.algorithmId.toByte(), + metadata.sessionKey!!.key) + } + return DecryptionResult(sessionKey, verificationList) + } + } + } + + override fun verifyNotAfter(timestamp: Date): Decrypt = apply { + consumerOptions.verifyNotAfter(timestamp) + } + + override fun verifyNotBefore(timestamp: Date): Decrypt = apply { + consumerOptions.verifyNotBefore(timestamp) + } + + override fun verifyWithCert(cert: InputStream): Decrypt = apply { + KeyReader.readPublicKeys(cert, true)?.let { consumerOptions.addVerificationCerts(it) } + } + + override fun withKey(key: InputStream): Decrypt = apply { + KeyReader.readSecretKeys(key, true).forEach { + protector.addSecretKey(it) + consumerOptions.addDecryptionKey(it, protector) + } + } + + override fun withKeyPassword(password: ByteArray): Decrypt = apply { + protector.addPassphrase(Passphrase.fromPassword(String(password, UTF8Util.UTF8))) + } + + override fun withPassword(password: String): Decrypt = apply { + consumerOptions.addDecryptionPassphrase(Passphrase.fromPassword(password)) + password.trimEnd().let { + if (it != password) { + consumerOptions.addDecryptionPassphrase(Passphrase.fromPassword(it)) + } + } + } + + override fun withSessionKey(sessionKey: SessionKey): Decrypt = apply { + consumerOptions.setSessionKey(mapSessionKey(sessionKey)) + } + + private fun mapSessionKey(sessionKey: SessionKey): org.pgpainless.util.SessionKey = + org.pgpainless.util.SessionKey( + SymmetricKeyAlgorithm.requireFromId(sessionKey.algorithm.toInt()), sessionKey.key) +} diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DetachedSignImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DetachedSignImpl.kt new file mode 100644 index 00000000..c3857ef5 --- /dev/null +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DetachedSignImpl.kt @@ -0,0 +1,130 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop + +import java.io.InputStream +import java.io.OutputStream +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.algorithm.HashAlgorithm +import org.pgpainless.bouncycastle.extensions.openPgpFingerprint +import org.pgpainless.encryption_signing.ProducerOptions +import org.pgpainless.encryption_signing.SigningOptions +import org.pgpainless.exception.KeyException.MissingSecretKeyException +import org.pgpainless.exception.KeyException.UnacceptableSigningKeyException +import org.pgpainless.util.ArmoredOutputStreamFactory +import org.pgpainless.util.Passphrase +import sop.MicAlg +import sop.ReadyWithResult +import sop.SigningResult +import sop.enums.SignAs +import sop.exception.SOPGPException +import sop.operation.DetachedSign +import sop.util.UTF8Util + +/** Implementation of the `sign` operation using PGPainless. */ +class DetachedSignImpl : DetachedSign { + + private val signingOptions = SigningOptions.get() + private val protector = MatchMakingSecretKeyRingProtector() + private val signingKeys = mutableListOf() + + private var armor = true + private var mode = SignAs.binary + + override fun data(data: InputStream): ReadyWithResult { + signingKeys.forEach { + try { + signingOptions.addDetachedSignature(protector, it, modeToSigType(mode)) + } catch (e: UnacceptableSigningKeyException) { + throw SOPGPException.KeyCannotSign("Key ${it.openPgpFingerprint} cannot sign.", e) + } catch (e: MissingSecretKeyException) { + throw SOPGPException.KeyCannotSign( + "Key ${it.openPgpFingerprint} cannot sign. Missing secret key.", e) + } catch (e: PGPException) { + throw SOPGPException.KeyIsProtected( + "Key ${it.openPgpFingerprint} cannot be unlocked.", e) + } + } + + // When creating a detached signature, the output of the signing stream is actually + // the unmodified plaintext data, so we can discard it. + // The detached signature will later be retrieved from the metadata object instead. + val sink = NullOutputStream() + + try { + val signingStream = + PGPainless.encryptAndOrSign() + .onOutputStream(sink) + .withOptions(ProducerOptions.sign(signingOptions).setAsciiArmor(armor)) + + return object : ReadyWithResult() { + override fun writeTo(outputStream: OutputStream): SigningResult { + check(!signingStream.isClosed) { "The operation is a one-shot object." } + + Streams.pipeAll(data, signingStream) + signingStream.close() + val result = signingStream.result + + // forget passphrases + protector.clear() + + val signatures = result.detachedSignatures.map { it.value }.flatten() + val out = + if (armor) ArmoredOutputStreamFactory.get(outputStream) else outputStream + + signatures.forEach { it.encode(out) } + out.close() + outputStream.close() + + return SigningResult.builder() + .setMicAlg(micAlgFromSignatures(signatures)) + .build() + } + } + } catch (e: PGPException) { + throw RuntimeException(e) + } + } + + override fun key(key: InputStream): DetachedSign = apply { + KeyReader.readSecretKeys(key, true).forEach { + val info = PGPainless.inspectKeyRing(it) + if (!info.isUsableForSigning) { + throw SOPGPException.KeyCannotSign( + "Key ${info.fingerprint} does not have valid, signing capable subkeys.") + } + protector.addSecretKey(it) + signingKeys.add(it) + } + } + + override fun mode(mode: SignAs): DetachedSign = apply { this.mode = mode } + + override fun noArmor(): DetachedSign = apply { armor = false } + + override fun withKeyPassword(password: ByteArray): DetachedSign = apply { + protector.addPassphrase(Passphrase.fromPassword(String(password, UTF8Util.UTF8))) + } + + private fun modeToSigType(mode: SignAs): DocumentSignatureType { + return when (mode) { + SignAs.binary -> DocumentSignatureType.BINARY_DOCUMENT + SignAs.text -> DocumentSignatureType.CANONICAL_TEXT_DOCUMENT + } + } + + private fun micAlgFromSignatures(signatures: List): MicAlg = + signatures + .mapNotNull { HashAlgorithm.fromId(it.hashAlgorithm) } + .toSet() + .singleOrNull() + ?.let { MicAlg.fromHashAlgorithmId(it.algorithmId) } + ?: MicAlg.empty() +} diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DetachedVerifyImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DetachedVerifyImpl.kt new file mode 100644 index 00000000..08472144 --- /dev/null +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DetachedVerifyImpl.kt @@ -0,0 +1,71 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop + +import java.io.IOException +import java.io.InputStream +import java.util.* +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.util.io.Streams +import org.pgpainless.PGPainless +import org.pgpainless.decryption_verification.ConsumerOptions +import org.pgpainless.exception.MalformedOpenPgpMessageException +import sop.Verification +import sop.exception.SOPGPException +import sop.operation.DetachedVerify +import sop.operation.VerifySignatures + +/** Implementation of the `verify` operation using PGPainless. */ +class DetachedVerifyImpl : DetachedVerify { + + private val options = ConsumerOptions.get().forceNonOpenPgpData() + + override fun cert(cert: InputStream): DetachedVerify = apply { + options.addVerificationCerts(KeyReader.readPublicKeys(cert, true)) + } + + override fun data(data: InputStream): List { + try { + val verificationStream = + PGPainless.decryptAndOrVerify().onInputStream(data).withOptions(options) + + Streams.drain(verificationStream) + verificationStream.close() + + val result = verificationStream.metadata + val verifications = + result.verifiedDetachedSignatures.map { VerificationHelper.mapVerification(it) } + + if (options.getCertificateSource().getExplicitCertificates().isNotEmpty() && + verifications.isEmpty()) { + throw SOPGPException.NoSignature() + } + + return verifications + } catch (e: MalformedOpenPgpMessageException) { + throw SOPGPException.BadData(e) + } catch (e: PGPException) { + throw SOPGPException.BadData(e) + } + } + + override fun notAfter(timestamp: Date): DetachedVerify = apply { + options.verifyNotAfter(timestamp) + } + + override fun notBefore(timestamp: Date): DetachedVerify = apply { + options.verifyNotBefore(timestamp) + } + + override fun signatures(signatures: InputStream): VerifySignatures = apply { + try { + options.addVerificationOfDetachedSignatures(signatures) + } catch (e: IOException) { + throw SOPGPException.BadData(e) + } catch (e: PGPException) { + throw SOPGPException.BadData(e) + } + } +} diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/EncryptImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/EncryptImpl.kt new file mode 100644 index 00000000..83d60aa5 --- /dev/null +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/EncryptImpl.kt @@ -0,0 +1,155 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +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.PGPSecretKeyRing +import org.bouncycastle.util.io.Streams +import org.pgpainless.PGPainless +import org.pgpainless.algorithm.DocumentSignatureType +import org.pgpainless.algorithm.StreamEncoding +import org.pgpainless.bouncycastle.extensions.openPgpFingerprint +import org.pgpainless.encryption_signing.EncryptionOptions +import org.pgpainless.encryption_signing.ProducerOptions +import org.pgpainless.encryption_signing.SigningOptions +import org.pgpainless.exception.KeyException.UnacceptableEncryptionKeyException +import org.pgpainless.exception.KeyException.UnacceptableSigningKeyException +import org.pgpainless.exception.WrongPassphraseException +import org.pgpainless.util.Passphrase +import sop.EncryptionResult +import sop.Profile +import sop.ReadyWithResult +import sop.enums.EncryptAs +import sop.exception.SOPGPException +import sop.operation.Encrypt +import sop.util.UTF8Util + +/** Implementation of the `encrypt` operation using PGPainless. */ +class EncryptImpl : Encrypt { + + companion object { + @JvmField val RFC4880_PROFILE = Profile("rfc4880", "Follow the packet format of rfc4880") + + @JvmField val SUPPORTED_PROFILES = listOf(RFC4880_PROFILE) + } + + private val encryptionOptions = EncryptionOptions.get() + private var signingOptions: SigningOptions? = null + private val signingKeys = mutableListOf() + private val protector = MatchMakingSecretKeyRingProtector() + + private var profile = RFC4880_PROFILE.name + private var mode = EncryptAs.binary + private var armor = true + + override fun mode(mode: EncryptAs): Encrypt = apply { this.mode = mode } + + override fun noArmor(): Encrypt = apply { this.armor = false } + + override fun plaintext(plaintext: InputStream): ReadyWithResult { + if (!encryptionOptions.hasEncryptionMethod()) { + throw SOPGPException.MissingArg("Missing encryption method.") + } + + val options = + if (signingOptions != null) { + ProducerOptions.signAndEncrypt(encryptionOptions, signingOptions!!) + } else { + ProducerOptions.encrypt(encryptionOptions) + } + .setAsciiArmor(armor) + .setEncoding(modeToStreamEncoding(mode)) + + signingKeys.forEach { + try { + signingOptions!!.addInlineSignature(protector, it, modeToSignatureType(mode)) + } catch (e: UnacceptableSigningKeyException) { + throw SOPGPException.KeyCannotSign("Key ${it.openPgpFingerprint} cannot sign", e) + } catch (e: WrongPassphraseException) { + throw SOPGPException.KeyIsProtected("Cannot unlock key ${it.openPgpFingerprint}", e) + } catch (e: PGPException) { + throw SOPGPException.BadData(e) + } + } + + try { + return object : ReadyWithResult() { + override fun writeTo(outputStream: OutputStream): EncryptionResult { + val encryptionStream = + PGPainless.encryptAndOrSign() + .onOutputStream(outputStream) + .withOptions(options) + Streams.pipeAll(plaintext, encryptionStream) + encryptionStream.close() + // TODO: Extract and emit session key once BC supports that + return EncryptionResult(null) + } + } + } catch (e: PGPException) { + throw IOException(e) + } + } + + override fun profile(profileName: String): Encrypt = apply { + profile = + SUPPORTED_PROFILES.find { it.name == profileName }?.name + ?: throw SOPGPException.UnsupportedProfile("encrypt", profileName) + } + + override fun signWith(key: InputStream): Encrypt = apply { + if (signingOptions == null) { + signingOptions = SigningOptions.get() + } + + val signingKey = + KeyReader.readSecretKeys(key, true).singleOrNull() + ?: throw SOPGPException.BadData( + AssertionError( + "Exactly one secret key at a time expected. Got zero or multiple instead.")) + + val info = PGPainless.inspectKeyRing(signingKey) + if (info.signingSubkeys.isEmpty()) { + throw SOPGPException.KeyCannotSign("Key ${info.fingerprint} cannot sign.") + } + + protector.addSecretKey(signingKey) + signingKeys.add(signingKey) + } + + override fun withCert(cert: InputStream): Encrypt = apply { + try { + encryptionOptions.addRecipients(KeyReader.readPublicKeys(cert, true)) + } catch (e: UnacceptableEncryptionKeyException) { + throw SOPGPException.CertCannotEncrypt(e.message ?: "Cert cannot encrypt", e) + } catch (e: IOException) { + throw SOPGPException.BadData(e) + } + } + + override fun withKeyPassword(password: ByteArray): Encrypt = apply { + protector.addPassphrase(Passphrase.fromPassword(String(password, UTF8Util.UTF8))) + } + + override fun withPassword(password: String): Encrypt = apply { + encryptionOptions.addPassphrase(Passphrase.fromPassword(password)) + } + + private fun modeToStreamEncoding(mode: EncryptAs): StreamEncoding { + return when (mode) { + EncryptAs.binary -> StreamEncoding.BINARY + EncryptAs.text -> StreamEncoding.UTF8 + } + } + + private fun modeToSignatureType(mode: EncryptAs): DocumentSignatureType { + return when (mode) { + EncryptAs.binary -> DocumentSignatureType.BINARY_DOCUMENT + EncryptAs.text -> DocumentSignatureType.CANONICAL_TEXT_DOCUMENT + } + } +} diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ExtractCertImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ExtractCertImpl.kt new file mode 100644 index 00000000..7fe66ee5 --- /dev/null +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ExtractCertImpl.kt @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop + +import java.io.InputStream +import java.io.OutputStream +import org.pgpainless.PGPainless +import org.pgpainless.util.ArmorUtils +import org.pgpainless.util.ArmoredOutputStreamFactory +import sop.Ready +import sop.operation.ExtractCert + +/** Implementation of the `extract-cert` operation using PGPainless. */ +class ExtractCertImpl : ExtractCert { + + private var armor = true + + override fun key(keyInputStream: InputStream): Ready { + val certs = + KeyReader.readSecretKeys(keyInputStream, true).map { PGPainless.extractCertificate(it) } + + return object : Ready() { + override fun writeTo(outputStream: OutputStream) { + if (armor) { + if (certs.size == 1) { + val cert = certs[0] + // This way we have a nice armor header with fingerprint and user-ids + val armorOut = ArmorUtils.toAsciiArmoredStream(cert, outputStream) + cert.encode(armorOut) + armorOut.close() + } else { + // for multiple certs, add no info headers to the ASCII armor + val armorOut = ArmoredOutputStreamFactory.get(outputStream) + certs.forEach { it.encode(armorOut) } + armorOut.close() + } + } else { + certs.forEach { it.encode(outputStream) } + } + } + } + } + + override fun noArmor(): ExtractCert = apply { armor = false } +} diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/GenerateKeyImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/GenerateKeyImpl.kt new file mode 100644 index 00000000..7438f0ae --- /dev/null +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/GenerateKeyImpl.kt @@ -0,0 +1,136 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop + +import java.io.OutputStream +import java.lang.RuntimeException +import java.security.InvalidAlgorithmParameterException +import java.security.NoSuchAlgorithmException +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.openpgp.PGPSecretKeyRing +import org.pgpainless.PGPainless +import org.pgpainless.algorithm.KeyFlag +import org.pgpainless.key.generation.KeyRingBuilder +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.rsa.RsaLength +import org.pgpainless.key.generation.type.xdh.XDHSpec +import org.pgpainless.util.ArmorUtils +import org.pgpainless.util.Passphrase +import sop.Profile +import sop.Ready +import sop.exception.SOPGPException +import sop.operation.GenerateKey + +/** Implementation of the `generate-key` operation using PGPainless. */ +class GenerateKeyImpl : GenerateKey { + + companion object { + @JvmField + val CURVE25519_PROFILE = + Profile( + "draft-koch-eddsa-for-openpgp-00", "Generate EdDSA / ECDH keys using Curve25519") + @JvmField val RSA4096_PROFILE = Profile("rfc4880", "Generate 4096-bit RSA keys") + + @JvmField val SUPPORTED_PROFILES = listOf(CURVE25519_PROFILE, RSA4096_PROFILE) + } + + private val userIds = mutableSetOf() + private var armor = true + private var signingOnly = false + private var passphrase = Passphrase.emptyPassphrase() + private var profile = CURVE25519_PROFILE.name + + override fun generate(): Ready { + try { + val key = generateKeyWithProfile(profile, userIds, passphrase, signingOnly) + return object : Ready() { + override fun writeTo(outputStream: OutputStream) { + if (armor) { + val armorOut = ArmorUtils.toAsciiArmoredStream(key, outputStream) + key.encode(armorOut) + armorOut.close() + } else { + key.encode(outputStream) + } + } + } + } catch (e: InvalidAlgorithmParameterException) { + throw SOPGPException.UnsupportedAsymmetricAlgo("Unsupported asymmetric algorithm.", e) + } catch (e: NoSuchAlgorithmException) { + throw SOPGPException.UnsupportedAsymmetricAlgo("Unsupported asymmetric algorithm.", e) + } catch (e: PGPException) { + throw RuntimeException(e) + } + } + + override fun noArmor(): GenerateKey = apply { armor = false } + + override fun profile(profile: String): GenerateKey = apply { + this.profile = + SUPPORTED_PROFILES.find { it.name == profile }?.name + ?: throw SOPGPException.UnsupportedProfile("generate-key", profile) + } + + override fun signingOnly(): GenerateKey = apply { signingOnly = true } + + override fun userId(userId: String): GenerateKey = apply { userIds.add(userId) } + + override fun withKeyPassword(password: String): GenerateKey = apply { + this.passphrase = Passphrase.fromPassword(password) + } + + private fun generateKeyWithProfile( + profile: String, + userIds: Set, + passphrase: Passphrase, + signingOnly: Boolean + ): PGPSecretKeyRing { + val keyBuilder: KeyRingBuilder = + when (profile) { + CURVE25519_PROFILE.name -> + PGPainless.buildKeyRing() + .setPrimaryKey( + KeySpec.getBuilder( + KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER)) + .addSubkey( + KeySpec.getBuilder( + KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.SIGN_DATA)) + .apply { + if (!signingOnly) { + addSubkey( + KeySpec.getBuilder( + KeyType.XDH(XDHSpec._X25519), + KeyFlag.ENCRYPT_COMMS, + KeyFlag.ENCRYPT_STORAGE)) + } + } + RSA4096_PROFILE.name -> { + PGPainless.buildKeyRing() + .setPrimaryKey( + KeySpec.getBuilder(KeyType.RSA(RsaLength._4096), KeyFlag.CERTIFY_OTHER)) + .addSubkey( + KeySpec.getBuilder(KeyType.RSA(RsaLength._4096), KeyFlag.SIGN_DATA)) + .apply { + if (!signingOnly) { + addSubkey( + KeySpec.getBuilder( + KeyType.RSA(RsaLength._4096), + KeyFlag.ENCRYPT_COMMS, + KeyFlag.ENCRYPT_STORAGE)) + } + } + } + else -> throw SOPGPException.UnsupportedProfile("generate-key", profile) + } + + userIds.forEach { keyBuilder.addUserId(it) } + if (!passphrase.isEmpty) { + keyBuilder.setPassphrase(passphrase) + } + return keyBuilder.build() + } +} diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/InlineDetachImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/InlineDetachImpl.kt new file mode 100644 index 00000000..82414a96 --- /dev/null +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/InlineDetachImpl.kt @@ -0,0 +1,134 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop + +import java.io.ByteArrayOutputStream +import java.io.InputStream +import java.io.OutputStream +import org.bouncycastle.bcpg.ArmoredInputStream +import org.bouncycastle.openpgp.PGPCompressedData +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.openpgp.PGPLiteralData +import org.bouncycastle.openpgp.PGPOnePassSignatureList +import org.bouncycastle.openpgp.PGPSignatureList +import org.bouncycastle.util.io.Streams +import org.pgpainless.decryption_verification.OpenPgpInputStream +import org.pgpainless.decryption_verification.cleartext_signatures.ClearsignedMessageUtil +import org.pgpainless.exception.WrongConsumingMethodException +import org.pgpainless.implementation.ImplementationFactory +import org.pgpainless.util.ArmoredOutputStreamFactory +import sop.ReadyWithResult +import sop.Signatures +import sop.exception.SOPGPException +import sop.operation.InlineDetach + +/** Implementation of the `inline-detach` operation using PGPainless. */ +class InlineDetachImpl : InlineDetach { + + private var armor = true + + override fun message(messageInputStream: InputStream): ReadyWithResult { + return object : ReadyWithResult() { + + private val sigOut = ByteArrayOutputStream() + + override fun writeTo(messageOutputStream: OutputStream): Signatures { + var pgpIn = OpenPgpInputStream(messageInputStream) + if (pgpIn.isNonOpenPgp) { + throw SOPGPException.BadData("Data appears to be non-OpenPGP.") + } + var signatures: PGPSignatureList? = null + + // Handle ASCII armor + if (pgpIn.isAsciiArmored) { + val armorIn = ArmoredInputStream(pgpIn) + + // Handle cleartext signature framework + if (armorIn.isClearText) { + try { + signatures = + ClearsignedMessageUtil.detachSignaturesFromInbandClearsignedMessage( + armorIn, messageOutputStream) + if (signatures.isEmpty) { + throw SOPGPException.BadData( + "Data did not contain OpenPGP signatures.") + } + } catch (e: WrongConsumingMethodException) { + throw SOPGPException.BadData(e) + } + } + + // else just dearmor + pgpIn = OpenPgpInputStream(armorIn) + } + + // If data was not using cleartext signature framework + if (signatures == null) { + if (!pgpIn.isBinaryOpenPgp) { + throw SOPGPException.BadData( + "Data was containing ASCII armored non-OpenPGP data.") + } + + // handle binary OpenPGP data + var objectFactory = + ImplementationFactory.getInstance().getPGPObjectFactory(pgpIn) + var next: Any? + + while (objectFactory.nextObject().also { next = it } != null) { + + if (next is PGPOnePassSignatureList) { + // Skip over OPSs + continue + } + + if (next is PGPLiteralData) { + // Write out contents of Literal Data packet + val literalIn = (next as PGPLiteralData).dataStream + Streams.pipeAll(literalIn, messageOutputStream) + literalIn.close() + continue + } + + if (next is PGPCompressedData) { + // Decompress compressed data + try { + objectFactory = + ImplementationFactory.getInstance() + .getPGPObjectFactory((next as PGPCompressedData).dataStream) + } catch (e: PGPException) { + throw SOPGPException.BadData( + "Cannot decompress PGPCompressedData", e) + } + } + + if (next is PGPSignatureList) { + signatures = next as PGPSignatureList + } + } + } + + if (signatures == null) { + throw SOPGPException.BadData("Data did not contain OpenPGP signatures.") + } + + if (armor) { + ArmoredOutputStreamFactory.get(sigOut).use { armoredOut -> + signatures.forEach { it.encode(armoredOut) } + } + } else { + signatures.forEach { it.encode(sigOut) } + } + + return object : Signatures() { + override fun writeTo(outputStream: OutputStream) { + sigOut.writeTo(outputStream) + } + } + } + } + } + + override fun noArmor(): InlineDetach = apply { armor = false } +} diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/InlineSignImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/InlineSignImpl.kt new file mode 100644 index 00000000..6fdf59a1 --- /dev/null +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/InlineSignImpl.kt @@ -0,0 +1,114 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop + +import java.io.InputStream +import java.io.OutputStream +import java.lang.RuntimeException +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.openpgp.PGPSecretKeyRing +import org.bouncycastle.util.io.Streams +import org.pgpainless.PGPainless +import org.pgpainless.algorithm.DocumentSignatureType +import org.pgpainless.bouncycastle.extensions.openPgpFingerprint +import org.pgpainless.encryption_signing.ProducerOptions +import org.pgpainless.encryption_signing.SigningOptions +import org.pgpainless.exception.KeyException.MissingSecretKeyException +import org.pgpainless.exception.KeyException.UnacceptableSigningKeyException +import org.pgpainless.util.Passphrase +import sop.Ready +import sop.enums.InlineSignAs +import sop.exception.SOPGPException +import sop.operation.InlineSign +import sop.util.UTF8Util + +/** Implementation of the `inline-sign` operation using PGPainless. */ +class InlineSignImpl : InlineSign { + + private val signingOptions = SigningOptions.get() + private val protector = MatchMakingSecretKeyRingProtector() + private val signingKeys = mutableListOf() + + private var armor = true + private var mode = InlineSignAs.binary + + override fun data(data: InputStream): Ready { + signingKeys.forEach { key -> + try { + if (mode == InlineSignAs.clearsigned) { + signingOptions.addDetachedSignature(protector, key, modeToSigType(mode)) + } else { + signingOptions.addInlineSignature(protector, key, modeToSigType(mode)) + } + } catch (e: UnacceptableSigningKeyException) { + throw SOPGPException.KeyCannotSign("Key ${key.openPgpFingerprint} cannot sign.", e) + } catch (e: MissingSecretKeyException) { + throw SOPGPException.KeyCannotSign( + "Key ${key.openPgpFingerprint} does not have the secret signing key component available.", + e) + } catch (e: PGPException) { + throw SOPGPException.KeyIsProtected( + "Key ${key.openPgpFingerprint} cannot be unlocked.", e) + } + } + + val producerOptions = + ProducerOptions.sign(signingOptions).apply { + if (mode == InlineSignAs.clearsigned) { + setCleartextSigned() + setAsciiArmor(true) // CSF is always armored + } else { + setAsciiArmor(armor) + } + } + + return object : Ready() { + override fun writeTo(outputStream: OutputStream) { + try { + val signingStream = + PGPainless.encryptAndOrSign() + .onOutputStream(outputStream) + .withOptions(producerOptions) + + Streams.pipeAll(data, signingStream) + signingStream.close() + + // forget passphrases + protector.clear() + } catch (e: PGPException) { + throw RuntimeException(e) + } + } + } + } + + override fun key(key: InputStream): InlineSign = apply { + KeyReader.readSecretKeys(key, true).forEach { + val info = PGPainless.inspectKeyRing(it) + if (!info.isUsableForSigning) { + throw SOPGPException.KeyCannotSign( + "Key ${info.fingerprint} does not have valid, signing capable subkeys.") + } + protector.addSecretKey(it) + signingKeys.add(it) + } + } + + override fun mode(mode: InlineSignAs): InlineSign = apply { this.mode = mode } + + override fun noArmor(): InlineSign = apply { armor = false } + + override fun withKeyPassword(password: ByteArray): InlineSign = apply { + protector.addPassphrase(Passphrase.fromPassword(String(password, UTF8Util.UTF8))) + } + + private fun modeToSigType(mode: InlineSignAs): DocumentSignatureType { + return when (mode) { + InlineSignAs.binary -> DocumentSignatureType.BINARY_DOCUMENT + InlineSignAs.text -> DocumentSignatureType.CANONICAL_TEXT_DOCUMENT + InlineSignAs.clearsigned -> DocumentSignatureType.CANONICAL_TEXT_DOCUMENT + } + } +} diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/InlineVerifyImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/InlineVerifyImpl.kt new file mode 100644 index 00000000..0b4e7d2f --- /dev/null +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/InlineVerifyImpl.kt @@ -0,0 +1,73 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop + +import java.io.InputStream +import java.io.OutputStream +import java.util.* +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.util.io.Streams +import org.pgpainless.PGPainless +import org.pgpainless.decryption_verification.ConsumerOptions +import org.pgpainless.exception.MalformedOpenPgpMessageException +import org.pgpainless.exception.MissingDecryptionMethodException +import sop.ReadyWithResult +import sop.Verification +import sop.exception.SOPGPException +import sop.operation.InlineVerify + +/** Implementation of the `inline-verify` operation using PGPainless. */ +class InlineVerifyImpl : InlineVerify { + + private val options = ConsumerOptions.get() + + override fun cert(cert: InputStream): InlineVerify = apply { + options.addVerificationCerts(KeyReader.readPublicKeys(cert, true)) + } + + override fun data(data: InputStream): ReadyWithResult> { + return object : ReadyWithResult>() { + override fun writeTo(outputStream: OutputStream): List { + try { + val verificationStream = + PGPainless.decryptAndOrVerify().onInputStream(data).withOptions(options) + + Streams.pipeAll(verificationStream, outputStream) + verificationStream.close() + + val result = verificationStream.metadata + val verifications = + if (result.isUsingCleartextSignatureFramework) { + result.verifiedDetachedSignatures + } else { + result.verifiedInlineSignatures + } + .map { VerificationHelper.mapVerification(it) } + + if (options.getCertificateSource().getExplicitCertificates().isNotEmpty() && + verifications.isEmpty()) { + throw SOPGPException.NoSignature() + } + + return verifications + } catch (e: MissingDecryptionMethodException) { + throw SOPGPException.BadData("Cannot verify encrypted message.", e) + } catch (e: MalformedOpenPgpMessageException) { + throw SOPGPException.BadData(e) + } catch (e: PGPException) { + throw SOPGPException.BadData(e) + } + } + } + } + + override fun notAfter(timestamp: Date): InlineVerify = apply { + options.verifyNotAfter(timestamp) + } + + override fun notBefore(timestamp: Date): InlineVerify = apply { + options.verifyNotBefore(timestamp) + } +} diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/KeyReader.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/KeyReader.kt new file mode 100644 index 00000000..0931a3a5 --- /dev/null +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/KeyReader.kt @@ -0,0 +1,77 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop + +import java.io.IOException +import java.io.InputStream +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.openpgp.PGPPublicKeyRingCollection +import org.bouncycastle.openpgp.PGPRuntimeOperationException +import org.bouncycastle.openpgp.PGPSecretKeyRingCollection +import org.pgpainless.PGPainless +import sop.exception.SOPGPException + +/** Reader for OpenPGP keys and certificates with error matching according to the SOP spec. */ +class KeyReader { + + companion object { + @JvmStatic + fun readSecretKeys( + keyInputStream: InputStream, + requireContent: Boolean + ): PGPSecretKeyRingCollection { + val keys = + try { + PGPainless.readKeyRing().secretKeyRingCollection(keyInputStream) + } catch (e: IOException) { + if (e.message == null) { + throw e + } + if (e.message!!.startsWith("unknown object in stream:") || + e.message!!.startsWith("invalid header encountered")) { + throw SOPGPException.BadData(e) + } + throw e + } + if (requireContent && keys.none()) { + throw SOPGPException.BadData(PGPException("No key data found.")) + } + + return keys + } + + @JvmStatic + fun readPublicKeys( + certIn: InputStream, + requireContent: Boolean + ): PGPPublicKeyRingCollection { + val certs = + try { + PGPainless.readKeyRing().keyRingCollection(certIn, false) + } catch (e: IOException) { + if (e.message == null) { + throw e + } + if (e.message!!.startsWith("unknown object in stream:") || + e.message!!.startsWith("invalid header encountered")) { + throw SOPGPException.BadData(e) + } + throw e + } catch (e: PGPRuntimeOperationException) { + throw SOPGPException.BadData(e) + } + + if (certs.pgpSecretKeyRingCollection.any()) { + throw SOPGPException.BadData( + "Secret key components encountered, while certificates were expected.") + } + + if (requireContent && certs.pgpPublicKeyRingCollection.none()) { + throw SOPGPException.BadData(PGPException("No cert data found.")) + } + return certs.pgpPublicKeyRingCollection + } + } +} diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ListProfilesImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ListProfilesImpl.kt new file mode 100644 index 00000000..39a5151d --- /dev/null +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ListProfilesImpl.kt @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop + +import sop.Profile +import sop.exception.SOPGPException +import sop.operation.ListProfiles + +/** Implementation of the `list-profiles` operation using PGPainless. */ +class ListProfilesImpl : ListProfiles { + + override fun subcommand(command: String): List = + when (command) { + "generate-key" -> GenerateKeyImpl.SUPPORTED_PROFILES + "encrypt" -> EncryptImpl.SUPPORTED_PROFILES + else -> throw SOPGPException.UnsupportedProfile(command) + } +} diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/MatchMakingSecretKeyRingProtector.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/MatchMakingSecretKeyRingProtector.kt new file mode 100644 index 00000000..13347721 --- /dev/null +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/MatchMakingSecretKeyRingProtector.kt @@ -0,0 +1,88 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop + +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.openpgp.PGPSecretKey +import org.bouncycastle.openpgp.PGPSecretKeyRing +import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor +import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor +import org.pgpainless.bouncycastle.extensions.isDecrypted +import org.pgpainless.bouncycastle.extensions.unlock +import org.pgpainless.key.protection.CachingSecretKeyRingProtector +import org.pgpainless.key.protection.SecretKeyRingProtector +import org.pgpainless.util.Passphrase + +/** + * Implementation of the [SecretKeyRingProtector] which can be handed passphrases and keys + * separately, and which then matches up passphrases and keys when needed. + */ +class MatchMakingSecretKeyRingProtector : SecretKeyRingProtector { + + private val passphrases = mutableSetOf() + private val keys = mutableSetOf() + private val protector = CachingSecretKeyRingProtector() + + fun addPassphrase(passphrase: Passphrase) = apply { + if (passphrase.isEmpty) { + return@apply + } + + if (!passphrases.add(passphrase)) { + return@apply + } + + keys.forEach { key -> + for (subkey in key) { + if (protector.hasPassphrase(subkey.keyID)) { + continue + } + + if (testPassphrase(passphrase, subkey)) { + protector.addPassphrase(subkey.keyID, passphrase) + } + } + } + } + + fun addSecretKey(key: PGPSecretKeyRing) = apply { + if (!keys.add(key)) { + return@apply + } + + key.forEach { subkey -> + if (subkey.isDecrypted()) { + protector.addPassphrase(subkey.keyID, Passphrase.emptyPassphrase()) + } else { + passphrases.forEach { passphrase -> + if (testPassphrase(passphrase, subkey)) { + protector.addPassphrase(subkey.keyID, passphrase) + } + } + } + } + } + + private fun testPassphrase(passphrase: Passphrase, key: PGPSecretKey): Boolean = + try { + key.unlock(passphrase) + true + } catch (e: PGPException) { + // Wrong passphrase + false + } + + override fun hasPassphraseFor(keyId: Long): Boolean = protector.hasPassphrase(keyId) + + override fun getDecryptor(keyId: Long): PBESecretKeyDecryptor? = protector.getDecryptor(keyId) + + override fun getEncryptor(keyId: Long): PBESecretKeyEncryptor? = protector.getEncryptor(keyId) + + /** Clear all known passphrases from the protector. */ + fun clear() { + passphrases.forEach { it.clear() } + keys.forEach { protector.forgetPassphrase(it) } + } +} diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/NullOutputStream.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/NullOutputStream.kt new file mode 100644 index 00000000..0f644881 --- /dev/null +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/NullOutputStream.kt @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop + +import java.io.OutputStream + +/** [OutputStream] that simply discards bytes written to it. */ +class NullOutputStream : OutputStream() { + + override fun write(p0: Int) { + // nop + } + + override fun write(b: ByteArray) { + // nop + } + + override fun write(b: ByteArray, off: Int, len: Int) { + // nop + } + + override fun close() { + // nop + } + + override fun flush() { + // nop + } +} diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/RevokeKeyImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/RevokeKeyImpl.kt new file mode 100644 index 00000000..ecc87e62 --- /dev/null +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/RevokeKeyImpl.kt @@ -0,0 +1,95 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop + +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream +import java.lang.RuntimeException +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.openpgp.PGPPublicKeyRing +import org.bouncycastle.openpgp.PGPPublicKeyRingCollection +import org.pgpainless.PGPainless +import org.pgpainless.bouncycastle.extensions.openPgpFingerprint +import org.pgpainless.exception.WrongPassphraseException +import org.pgpainless.key.util.KeyRingUtils +import org.pgpainless.key.util.RevocationAttributes +import org.pgpainless.util.ArmoredOutputStreamFactory +import org.pgpainless.util.Passphrase +import sop.Ready +import sop.exception.SOPGPException +import sop.operation.RevokeKey +import sop.util.UTF8Util + +class RevokeKeyImpl : RevokeKey { + + private val protector = MatchMakingSecretKeyRingProtector() + private var armor = true + + override fun keys(keys: InputStream): Ready { + val secretKeyRings = + try { + KeyReader.readSecretKeys(keys, true) + } catch (e: IOException) { + throw SOPGPException.BadData("Cannot decode secret keys.", e) + } + + secretKeyRings.forEach { protector.addSecretKey(it) } + + val revocationCertificates = mutableListOf() + secretKeyRings.forEach { secretKeys -> + val editor = PGPainless.modifyKeyRing(secretKeys) + try { + val attributes = + RevocationAttributes.createKeyRevocation() + .withReason(RevocationAttributes.Reason.NO_REASON) + .withoutDescription() + if (secretKeys.publicKey.version == 6) { + revocationCertificates.add( + editor.createMinimalRevocationCertificate(protector, attributes)) + } else { + val certificate = PGPainless.extractCertificate(secretKeys) + val revocation = editor.createRevocation(protector, attributes) + revocationCertificates.add( + KeyRingUtils.injectCertification(certificate, revocation)) + } + } catch (e: WrongPassphraseException) { + throw SOPGPException.KeyIsProtected( + "Missing or wrong passphrase for key ${secretKeys.openPgpFingerprint}", e) + } catch (e: PGPException) { + throw RuntimeException( + "Cannot generate revocation certificate for key ${secretKeys.openPgpFingerprint}", + e) + } + } + + return object : Ready() { + override fun writeTo(outputStream: OutputStream) { + val collection = PGPPublicKeyRingCollection(revocationCertificates) + if (armor) { + val armorOut = ArmoredOutputStreamFactory.get(outputStream) + collection.encode(armorOut) + armorOut.close() + } else { + collection.encode(outputStream) + } + } + } + } + + override fun noArmor(): RevokeKey = apply { armor = false } + + override fun withKeyPassword(password: ByteArray): RevokeKey = apply { + val string = + try { + UTF8Util.decodeUTF8(password) + } catch (e: CharacterCodingException) { + // TODO: Add cause + throw SOPGPException.PasswordNotHumanReadable( + "Cannot UTF8-decode password: ${e.stackTraceToString()}") + } + protector.addPassphrase(Passphrase.fromPassword(string)) + } +} diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/SOPImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/SOPImpl.kt new file mode 100644 index 00000000..16f54a22 --- /dev/null +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/SOPImpl.kt @@ -0,0 +1,56 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop + +import sop.SOP +import sop.SOPV +import sop.operation.Armor +import sop.operation.ChangeKeyPassword +import sop.operation.Dearmor +import sop.operation.Decrypt +import sop.operation.DetachedSign +import sop.operation.DetachedVerify +import sop.operation.Encrypt +import sop.operation.ExtractCert +import sop.operation.GenerateKey +import sop.operation.InlineDetach +import sop.operation.InlineSign +import sop.operation.InlineVerify +import sop.operation.ListProfiles +import sop.operation.RevokeKey +import sop.operation.Version + +class SOPImpl(private val sopv: SOPV = SOPVImpl()) : SOP { + + override fun armor(): Armor = ArmorImpl() + + override fun changeKeyPassword(): ChangeKeyPassword = ChangeKeyPasswordImpl() + + override fun dearmor(): Dearmor = DearmorImpl() + + override fun decrypt(): Decrypt = DecryptImpl() + + override fun detachedSign(): DetachedSign = DetachedSignImpl() + + override fun detachedVerify(): DetachedVerify = sopv.detachedVerify() + + override fun encrypt(): Encrypt = EncryptImpl() + + override fun extractCert(): ExtractCert = ExtractCertImpl() + + override fun generateKey(): GenerateKey = GenerateKeyImpl() + + override fun inlineDetach(): InlineDetach = InlineDetachImpl() + + override fun inlineSign(): InlineSign = InlineSignImpl() + + override fun inlineVerify(): InlineVerify = sopv.inlineVerify() + + override fun listProfiles(): ListProfiles = ListProfilesImpl() + + override fun revokeKey(): RevokeKey = RevokeKeyImpl() + + override fun version(): Version = sopv.version() +} diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/SOPVImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/SOPVImpl.kt new file mode 100644 index 00000000..43b4c64f --- /dev/null +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/SOPVImpl.kt @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop + +import org.pgpainless.util.ArmoredOutputStreamFactory +import sop.SOPV +import sop.operation.DetachedVerify +import sop.operation.InlineVerify +import sop.operation.Version + +class SOPVImpl : SOPV { + + init { + ArmoredOutputStreamFactory.setVersionInfo(null) + } + + override fun detachedVerify(): DetachedVerify = DetachedVerifyImpl() + + override fun inlineVerify(): InlineVerify = InlineVerifyImpl() + + override fun version(): Version = VersionImpl() +} diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/VerificationHelper.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/VerificationHelper.kt new file mode 100644 index 00000000..9198e3b7 --- /dev/null +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/VerificationHelper.kt @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop + +import org.bouncycastle.openpgp.PGPSignature +import org.pgpainless.decryption_verification.SignatureVerification +import sop.Verification +import sop.enums.SignatureMode + +/** Helper class for shared methods related to [Verification] objects. */ +class VerificationHelper { + + companion object { + + /** + * Map a [SignatureVerification] object to a [Verification]. + * + * @param sigVerification signature verification + * @return verification + */ + @JvmStatic + fun mapVerification(sigVerification: SignatureVerification): Verification = + Verification( + sigVerification.signature.creationTime, + sigVerification.signingKey.subkeyFingerprint.toString(), + sigVerification.signingKey.primaryKeyFingerprint.toString(), + getMode(sigVerification.signature), + null) + + /** + * Map an OpenPGP signature type to a [SignatureMode] enum. Note: This method only maps + * [PGPSignature.BINARY_DOCUMENT] and [PGPSignature.CANONICAL_TEXT_DOCUMENT]. Other values + * are mapped to `null`. + * + * @param signature signature + * @return signature mode enum or null + */ + @JvmStatic + fun getMode(signature: PGPSignature): SignatureMode? = + when (signature.signatureType) { + PGPSignature.BINARY_DOCUMENT -> SignatureMode.binary + PGPSignature.CANONICAL_TEXT_DOCUMENT -> SignatureMode.text + else -> null + } + } +} diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/VersionImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/VersionImpl.kt new file mode 100644 index 00000000..7ebdade9 --- /dev/null +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/VersionImpl.kt @@ -0,0 +1,63 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop + +import java.io.IOException +import java.io.InputStream +import java.util.* +import org.bouncycastle.jce.provider.BouncyCastleProvider +import sop.operation.Version + +/** Implementation of the `version` operation using PGPainless. */ +class VersionImpl : Version { + + companion object { + const val SOP_VERSION = 10 + const val SOPV_VERSION = "1.0" + } + + override fun getBackendVersion(): String = "PGPainless ${getVersion()}" + + override fun getExtendedVersion(): String { + val bcVersion = + String.format(Locale.US, "Bouncy Castle %.2f", BouncyCastleProvider().version) + val specVersion = String.format("%02d", SOP_VERSION) + return """${getName()} ${getVersion()} +https://codeberg.org/PGPainless/pgpainless/src/branch/master/pgpainless-sop + +Implementation of the Stateless OpenPGP Protocol Version $specVersion +https://datatracker.ietf.org/doc/html/draft-dkg-openpgp-stateless-cli-$specVersion + +Based on pgpainless-core ${getVersion()} +https://pgpainless.org + +Using $bcVersion +https://www.bouncycastle.org/java.html""" + } + + override fun getName(): String = "PGPainless-SOP" + + override fun getSopSpecImplementationRemarks(): String? = null + + override fun getSopSpecRevisionNumber(): Int = SOP_VERSION + + override fun getSopVVersion(): String = SOPV_VERSION + + override fun getVersion(): String { + // See https://stackoverflow.com/a/50119235 + return try { + val resourceIn: InputStream = + javaClass.getResourceAsStream("/version.properties") + ?: throw IOException("File version.properties not found.") + + val properties = Properties().apply { load(resourceIn) } + properties.getProperty("version") + } catch (e: IOException) { + "DEVELOPMENT" + } + } + + override fun isSopSpecImplementationIncomplete(): Boolean = false +}