1
0
Fork 0
mirror of https://github.com/pgpainless/pgpainless.git synced 2024-12-23 03:17:58 +01:00

Port pgpainless-sop to Kotlin

This commit is contained in:
Paul Schaub 2024-03-24 16:16:29 +01:00
parent 8066650584
commit b393a90da4
Signed by: vanitasvitae
GPG key ID: 62BEE9264BF17311
43 changed files with 1660 additions and 2170 deletions

View file

@ -1,63 +0,0 @@
// SPDX-FileCopyrightText: 2020 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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 <pre>armor</pre> 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();
}
};
}
}

View file

@ -1,96 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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<PGPSecretKeyRing> 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);
}
}
};
}
}

View file

@ -1,45 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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 <pre>dearmor</pre> 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();
}
};
}
}

View file

@ -1,176 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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 <pre>decrypt</pre> 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<DecryptionResult> 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<DecryptionResult>() {
@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<Verification> 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);
}
};
}
}

View file

@ -1,168 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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 <pre>sign</pre> 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<PGPSecretKeyRing> 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<SigningResult> 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<SigningResult>() {
@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<PGPSignature> 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<PGPSignature> signatures) {
int algorithmId = 0;
for (PGPSignature signature : signatures) {
int sigAlg = signature.getHashAlgorithm();
if (algorithmId == 0 || algorithmId == sigAlg) {
algorithmId = sigAlg;
} else {
return MicAlg.empty();
}
}
return algorithmId == 0 ? MicAlg.empty() : MicAlg.fromHashAlgorithmId(algorithmId);
}
private static DocumentSignatureType modeToSigType(SignAs mode) {
return mode == SignAs.binary ? DocumentSignatureType.BINARY_DOCUMENT
: DocumentSignatureType.CANONICAL_TEXT_DOCUMENT;
}
}

View file

@ -1,100 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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 <pre>verify</pre> 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<Verification> 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<Verification> 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);
}
}
}

View file

@ -1,201 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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 <pre>encrypt</pre> 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<Profile> SUPPORTED_PROFILES = Arrays.asList(RFC4880_PROFILE);
EncryptionOptions encryptionOptions = EncryptionOptions.get();
SigningOptions signingOptions = null;
MatchMakingSecretKeyRingProtector protector = new MatchMakingSecretKeyRingProtector();
private final Set<PGPSecretKeyRing> 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<sop.EncryptionResult> 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<EncryptionResult>() {
@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);
}
}

View file

@ -1,64 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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 <pre>extract-cert</pre> 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<PGPPublicKeyRing> 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();
}
}
}
};
}
}

View file

@ -1,154 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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 <pre>generate-key</pre> 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<Profile> SUPPORTED_PROFILES = Arrays.asList(CURVE25519_PROFILE, RSA4096_PROFILE);
private boolean armor = true;
private boolean signingOnly = false;
private final Set<String> 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<String> 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();
}
}

View file

@ -1,156 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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 <pre>inline-detach</pre> 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<Signatures> message(@Nonnull InputStream messageInputStream) {
return new ReadyWithResult<Signatures>() {
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);
}
};
}
};
}
}

View file

@ -1,135 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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 <pre>inline-sign</pre> 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<PGPSecretKeyRing> 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;
}
}

View file

@ -1,101 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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 <pre>inline-verify</pre> 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<List<Verification>> data(@Nonnull InputStream data) throws SOPGPException.NoSignature, SOPGPException.BadData {
return new ReadyWithResult<List<Verification>>() {
@Override
public List<Verification> 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<Verification> verificationList = new ArrayList<>();
List<SignatureVerification> 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);
}
}
};
}
}

View file

@ -1,69 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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();
}
}

View file

@ -1,36 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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 <pre>list-profiles</pre> operation using PGPainless.
*
*/
public class ListProfilesImpl implements ListProfiles {
@Override
@Nonnull
public List<Profile> 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);
}
}
}

View file

@ -1,119 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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<Passphrase> passphrases = new HashSet<>();
private final Set<PGPSecretKeyRing> 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);
}
}
}

View file

@ -1,17 +0,0 @@
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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
}
}

View file

@ -1,123 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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<PGPPublicKeyRing> 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);
}
}
};
}
}

View file

@ -1,149 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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 <pre>sop</pre> API using PGPainless.
* <pre> {@code
* SOP sop = new SOPImpl();
* }</pre>
*
* 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();
}
}

View file

@ -1,40 +0,0 @@
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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 <pre>sopv</pre> 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();
}
}

View file

@ -1,52 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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 <pre>null</pre>.
*
* @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;
}
}

View file

@ -1,98 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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 <pre>version</pre> 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;
}
}

View file

@ -1,8 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
/**
* Implementation of the java-sop package using pgpainless-core.
*/
package org.pgpainless.sop;

View file

@ -0,0 +1,54 @@
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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.")
}
}

View file

@ -0,0 +1,81 @@
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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))
}
}

View file

@ -0,0 +1,38 @@
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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()
}
}
}
}
}

View file

@ -0,0 +1,125 @@
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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<DecryptionResult> {
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<DecryptionResult>() {
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)
}

View file

@ -0,0 +1,130 @@
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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<PGPSecretKeyRing>()
private var armor = true
private var mode = SignAs.binary
override fun data(data: InputStream): ReadyWithResult<SigningResult> {
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<SigningResult>() {
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<PGPSignature>): MicAlg =
signatures
.mapNotNull { HashAlgorithm.fromId(it.hashAlgorithm) }
.toSet()
.singleOrNull()
?.let { MicAlg.fromHashAlgorithmId(it.algorithmId) }
?: MicAlg.empty()
}

View file

@ -0,0 +1,71 @@
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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<Verification> {
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)
}
}
}

View file

@ -0,0 +1,155 @@
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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<PGPSecretKeyRing>()
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<EncryptionResult> {
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<EncryptionResult>() {
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
}
}
}

View file

@ -0,0 +1,47 @@
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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 }
}

View file

@ -0,0 +1,136 @@
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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<String>()
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<String>,
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()
}
}

View file

@ -0,0 +1,134 @@
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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<Signatures> {
return object : ReadyWithResult<Signatures>() {
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 }
}

View file

@ -0,0 +1,114 @@
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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<PGPSecretKeyRing>()
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
}
}
}

View file

@ -0,0 +1,73 @@
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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<List<Verification>> {
return object : ReadyWithResult<List<Verification>>() {
override fun writeTo(outputStream: OutputStream): List<Verification> {
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)
}
}

View file

@ -0,0 +1,77 @@
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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
}
}
}

View file

@ -0,0 +1,20 @@
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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<Profile> =
when (command) {
"generate-key" -> GenerateKeyImpl.SUPPORTED_PROFILES
"encrypt" -> EncryptImpl.SUPPORTED_PROFILES
else -> throw SOPGPException.UnsupportedProfile(command)
}
}

View file

@ -0,0 +1,88 @@
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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<Passphrase>()
private val keys = mutableSetOf<PGPSecretKeyRing>()
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) }
}
}

View file

@ -0,0 +1,31 @@
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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
}
}

View file

@ -0,0 +1,95 @@
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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<PGPPublicKeyRing>()
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))
}
}

View file

@ -0,0 +1,56 @@
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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()
}

View file

@ -0,0 +1,24 @@
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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()
}

View file

@ -0,0 +1,48 @@
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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
}
}
}

View file

@ -0,0 +1,63 @@
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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
}