mirror of
https://github.com/pgpainless/pgpainless.git
synced 2024-12-23 03:17:58 +01:00
Merge branch 'sopKotlin'
This commit is contained in:
commit
80cf1a7446
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