mirror of
https://github.com/pgpainless/pgpainless.git
synced 2024-11-22 12:22:06 +01:00
Port pgpainless-sop to Kotlin
This commit is contained in:
parent
8066650584
commit
b393a90da4
43 changed files with 1660 additions and 2170 deletions
|
@ -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();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
|
|
@ -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.")
|
||||||
|
}
|
||||||
|
}
|
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
125
pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DecryptImpl.kt
Normal file
125
pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DecryptImpl.kt
Normal 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)
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
155
pgpainless-sop/src/main/kotlin/org/pgpainless/sop/EncryptImpl.kt
Normal file
155
pgpainless-sop/src/main/kotlin/org/pgpainless/sop/EncryptImpl.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 }
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 }
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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) }
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
56
pgpainless-sop/src/main/kotlin/org/pgpainless/sop/SOPImpl.kt
Normal file
56
pgpainless-sop/src/main/kotlin/org/pgpainless/sop/SOPImpl.kt
Normal 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()
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
Loading…
Reference in a new issue