name.neuhalfen.projects.crypto.bouncycastle.openpgp.BuildEncryptionOutputStreamAPI Maven / Gradle / Ivy
Show all versions of bouncy-gpg Show documentation
package name.neuhalfen.projects.crypto.bouncycastle.openpgp;
import static java.util.Objects.requireNonNull;
import java.io.IOException;
import java.io.OutputStream;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.SignatureException;
import java.time.Instant;
import java.util.HashSet;
import java.util.Set;
import javax.annotation.Nullable;
import name.neuhalfen.projects.crypto.bouncycastle.openpgp.algorithms.DefaultPGPAlgorithmSuites;
import name.neuhalfen.projects.crypto.bouncycastle.openpgp.algorithms.PGPAlgorithmSuite;
import name.neuhalfen.projects.crypto.bouncycastle.openpgp.encrypting.PGPEncryptingStream;
import name.neuhalfen.projects.crypto.bouncycastle.openpgp.keys.callbacks.ByEMailKeySelectionStrategy;
import name.neuhalfen.projects.crypto.bouncycastle.openpgp.keys.callbacks.KeySelectionStrategy;
import name.neuhalfen.projects.crypto.bouncycastle.openpgp.keys.callbacks.KeySelectionStrategy.PURPOSE;
import name.neuhalfen.projects.crypto.bouncycastle.openpgp.keys.callbacks.KeyringConfigCallback;
import name.neuhalfen.projects.crypto.bouncycastle.openpgp.keys.callbacks.Rfc4880KeySelectionStrategy;
import name.neuhalfen.projects.crypto.bouncycastle.openpgp.keys.keyrings.InMemoryKeyring;
import name.neuhalfen.projects.crypto.bouncycastle.openpgp.keys.keyrings.KeyringConfig;
import name.neuhalfen.projects.crypto.bouncycastle.openpgp.keys.keyrings.KeyringConfigs;
import name.neuhalfen.projects.crypto.internal.Preconditions;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPSecretKey;
@SuppressWarnings({"PMD.GodClass", "PMD.AtLeastOneConstructor",
"PMD.AccessorMethodGeneration", "PMD.LawOfDemeter", "Checkstyle.AbbreviationAsWordInName"})
public final class BuildEncryptionOutputStreamAPI {
private static final org.slf4j.Logger LOGGER = org.slf4j.LoggerFactory
.getLogger(BuildEncryptionOutputStreamAPI.class);
@SuppressWarnings({"PMD.ImmutableField"})
private WithKeySelectionStrategy keySelectionStrategyBuilder;
/*
* lazily populated by getKeySelectionStrategy()
*/
private KeySelectionStrategy keySelectionStrategy;
private OutputStream sinkForEncryptedData;
private KeyringConfig encryptionConfig;
private PGPAlgorithmSuite algorithmSuite;
@Nullable
private String signWith;
private Set recipients;
private boolean armorOutput;
// Signature
/**
* Use the passed keyring config for the crypto operations. The KeyringConfig wraps the
* public- and private keyrings.
*
* Generally the best KeyringConfig variant to use is the {@link InMemoryKeyring} which can be
* created by calling {@link KeyringConfigs#forGpgExportedKeys(KeyringConfigCallback)}.
*
* @param encryptionConfig the keyring config.
*
* @return the next step in the builder
*
* @throws IOException bouncy castle uses IO
* @throws PGPException errors in the config
* @see name.neuhalfen.projects.crypto.bouncycastle.openpgp.keys.keyrings.KeyringConfigs
* @see name.neuhalfen.projects.crypto.bouncycastle.openpgp.keys.keyrings.InMemoryKeyring
*/
@SuppressWarnings("PMD.AccessorClassGeneration")
public WithKeySelectionStrategy withConfig(final KeyringConfig encryptionConfig)
throws IOException, PGPException {
requireNonNull(encryptionConfig, "encryptionConfig must not be null");
requireNonNull(encryptionConfig.getKeyFingerPrintCalculator(),
"encryptionConfig.getKeyFingerPrintCalculator() must not be null");
requireNonNull(encryptionConfig.getPublicKeyRings(),
"encryptionConfig.getPublicKeyRings() must not be null");
this.encryptionConfig = encryptionConfig;
return new WithKeySelectionStrategy();
}
private KeySelectionStrategy getKeySelectionStrategy() {
if (this.keySelectionStrategy == null) {
this.keySelectionStrategy = this.keySelectionStrategyBuilder
.buildKeySelectionStrategy();
}
return this.keySelectionStrategy;
}
public interface Build {
OutputStream andWriteTo(OutputStream sinkForEncryptedData)
throws PGPException, SignatureException, NoSuchAlgorithmException, NoSuchProviderException, IOException;
}
public interface WithAlgorithmSuite {
/**
* The (older) default suite for gpg.:
*
* - hash
- SHA-1
* - chipher
- CAST 5
* - compression
- ZLIB
*
*
* Only recommended if {@link #withStrongAlgorithms()} cannot be used.
*
* @return next step
*/
To withDefaultAlgorithms();
/**
* Use a strong suite of algorithms that is understood by gpg.
* It is a sensible suite with strong algorithms:
*
* - hash
- SHA-256
* - chipher
- AES-128
* - compression
- ZLIB
*
* This is recommended over {@link #withDefaultAlgorithms()}.
*
* @return next step
*/
To withStrongAlgorithms();
/**
* Use the default algorithm suite for XEP-0373,
* OpenPGP for XMPP.
* It is a sensible suite with strong algorithms but without compression.:
*
* - hash
- SHA-256
* - chipher
- AES-128
* - compression
- uncompressed
*
*
* @return next step
*/
To withOxAlgorithms();
/**
* Use a custom algorithm set.
*
* @param algorithmSuite algorithm suite to use
*
* @return next step
*
* @see DefaultPGPAlgorithmSuites
*/
To withAlgorithms(PGPAlgorithmSuite algorithmSuite);
@SuppressWarnings("PMD.ShortClassName")
interface To {
/**
* Encrypt to the following recipient.
* The meaning of 'recipient' changes with how the {@link KeySelectionStrategy} is
* configured. Specifically the call to {@link WithKeySelectionStrategy#selectUidByAnyUidPart}
* will change the way key selection is done.
*
*
* @param recipient The single recipients UID (e.g. email address)
*
* @return the next step
*
* @throws PGPException e.g. recipient could not be found
* @see KeySelectionStrategy
* @see WithKeySelectionStrategy
*/
SignWith toRecipient(String recipient) throws PGPException;
/**
* Encrypt to the following recipients (multiple).
* The meaning of 'recipients' changes with how the {@link KeySelectionStrategy} is
* configured. Specifically the call to {@link WithKeySelectionStrategy#selectUidByAnyUidPart}
* will change the way key selection is done.
*
*
* @param recipients The recipients UIDs (e.g. email address)
*
* @return the next step
*
* @throws PGPException e.g. recipients could not be found
* @see KeySelectionStrategy
* @see WithKeySelectionStrategy
*/
SignWith toRecipients(String... recipients) throws PGPException;
interface SignWith {
/**
* Sign the message with the following user id. The key used will be sought by the
* key selection strategy.
*
* @param userId sign with this userid
*
* @return next step
*
* @throws IOException IO is dangerous
* @throws PGPException Something with GPG went wrong (e.g. key not found)
*/
Armor andSignWith(String userId) throws IOException, PGPException;
/**
* Do not sign the message.
*
* @return next step
*/
Armor andDoNotSign();
interface Armor {
/**
* Write as binary output.
*
* @return next step
*/
Build binaryOutput();
/**
* Ascii armor the output, e.g. for usage in text protocols.
*
* @return next step
*/
Build armorAsciiOutput();
}
}
}
}
/**
* Combined step for key- and algorithm selection.
*/
public final class WithKeySelectionStrategy extends WithAlgorithmSuiteImpl {
private static final boolean SELECT_UID_BY_E_MAIL_ONLY_DEFAULT = true;
@Nullable
private Instant dateOfTimestampVerification;
@Nullable
@SuppressWarnings({"PMD.LinguisticNaming"})
private Boolean selectUidByEMailOnly;
@Nullable
private KeySelectionStrategy keySelectionStrategy;
private WithKeySelectionStrategy() {
super();
BuildEncryptionOutputStreamAPI.this.keySelectionStrategyBuilder = this;
}
/**
*
* Normally keys are only searched by e-mail (between < and >). Calling
* selectUidByAnyUidPart() will search everywhere.
*
* E.g. given the uid 'Juliet Capulet <[email protected]>' a search normally would
* look for the e-mail '[email protected]'. E.g. searching for 'juliet' would be found,
* searching for 'Capulet' would not be found.
*
*
* After calling selectUidByAnyUidPart() the key will also be found by searching for 'Capulet'
*
*
* @return next step
*/
public WithKeySelectionStrategy selectUidByAnyUidPart() {
Preconditions.checkState(keySelectionStrategy == null,
"selectUidByAnyUidPart/setReferenceDateForKeyValidityTo cannot be" +
" used together with 'withKeySelectionStrategy' ");
selectUidByEMailOnly = false;
return this;
}
/**
* In order to determine key validity a reference point in time for "now" is needed.
* The default value is "Instant.now()". If this needs to be overridden, pass the value
* here. To effectively disable time based key verification pass Instant.MAX (NOT recommended)
*
* This is not possible in combination with #withKeySelectionStrategy.
*
* @param dateOfTimestampVerification reference point in time
*
* @return next step in build
*/
@SuppressWarnings("PMD.LinguisticNaming")
public WithAlgorithmSuite setReferenceDateForKeyValidityTo(
final Instant dateOfTimestampVerification) {
Preconditions.checkState(keySelectionStrategy == null,
"selectUidByAnyUidPart/setReferenceDateForKeyValidityTo cannot be"
+ " used together with 'withKeySelectionStrategy' ");
requireNonNull(dateOfTimestampVerification,
"dateOfTimestampVerification must not be null");
this.dateOfTimestampVerification = dateOfTimestampVerification;
LOGGER.trace("WithKeySelectionStrategy: setReferenceDateForKeyValidityTo {}",
dateOfTimestampVerification);
return this;
}
/**
* The default strategy to search for keys is to *just* search for the email address (the part
* between < and >).
*
* Set this flag to search for any part in the user id.
*
* @param strategy instance to use
*
* @return next build step
*/
public WithAlgorithmSuite withKeySelectionStrategy(final KeySelectionStrategy strategy) {
requireNonNull(strategy, "strategy must not be null");
Preconditions.checkState(
selectUidByEMailOnly == null && dateOfTimestampVerification == null,
"selectUidByAnyUidPart/setReferenceDateForKeyValidityTo cannot be used together"
+ " with 'withKeySelectionStrategy' ");
this.keySelectionStrategy = strategy;
LOGGER.trace("WithKeySelectionStrategy: override strategy to {}",
strategy.getClass().toGenericString());
return this;
}
// Duplicate of BuildDecryptionInputStreamAPI
@SuppressWarnings({"PMD.OnlyOneReturn"})
private KeySelectionStrategy buildKeySelectionStrategy() {
final boolean hasExistingStrategy = this.keySelectionStrategy != null;
if (hasExistingStrategy) {
return this.keySelectionStrategy;
} else {
if (this.selectUidByEMailOnly == null) {
this.selectUidByEMailOnly = SELECT_UID_BY_E_MAIL_ONLY_DEFAULT;
}
if (this.dateOfTimestampVerification == null) {
this.dateOfTimestampVerification = Instant.now();
}
if (this.selectUidByEMailOnly) {
return new ByEMailKeySelectionStrategy(this.dateOfTimestampVerification);
} else {
return new Rfc4880KeySelectionStrategy(this.dateOfTimestampVerification);
}
}
}
}
private class WithAlgorithmSuiteImpl implements WithAlgorithmSuite {
@Override
public To withDefaultAlgorithms() {
BuildEncryptionOutputStreamAPI.this.algorithmSuite = DefaultPGPAlgorithmSuites
.defaultSuiteForGnuPG();
LOGGER
.trace("use algorithms {}",
BuildEncryptionOutputStreamAPI.this.algorithmSuite.toString());
return new ToImpl();
}
@Override
public To withStrongAlgorithms() {
BuildEncryptionOutputStreamAPI.this.algorithmSuite = DefaultPGPAlgorithmSuites.strongSuite();
LOGGER
.trace("use algorithms {}",
BuildEncryptionOutputStreamAPI.this.algorithmSuite.toString());
return new ToImpl();
}
@Override
public To withOxAlgorithms() {
BuildEncryptionOutputStreamAPI.this.algorithmSuite = DefaultPGPAlgorithmSuites.oxSuite();
LOGGER
.trace("use algorithms {}",
BuildEncryptionOutputStreamAPI.this.toString());
return new ToImpl();
}
@Override
public To withAlgorithms(final PGPAlgorithmSuite algorithmSuite) {
requireNonNull(algorithmSuite, "algorithmSuite must not be null");
BuildEncryptionOutputStreamAPI.this.algorithmSuite = algorithmSuite;
LOGGER
.trace("use algorithms {}",
BuildEncryptionOutputStreamAPI.this.algorithmSuite.toString());
return new ToImpl();
}
@SuppressWarnings("PMD.ShortClassName")
final class ToImpl implements To {
private PGPPublicKey extractValidKey(final String recipient) throws PGPException {
requireNonNull(recipient, "recipient must not be null");
Preconditions.checkArgument(!recipient.isEmpty(), "recipient must not be empty");
try {
final PGPPublicKey recipientEncryptionKey = getKeySelectionStrategy()
.selectPublicKey(PURPOSE.FOR_ENCRYPTION, recipient, encryptionConfig);
if (recipientEncryptionKey == null) {
throw new PGPException(
"No (suitable) public key for encryption to " + recipient + " found");
}
LOGGER.trace("encrypt to recipient {} using key 0x{}", recipient,
Long.toHexString(recipientEncryptionKey.getKeyID()));
return recipientEncryptionKey;
} catch (IOException e) {
throw new PGPException("Failed to load keys", e);
}
}
@Override
public SignWith toRecipient(final String recipient) throws PGPException {
BuildEncryptionOutputStreamAPI.this.recipients = new HashSet<>();
BuildEncryptionOutputStreamAPI.this.recipients.add(extractValidKey(recipient));
return new SignWithImpl();
}
@Override
public SignWith toRecipients(String... recipients) throws PGPException {
BuildEncryptionOutputStreamAPI.this.recipients = new HashSet<>();
for (final String recipient : recipients) {
BuildEncryptionOutputStreamAPI.this.recipients.add(extractValidKey(recipient));
}
return new SignWithImpl();
}
final class SignWithImpl implements SignWith {
@Override
public Armor andSignWith(String userId) throws IOException, PGPException {
Preconditions.checkState(encryptionConfig.getSecretKeyRings() != null,
"encryptionConfig.getSecretKeyRings() must not be null");
final PGPPublicKey signingKeyPubKey = getKeySelectionStrategy()
.selectPublicKey(PURPOSE.FOR_SIGNING, userId, encryptionConfig);
if (signingKeyPubKey == null) {
throw new PGPException(
"No (suitable) public key for signing with '" + userId + "' found");
}
final PGPSecretKey signingKey = encryptionConfig.getSecretKeyRings()
.getSecretKey(signingKeyPubKey.getKeyID());
if (signingKey == null) {
throw new PGPException(
"No (suitable) secret key for signing with " + userId
+ " found (public key exists!)");
}
BuildEncryptionOutputStreamAPI.this.signWith = userId;
LOGGER.trace("sign with {}", BuildEncryptionOutputStreamAPI.this.signWith);
return new ArmorImpl();
}
@Override
@SuppressWarnings("PMD.NullAssignment")
public Armor andDoNotSign() {
BuildEncryptionOutputStreamAPI.this.signWith = null;
LOGGER.trace("do not sign ");
return new ArmorImpl();
}
public final class ArmorImpl implements Armor {
@Override
public Build binaryOutput() {
BuildEncryptionOutputStreamAPI.this.armorOutput = false;
LOGGER.trace("binary output");
return new Builder();
}
@Override
public Build armorAsciiOutput() {
BuildEncryptionOutputStreamAPI.this.armorOutput = true;
LOGGER.trace("ascii armor output");
return new Builder();
}
public final class Builder implements Build {
@Override
public OutputStream andWriteTo(OutputStream sinkForEncryptedData)
throws PGPException, SignatureException, NoSuchAlgorithmException, NoSuchProviderException, IOException {
BuildEncryptionOutputStreamAPI.this.sinkForEncryptedData = sinkForEncryptedData;
return PGPEncryptingStream.create(
BuildEncryptionOutputStreamAPI.this.encryptionConfig,
BuildEncryptionOutputStreamAPI.this.algorithmSuite,
BuildEncryptionOutputStreamAPI.this.signWith,
BuildEncryptionOutputStreamAPI.this.sinkForEncryptedData,
getKeySelectionStrategy(),
BuildEncryptionOutputStreamAPI.this.armorOutput,
BuildEncryptionOutputStreamAPI.this.recipients);
}
}
}
}
}
}
}